feat(settings): add cover size settings and implement responsive cover sizing

Entire-Checkpoint: c2cea9c530e6
This commit is contained in:
4gray committed 2026-04-30 18:56:06 +02:00
1 parent cbedb22698
commit 4b2076fa53
25 files changed
+185 -35

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
// Cover size tokens — consumed by all cover/poster grids and rails.
// Toggled via [data-cover-size] attribute on <html>, set from SettingsStore.
:root {
--cover-grid-min-width: 148px;
--cover-rail-width: 172px;
--cover-gap: 16px;
}
:root[data-cover-size='small'] {
--cover-grid-min-width: 120px;
--cover-rail-width: 140px;
--cover-gap: 12px;
}
:root[data-cover-size='medium'] {
--cover-grid-min-width: 148px;
--cover-rail-width: 172px;
--cover-gap: 16px;
}
:root[data-cover-size='large'] {
--cover-grid-min-width: 200px;
--cover-rail-width: 220px;
--cover-gap: 20px;
}
@media (max-width: 600px) {
:root[data-cover-size='large'] {
--cover-grid-min-width: 160px;
--cover-rail-width: 170px;
}
}
+8 -2
View File
@@ -1,4 +1,4 @@
import { Component, HostBinding, inject, OnInit } from '@angular/core';
import { Component, effect, HostBinding, inject, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router, RouterOutlet } from '@angular/router';
import { Actions, ofType } from '@ngrx/effects';
@@ -9,7 +9,7 @@ import { WORKSPACE_SHELL_ACTIONS } from '@iptvnator/workspace/shell/util';
import { EpgProgressPanelComponent } from '@iptvnator/ui/epg/progress-panel';
import { PlaylistActions, selectAllPlaylistsMeta } from 'm3u-state';
import { filter, take } from 'rxjs';
import { DataService } from 'services';
import { DataService, SettingsStore } from 'services';
import {
AUTO_UPDATE_PLAYLISTS,
Language,
@@ -39,6 +39,7 @@ export class AppComponent implements OnInit {
private store = inject(Store);
private translate = inject(TranslateService);
private settingsService = inject(SettingsService);
private settingsStore = inject(SettingsStore);
private readonly workspaceShellActions = inject(WORKSPACE_SHELL_ACTIONS);
/** Default language as fallback */
@@ -64,6 +65,11 @@ export class AppComponent implements OnInit {
});
}
}
effect(() => {
const size = this.settingsStore.coverSize?.() ?? 'medium';
document.documentElement.dataset.coverSize = size;
});
if (window.electron) {
document.addEventListener('keydown', (event) => {
if (event.ctrlKey || event.metaKey) {
@@ -119,6 +119,49 @@
</div>
</div>
<div class="setting-item setting-item--full-control">
<div class="setting-item__meta">
<h4>{{ 'SETTINGS.COVER_SIZE' | translate }}</h4>
<p>{{ 'SETTINGS.COVER_SIZE_DESCRIPTION' | translate }}</p>
</div>
<div class="setting-item__control">
<div
class="theme-switcher"
role="radiogroup"
[attr.aria-label]="'SETTINGS.COVER_SIZE' | translate"
data-test-id="select-cover-size"
>
@for (
option of coverSizeOptions;
track option.value
) {
<button
type="button"
class="theme-switcher__option"
[class.theme-switcher__option--selected]="
settingsForm.value.coverSize ===
option.value
"
[attr.aria-checked]="
settingsForm.value.coverSize ===
option.value
"
[attr.data-test-id]="
'cover-size-' + option.value
"
role="radio"
(click)="selectCoverSize(option.value)"
>
<mat-icon>{{ option.icon }}</mat-icon>
<span>{{
option.labelKey | translate
}}</span>
</button>
}
</div>
</div>
</div>
<div class="setting-item">
<div class="setting-item__meta">
<h4>{{ 'SETTINGS.SHOW_DASHBOARD' | translate }}</h4>
@@ -20,6 +20,7 @@ import {
Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCheckboxModule } from '@angular/material/checkbox';
import {
MAT_DIALOG_DATA,
@@ -57,6 +58,7 @@ import {
} from 'services';
import {
/* EmbeddedMpvSupport, */
CoverSize,
Language,
StartupBehavior,
StreamFormat,
@@ -93,6 +95,12 @@ interface StartupBehaviorOption {
labelKey: string;
}
interface CoverSizeOption {
value: CoverSize;
icon: string;
labelKey: string;
}
type SettingsPlaylistDeleteSummary =
SettingsDeleteAllPlaylistsDialogData['summary'];
@@ -104,6 +112,7 @@ type SettingsPlaylistDeleteSummary =
EpgSourceStatusComponent,
FormsModule,
MatButtonModule,
MatButtonToggleModule,
MatCheckboxModule,
MatDividerModule,
MatIconModule,
@@ -225,6 +234,24 @@ export class SettingsComponent implements OnInit, OnDestroy {
},
];
readonly coverSizeOptions: CoverSizeOption[] = [
{
value: 'small',
icon: 'view_module',
labelKey: 'SETTINGS.COVER_SIZE_SMALL',
},
{
value: 'medium',
icon: 'view_comfy',
labelKey: 'SETTINGS.COVER_SIZE_MEDIUM',
},
{
value: 'large',
icon: 'view_quilt',
labelKey: 'SETTINGS.COVER_SIZE_LARGE',
},
];
readonly startupBehaviorOptions: StartupBehaviorOption[] = [
{
value: StartupBehavior.FirstView,
@@ -260,6 +287,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
Validators.pattern(/^\d+$/),
],
],
coverSize: 'medium' as CoverSize,
});
/** Form array with epg sources */
@@ -534,6 +562,17 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.settingsService.changeTheme(theme);
}
selectCoverSize(size: CoverSize): void {
if (this.settingsForm.value.coverSize === size) {
return;
}
this.settingsForm.patchValue({ coverSize: size });
this.settingsForm.get('coverSize')?.markAsDirty();
this.settingsForm.markAsDirty();
this.settingsStore.updateSettings({ coverSize: size });
}
/**
* Sets the epg urls to the form array
* @param epgUrls urls of the EPG sources
+5
View File
@@ -264,6 +264,11 @@
"STREAM_FORMAT_DESCRIPTION": "Select default stream format (xtream)",
"LANGUAGE_DESCRIPTION": "Select language of the application",
"THEME_DESCRIPTION": "Select visual theme of the application",
"COVER_SIZE": "Cover size",
"COVER_SIZE_DESCRIPTION": "Adjust the size of poster cards across grids and rails. Larger sizes are easier to see on TVs; smaller sizes show more content per row on big monitors.",
"COVER_SIZE_SMALL": "Small",
"COVER_SIZE_MEDIUM": "Medium",
"COVER_SIZE_LARGE": "Large",
"STARTUP_BEHAVIOR": "Startup view",
"STARTUP_BEHAVIOR_DESCRIPTION": "Choose whether IPTVnator opens the first available workspace view or restores your last section-level view. First view means Dashboard when enabled, otherwise Sources.",
"STARTUP_BEHAVIOR_FIRST_VIEW": "First available view",
+1
View File
@@ -1,5 +1,6 @@
@use '../../../libs/ui/components/src/lib/resizable/resizable.scss';
@use './settings-theme';
@use './cover-size';
// ─── Tailwind CSS ─────────────────────────────────────────────────────────────
// Preflight is disabled in tailwind.config.js to avoid conflicts with