mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
refactor(dashboard): improvements for dashboard widgets
- Updated dashboard activity items styles for improved layout and media representation. - Introduced new styles for movie and series media types. - Enhanced placeholder styles for live, movie, and series items using oklch color model. - Added hover effects for activity items and action buttons in grid mode. - Refactored dashboard widget shell styles for better alignment and padding. - Created new on-air widget component with category filtering and loading skeletons. - Implemented responsive design for on-air widget and improved empty state handling. - Removed deprecated recent sources widget and integrated recent sources functionality into existing components. - Updated recently watched widget to include action links for adding sources. - Enhanced source stats widget with new styling for different source types (Xtream, Stalker, M3U).
This commit is contained in:
1 parent
a8fd16b16d
commit
268a710bf2
27 files changed
+1487
-352
No files matched your search
@@ -1,43 +1,45 @@
|
||||
<section class="dashboard-page">
|
||||
<header class="dashboard-header">
|
||||
<div>
|
||||
<p class="eyebrow">Workspace Dashboard</p>
|
||||
<h1>Control Center</h1>
|
||||
<p class="description">
|
||||
Keep your daily sources in one place. In customize mode, edit
|
||||
directly on the canvas, then use the sidebar for visibility and
|
||||
scope.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section
|
||||
class="dashboard-toolbar"
|
||||
[class.dashboard-toolbar--editing]="editMode()"
|
||||
class="dashboard-topbar"
|
||||
[class.dashboard-topbar--editing]="editMode()"
|
||||
>
|
||||
<p class="toolbar-hint">
|
||||
@if (editMode()) {
|
||||
Drag cards by the handle. Resize inline. Use the sidebar for
|
||||
visibility and scope.
|
||||
} @else {
|
||||
Start customize mode to rearrange and resize widgets in place.
|
||||
}
|
||||
</p>
|
||||
<div class="toolbar-actions">
|
||||
@if (editMode()) {
|
||||
@if (editMode()) {
|
||||
<p class="topbar-edit-hint">
|
||||
{{ 'WORKSPACE.DASHBOARD.TOOLBAR_HINT_EDITING' | translate }}
|
||||
</p>
|
||||
<div class="topbar-edit-actions">
|
||||
<button
|
||||
type="button"
|
||||
mat-button
|
||||
(click)="layoutService.reset()"
|
||||
>
|
||||
Reset Layout
|
||||
{{ 'WORKSPACE.DASHBOARD.TOOLBAR_RESET' | translate }}
|
||||
</button>
|
||||
}
|
||||
<button type="button" mat-flat-button (click)="toggleEditMode()">
|
||||
<mat-icon>{{ editMode() ? 'check' : 'tune' }}</mat-icon>
|
||||
{{ editMode() ? 'Done' : 'Customize' }}
|
||||
<button
|
||||
type="button"
|
||||
mat-stroked-button
|
||||
(click)="toggleEditMode()"
|
||||
>
|
||||
<mat-icon>check</mat-icon>
|
||||
{{ 'WORKSPACE.DASHBOARD.TOOLBAR_DONE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="topbar-label">
|
||||
{{ 'WORKSPACE.DASHBOARD.TOPBAR_LABEL' | translate }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
(click)="toggleEditMode()"
|
||||
[matTooltip]="
|
||||
'WORKSPACE.DASHBOARD.TOOLBAR_CUSTOMIZE' | translate
|
||||
"
|
||||
aria-label="Customize dashboard layout"
|
||||
>
|
||||
<mat-icon>tune</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section
|
||||
@@ -80,26 +82,31 @@
|
||||
>
|
||||
<mat-icon>drag_indicator</mat-icon>
|
||||
</button>
|
||||
<p class="widget-inline-editor__title">
|
||||
{{ widget.title }}
|
||||
</p>
|
||||
<mat-button-toggle-group
|
||||
class="inline-size-control"
|
||||
[value]="widget.size"
|
||||
(change)="
|
||||
setWidgetSize(widget.id, $event.value)
|
||||
"
|
||||
aria-label="Widget width"
|
||||
>
|
||||
@for (
|
||||
size of sizeOptions;
|
||||
track size.value
|
||||
) {
|
||||
<mat-button-toggle [value]="size.value">
|
||||
{{ size.label }}
|
||||
</mat-button-toggle>
|
||||
}
|
||||
</mat-button-toggle-group>
|
||||
<div class="size-stepper">
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="size-stepper__btn"
|
||||
[disabled]="isFirstSize(widget.size)"
|
||||
(click)="cycleWidgetSize(widget.id, -1)"
|
||||
aria-label="Make widget narrower"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<span class="size-stepper__label">{{
|
||||
getSizeLabel(widget.size)
|
||||
}}</span>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="size-stepper__btn"
|
||||
[disabled]="isLastSize(widget.size)"
|
||||
(click)="cycleWidgetSize(widget.id, 1)"
|
||||
aria-label="Make widget wider"
|
||||
>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
@@ -115,8 +122,17 @@
|
||||
</article>
|
||||
} @empty {
|
||||
<article class="empty-state">
|
||||
<h3>No visible widgets</h3>
|
||||
<p>Enable at least one widget from the sidebar.</p>
|
||||
<h3>
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.EMPTY_WIDGETS' | translate
|
||||
}}
|
||||
</h3>
|
||||
<p>
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.EMPTY_WIDGETS_HINT'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
</article>
|
||||
}
|
||||
</section>
|
||||
@@ -125,10 +141,17 @@
|
||||
@if (editMode()) {
|
||||
<aside class="widget-library">
|
||||
<section class="layout-editor">
|
||||
<h2>Widget Library</h2>
|
||||
<h2>
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_LIBRARY_TITLE'
|
||||
| translate
|
||||
}}
|
||||
</h2>
|
||||
<p class="layout-editor__hint">
|
||||
Show/hide widgets and configure provider scope without
|
||||
leaving the canvas.
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_LIBRARY_HINT'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
<div class="editor-list">
|
||||
@for (widget of allWidgets(); track widget.id) {
|
||||
@@ -139,7 +162,10 @@
|
||||
</div>
|
||||
<div class="editor-item__actions">
|
||||
<p class="editor-item__action-label">
|
||||
Visibility
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_VISIBILITY'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
<mat-slide-toggle
|
||||
[checked]="widget.enabled"
|
||||
@@ -148,19 +174,16 @@
|
||||
widget.id
|
||||
)
|
||||
"
|
||||
color="primary"
|
||||
>
|
||||
Visible
|
||||
</mat-slide-toggle>
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
@if (supportsScope(widget)) {
|
||||
<mat-expansion-panel class="advanced-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title
|
||||
>Advanced
|
||||
settings</mat-panel-title
|
||||
>
|
||||
<mat-panel-title>{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_ADVANCED'
|
||||
| translate
|
||||
}}</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Scope ({{
|
||||
getScopeProviderSummary(
|
||||
@@ -171,10 +194,16 @@
|
||||
</mat-expansion-panel-header>
|
||||
<section class="scope-editor">
|
||||
<p class="scope-editor__title">
|
||||
Scope
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_SCOPE'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
<p class="scope-editor__subtitle">
|
||||
Provider filters:
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_SCOPE_PROVIDERS'
|
||||
| translate
|
||||
}}
|
||||
{{
|
||||
getScopeProviderSummary(
|
||||
widget
|
||||
@@ -185,8 +214,10 @@
|
||||
<p
|
||||
class="scope-editor__subtitle"
|
||||
>
|
||||
No providers selected means
|
||||
all providers are included.
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_SCOPE_ALL_HINT'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
<mat-chip-listbox
|
||||
@@ -221,9 +252,10 @@
|
||||
</mat-chip-listbox>
|
||||
|
||||
<p class="scope-editor__subtitle">
|
||||
Playlists (leave empty to use
|
||||
all playlists in current
|
||||
provider scope)
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_SCOPE_PLAYLISTS'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
<div class="scope-playlists">
|
||||
@for (
|
||||
@@ -254,8 +286,10 @@
|
||||
</mat-checkbox>
|
||||
} @empty {
|
||||
<p class="scope-empty">
|
||||
No playlists available
|
||||
for selected providers.
|
||||
{{
|
||||
'WORKSPACE.DASHBOARD.WIDGET_SCOPE_EMPTY'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
var(--mat-sys-surface-container-high) 88%
|
||||
);
|
||||
--widget-grid-gap: 12px;
|
||||
--dashboard-widget-height: 530px;
|
||||
--dashboard-widget-height: 400px;
|
||||
}
|
||||
|
||||
.dashboard-page {
|
||||
@@ -24,84 +24,62 @@
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
border: 1px solid var(--dashboard-frame-border);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
radial-gradient(
|
||||
1100px 300px at 105% -40%,
|
||||
color-mix(in srgb, var(--dashboard-accent) 24%, transparent),
|
||||
transparent 58%
|
||||
),
|
||||
linear-gradient(
|
||||
165deg,
|
||||
var(--mat-sys-surface-container-highest) 0%,
|
||||
var(--mat-sys-surface-container-high) 55%,
|
||||
var(--mat-sys-surface-container) 100%
|
||||
);
|
||||
padding: 20px;
|
||||
box-shadow: 0 16px 30px -28px var(--dashboard-frame-glow);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.11em;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 8px 0 10px;
|
||||
font-size: clamp(1.44rem, 2vw, 1.95rem);
|
||||
line-height: 1.14;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
max-width: 780px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dashboard-toolbar {
|
||||
border: 1px solid var(--dashboard-frame-border);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--mat-sys-surface-container-high) 0%,
|
||||
var(--mat-sys-surface-container) 100%
|
||||
);
|
||||
padding: 10px 12px;
|
||||
// ─── Dashboard Topbar ────────────────────────────────────────────────────────
|
||||
.dashboard-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
height: 44px;
|
||||
padding: 0 4px 0 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dashboard-toolbar--editing {
|
||||
.dashboard-topbar--editing {
|
||||
position: sticky;
|
||||
top: 8px;
|
||||
z-index: 20;
|
||||
box-shadow: 0 14px 26px -24px var(--dashboard-frame-glow);
|
||||
padding: 0 8px 0 12px;
|
||||
border: 1px solid var(--dashboard-frame-border);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--mat-sys-surface-container-highest) 0%,
|
||||
var(--mat-sys-surface-container-high) 100%
|
||||
);
|
||||
box-shadow: 0 8px 24px -16px var(--dashboard-frame-glow);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.toolbar-hint {
|
||||
.topbar-label {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
font-size: 0.82rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
.topbar-edit-hint {
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topbar-edit-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -146,12 +124,16 @@ h1 {
|
||||
}
|
||||
|
||||
.layout-editor {
|
||||
padding: 14px;
|
||||
padding: 14px 14px 24px;
|
||||
}
|
||||
|
||||
.layout-editor h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
margin: 0 0 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
}
|
||||
|
||||
.layout-editor__hint {
|
||||
@@ -184,7 +166,9 @@ h1 {
|
||||
|
||||
.editor-item__meta h3 {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mat-sys-on-surface);
|
||||
}
|
||||
|
||||
.editor-item__meta p {
|
||||
@@ -199,7 +183,9 @@ h1 {
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding-top: 4px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid color-mix(in srgb, var(--dashboard-frame-border) 50%, transparent);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.editor-item__action-label {
|
||||
@@ -327,6 +313,8 @@ h1 {
|
||||
border-color 0.2s ease;
|
||||
z-index: 1;
|
||||
height: var(--dashboard-widget-height);
|
||||
// Suppress the inner mat-card outlined border — slot provides the only visible border
|
||||
--mat-card-outline-width: 0;
|
||||
}
|
||||
|
||||
.widget-slot:hover {
|
||||
@@ -336,13 +324,17 @@ h1 {
|
||||
0 18px 28px -20px var(--dashboard-frame-glow);
|
||||
}
|
||||
|
||||
.widget-slot[data-widget-type="source-stats"] {
|
||||
// Content-light widgets: collapse to their natural height instead of the fixed row height
|
||||
.widget-slot[data-widget-type='source-stats'],
|
||||
.widget-slot[data-widget-type='continue-watching'] {
|
||||
height: auto;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.widget-slot--editing {
|
||||
cursor: grab;
|
||||
// Push mat-card content below the floating inline editor bar
|
||||
padding-top: 52px;
|
||||
}
|
||||
|
||||
.widget-slot--editing:active {
|
||||
@@ -374,29 +366,60 @@ h1 {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.widget-inline-editor__title {
|
||||
margin: 0;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 600;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inline-size-control {
|
||||
// ── Size stepper ─────────────────────────────────────────────────────────────
|
||||
|
||||
.size-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin-left: auto;
|
||||
background: var(--mat-sys-surface-container);
|
||||
border: 1px solid var(--mat-sys-outline-variant);
|
||||
border-radius: 8px;
|
||||
background: var(--mat-sys-surface-container);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.size-stepper__btn {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
border-radius: 0 !important;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.size-stepper__label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--mat-sys-on-surface);
|
||||
min-width: 26px;
|
||||
text-align: center;
|
||||
letter-spacing: 0.02em;
|
||||
border-left: 1px solid var(--mat-sys-outline-variant);
|
||||
border-right: 1px solid var(--mat-sys-outline-variant);
|
||||
padding: 0 2px;
|
||||
line-height: 32px;
|
||||
height: 32px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.widget-inline-editor__hide {
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.widget-slot app-dashboard-widget-host {
|
||||
@@ -471,15 +494,10 @@ h1 {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.dashboard-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
.dashboard-topbar--editing {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
.dashboard-editor-shell--editing {
|
||||
@@ -496,15 +514,11 @@ h1 {
|
||||
}
|
||||
|
||||
.widget-inline-editor {
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.widget-inline-editor__title {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.inline-size-control {
|
||||
margin-left: 0;
|
||||
.size-stepper__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.widget-grid {
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { computed, Component, inject, signal } from '@angular/core';
|
||||
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import {
|
||||
CdkDragDrop,
|
||||
DragDropModule,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { selectAllPlaylistsMeta } from 'm3u-state';
|
||||
import { DashboardLayoutService } from 'workspace-dashboard-data-access';
|
||||
import { DashboardWidgetHostComponent } from 'workspace-dashboard-ui';
|
||||
import {
|
||||
DashboardLayoutService,
|
||||
DashboardWidgetConfig,
|
||||
DashboardWidgetProvider,
|
||||
DashboardWidgetSize,
|
||||
createDefaultWidgetScope,
|
||||
} from 'workspace-dashboard-data-access';
|
||||
import { DashboardWidgetHostComponent } from 'workspace-dashboard-ui';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-dashboard',
|
||||
@@ -24,12 +29,13 @@ import {
|
||||
DashboardWidgetHostComponent,
|
||||
DragDropModule,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
MatCheckboxModule,
|
||||
MatChipsModule,
|
||||
MatExpansionModule,
|
||||
MatIcon,
|
||||
MatSlideToggleModule,
|
||||
MatTooltip,
|
||||
TranslatePipe,
|
||||
],
|
||||
templateUrl: './workspace-dashboard.component.html',
|
||||
styleUrl: './workspace-dashboard.component.scss',
|
||||
@@ -44,21 +50,26 @@ export class WorkspaceDashboardComponent {
|
||||
'xtream',
|
||||
'stalker',
|
||||
];
|
||||
readonly sizeOptions: Array<{ value: DashboardWidgetSize; label: string }> = [
|
||||
{ value: 'one-third', label: '1/3' },
|
||||
{ value: 'half', label: '1/2' },
|
||||
{ value: 'two-thirds', label: '2/3' },
|
||||
{ value: 'full', label: 'Full' },
|
||||
private readonly sizeOrder: DashboardWidgetSize[] = [
|
||||
'one-third',
|
||||
'half',
|
||||
'two-thirds',
|
||||
'full',
|
||||
];
|
||||
|
||||
readonly allWidgets = computed(() =>
|
||||
[...this.layoutService.state().widgets].sort((a, b) => a.order - b.order)
|
||||
[...this.layoutService.state().widgets].sort(
|
||||
(a, b) => a.order - b.order
|
||||
)
|
||||
);
|
||||
readonly visibleWidgets = computed(() =>
|
||||
this.allWidgets().filter((widget) => widget.enabled)
|
||||
);
|
||||
|
||||
constructor(readonly layoutService: DashboardLayoutService) {}
|
||||
constructor(readonly layoutService: DashboardLayoutService) {
|
||||
// Data reload is handled reactively by DashboardDataService
|
||||
// via Router NavigationEnd events
|
||||
}
|
||||
|
||||
toggleEditMode(): void {
|
||||
this.editMode.update((value) => !value);
|
||||
@@ -70,7 +81,9 @@ export class WorkspaceDashboardComponent {
|
||||
}
|
||||
const reordered = [...this.visibleWidgets()];
|
||||
moveItemInArray(reordered, event.previousIndex, event.currentIndex);
|
||||
this.layoutService.reorderVisibleWidgets(reordered.map((widget) => widget.id));
|
||||
this.layoutService.reorderVisibleWidgets(
|
||||
reordered.map((widget) => widget.id)
|
||||
);
|
||||
}
|
||||
|
||||
setWidgetSize(widgetId: string, size: unknown): void {
|
||||
@@ -84,6 +97,32 @@ export class WorkspaceDashboardComponent {
|
||||
}
|
||||
}
|
||||
|
||||
cycleWidgetSize(widgetId: string, dir: 1 | -1): void {
|
||||
const widget = this.allWidgets().find((w) => w.id === widgetId);
|
||||
if (!widget) return;
|
||||
const idx = this.sizeOrder.indexOf(widget.size);
|
||||
const next = this.sizeOrder[idx + dir];
|
||||
if (next) this.layoutService.setWidgetSize(widgetId, next);
|
||||
}
|
||||
|
||||
getSizeLabel(size: DashboardWidgetSize): string {
|
||||
const labels: Record<DashboardWidgetSize, string> = {
|
||||
'one-third': '1/3',
|
||||
half: '1/2',
|
||||
'two-thirds': '2/3',
|
||||
full: 'Full',
|
||||
};
|
||||
return labels[size];
|
||||
}
|
||||
|
||||
isFirstSize(size: DashboardWidgetSize): boolean {
|
||||
return this.sizeOrder.indexOf(size) === 0;
|
||||
}
|
||||
|
||||
isLastSize(size: DashboardWidgetSize): boolean {
|
||||
return this.sizeOrder.indexOf(size) === this.sizeOrder.length - 1;
|
||||
}
|
||||
|
||||
hideWidget(widgetId: string): void {
|
||||
this.layoutService.toggleWidget(widgetId);
|
||||
}
|
||||
@@ -103,7 +142,8 @@ export class WorkspaceDashboardComponent {
|
||||
}
|
||||
|
||||
getScopeProviderCount(widget: DashboardWidgetConfig): number {
|
||||
return (widget.settings?.scope ?? createDefaultWidgetScope()).providers.length;
|
||||
return (widget.settings?.scope ?? createDefaultWidgetScope()).providers
|
||||
.length;
|
||||
}
|
||||
|
||||
getScopeProviderSummary(widget: DashboardWidgetConfig): string {
|
||||
@@ -120,27 +160,28 @@ export class WorkspaceDashboardComponent {
|
||||
provider: DashboardWidgetProvider
|
||||
): boolean {
|
||||
return (
|
||||
(widget.settings?.scope ?? createDefaultWidgetScope()).providers.includes(
|
||||
provider
|
||||
)
|
||||
);
|
||||
widget.settings?.scope ?? createDefaultWidgetScope()
|
||||
).providers.includes(provider);
|
||||
}
|
||||
|
||||
getScopePlaylists(widget: DashboardWidgetConfig) {
|
||||
const scope = widget.settings?.scope ?? createDefaultWidgetScope();
|
||||
const scopedProviders =
|
||||
scope.providers.length === 0 ? this.providerOptions : scope.providers;
|
||||
scope.providers.length === 0
|
||||
? this.providerOptions
|
||||
: scope.providers;
|
||||
return this.playlists().filter((playlist) =>
|
||||
scopedProviders.includes(this.getPlaylistProviderType(playlist))
|
||||
);
|
||||
}
|
||||
|
||||
isPlaylistSelected(widget: DashboardWidgetConfig, playlistId: string): boolean {
|
||||
isPlaylistSelected(
|
||||
widget: DashboardWidgetConfig,
|
||||
playlistId: string
|
||||
): boolean {
|
||||
return (
|
||||
(widget.settings?.scope ?? createDefaultWidgetScope()).playlistIds.includes(
|
||||
playlistId
|
||||
)
|
||||
);
|
||||
widget.settings?.scope ?? createDefaultWidgetScope()
|
||||
).playlistIds.includes(playlistId);
|
||||
}
|
||||
|
||||
toggleScopeProvider(
|
||||
|
||||
Reference in new issue
Block a user