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

@@ -30,7 +30,7 @@
/>
}
@if (type()) {
@if (type() && showTypeBadge()) {
<span class="type-badge" [class]="type()">
{{ type() }}
</span>
@@ -51,6 +51,9 @@ export class ContentCardComponent {
/** Whether to show placeholder when no poster */
readonly showPlaceholder = input<boolean>(true);
/** Whether to render the type badge (live/movie/series) on the poster */
readonly showTypeBadge = input<boolean>(true);
/** Emitted when the card is clicked */
readonly cardClick = output<void>();
@@ -1,6 +1,6 @@
:host {
display: block;
--rail-gap: 16px;
--rail-gap: var(--cover-gap, 16px);
}
.rail {
@@ -55,6 +55,7 @@
'Unknown'
"
[type]="item.type"
[showTypeBadge]="false"
[date]="item.added_at"
[showRemoveButton]="true"
[removeTooltip]="
@@ -81,7 +81,7 @@
}
.items-grid {
@include grid.content-grid(140px, 20px);
@include grid.content-grid;
padding: 16px;
width: 100%;
box-sizing: border-box;
@@ -16,7 +16,14 @@ mat-card:hover {
}
// ─── Grid layout ─────────────────────────────────────────────────────────────
.grid {
.grid-list__grid {
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(var(--cover-grid-min-width, 148px), 1fr)
);
gap: var(--cover-gap, 16px);
mat-card {
cursor: pointer;
width: 100%;
@@ -56,9 +56,7 @@ export function resolveGridRating(
@Component({
selector: 'app-grid-list',
template: `<div
class="grid grid-cols-[repeat(auto-fill,minmax(148px,1fr))] gap-4"
>
template: `<div class="grid-list__grid">
@if (isLoading()) {
@for (row of skeletonRows(); track row) {
<div class="grid-skeleton-card" aria-hidden="true">
@@ -151,7 +151,7 @@
// Results grid mixin for consumers
::ng-deep .results-grid {
@include grid.content-grid(140px, 20px);
@include grid.content-grid;
padding-bottom: 1rem;
}
@@ -16,8 +16,11 @@
flex: 1;
overflow: hidden;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
gap: 16px;
grid-template-columns: repeat(
auto-fill,
minmax(var(--cover-grid-min-width, 148px), 1fr)
);
gap: var(--cover-gap, 16px);
padding: 16px;
align-content: start;
}
@@ -23,6 +23,7 @@
[posterUrl]="item.posterUrl ?? ''"
[title]="item.name"
[type]="item.contentType"
[showTypeBadge]="false"
[date]="item.addedAt ?? item.viewedAt"
[showRemoveButton]="true"
[removeTooltip]="
@@ -37,8 +37,11 @@
overflow-y: auto;
overflow-x: hidden;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
grid-template-columns: repeat(
auto-fill,
minmax(var(--cover-grid-min-width, 148px), 1fr)
);
gap: var(--cover-gap, 16px);
padding: 16px;
align-content: start;
}
@@ -15,6 +15,6 @@
}
.results-grid {
@include grid.content-grid(140px, 20px);
@include grid.content-grid;
padding-bottom: 1rem;
}
@@ -27,8 +27,8 @@
// Projected card sizing lives on the caller — the rail shell can't reach
// projected children through emulated encapsulation.
.recently-added-page__rail app-content-card {
flex: 0 0 160px;
width: 160px;
flex: 0 0 var(--cover-rail-width, 160px);
width: var(--cover-rail-width, 160px);
scroll-snap-align: start;
}
@@ -57,8 +57,8 @@
}
.recently-added-page__skeleton-card {
flex: 0 0 160px;
width: 160px;
flex: 0 0 var(--cover-rail-width, 160px);
width: var(--cover-rail-width, 160px);
aspect-ratio: 2 / 3;
border-radius: 12px;
}
@@ -100,7 +100,7 @@
}
.recently-added-page__skeleton-card {
flex: 0 0 136px;
width: 136px;
flex: 0 0 min(var(--cover-rail-width, 160px), 136px);
width: min(var(--cover-rail-width, 160px), 136px);
}
}
@@ -10,7 +10,7 @@
// Results grid styling
.results-grid {
@include grid.content-grid(140px, 20px);
@include grid.content-grid;
padding-bottom: 1rem;
}
@@ -34,6 +34,7 @@ const DEFAULT_SETTINGS: Settings = {
remoteControlPort: 8765,
epgUrl: [],
downloadFolder: '',
coverSize: 'medium',
};
export const SettingsStore = signalStore(
@@ -88,6 +89,7 @@ export const SettingsStore = signalStore(
remoteControlPort: store.remoteControlPort(),
epgUrl: store.epgUrl(),
downloadFolder: store.downloadFolder!(),
coverSize: store.coverSize!(),
};
},
@@ -18,6 +18,8 @@ export enum StartupBehavior {
RestoreLastView = 'restore-last-view',
}
export type CoverSize = 'small' | 'medium' | 'large';
/**
* Describes all available settings options of the application
*/
@@ -39,4 +41,6 @@ export interface Settings {
remoteControlPort: number;
/** Custom download folder path (uses system Downloads folder if not set) */
downloadFolder?: string;
/** Cover/poster sizing preset applied across grids and rails */
coverSize?: CoverSize;
}
+5 -2
View File
@@ -5,9 +5,12 @@
// @include grid.content-grid;
// @include grid.content-card;
@mixin content-grid($min-width: 140px, $gap: 20px) {
@mixin content-grid(
$min-width: var(--cover-grid-min-width, 148px),
$gap: var(--cover-gap, 16px)
) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax($min-width, 1fr));
grid-template-columns: repeat(auto-fill, minmax(#{$min-width}, 1fr));
gap: $gap;
}
@@ -1,7 +1,7 @@
:host {
display: block;
--rail-card-width: 172px;
--rail-gap: 14px;
--rail-card-width: var(--cover-rail-width, 172px);
--rail-gap: var(--cover-gap, 14px);
--rail-radius: 12px;
}
@@ -444,6 +444,6 @@
@media (max-width: 720px) {
:host {
--rail-card-width: 140px;
--rail-card-width: min(var(--cover-rail-width, 172px), 140px);
}
}
@@ -417,18 +417,17 @@
.rails-page__skeleton-track {
display: flex;
gap: 14px;
gap: var(--cover-gap, 14px);
padding: 4px 0 16px;
overflow: hidden;
}
.rails-page__skeleton-card {
@extend %skeleton-shimmer;
// Matches the real .rail__card width (--rail-card-width: 172px) and
// .rail__art aspect-ratio 2/3, so when cards arrive the geometry is
// identical and there's no layout shift.
flex: 0 0 172px;
width: 172px;
// Mirrors the real .rail__card width (--cover-rail-width) so geometry
// matches when cards arrive — no layout shift.
flex: 0 0 var(--cover-rail-width, 172px);
width: var(--cover-rail-width, 172px);
aspect-ratio: 2 / 3;
border-radius: 12px;
}
@@ -474,8 +473,8 @@
}
.rails-page__skeleton-card {
flex: 0 0 140px;
width: 140px;
flex: 0 0 min(var(--cover-rail-width, 172px), 140px);
width: min(var(--cover-rail-width, 172px), 140px);
}
.hero__title {