mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(sort): implement sorting functionality for playlists with user-defined options
This commit is contained in:
1 parent
6b9e021d74
commit
996cb30f4c
7 files changed
+256
-28
No files matched your search
@@ -28,6 +28,7 @@ import { IpcCommand } from '../../../../shared/ipc-command.class';
|
||||
import { Playlist } from '../../../../shared/playlist.interface';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { DatabaseService } from '../../services/database.service';
|
||||
import { SortService } from '../../services/sort.service';
|
||||
import * as PlaylistActions from '../../state/actions';
|
||||
import {
|
||||
selectActiveTypeFilters,
|
||||
@@ -73,11 +74,11 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
playlists$ = combineLatest([
|
||||
this.store.select(selectAllPlaylistsMeta),
|
||||
this.searchQuery,
|
||||
// eslint-disable-next-line @ngrx/avoid-combining-selectors
|
||||
this.store.select(selectActiveTypeFilters),
|
||||
this.sortService.getSortOptions(),
|
||||
]).pipe(
|
||||
map(([playlists, searchQuery, filters]) =>
|
||||
playlists
|
||||
map(([playlists, searchQuery, filters, sortOptions]) => {
|
||||
const filteredPlaylists = playlists
|
||||
.filter((item) => {
|
||||
const isStalkerFilter =
|
||||
item.macAddress && filters.includes('stalker');
|
||||
@@ -101,9 +102,14 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
})
|
||||
.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
.sort((a, b) => a.position - b.position)
|
||||
)
|
||||
);
|
||||
|
||||
// Apply sorting using the SortService
|
||||
return this.sortService.sortPlaylists(
|
||||
filteredPlaylists,
|
||||
sortOptions
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
allPlaylistsLoaded = this.store.selectSignal(selectPlaylistsLoadingFlag);
|
||||
@@ -138,15 +144,16 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
];
|
||||
|
||||
constructor(
|
||||
private databaseService: DatabaseService,
|
||||
private dialog: MatDialog,
|
||||
private dialogService: DialogService,
|
||||
private electronService: DataService,
|
||||
private ngZone: NgZone,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private readonly databaseService: DatabaseService,
|
||||
private readonly dialog: MatDialog,
|
||||
private readonly dialogService: DialogService,
|
||||
private readonly electronService: DataService,
|
||||
private readonly ngZone: NgZone,
|
||||
private readonly router: Router,
|
||||
private readonly snackBar: MatSnackBar,
|
||||
private readonly sortService: SortService,
|
||||
private readonly store: Store,
|
||||
private translate: TranslateService
|
||||
private readonly translate: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -37,9 +37,6 @@ export class PlaylistsService {
|
||||
data.map(({ playlist, items, header, ...rest }) => ({
|
||||
...rest,
|
||||
}))
|
||||
),
|
||||
map((playlists) =>
|
||||
playlists.sort((a, b) => a.position - b.position)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
|
||||
export enum SortBy {
|
||||
DATE_ADDED = 'date',
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
export enum SortOrder {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
export interface SortOptions {
|
||||
by: SortBy;
|
||||
order: SortOrder;
|
||||
}
|
||||
|
||||
const SORT_OPTIONS_STORAGE_KEY = 'iptvnator-sort-options';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SortService {
|
||||
private sortOptions = new BehaviorSubject<SortOptions>(
|
||||
this.getSavedSortOptions()
|
||||
);
|
||||
|
||||
getSortOptions(): Observable<SortOptions> {
|
||||
return this.sortOptions.asObservable();
|
||||
}
|
||||
|
||||
setSortOptions(options: SortOptions): void {
|
||||
this.sortOptions.next(options);
|
||||
this.saveSortOptions(options);
|
||||
}
|
||||
|
||||
private getSavedSortOptions(): SortOptions {
|
||||
const defaultOptions: SortOptions = {
|
||||
by: SortBy.DATE_ADDED,
|
||||
order: SortOrder.DESC,
|
||||
};
|
||||
|
||||
try {
|
||||
const savedOptions = localStorage.getItem(SORT_OPTIONS_STORAGE_KEY);
|
||||
if (!savedOptions) {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
const parsedOptions = JSON.parse(savedOptions) as SortOptions;
|
||||
|
||||
// Validate that the saved options match our expected types
|
||||
if (
|
||||
(parsedOptions.by === SortBy.DATE_ADDED ||
|
||||
parsedOptions.by === SortBy.NAME) &&
|
||||
(parsedOptions.order === SortOrder.ASC ||
|
||||
parsedOptions.order === SortOrder.DESC)
|
||||
) {
|
||||
return parsedOptions;
|
||||
}
|
||||
|
||||
return defaultOptions;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error retrieving sort options from localStorage:',
|
||||
error
|
||||
);
|
||||
return defaultOptions;
|
||||
}
|
||||
}
|
||||
|
||||
private saveSortOptions(options: SortOptions): void {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
SORT_OPTIONS_STORAGE_KEY,
|
||||
JSON.stringify(options)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error saving sort options to localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
sortPlaylists(
|
||||
playlists: PlaylistMeta[],
|
||||
options: SortOptions
|
||||
): PlaylistMeta[] {
|
||||
const { by, order } = options;
|
||||
|
||||
return [...playlists].sort((a, b) => {
|
||||
let comparison = 0;
|
||||
|
||||
if (by === SortBy.NAME) {
|
||||
comparison = a.title
|
||||
.toLowerCase()
|
||||
.localeCompare(b.title.toLowerCase());
|
||||
} else if (by === SortBy.DATE_ADDED) {
|
||||
const dateA = new Date(a.importDate).getTime();
|
||||
const dateB = new Date(b.importDate).getTime();
|
||||
comparison = dateA - dateB;
|
||||
}
|
||||
|
||||
return order === SortOrder.ASC ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,20 +16,22 @@
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
<!-- <button
|
||||
mat-icon-button
|
||||
[matTooltip]="'HOME.FILTER_BY_NAME' | translate"
|
||||
data-test-id="filter-playlist-by-name"
|
||||
>
|
||||
<mat-icon>search</mat-icon>
|
||||
</button> -->
|
||||
<!-- Sort button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="sortMenu"
|
||||
[matTooltip]="'HOME.SORT_PLAYLISTS' | translate"
|
||||
data-test-id="sort-playlists"
|
||||
>
|
||||
<mat-icon>sort</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="filterPlaylistMenu"
|
||||
[matTooltip]="'HOME.FILTER_BY_TYPE' | translate"
|
||||
data-test-id="filter-playlist-by-type"
|
||||
>
|
||||
<mat-icon>filter_list</mat-icon>
|
||||
<mat-icon>filter_alt</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="isElectron || isTauri; else pwaMenu"
|
||||
@@ -41,12 +43,84 @@
|
||||
<mat-icon>settings</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Sort menu -->
|
||||
<mat-menu #sortMenu="matMenu">
|
||||
<div
|
||||
class="dropdown-menu-header"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
{{ 'HOME.SORT_BY' | translate }}
|
||||
</div>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.NAME, SortOrder.ASC)"
|
||||
[class.active-sort]="isSortActive(SortBy.NAME, SortOrder.ASC)"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NAME_ASC' | translate }}</span>
|
||||
<mat-icon
|
||||
*ngIf="isSortActive(SortBy.NAME, SortOrder.ASC)"
|
||||
class="check-icon"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.NAME, SortOrder.DESC)"
|
||||
[class.active-sort]="isSortActive(SortBy.NAME, SortOrder.DESC)"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NAME_DESC' | translate }}</span>
|
||||
<mat-icon
|
||||
*ngIf="isSortActive(SortBy.NAME, SortOrder.DESC)"
|
||||
class="check-icon"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.DATE_ADDED, SortOrder.DESC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.DESC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NEWEST' | translate }}</span>
|
||||
<mat-icon
|
||||
*ngIf="isSortActive(SortBy.DATE_ADDED, SortOrder.DESC)"
|
||||
class="check-icon"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.DATE_ADDED, SortOrder.ASC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.ASC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.OLDEST' | translate }}</span>
|
||||
<mat-icon
|
||||
*ngIf="isSortActive(SortBy.DATE_ADDED, SortOrder.ASC)"
|
||||
class="check-icon"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #filterPlaylistMenu="matMenu">
|
||||
<section
|
||||
(click)="$event.stopPropagation()"
|
||||
class="filter-container"
|
||||
>
|
||||
<h4>{{ 'HOME.FILTER_BY_TYPE' | translate }}:</h4>
|
||||
<div
|
||||
class="dropdown-menu-header"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
{{ 'HOME.FILTER_BY_TYPE' | translate }}
|
||||
</div>
|
||||
@for (type of playlistTypes; track $index) {
|
||||
<p>
|
||||
<mat-checkbox
|
||||
|
||||
@@ -50,6 +50,21 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dropdown-menu-header {
|
||||
padding: 0 16px;
|
||||
margin: 8px 0;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.active-sort {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.subtitle {
|
||||
display: none;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { open } from '@tauri-apps/plugin-shell';
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import { HomeComponent } from '../../../home/home.component';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { SortBy, SortOrder, SortService } from '../../../services/sort.service';
|
||||
import { WhatsNewService } from '../../../services/whats-new.service';
|
||||
import { setSelectedFilters } from '../../../state/actions';
|
||||
import { selectActiveTypeFilters } from '../../../state/selectors';
|
||||
@@ -91,13 +92,18 @@ export class HeaderComponent implements OnInit {
|
||||
|
||||
selectedTypeFilters = this.store.selectSignal(selectActiveTypeFilters);
|
||||
|
||||
SortBy = SortBy;
|
||||
SortOrder = SortOrder;
|
||||
currentSortOptions: { by: SortBy; order: SortOrder };
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private dialog: MatDialog,
|
||||
private dataService: DataService,
|
||||
private router: Router,
|
||||
private store: Store,
|
||||
private whatsNewService: WhatsNewService
|
||||
private whatsNewService: WhatsNewService,
|
||||
private sortService: SortService
|
||||
) {
|
||||
effect(() => {
|
||||
if (this.selectedTypeFilters) {
|
||||
@@ -107,6 +113,10 @@ export class HeaderComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.sortService.getSortOptions().subscribe((options) => {
|
||||
this.currentSortOptions = options;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -174,4 +184,15 @@ export class HeaderComponent implements OnInit {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setSortOptions(by: SortBy, order: SortOrder): void {
|
||||
this.sortService.setSortOptions({ by, order });
|
||||
}
|
||||
|
||||
isSortActive(by: SortBy, order: SortOrder): boolean {
|
||||
return (
|
||||
this.currentSortOptions?.by === by &&
|
||||
this.currentSortOptions?.order === order
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,14 @@
|
||||
},
|
||||
"FILTER_BY_NAME": "Filter by name",
|
||||
"FILTER_BY_TYPE": "Filter by type",
|
||||
"SORT_PLAYLISTS": "Sort playlists",
|
||||
"SORT_BY": "Sort by",
|
||||
"SORT_OPTIONS": {
|
||||
"NAME_ASC": "Name (A-Z)",
|
||||
"NAME_DESC": "Name (Z-A)",
|
||||
"NEWEST": "Date added (Newest first)",
|
||||
"OLDEST": "Date added (Oldest first)"
|
||||
},
|
||||
"IMPORTED_AS_TEXT": "Imported as text",
|
||||
"PARSING_ERROR": "Error: Parsing failed, not a valid playlist:"
|
||||
},
|
||||
@@ -124,7 +132,7 @@
|
||||
"IMPORT_EXPORT_DATA_DESCRIPTION": "Export or import playlists in the JSON format used within the application.",
|
||||
"IMPORT_DATA": "Import",
|
||||
"EXPORT_DATA": "Export",
|
||||
"REMOVE_ALL": "Remove playlist",
|
||||
"REMOVE_ALL": "Remove playlists",
|
||||
"REMOVE_ALL_BUTTON": "Remove",
|
||||
"REMOVE_ALL_DESCRIPTION": "It will remove all existing playlists from the application.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
Reference in new issue
Block a user