- {{ 'SETTINGS.REMOTE_CONTROL_URL' | translate }}
+
+
+
-
- @for (ip of localIpAddresses(); track ip) {
-
-
- @if (visibleQrCodeIp() === ip) {
-
-
-
- }
-
- }
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
+ }}
+
+ @for (player of players; track player) {
+ {{ player.label }}
+
+ }
+
+
+
+
+
+
+
+
+
+ {{
+ 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
+ }}
+
+ @for (
+ streamFormat of streamFormatEnum | keyvalue;
+ track streamFormat
+ ) {
+ {{ streamFormat.value }}
+
+ }
+
+
+
+
+
+ @if (settingsForm.value.player === 'mpv') {
+
+
+
+
+ {{
+ 'SETTINGS.MPV_PLAYER_PATH' | translate
+ }}
+
+
+
+
+
+
+ }
+
+ @if (settingsForm.value.player === 'vlc') {
+
+
+
+
+ {{
+ 'SETTINGS.VLC_PLAYER_PATH' | translate
+ }}
+
+
+
+
+ }
+
+
+ @if (isDesktop) {
+
+
+
+
+
+
+
+ @for (
+ _ of epgUrl.controls;
+ track _;
+ let i = $index
+ ) {
+
+
+ {{
+ 'SETTINGS.EPG_URL_PLACEHOLDER'
+ | translate
+ }}
+
+ {{
+ 'SETTINGS.EPG_URL_ERROR'
+ | translate
+ }}
+
+
+
+
+ }
+
+
+
+
+
+
+
}
- }
-
-
-
- {{ 'SETTINGS.VERSION' | translate }}
-
{{ 'SETTINGS.VERSION_DESCRIPTION' | translate }}
-
-
- {{ version }}
- {{ updateMessage }}
-
-
-
-
-
- {{ 'SETTINGS.IMPORT_EXPORT_DATA' | translate }}
-
- {{ 'SETTINGS.IMPORT_EXPORT_DATA_DESCRIPTION' | translate }}
-
-
-
+
+ @if (isDesktop) {
+
+
+
+
+
+ @if (settingsForm.value.remoteControl === true) {
+
+
+
+
+ Port (1-65535)
+
+ @if (
+ settingsForm
+ .get('remoteControlPort')
+ ?.hasError('required')
+ ) {
+ {{
+ 'SETTINGS.PORT_REQUIRED' | translate
+ }}
+ }
+ @if (
+ settingsForm
+ .get('remoteControlPort')
+ ?.hasError('min')
+ ) {
+ {{
+ 'SETTINGS.PORT_MIN' | translate
+ }}
+ }
+ @if (
+ settingsForm
+ .get('remoteControlPort')
+ ?.hasError('max')
+ ) {
+ {{
+ 'SETTINGS.PORT_MAX' | translate
+ }}
+ }
+ @if (
+ settingsForm
+ .get('remoteControlPort')
+ ?.hasError('pattern')
+ ) {
+ {{
+ 'SETTINGS.PORT_PATTERN' | translate
+ }}
+ }
+
+
+
+
+ @if (localIpAddresses().length > 0) {
+
+
+
+ @for (ip of localIpAddresses(); track ip) {
+
+
+ @if (visibleQrCodeIp() === ip) {
+
+
+
+ }
+
+ }
+
+
+ }
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (isPwa) {
+
+ }
+
+
+
+
+
+
+
+
+ {{ version }}
+ {{ updateMessage }}
+
+
+
+
+
-
-
-
- {{ 'SETTINGS.REMOVE_ALL' | translate }}
-
- {{ 'SETTINGS.REMOVE_ALL_DESCRIPTION' | translate }}
-
-
-
-
-
-
-
- @if (isPwa) {
-
- }
-
-
-
-
-
-
diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss
index da5e2ff73..ce0416361 100644
--- a/apps/web/src/app/settings/settings.component.scss
+++ b/apps/web/src/app/settings/settings.component.scss
@@ -1,55 +1,228 @@
-button {
- cursor: pointer !important;
- text-transform: uppercase;
+:host {
+ display: block;
+}
+
+.settings-page-header {
+ position: sticky;
+ top: 0;
+ z-index: 5;
}
.settings-container {
- overflow: auto;
- height: calc(100vh - 70px);
+ --settings-bg: color-mix(in srgb, currentColor 3%, transparent);
+ --settings-card: color-mix(in srgb, currentColor 7%, transparent);
+ --settings-border: color-mix(in srgb, currentColor 14%, transparent);
+ --settings-muted: color-mix(in srgb, currentColor 65%, transparent);
+
+ height: auto;
+ overflow: visible;
+ padding: 20px;
}
-.row {
+.settings-container.dialog-mode {
+ height: auto;
+ max-height: none;
+ overflow: visible;
+ padding: 12px 8px 16px;
+}
+
+.settings-layout {
+ display: grid;
+ grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
+ gap: 24px;
+ align-items: start;
+}
+
+.settings-sidebar {
+ position: sticky;
+ top: 0;
+ padding: 18px;
+ border: 1px solid var(--settings-border);
+ border-radius: 16px;
+ background: var(--settings-bg);
+}
+
+.settings-container:not(.dialog-mode) .settings-sidebar {
+ top: 82px;
+ max-height: calc(100vh - 102px);
+ overflow: auto;
+}
+
+.sidebar-eyebrow {
+ margin: 0 0 14px;
+ text-transform: uppercase;
+ letter-spacing: 0.14em;
+ font-size: 11px;
+ font-weight: 700;
+ opacity: 0.72;
+}
+
+.sidebar-nav {
display: flex;
- padding: 10px;
+ flex-direction: column;
+ gap: 8px;
+}
- :nth-child(2) {
- text-align: right;
+.sidebar-item {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ border: 1px solid transparent;
+ border-radius: 12px;
+ background: transparent;
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
+ transition: 180ms ease;
- mat-form-field {
- font-size: 14px;
- }
+ span {
+ font-size: 0.92rem;
+ font-weight: 600;
+ }
+
+ .mat-icon {
+ width: 18px;
+ height: 18px;
+ font-size: 18px;
+ opacity: 0.84;
+ }
+
+ &:hover {
+ border-color: var(--settings-border);
+ background: color-mix(in srgb, currentColor 6%, transparent);
}
}
-.column {
- flex: 1;
+.sidebar-item.active {
+ border-color: var(--settings-border);
+ background: color-mix(in srgb, currentColor 10%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 8%, transparent);
+}
+
+.settings-content {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+
+.settings-group {
+ border: 1px solid var(--settings-border);
+ border-radius: 16px;
+ background: var(--settings-card);
+ overflow: hidden;
+ scroll-margin-top: 20px;
+}
+
+.settings-group__header {
+ padding: 18px 20px 16px;
+ border-bottom: 1px solid var(--settings-border);
+
+ h3 {
+ margin: 0;
+ font-size: 1.28rem;
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ }
p {
- font-size: 0.9em;
- opacity: 0.6;
- padding: 2px 0;
+ margin: 6px 0 0;
+ opacity: 0.72;
+ font-size: 0.9rem;
+ }
+}
+
+.settings-group__footer {
+ padding: 12px 20px 16px;
+ border-top: 1px solid var(--settings-border);
+}
+
+.setting-item {
+ display: grid;
+ grid-template-columns: minmax(220px, 1fr) minmax(240px, 440px);
+ gap: 18px;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--settings-border);
+}
+
+.settings-group .setting-item:last-child {
+ border-bottom: none;
+}
+
+.setting-item__meta {
+ h4 {
margin: 0;
+ font-size: 1.02rem;
+ font-weight: 700;
+ }
+
+ p {
+ margin: 4px 0 0;
+ color: var(--settings-muted);
+ font-size: 0.9rem;
+ line-height: 1.35;
}
}
-.margin-right {
- margin-right: 10px;
+.setting-item__control {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 10px;
+
+ mat-form-field,
+ .full-width {
+ width: min(100%, 380px);
+ }
}
-.action-buttons {
- margin-top: 10px;
+.setting-item__toggle {
+ justify-content: flex-end;
+}
- button {
- margin: 0 10px;
+.setting-item--full-control {
+ grid-template-columns: 1fr;
+}
+
+.setting-item--full-control .setting-item__control {
+ justify-content: stretch;
+}
+
+.epg-sources {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.epg-source-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+
+ .full-width {
+ width: 100%;
}
}
+.inline-actions {
+ display: flex;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
.remote-control-urls {
- text-align: right;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 6px;
}
.remote-control-url-item {
- margin-bottom: 8px;
+ width: min(100%, 380px);
}
.url-row {
@@ -60,9 +233,10 @@ button {
}
.remote-control-url {
- font-family: monospace;
+ font-family: 'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas, monospace;
color: inherit;
text-decoration: none;
+ font-size: 0.84rem;
&:hover {
text-decoration: underline;
@@ -74,3 +248,116 @@ button {
justify-content: flex-end;
padding: 8px 0;
}
+
+.version-block {
+ flex-direction: column;
+ align-items: flex-end;
+
+ span {
+ font-weight: 700;
+ }
+
+ small {
+ margin-top: 4px;
+ color: var(--settings-muted);
+ text-align: right;
+ }
+}
+
+.settings-note {
+ margin: 0 20px 20px;
+ padding: 12px 14px;
+ border-radius: 12px;
+ font-size: 0.9rem;
+ background: color-mix(in srgb, currentColor 6%, transparent);
+ border: 1px solid var(--settings-border);
+
+ a {
+ color: inherit;
+ font-weight: 700;
+ }
+}
+
+.danger-zone {
+ background: color-mix(in srgb, currentColor 4%, transparent);
+}
+
+.action-bar {
+ position: sticky;
+ bottom: 8px;
+ z-index: 3;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+ padding: 12px;
+ border: 1px solid var(--settings-border);
+ border-radius: 14px;
+ background: color-mix(in srgb, var(--settings-bg) 88%, transparent);
+ backdrop-filter: blur(8px);
+}
+
+.settings-container.dialog-mode .action-bar {
+ margin-top: 8px;
+}
+
+@media (max-width: 1100px) {
+ .settings-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .settings-sidebar {
+ position: static;
+ }
+
+ .sidebar-nav {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .sidebar-item {
+ width: auto;
+ }
+}
+
+@media (max-width: 860px) {
+ .settings-container {
+ height: auto;
+ padding: 12px;
+ }
+
+ .setting-item {
+ grid-template-columns: 1fr;
+ gap: 12px;
+ }
+
+ .setting-item__control {
+ justify-content: flex-start;
+
+ mat-form-field,
+ .full-width,
+ .epg-sources,
+ .remote-control-url-item {
+ width: 100%;
+ }
+ }
+
+ .remote-control-urls,
+ .version-block {
+ align-items: flex-start;
+ }
+
+ .url-row {
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ }
+
+ .action-bar {
+ flex-direction: column-reverse;
+ align-items: stretch;
+
+ button {
+ width: 100%;
+ }
+ }
+}
diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts
index 11b7b51d1..eb17ecb73 100644
--- a/apps/web/src/app/settings/settings.component.ts
+++ b/apps/web/src/app/settings/settings.component.ts
@@ -49,6 +49,13 @@ import { SettingsStore } from '../services/settings-store.service';
import { HeaderComponent } from '../shared/components/header/header.component';
import { SettingsService } from './../services/settings.service';
+interface SettingsSection {
+ id: string;
+ label: string;
+ icon: string;
+ visible: boolean;
+}
+
@Component({
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
@@ -94,6 +101,8 @@ export class SettingsComponent implements OnInit {
isPwa = this.dataService.getAppEnvironment() === 'pwa';
+ activeSection = signal('general');
+
readonly osPlayers = [
{
id: VideoPlayer.MPV,
@@ -168,12 +177,55 @@ export class SettingsComponent implements OnInit {
private settingsStore = inject(SettingsStore);
+ readonly sectionNavItems: SettingsSection[] = [
+ {
+ id: 'general',
+ label: 'SETTINGS.TITLE',
+ icon: 'tune',
+ visible: true,
+ },
+ {
+ id: 'playback',
+ label: 'SETTINGS.VIDEO_PLAYER_LABEL',
+ icon: 'play_circle',
+ visible: true,
+ },
+ {
+ id: 'epg',
+ label: 'SETTINGS.EPG_URL_LABEL',
+ icon: 'calendar_month',
+ visible: this.isDesktop,
+ },
+ {
+ id: 'remote-control',
+ label: 'SETTINGS.REMOTE_CONTROL',
+ icon: 'smartphone',
+ visible: this.isDesktop,
+ },
+ {
+ id: 'data',
+ label: 'SETTINGS.IMPORT_EXPORT_DATA',
+ icon: 'swap_horiz',
+ visible: true,
+ },
+ {
+ id: 'about',
+ label: 'SETTINGS.VERSION',
+ icon: 'info',
+ visible: true,
+ },
+ ];
+
constructor(
@Optional() @Inject(MAT_DIALOG_DATA) data?: { isDialog: boolean }
) {
this.isDialog = data?.isDialog ?? false;
}
+ get sectionNav(): SettingsSection[] {
+ return this.sectionNavItems.filter((section) => section.visible);
+ }
+
/**
* Reads the config object from the browsers
* storage (indexed db)
@@ -207,6 +259,13 @@ export class SettingsComponent implements OnInit {
}
}
+ scrollToSection(sectionId: string): void {
+ this.activeSection.set(sectionId);
+ document
+ .getElementById(sectionId)
+ ?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+
/**
* Sets saved settings from the indexed db store
*/
diff --git a/apps/web/src/app/xtream-electron/navigation/navigation.component.ts b/apps/web/src/app/xtream-electron/navigation/navigation.component.ts
index f4d265d91..af0ff532c 100644
--- a/apps/web/src/app/xtream-electron/navigation/navigation.component.ts
+++ b/apps/web/src/app/xtream-electron/navigation/navigation.component.ts
@@ -99,9 +99,9 @@ export class NavigationComponent {
openSettings() {
this.dialog.open(SettingsComponent, {
- width: '80%',
- maxWidth: '1200px',
- maxHeight: '90vh',
+ width: '1200px',
+ maxWidth: '96vw',
+ maxHeight: '92vh',
data: {
isDialog: true,
},
diff --git a/apps/web/src/app/xtream/navigation-bar/navigation-bar.component.ts b/apps/web/src/app/xtream/navigation-bar/navigation-bar.component.ts
index 85f9ac241..384b452a8 100644
--- a/apps/web/src/app/xtream/navigation-bar/navigation-bar.component.ts
+++ b/apps/web/src/app/xtream/navigation-bar/navigation-bar.component.ts
@@ -163,8 +163,9 @@ export class NavigationBarComponent implements OnChanges {
openSettings(): void {
this.dialog.open(SettingsComponent, {
- width: '1000px',
- height: '90%',
+ width: '1200px',
+ maxWidth: '96vw',
+ maxHeight: '92vh',
data: { isDialog: true },
});
}