style: enhance workspace sources with sorting functionality and UI improvements

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-14 17:04:30 +02:00
1 parent d90a0a8f89
commit b1c72a7782
6 files changed
+145 -89

No files matched your search

@@ -14,11 +14,11 @@
{{ 'WORKSPACE.SOURCES.TYPE' | translate }}
</h3>
@for (option of typeOptions; track option.id) {
<a
href="#type-{{ option.id }}"
<button
type="button"
class="nav-item category-item option-row"
[class.selected]="isTypeActive(option.id)"
(click)="$event.preventDefault(); selectType(option.id)"
(click)="selectType(option.id)"
>
<mat-icon>{{ option.icon }}</mat-icon>
<span class="nav-item-label option-label">
@@ -31,28 +31,7 @@
<div class="nav-item-meta option-meta">
{{ getTypeCount(option.id) }}
</div>
</a>
}
</div>
</section>
<section class="panel-section">
<div class="nav-list option-list">
<h3 class="panel-section-title">
{{ 'HOME.SORT_BY' | translate }}
</h3>
@for (option of sortOptions; track option.translationKey) {
<a
href="#sort-{{ option.translationKey }}"
class="nav-item category-item option-row"
[class.selected]="isSortActive(option)"
(click)="$event.preventDefault(); setSortOption(option)"
>
<mat-icon>{{ option.icon }}</mat-icon>
<span class="nav-item-label option-label">{{
option.translationKey | translate
}}</span>
</a>
</button>
}
</div>
</section>
@@ -98,5 +98,5 @@
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--mat-sys-on-surface-variant);
opacity: 0.6;
opacity: 0.85;
}
@@ -1,5 +1,4 @@
import { computed, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatIcon } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { Store } from '@ngrx/store';
@@ -9,7 +8,6 @@ import {
selectAllPlaylistsMeta,
} from 'm3u-state';
import { TranslatePipe } from '@ngx-translate/core';
import { SortBy, SortOrder, SortService } from 'services';
type PlaylistFilterId = 'all' | 'm3u' | 'xtream' | 'stalker';
@@ -20,13 +18,6 @@ interface PlaylistFilterOption {
translationKey?: string;
}
interface SortOption {
by: SortBy;
order: SortOrder;
icon: string;
translationKey: string;
}
const ALL_FILTERS = ['m3u', 'xtream', 'stalker'];
@Component({
@@ -37,17 +28,12 @@ const ALL_FILTERS = ['m3u', 'xtream', 'stalker'];
})
export class WorkspaceSourcesFiltersPanelComponent {
private readonly store = inject(Store);
private readonly sortService = inject(SortService);
private readonly activeTypeFilters = this.store.selectSignal(
selectActiveTypeFilters
);
private readonly playlists = this.store.selectSignal(selectAllPlaylistsMeta);
readonly currentSortOptions = toSignal(this.sortService.getSortOptions(), {
requireSync: true,
});
readonly typeOptions: PlaylistFilterOption[] = [
{
id: 'all',
@@ -71,39 +57,6 @@ export class WorkspaceSourcesFiltersPanelComponent {
},
];
readonly sortOptions: SortOption[] = [
{
by: SortBy.DATE_ADDED,
order: SortOrder.DESC,
icon: 'schedule',
translationKey: 'HOME.SORT_OPTIONS.NEWEST',
},
{
by: SortBy.DATE_ADDED,
order: SortOrder.ASC,
icon: 'history',
translationKey: 'HOME.SORT_OPTIONS.OLDEST',
},
{
by: SortBy.NAME,
order: SortOrder.ASC,
icon: 'sort_by_alpha',
translationKey: 'HOME.SORT_OPTIONS.NAME_ASC',
},
{
by: SortBy.NAME,
order: SortOrder.DESC,
icon: 'sort_by_alpha',
translationKey: 'HOME.SORT_OPTIONS.NAME_DESC',
},
{
by: SortBy.CUSTOM,
order: SortOrder.ASC,
icon: 'drag_indicator',
translationKey: 'HOME.SORT_OPTIONS.CUSTOM_ORDER',
},
];
readonly typeCounts = computed(() => {
const items = this.playlists();
return {
@@ -144,16 +97,4 @@ export class WorkspaceSourcesFiltersPanelComponent {
}
return counts[filterId];
}
isSortActive(option: SortOption): boolean {
const current = this.currentSortOptions();
return current.by === option.by && current.order === option.order;
}
setSortOption(option: SortOption): void {
this.sortService.setSortOptions({
by: option.by,
order: option.order,
});
}
}
@@ -1,7 +1,35 @@
<section class="sources-page">
<header class="sources-header">
<h2 class="sources-title">{{ title() }}</h2>
<p class="sources-subtitle">{{ subtitle() }}</p>
<div class="sources-header__text">
<h2 class="sources-title">{{ title() }}</h2>
<p class="sources-subtitle">{{ subtitle() }}</p>
</div>
<button
type="button"
mat-stroked-button
class="sort-trigger"
[matMenuTriggerFor]="sortMenu"
>
<mat-icon>sort</mat-icon>
<span class="sort-trigger__label">{{ activeSortLabel() }}</span>
<mat-icon class="sort-trigger__caret">arrow_drop_down</mat-icon>
</button>
<mat-menu #sortMenu="matMenu" xPosition="before">
@for (option of sortOptions; track option.translationKey) {
<button
mat-menu-item
type="button"
[class.active-option]="isSortActive(option)"
(click)="setSortOption(option)"
>
<mat-icon>{{ option.icon }}</mat-icon>
<span>{{ option.translationKey | translate }}</span>
@if (isSortActive(option)) {
<mat-icon class="sort-menu__check">check</mat-icon>
}
</button>
}
</mat-menu>
</header>
<div class="sources-content">
<app-recent-playlists
@@ -18,6 +18,15 @@
top: 0;
z-index: 2;
padding: 20px 20px 8px;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.sources-header__text {
min-width: 0;
flex: 1;
}
.sources-title {
@@ -33,6 +42,25 @@
color: var(--mat-sys-on-surface-variant, rgba(255, 255, 255, 0.6));
}
.sort-trigger {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 4px;
.sort-trigger__label {
font-size: 0.85rem;
font-weight: 500;
}
.sort-trigger__caret {
font-size: 18px;
width: 18px;
height: 18px;
margin-left: -2px;
}
}
.sources-content {
flex: 1;
min-height: 0;
@@ -1,16 +1,33 @@
import { Component, computed, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { RecentPlaylistsComponent } from '@iptvnator/playlist/shared/ui';
import { TranslateService } from '@ngx-translate/core';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { selectActiveTypeFilters, selectAllPlaylistsMeta } from 'm3u-state';
import { map, startWith } from 'rxjs';
import { SortBy, SortOrder, SortService } from 'services';
import { WORKSPACE_SHELL_ACTIONS } from '@iptvnator/workspace/shell/util';
interface SortOption {
by: SortBy;
order: SortOrder;
icon: string;
translationKey: string;
}
@Component({
selector: 'app-workspace-sources',
imports: [RecentPlaylistsComponent],
imports: [
MatButtonModule,
MatIconModule,
MatMenuModule,
RecentPlaylistsComponent,
TranslatePipe,
],
templateUrl: './workspace-sources.component.html',
styleUrl: './workspace-sources.component.scss',
})
@@ -19,6 +36,7 @@ export class WorkspaceSourcesComponent {
private readonly store = inject(Store);
private readonly workspaceActions = inject(WORKSPACE_SHELL_ACTIONS);
private readonly translate = inject(TranslateService);
private readonly sortService = inject(SortService);
private readonly activeTypeFilters = this.store.selectSignal(
selectActiveTypeFilters
@@ -31,6 +49,44 @@ export class WorkspaceSourcesComponent {
{ initialValue: null }
);
readonly sortOptions: SortOption[] = [
{
by: SortBy.DATE_ADDED,
order: SortOrder.DESC,
icon: 'schedule',
translationKey: 'HOME.SORT_OPTIONS.NEWEST',
},
{
by: SortBy.DATE_ADDED,
order: SortOrder.ASC,
icon: 'history',
translationKey: 'HOME.SORT_OPTIONS.OLDEST',
},
{
by: SortBy.NAME,
order: SortOrder.ASC,
icon: 'sort_by_alpha',
translationKey: 'HOME.SORT_OPTIONS.NAME_ASC',
},
{
by: SortBy.NAME,
order: SortOrder.DESC,
icon: 'sort_by_alpha',
translationKey: 'HOME.SORT_OPTIONS.NAME_DESC',
},
{
by: SortBy.CUSTOM,
order: SortOrder.ASC,
icon: 'drag_indicator',
translationKey: 'HOME.SORT_OPTIONS.CUSTOM_ORDER',
},
];
private readonly currentSortOptions = toSignal(
this.sortService.getSortOptions(),
{ requireSync: true }
);
readonly searchQuery = toSignal(
this.route.queryParamMap.pipe(map((params) => params.get('q') ?? '')),
{ initialValue: '' }
@@ -97,10 +153,34 @@ export class WorkspaceSourcesComponent {
});
});
readonly activeSortLabel = computed(() => {
this.languageTick();
const current = this.currentSortOptions();
const match = this.sortOptions.find(
(option) =>
option.by === current.by && option.order === current.order
);
return this.translateText(
match?.translationKey ?? 'HOME.SORT_OPTIONS.NEWEST'
);
});
onAddPlaylist(): void {
this.workspaceActions.openAddPlaylistDialog();
}
isSortActive(option: SortOption): boolean {
const current = this.currentSortOptions();
return current.by === option.by && current.order === option.order;
}
setSortOption(option: SortOption): void {
this.sortService.setSortOptions({
by: option.by,
order: option.order,
});
}
private translateText(
key: string,
params?: Record<string, string | number>