mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
style: enhance workspace sources with sorting functionality and UI improvements
Entire-Checkpoint: c6e522b4276c
This commit is contained in:
1 parent
d90a0a8f89
commit
b1c72a7782
6 files changed
+145
-89
No files matched your search
+4
-25
@@ -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>
|
||||
|
||||
+1
-1
@@ -98,5 +98,5 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
opacity: 0.6;
|
||||
opacity: 0.85;
|
||||
}
|
||||
-59
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
+30
-2
@@ -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
|
||||
|
||||
+28
@@ -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;
|
||||
|
||||
+82
-2
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user