Files
iptvnator/apps/web/src/app/settings/settings.component.html
T
4grayandClaude Fable 5.1 6f7973a9fb feat(updater): nightly builds and a stable/nightly update channel (#1608)
* feat(updater): nightly builds and a stable/nightly update channel

Every master push publishes its artifacts as a prerelease of
4gray/iptvnator-nightly instead of the rolling test-master draft, with a
version of <next patch>-nightly.<commit date>.<run number> applied in
every build job. Settings → About gains an Update channel switch;
AppUpdateService re-points electron-updater per check (feed repository,
allowPrerelease, channel name, allowDowngrade reset) and reads release
notes from the repository the requested version belongs to. Channel
switches are forward-only: a nightly build stays until a newer stable
release exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(updater): compute the nightly version once and keep re-runs safe

Review follow-ups: the nightly version is resolved by a leading job and
handed to every build job, and the patch is bumped only when the base
tag already exists so the release-cut window stays below the imminent
release. A re-run never deletes a published nightly; only a draft left
by a failed run is replaced. Typed update-status literals in the
remaining specs carry the new channel fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(packaging): expect the nightly-version prerequisite in the build workflow graph

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 17:49:22 +02:00

163 lines
7.6 KiB
HTML

<div class="settings-container" data-test-id="settings-container">
<form
[formGroup]="settingsForm"
(ngSubmit)="onSubmit()"
class="settings-layout"
novalidate
>
<!-- Page-level "Settings" h1 intentionally removed: the left rail
already labels the page "Settings" (sg-nav-title), and each
section below provides its own h3 + subtitle. -->
<span class="visually-hidden" data-test-id="settings-page-header">
{{ 'SETTINGS.TITLE' | translate }}
</span>
<div class="settings-content">
<!-- Only the routed section renders. The parent keeps the shared
form alive across section switches, so edits staged on one
page survive a visit to another. -->
@switch (activeSection()) {
@case ('playback') {
<app-settings-playback-section
[form]="settingsForm"
[players]="players()"
[streamFormatEnum]="streamFormatEnum"
[isDesktop]="isDesktop"
[supportsManagedExternalPlayers]="
supportsManagedExternalPlayers
"
[supportsExternalPlayerPathSettings]="
supportsExternalPlayerPathSettings
"
[supportsVodMultiSource]="supportsVodMultiSource"
[frameCopyAvailable]="embeddedMpv.frameCopyAvailable()"
[frameCopyActive]="embeddedMpv.frameCopyActive()"
(selectRecordingFolder)="selectRecordingFolder()"
/>
}
@case ('epg') {
<app-settings-epg-section
[form]="settingsForm"
[epgUrl]="form.epgUrl"
[isClearingEpgData]="epg.isClearing()"
[canBrowseFiles]="epg.supportsFilePicker"
[epgViewModeOptions]="epgViewModeOptions"
(refreshEpg)="epg.refresh($event)"
(browseEpgSource)="epg.browse($event)"
(removeEpgSource)="form.removeEpgSource($event)"
(addEpgSource)="form.addEpgSource()"
(refreshAllEpg)="epg.refreshAll()"
(clearEpgData)="epg.clear()"
(selectEpgViewMode)="form.selectEpgViewMode($event)"
/>
}
@case ('dashboard') {
<app-settings-dashboard-section [form]="settingsForm" />
}
@case ('remote-control') {
<app-settings-remote-control-section
[form]="settingsForm"
[localIpAddresses]="remoteControl.localIpAddresses()"
[visibleQrCodeIp]="remoteControl.visibleQrCodeIp()"
(toggleQrCode)="remoteControl.toggleQrCode($event)"
/>
}
@case ('tmdb') {
<app-settings-tmdb-section [form]="settingsForm" />
}
@case ('backup') {
<app-settings-backup-section
[isPwa]="isPwa"
[isRemovingAllPlaylists]="
playlistReset.isRemovingAllPlaylists()
"
[isExportingData]="backup.isExportingData()"
(importData)="importData()"
(exportData)="exportData()"
/>
}
@case ('reset') {
<app-settings-reset-section
[isRemovingAllPlaylists]="
playlistReset.isRemovingAllPlaylists()
"
[canRemoveAllPlaylists]="playlistReset.canRemoveAll()"
[playlistDeleteSummary]="playlistReset.deleteSummary()"
[removeAllProgressLabel]="
playlistReset.removeAllProgressLabel()
"
(removeAll)="removeAll()"
/>
}
@case ('about') {
<app-settings-about-section
[isDesktop]="isDesktop"
[form]="settingsForm"
[updateChannelOptions]="updateChannelOptions"
[version]="appUpdate.version()"
[buildCommit]="buildCommit"
[updateMessage]="appUpdate.updateMessage()"
[appUpdateStatus]="appUpdate.status()"
(checkForAppUpdate)="appUpdate.checkForAppUpdate()"
(downloadAppUpdate)="appUpdate.downloadAppUpdate()"
(installAppUpdate)="appUpdate.installAppUpdate()"
(openManualAppUpdate)="appUpdate.openManualAppUpdate()"
(openAppUpdateReleaseNotes)="
appUpdate.openReleaseNotes()
"
/>
}
@default {
<app-settings-general-section
[form]="settingsForm"
[languageEnum]="languageEnum"
[themeOptions]="themeOptions"
[coverSizeOptions]="coverSizeOptions"
[startupBehaviorOptions]="startupBehaviorOptions"
[startupWindowModeOptions]="startupWindowModeOptions"
[supportsPortalConnectivityGuard]="
supportsPortalConnectivityGuard
"
[supportsStartupWindowMode]="supportsStartupWindowMode"
(selectTheme)="form.selectTheme($event)"
(selectCoverSize)="form.selectCoverSize($event)"
/>
}
}
</div>
<!-- Discord-style unsaved-changes bar: appears the moment any edit
makes the form dirty — on whichever section page — and vanishes
once saved or discarded. It replaces the always-visible footer
Save button of the single-scroll layout. -->
@if (!settingsForm.pristine) {
<footer class="unsaved-bar" data-test-id="settings-unsaved-bar">
<div class="unsaved-bar__message">
<mat-icon>edit</mat-icon>
<span>{{ 'SETTINGS.UNSAVED_CHANGES' | translate }}</span>
</div>
<div class="unsaved-bar__actions">
<button
mat-button
type="button"
(click)="discardChanges()"
data-test-id="discard-settings"
>
{{ 'SETTINGS.DISCARD_CHANGES' | translate }}
</button>
<button
mat-flat-button
type="submit"
class="settings-save-button"
[disabled]="!settingsForm.valid"
data-test-id="save-settings"
>
<mat-icon>save</mat-icon>
{{ 'SETTINGS.SAVE_CHANGES' | translate }}
</button>
</div>
</footer>
}
</form>
</div>