Merge pull request #749 from 4gray/feat/custom-sorting-for-playlists

Feat/custom-sorting-for-playlists
This commit is contained in:
4gray authored and GitHub committed 2026-02-01 16:40:56 +01:00
commit 6b18d91ca7
8 files changed
+79 -6

No files matched your search

@@ -112,6 +112,26 @@
>check</mat-icon
>
</button>
<button
mat-menu-item
(click)="
setSortOptions(SortBy.CUSTOM, SortOrder.ASC);
$event.stopPropagation()
"
[class.active-option]="isSortActive(SortBy.CUSTOM, SortOrder.ASC)"
>
<mat-icon>drag_indicator</mat-icon>
<span class="menu-item-text">{{
'HOME.SORT_OPTIONS.CUSTOM_ORDER' | translate
}}</span>
<mat-icon
class="check-icon"
[class.check-visible]="
isSortActive(SortBy.CUSTOM, SortOrder.ASC)
"
>check</mat-icon
>
</button>
</section>
<!-- Close button -->
+2 -1
View File
@@ -129,7 +129,8 @@
"NAME_ASC": "Name (A-Z)",
"NAME_DESC": "Name (Z-A)",
"NEWEST": "Date added (Newest first)",
"OLDEST": "Date added (Oldest first)"
"OLDEST": "Date added (Oldest first)",
"CUSTOM_ORDER": "Custom order (Drag & Drop)"
},
"PLAYLIST_TYPES": {
"M3U": "M3U (local, url, text)",
+14 -1
View File
@@ -5,6 +5,7 @@ import { PlaylistMeta } from 'shared-interfaces';
export enum SortBy {
DATE_ADDED = 'date',
NAME = 'name',
CUSTOM = 'custom',
}
export enum SortOrder {
@@ -53,7 +54,8 @@ export class SortService {
// Validate that the saved options match our expected types
if (
(parsedOptions.by === SortBy.DATE_ADDED ||
parsedOptions.by === SortBy.NAME) &&
parsedOptions.by === SortBy.NAME ||
parsedOptions.by === SortBy.CUSTOM) &&
(parsedOptions.order === SortOrder.ASC ||
parsedOptions.order === SortOrder.DESC)
) {
@@ -98,9 +100,20 @@ export class SortService {
const dateA = new Date(a.importDate).getTime();
const dateB = new Date(b.importDate).getTime();
comparison = dateA - dateB;
} else if (by === SortBy.CUSTOM) {
// Sort by position field (lower position comes first)
const posA = a.position ?? Number.MAX_SAFE_INTEGER;
const posB = b.position ?? Number.MAX_SAFE_INTEGER;
comparison = posA - posB;
// Custom sort always uses ASC order
return comparison;
}
return order === SortOrder.ASC ? comparison : -comparison;
});
}
isCustomSort(): boolean {
return this.sortOptions.getValue().by === SortBy.CUSTOM;
}
}
@@ -1,4 +1,14 @@
<mat-list-item (click)="playlistClicked.emit(item._id)">
<mat-list-item
cdkDrag
[cdkDragDisabled]="!isDraggable()"
cdkDragPreviewContainer="parent"
(click)="playlistClicked.emit(item._id)"
>
@if (isDraggable()) {
<mat-icon cdkDragHandle class="drag-icon" matListItemIcon
>drag_indicator</mat-icon
>
}
@if (item.url) {
<div class="icon-container" matListItemIcon>
<mat-icon
@@ -47,6 +47,12 @@ mat-list-item {
.drag-icon {
cursor: move;
margin-right: 0;
opacity: 0.5;
transition: opacity 0.2s ease;
&:hover {
opacity: 1;
}
}
::ng-deep {
@@ -1,3 +1,4 @@
import { DragDropModule } from '@angular/cdk/drag-drop';
import { DatePipe } from '@angular/common';
import { Component, Input, OnInit, inject, input, output } from '@angular/core';
import { MatIconButton } from '@angular/material/button';
@@ -14,6 +15,7 @@ import { PlaylistMeta } from 'shared-interfaces';
styleUrls: ['./playlist-item.component.scss'],
imports: [
DatePipe,
DragDropModule,
MatIconButton,
MatIcon,
MatListModule,
@@ -24,6 +26,7 @@ import { PlaylistMeta } from 'shared-interfaces';
export class PlaylistItemComponent implements OnInit {
@Input() item!: PlaylistMeta;
readonly showActions = input(true);
readonly isDraggable = input(false);
readonly editPlaylistClicked = output<PlaylistMeta>();
readonly playlistClicked = output<string>();
@@ -13,7 +13,12 @@
<app-empty-state [type]="'no-results'" />
}
} @else {
<mat-nav-list class="playlists-list">
<mat-nav-list
class="playlists-list"
cdkDropList
[cdkDropListDisabled]="!isCustomSortActive()"
(cdkDropListDropped)="drop($event, data.playlists)"
>
@for (
item of data.playlists;
track item._id;
@@ -22,6 +27,7 @@
<app-playlist-item
[item]="item"
[showActions]="!sidebarMode()"
[isDraggable]="isCustomSortActive()"
(editPlaylistClicked)="openInfoDialog($event)"
(playlistClicked)="getPlaylist(item)"
(refreshClicked)="refreshPlaylist($event)"
@@ -1,6 +1,11 @@
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import {
CdkDragDrop,
DragDropModule,
moveItemInArray,
} from '@angular/cdk/drag-drop';
import { AsyncPipe } from '@angular/common';
import { Component, effect, inject, input, output } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
@@ -16,7 +21,7 @@ import {
} from 'm3u-state';
import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
import { BehaviorSubject, combineLatest, map } from 'rxjs';
import { DatabaseService, DataService, SortService } from 'services';
import { DatabaseService, DataService, SortBy, SortService } from 'services';
import { PLAYLIST_UPDATE, PlaylistMeta } from 'shared-interfaces';
import { PlaylistType } from '../add-playlist-menu/add-playlist-menu.component';
import { DialogService } from '../confirm-dialog/dialog.service';
@@ -30,6 +35,7 @@ import { PlaylistItemComponent } from './playlist-item/playlist-item.component';
styleUrls: ['./recent-playlists.component.scss'],
imports: [
AsyncPipe,
DragDropModule,
EmptyStateComponent,
MatInputModule,
MatListModule,
@@ -57,6 +63,14 @@ export class RecentPlaylistsComponent {
selectPlaylistsLoadingFlag
);
private readonly currentSortOptions = toSignal(
this.sortService.getSortOptions(),
{ requireSync: true }
);
readonly isCustomSortActive = () =>
this.currentSortOptions().by === SortBy.CUSTOM;
readonly searchQuery = new BehaviorSubject<string>('');
readonly ghostElements = new Array(10);