mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 09:26:14 -08:00
* 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>
163 lines
7.6 KiB
HTML
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>
|