mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #749 from 4gray/feat/custom-sorting-for-playlists
Feat/custom-sorting-for-playlists
This commit is contained in:
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 -->
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user