From 20485972c35b574e87b6933191d06e924c55ba0f Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 2 Mar 2026 22:30:16 +0100 Subject: [PATCH] feat: refactor dashboard widgets, and remove the home component. --- .../src/lib/dashboard-data.service.ts | 97 +++++++++++- .../src/lib/dashboard-widget.model.ts | 6 +- .../lib/workspace-dashboard.component.html | 146 ++++++++++++++---- .../lib/workspace-dashboard.component.scss | 5 + .../continue-watching-widget.component.html | 17 +- .../continue-watching-widget.component.scss | 37 ++++- .../dashboard-activity-items.component.html | 22 ++- .../dashboard-activity-items.component.scss | 82 +++++++++- .../dashboard-activity-items.component.ts | 16 +- .../global-favorites-widget.component.html | 5 + .../global-favorites-widget.component.ts | 22 +++ .../recent-sources-widget.component.html | 12 +- .../recent-sources-widget.component.scss | 48 ++++-- .../recent-sources-widget.component.ts | 10 +- .../recently-watched-widget.component.html | 5 + .../recently-watched-widget.component.ts | 22 +++ .../source-stats-widget.component.html | 28 +++- .../source-stats-widget.component.scss | 54 ++++++- .../widgets/source-stats-widget.component.ts | 3 +- 19 files changed, 562 insertions(+), 75 deletions(-) diff --git a/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts b/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts index 677707bcd..33db2fbf8 100644 --- a/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts +++ b/libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts @@ -1,6 +1,10 @@ import { Injectable, computed, inject, signal } from '@angular/core'; import { Store } from '@ngrx/store'; -import { selectActivePlaylist, selectAllPlaylistsMeta } from 'm3u-state'; +import { + PlaylistActions, + selectActivePlaylist, + selectAllPlaylistsMeta, +} from 'm3u-state'; import { DatabaseService, GlobalFavoriteItem as DbGlobalFavoriteItem, @@ -17,7 +21,7 @@ type RecentActivityType = 'live' | 'movie' | 'series'; export type DashboardContentKind = 'all' | 'channels' | 'vod' | 'series'; -interface GlobalRecentItem { +export interface GlobalRecentItem { id: string | number; title: string; type: RecentActivityType; @@ -259,6 +263,51 @@ export class DashboardDataService { return this.getRecentItemNavigation(item).state; } + async removeGlobalRecentItem(item: GlobalRecentItem): Promise { + if (item.source === 'xtream') { + await this.dbService.removeRecentItem( + item.id as number, + item.playlist_id + ); + await this.reloadGlobalRecentItems(); + return; + } + + if (item.source === 'stalker') { + const playlist = this.playlists().find( + (p) => p._id === item.playlist_id + ); + if (!playlist) return; + + const currentRecent = Array.isArray( + (playlist as any).recentlyViewed + ) + ? [...(playlist as any).recentlyViewed] + : []; + + const itemMatchStr = String(item.id); + const filteredRecent = currentRecent.filter((raw, index) => { + const rawId = String( + (raw as any)?.id ?? + (raw as any)?.stream_id ?? + (raw as any)?.series_id ?? + (raw as any)?.movie_id ?? + `${playlist._id}-${index}` + ); + return rawId !== itemMatchStr && rawId !== ''; + }); + + this.store.dispatch( + PlaylistActions.updatePlaylistMeta({ + playlist: { + _id: item.playlist_id, + recentlyViewed: filteredRecent, + } as unknown as PlaylistMeta, + }) as any + ); + } + } + getFavoriteItemProviderLabel(item: DashboardFavoriteItem): string { if (item.source === 'stalker') { return 'Stalker'; @@ -283,6 +332,50 @@ export class DashboardDataService { return this.getGlobalFavoriteNavigation(item).state; } + async removeGlobalFavorite(item: DashboardFavoriteItem): Promise { + if (item.source === 'xtream') { + await this.dbService.removeFromFavorites( + item.id as number, + item.playlist_id + ); + await this.reloadGlobalFavorites(); + return; + } + + if (item.source === 'stalker') { + const playlist = this.playlists().find( + (p) => p._id === item.playlist_id + ); + if (!playlist) return; + + const currentFavorites = Array.isArray(playlist.favorites) + ? [...playlist.favorites] + : []; + const itemMatchStr = String(item.id); + + // Stalker favorite objects are usually stored entirely in the favorites array + const filteredFavorites = currentFavorites.filter((raw, index) => { + const rawId = String( + (raw as any)?.id ?? + (raw as any)?.stream_id ?? + (raw as any)?.series_id ?? + (raw as any)?.movie_id ?? + `${playlist._id}-${index}` + ); + return rawId !== itemMatchStr && rawId !== ''; + }); + + this.store.dispatch( + PlaylistActions.updatePlaylistMeta({ + playlist: { + _id: item.playlist_id, + favorites: filteredFavorites, + } as unknown as PlaylistMeta, + }) as any + ); + } + } + formatTimestamp(value?: string | number): string { const timestamp = this.toTimestamp(value); if (!timestamp) { diff --git a/libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts b/libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts index ca4db4cde..4f1dba321 100644 --- a/libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts +++ b/libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts @@ -33,7 +33,7 @@ export interface DashboardLayoutState { widgets: DashboardWidgetConfig[]; } -export const DASHBOARD_LAYOUT_VERSION = 5; +export const DASHBOARD_LAYOUT_VERSION = 6; export const DASHBOARD_WIDGET_SIZE_OPTIONS: DashboardWidgetSize[] = [ 'one-third', 'half', @@ -90,7 +90,7 @@ export const DEFAULT_DASHBOARD_WIDGETS: DashboardWidgetConfig[] = [ type: 'recently-watched', title: 'Recently Watched', description: 'Global watch history across channels, VOD and series.', - size: 'one-third', + size: 'two-thirds', enabled: true, order: 3, settings: { @@ -102,7 +102,7 @@ export const DEFAULT_DASHBOARD_WIDGETS: DashboardWidgetConfig[] = [ type: 'global-favorites', title: 'Global Favorites', description: 'Pinned items across all connected providers.', - size: 'two-thirds', + size: 'one-third', enabled: true, order: 4, settings: { diff --git a/libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.html b/libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.html index 69ae22158..809fcaed3 100644 --- a/libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.html +++ b/libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.html @@ -4,23 +4,32 @@

Workspace Dashboard

Control Center

- Keep your daily sources in one place. In customize mode, edit directly - on the canvas, then use the sidebar for visibility and scope. + Keep your daily sources in one place. In customize mode, edit + directly on the canvas, then use the sidebar for visibility and + scope.

-
+

@if (editMode()) { - Drag cards by the handle. Resize inline. Use the sidebar for visibility and scope. + 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. }

@if (editMode()) { - } @@ -31,7 +40,10 @@
-
+
@@ -63,14 +80,21 @@ > drag_indicator -

{{ widget.title }}

+

+ {{ widget.title }} +

- @for (size of sizeOptions; track size.value) { + @for ( + size of sizeOptions; + track size.value + ) { {{ size.label }} @@ -103,7 +127,8 @@

Widget Library

- Show/hide widgets and configure provider scope without leaving the canvas. + Show/hide widgets and configure provider scope without + leaving the canvas.

@for (widget of allWidgets(); track widget.id) { @@ -113,10 +138,16 @@

{{ widget.description }}

-

Visibility

+

+ Visibility +

Visible @@ -126,52 +157,105 @@ @if (supportsScope(widget)) { - Advanced settings + Advanced + settings - Scope ({{ getScopeProviderSummary(widget) }}) + Scope ({{ + getScopeProviderSummary( + widget + ) + }})
-

Scope

+

+ Scope +

- Provider filters: {{ getScopeProviderSummary(widget) }} + Provider filters: + {{ + getScopeProviderSummary( + widget + ) + }}

@if (isAllProvidersScope(widget)) { -

- No providers selected means all providers are included. +

+ No providers selected means + all providers are included.

} - - @for (provider of providerOptions; track provider) { + + @for ( + provider of providerOptions; + track provider + ) { - {{ getProviderLabel(provider) }} + {{ + getProviderLabel( + provider + ) + }} }

- Playlists (leave empty to use all playlists in current provider scope) + Playlists (leave empty to use + all playlists in current + provider scope)

- @for (playlist of getScopePlaylists(widget); track playlist._id) { + @for ( + playlist of getScopePlaylists( + widget + ); + track playlist._id + ) { - {{ getPlaylistTitle(playlist) }} + {{ + getPlaylistTitle( + playlist + ) + }} } @empty {

- No playlists available for selected providers. + No playlists available + for selected providers.

}
diff --git a/libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.scss b/libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.scss index e4f71dc78..a9ab767f5 100644 --- a/libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.scss +++ b/libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.scss @@ -336,6 +336,11 @@ h1 { 0 18px 28px -20px var(--dashboard-frame-glow); } +.widget-slot[data-widget-type="source-stats"] { + height: auto; + align-self: start; +} + .widget-slot--editing { cursor: grab; } diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/continue-watching-widget.component.html b/libs/workspace/dashboard/ui/src/lib/widgets/continue-watching-widget.component.html index 85b7acf96..1663fe5dd 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/continue-watching-widget.component.html +++ b/libs/workspace/dashboard/ui/src/lib/widgets/continue-watching-widget.component.html @@ -34,7 +34,22 @@ } @else {
-

{{ 'WORKSPACE.DASHBOARD.NO_ACTIVE_SOURCE' | translate }}

+
+ play_circle_outline +
+

+ {{ 'WORKSPACE.DASHBOARD.NO_ACTIVE_SOURCE' | translate }} +

+

+ {{ + 'WORKSPACE.DASHBOARD.NO_ACTIVE_SOURCE_HINT' + | translate + }} +

+
+
add {{ 'WORKSPACE.DASHBOARD.ADD_SOURCE' | translate }} diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/continue-watching-widget.component.scss b/libs/workspace/dashboard/ui/src/lib/widgets/continue-watching-widget.component.scss index d6ec59c58..869b1ae83 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/continue-watching-widget.component.scss +++ b/libs/workspace/dashboard/ui/src/lib/widgets/continue-watching-widget.component.scss @@ -39,16 +39,47 @@ border: 1px dashed var(--mat-sys-outline); background: var(--mat-sys-surface-container-low); border-radius: 12px; - padding: 12px; + padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; - gap: 12px; + gap: 16px; + flex-wrap: wrap; } -.focus-empty p { +.focus-empty__content { + display: flex; + align-items: center; + gap: 16px; +} + +.focus-empty__icon { + font-size: 32px; + width: 32px; + height: 32px; + color: var(--mat-sys-outline); + opacity: 0.7; +} + +.focus-empty__text { + display: flex; + flex-direction: column; + gap: 4px; +} + +.focus-empty__title { margin: 0; + font-weight: 500; + font-size: 0.9rem; + color: var(--mat-sys-on-surface); +} + +.focus-empty__hint { + margin: 0; + font-size: 0.75rem; color: var(--mat-sys-on-surface-variant); + max-width: 320px; + line-height: 1.4; } .quick-list { diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.html b/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.html index 8aed4a658..a8d1a868f 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.html +++ b/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.html @@ -51,9 +51,29 @@ {{ item.subtitle }} + + @if (showRemoveAction()) { + + } }
} @else { -

{{ emptyLabel() }}

+
+ @if (emptyIcon()) { + {{ emptyIcon() }} + } +

{{ emptyLabel() }}

+ @if (emptyHint()) { +

{{ emptyHint() }}

+ } +
} diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.scss b/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.scss index b3c75a8c9..0d130e481 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.scss +++ b/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.scss @@ -51,18 +51,52 @@ ); padding: 8px; display: grid; - grid-template-columns: 44px minmax(0, 1fr); + grid-template-columns: 44px minmax(0, 1fr) auto; align-items: center; gap: 10px; + transition: transform 0.2s ease, border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; +} + +.activity-item__action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + border: none; + background: transparent; + color: var(--mat-sys-on-surface-variant); + cursor: pointer; + opacity: 0; + transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} + +.activity-item:hover .activity-item__action-btn { + opacity: 0.6; +} + +.activity-item__action-btn:hover { + opacity: 1 !important; + background: var(--mat-sys-surface-container-highest); + color: var(--mat-sys-on-surface); } .activity-item:hover { - border-color: color-mix(in srgb, var(--mat-sys-outline) 72%, var(--mat-sys-primary) 28%); + transform: translateY(-2px); + border-color: color-mix(in srgb, var(--mat-sys-outline) 60%, var(--mat-sys-primary) 40%); background: linear-gradient( 180deg, var(--mat-sys-surface-container-high) 0%, - var(--mat-sys-surface-container) 100% + var(--mat-sys-surface-container-low) 100% ); + box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.15); } .activity-item__media { @@ -184,10 +218,46 @@ font-size: 0.68rem; } -.empty { - margin: 0; +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 36px 16px; + text-align: center; + border: 1px dashed var(--mat-sys-outline-variant); + border-radius: 12px; + margin: 8px 0; color: var(--mat-sys-on-surface-variant); - font-size: 0.82rem; + flex: 1; + + &.empty-state--grid { + min-height: 180px; + } +} + +.empty-state__icon { + font-size: 36px; + width: 36px; + height: 36px; + margin-bottom: 12px; + color: var(--mat-sys-outline); + opacity: 0.7; +} + +.empty-state__title { + margin: 0; + font-size: 0.875rem; + font-weight: 500; + color: var(--mat-sys-on-surface); +} + +.empty-state__hint { + margin: 6px 0 0; + font-size: 0.78rem; + color: var(--mat-sys-on-surface-variant); + max-width: 280px; + line-height: 1.45; } @media (max-width: 920px) { diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.ts b/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.ts index c60bf71e6..7c96b6250 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.ts +++ b/libs/workspace/dashboard/ui/src/lib/widgets/dashboard-activity-items.component.ts @@ -25,7 +25,14 @@ export class DashboardActivityItemsComponent { readonly items = input.required(); readonly emptyLabel = input('No items to show.'); + readonly emptyIcon = input(); + readonly emptyHint = input(); readonly viewMode = input('list'); + readonly showRemoveAction = input(false); + readonly removeActionLabel = input('Remove item'); + + @Output() readonly itemAction = + new EventEmitter(); readonly failedImages = signal>({}); @@ -68,9 +75,16 @@ export class DashboardActivityItemsComponent { return; } - const remaining = target.scrollHeight - target.scrollTop - target.clientHeight; + const remaining = + target.scrollHeight - target.scrollTop - target.clientHeight; if (remaining <= 120) { this.scrolledToEnd.emit(); } } + + onActionClick(event: Event, item: DashboardActivityItemViewModel): void { + event.preventDefault(); + event.stopPropagation(); + this.itemAction.emit(item); + } } diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/global-favorites-widget.component.html b/libs/workspace/dashboard/ui/src/lib/widgets/global-favorites-widget.component.html index 616daa189..6ab428512 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/global-favorites-widget.component.html +++ b/libs/workspace/dashboard/ui/src/lib/widgets/global-favorites-widget.component.html @@ -79,7 +79,12 @@ diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/global-favorites-widget.component.ts b/libs/workspace/dashboard/ui/src/lib/widgets/global-favorites-widget.component.ts index d5203fbb2..65cd2def3 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/global-favorites-widget.component.ts +++ b/libs/workspace/dashboard/ui/src/lib/widgets/global-favorites-widget.component.ts @@ -117,6 +117,28 @@ export class GlobalFavoritesWidgetComponent { this.visibleItemLimit.update((value) => value + PAGE_SIZE); } + onRemoveItem(item: DashboardActivityItemViewModel): void { + const globalItem = this.data + .globalFavoriteItems() + .find((i) => `${i.id}-${i.playlist_id}-${i.added_at}` === item.id); + + if (globalItem) { + void this.data.removeGlobalFavorite(globalItem); + } else { + // Fallback strategy + const fallbackItem = this.data + .globalFavoriteItems() + .find( + (i) => + item.link.includes(String(i.xtream_id)) || + item.link.includes(i.playlist_id) + ); + if (fallbackItem) { + void this.data.removeGlobalFavorite(fallbackItem); + } + } + } + private setKind(value: unknown): void { if ( value === 'all' || diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.html b/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.html index 763b9ef40..5e5ceb506 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.html +++ b/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.html @@ -48,8 +48,16 @@ } @else { diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.scss b/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.scss index d5109e956..777f75914 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.scss +++ b/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.scss @@ -24,11 +24,14 @@ text-decoration: none; color: inherit; min-width: 0; + transition: transform 0.2s ease, border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; } .source-row:hover { - border-color: var(--mat-sys-outline); + transform: translateY(-2px); + border-color: color-mix(in srgb, var(--mat-sys-outline) 60%, var(--mat-sys-primary) 40%); background: var(--mat-sys-surface-container-high); + box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.15); } .source-row__head { @@ -70,18 +73,43 @@ border: 1px dashed var(--mat-sys-outline); border-radius: 12px; background: var(--mat-sys-surface-container-low); - padding: 14px; + padding: 24px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 16px; + font-size: 0.82rem; } -.empty-state p { - margin: 0 0 8px; +.empty-state__icon { + font-size: 32px; + width: 32px; + height: 32px; + color: var(--mat-sys-outline); + opacity: 0.7; +} + +.empty-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.empty-title { + margin: 0; + font-size: 0.875rem; + font-weight: 500; + color: var(--mat-sys-on-surface); +} + +.empty-hint { + margin: 0; + font-size: 0.75rem; color: var(--mat-sys-on-surface-variant); -} - -.empty-state a { - color: var(--mat-sys-primary); - text-decoration: none; - font-weight: 600; + max-width: 240px; + line-height: 1.4; } @media (max-width: 920px) { diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.ts b/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.ts index c11b5b368..987865560 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.ts +++ b/libs/workspace/dashboard/ui/src/lib/widgets/recent-sources-widget.component.ts @@ -1,4 +1,6 @@ import { Component, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; import { RouterLink } from '@angular/router'; import { TranslatePipe } from '@ngx-translate/core'; import { DashboardDataService } from 'workspace-dashboard-data-access'; @@ -6,7 +8,13 @@ import { DashboardWidgetShellComponent } from './dashboard-widget-shell.componen @Component({ selector: 'app-recent-sources-widget', - imports: [RouterLink, DashboardWidgetShellComponent, TranslatePipe], + imports: [ + MatButtonModule, + MatIcon, + RouterLink, + DashboardWidgetShellComponent, + TranslatePipe, + ], templateUrl: './recent-sources-widget.component.html', styleUrl: './recent-sources-widget.component.scss', }) diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/recently-watched-widget.component.html b/libs/workspace/dashboard/ui/src/lib/widgets/recently-watched-widget.component.html index c9fd5f005..752fd86bd 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/recently-watched-widget.component.html +++ b/libs/workspace/dashboard/ui/src/lib/widgets/recently-watched-widget.component.html @@ -79,7 +79,12 @@ diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/recently-watched-widget.component.ts b/libs/workspace/dashboard/ui/src/lib/widgets/recently-watched-widget.component.ts index 71edb2e21..cd045951b 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/recently-watched-widget.component.ts +++ b/libs/workspace/dashboard/ui/src/lib/widgets/recently-watched-widget.component.ts @@ -116,6 +116,28 @@ export class RecentlyWatchedWidgetComponent { this.visibleItemLimit.update((value) => value + PAGE_SIZE); } + onRemoveItem(item: DashboardActivityItemViewModel): void { + const globalItem = this.data + .globalRecentItems() + .find((i) => `${i.id}-${i.playlist_id}-${i.viewed_at}` === item.id); + + if (globalItem) { + void this.data.removeGlobalRecentItem(globalItem); + } else { + // Fallback strategy if custom ID mapping changed + const fallbackItem = this.data + .globalRecentItems() + .find( + (i) => + item.link.includes(String(i.xtream_id)) || + item.link.includes(i.playlist_id) + ); + if (fallbackItem) { + void this.data.removeGlobalRecentItem(fallbackItem); + } + } + } + private setKind(value: unknown): void { if ( value === 'all' || diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.html b/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.html index 23300cab8..a5126ab8b 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.html +++ b/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.html @@ -3,20 +3,38 @@ [title]="'WORKSPACE.DASHBOARD.SOURCE_STATS' | translate" >
-
-

{{ 'WORKSPACE.DASHBOARD.TOTAL' | translate }}

+
+
+ dns +

+ {{ 'WORKSPACE.DASHBOARD.TOTAL' | translate }} +

+

{{ data.stats().total }}

-

{{ 'WORKSPACE.DASHBOARD.XTREAM' | translate }}

+
+ tv +

+ {{ 'WORKSPACE.DASHBOARD.XTREAM' | translate }} +

+

{{ data.stats().xtream }}

-

{{ 'WORKSPACE.DASHBOARD.STALKER' | translate }}

+
+ router +

+ {{ 'WORKSPACE.DASHBOARD.STALKER' | translate }} +

+

{{ data.stats().stalker }}

-

{{ 'WORKSPACE.DASHBOARD.M3U' | translate }}

+
+ list_alt +

{{ 'WORKSPACE.DASHBOARD.M3U' | translate }}

+

{{ data.stats().m3u }}

diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.scss b/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.scss index 2cd569edf..26b62c0b5 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.scss +++ b/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.scss @@ -1,26 +1,64 @@ .stats-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; + display: flex; + flex-wrap: wrap; + gap: 12px; } .stat-tile { + flex: 1 1 120px; border-radius: 12px; border: 1px solid var(--mat-sys-outline-variant); background: var(--mat-sys-surface-container-low); - padding: 10px; + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + + &--total { + background: linear-gradient(135deg, var(--mat-sys-primary-container) 0%, transparent 100%); + border-color: var(--mat-sys-primary); + + .stat-tile__header mat-icon, + .label { + color: var(--mat-sys-on-primary-container); + } + + .value { + color: var(--mat-sys-on-primary-container); + } + } +} + +.stat-tile__header { + display: flex; + align-items: center; + gap: 6px; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: var(--mat-sys-on-surface-variant); + } } .label { margin: 0; color: var(--mat-sys-on-surface-variant); - font-size: 0.76rem; + font-size: 0.72rem; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.08em; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .value { - margin: 6px 0 0; - font-size: 1.25rem; + margin: 0; + font-size: 1.5rem; font-weight: 700; + color: var(--mat-sys-on-surface); + line-height: 1; } diff --git a/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.ts b/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.ts index 98a7fbadb..bf777aa6c 100644 --- a/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.ts +++ b/libs/workspace/dashboard/ui/src/lib/widgets/source-stats-widget.component.ts @@ -1,11 +1,12 @@ import { Component, inject } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; import { TranslatePipe } from '@ngx-translate/core'; import { DashboardDataService } from 'workspace-dashboard-data-access'; import { DashboardWidgetShellComponent } from './dashboard-widget-shell.component'; @Component({ selector: 'app-source-stats-widget', - imports: [DashboardWidgetShellComponent, TranslatePipe], + imports: [DashboardWidgetShellComponent, TranslatePipe, MatIcon], templateUrl: './source-stats-widget.component.html', styleUrl: './source-stats-widget.component.scss', })