This commit is contained in:
vcoppe committed 2026-10-08 18:34:19 +02:00
1 parent 606a0e974a
commit 67a4c090bd
107 files changed
+3474 -3948

No files matched your search

@@ -3,8 +3,7 @@
import FileListNodeStore from './FileListNodeStore.svelte';
import FileListNode from './FileListNode.svelte';
import FileListNodeContent from './FileListNodeContent.svelte';
import { ListLevel } from './file-list';
import { allowedMoves, dragging, SortableFileList } from './sortable-file-list';
import { allowedMoves, dragging, SortableFileList, type ListLevel } from './sortable-file-list';
import { engine, type FileState } from '$lib/engine';
import type { FileTreeNode } from '$lib/selection-helpers';
@@ -26,14 +25,14 @@
let container: HTMLElement;
let sortableLevel: ListLevel =
node === null
? ListLevel.FILE
? 'file'
: node.type === 'file'
? waypointRoot
? ListLevel.WAYPOINTS
: ListLevel.TRACK
? 'waypoints'
: 'track'
: node.type === 'waypoints'
? ListLevel.WAYPOINT
: ListLevel.SEGMENT;
? 'waypoint'
: 'segment';
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let canDrop = $derived($dragging !== null && allowedMoves[$dragging].includes(sortableLevel));
@@ -18,7 +18,6 @@
Scissors,
FileStack,
} from '@lucide/svelte';
import { ListLevel } from './file-list';
import { getContext } from 'svelte';
import { i18n } from '$lib/i18n.svelte';
import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte';
@@ -56,14 +55,7 @@
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let embedding = getContext<boolean>('embedding');
const levels = {
file: ListLevel.FILE,
track: ListLevel.TRACK,
segment: ListLevel.SEGMENT,
waypoints: ListLevel.WAYPOINTS,
waypoint: ListLevel.WAYPOINT,
};
let level = $derived(levels[node.type]);
let level = $derived(node.type);
let singleSelection = $derived(selectionSize($selection) === 1);
@@ -128,7 +120,7 @@
<MetadataDialog bind:open={openEditMetadata} {fileState} {node} />
<StyleDialog bind:open={openEditStyle} {fileState} {node} />
{/if}
{#if level === ListLevel.FILE || level === ListLevel.TRACK}
{#if level === 'file' || level === 'track'}
<div
class="absolute {orientation === 'vertical'
? 'top-0 bottom-0 right-0 w-1'
@@ -178,9 +170,9 @@
}
}}
>
{#if level === ListLevel.SEGMENT}
{#if level === 'segment'}
<Waypoints size="16" class="mx-1 shrink-0" />
{:else if level === ListLevel.WAYPOINT}
{:else if level === 'waypoint'}
{#if symbolKey && symbols[symbolKey].icon}
{@const SymbolIcon = symbols[symbolKey].icon}
<SymbolIcon size="16" class="mx-1 shrink-0" />
@@ -250,7 +242,7 @@
<ContextMenu.Separator />
{/if}
{/if}
{#if level !== ListLevel.WAYPOINTS}
{#if level !== 'waypoints'}
<ContextMenu.Item onclick={() => engine.selectAll()}>
<FileStack size="16" />
{i18n._('menu.select_all')}
@@ -1,300 +0,0 @@
export enum ListLevel {
ROOT,
FILE,
TRACK,
SEGMENT,
WAYPOINTS,
WAYPOINT,
}
export abstract class ListItem {
[x: string]: any;
level: ListLevel;
constructor(level: ListLevel) {
this.level = level;
}
abstract getId(): string | number;
abstract getFullId(): string;
abstract getIdAtLevel(level: ListLevel): string | number | undefined;
abstract getFileId(): string;
abstract getParent(): ListItem;
abstract extend(id: string | number): ListItem;
}
export class ListRootItem extends ListItem {
constructor() {
super(ListLevel.ROOT);
}
getId(): string {
return 'root';
}
getFullId(): string {
return 'root';
}
getIdAtLevel(level: ListLevel): string | number | undefined {
return undefined;
}
getFileId(): string {
return '';
}
getParent(): ListItem {
return this;
}
extend(id: string): ListFileItem {
return new ListFileItem(id);
}
}
export class ListFileItem extends ListItem {
fileId: string;
constructor(fileId: string) {
super(ListLevel.FILE);
this.fileId = fileId;
}
getId(): string {
return this.fileId;
}
getFullId(): string {
return this.fileId;
}
getIdAtLevel(level: ListLevel): string | number | undefined {
switch (level) {
case ListLevel.ROOT:
return this.fileId;
default:
return undefined;
}
}
getFileId(): string {
return this.fileId;
}
getParent(): ListItem {
return new ListRootItem();
}
extend(id: number | 'waypoints'): ListTrackItem | ListWaypointsItem {
if (id === 'waypoints') {
return new ListWaypointsItem(this.fileId);
} else {
return new ListTrackItem(this.fileId, id);
}
}
}
export class ListTrackItem extends ListItem {
fileId: string;
trackIndex: number;
constructor(fileId: string, trackIndex: number) {
super(ListLevel.TRACK);
this.fileId = fileId;
this.trackIndex = trackIndex;
}
getId(): number {
return this.trackIndex;
}
getFullId(): string {
return `${this.fileId}-track-${this.trackIndex}`;
}
getIdAtLevel(level: ListLevel): string | number | undefined {
switch (level) {
case ListLevel.ROOT:
return this.fileId;
case ListLevel.FILE:
return this.trackIndex;
default:
return undefined;
}
}
getFileId(): string {
return this.fileId;
}
getTrackIndex(): number {
return this.trackIndex;
}
getParent(): ListItem {
return new ListFileItem(this.fileId);
}
extend(id: number): ListTrackSegmentItem {
return new ListTrackSegmentItem(this.fileId, this.trackIndex, id);
}
}
export class ListTrackSegmentItem extends ListItem {
fileId: string;
trackIndex: number;
segmentIndex: number;
constructor(fileId: string, trackIndex: number, segmentIndex: number) {
super(ListLevel.SEGMENT);
this.fileId = fileId;
this.trackIndex = trackIndex;
this.segmentIndex = segmentIndex;
}
getId(): number {
return this.segmentIndex;
}
getFullId(): string {
return `${this.fileId}-track-${this.trackIndex}--${this.segmentIndex}`;
}
getIdAtLevel(level: ListLevel): string | number | undefined {
switch (level) {
case ListLevel.ROOT:
return this.fileId;
case ListLevel.FILE:
return this.trackIndex;
case ListLevel.TRACK:
return this.segmentIndex;
default:
return undefined;
}
}
getFileId(): string {
return this.fileId;
}
getTrackIndex(): number {
return this.trackIndex;
}
getSegmentIndex(): number {
return this.segmentIndex;
}
getParent(): ListItem {
return new ListTrackItem(this.fileId, this.trackIndex);
}
extend(): ListTrackSegmentItem {
return this;
}
}
export class ListWaypointsItem extends ListItem {
fileId: string;
constructor(fileId: string) {
super(ListLevel.WAYPOINTS);
this.fileId = fileId;
}
getId(): string {
return 'waypoints';
}
getFullId(): string {
return `${this.fileId}-waypoints`;
}
getIdAtLevel(level: ListLevel): string | number | undefined {
switch (level) {
case ListLevel.ROOT:
return this.fileId;
case ListLevel.FILE:
return 'waypoints';
default:
return undefined;
}
}
getFileId(): string {
return this.fileId;
}
getParent(): ListItem {
return new ListFileItem(this.fileId);
}
extend(id: number): ListWaypointItem {
return new ListWaypointItem(this.fileId, id);
}
}
export class ListWaypointItem extends ListItem {
fileId: string;
waypointIndex: number;
constructor(fileId: string, waypointIndex: number) {
super(ListLevel.WAYPOINT);
this.fileId = fileId;
this.waypointIndex = waypointIndex;
}
getId(): number {
return this.waypointIndex;
}
getFullId(): string {
return `${this.fileId}-waypoint-${this.waypointIndex}`;
}
getIdAtLevel(level: ListLevel): string | number | undefined {
switch (level) {
case ListLevel.ROOT:
return this.fileId;
case ListLevel.FILE:
return 'waypoints';
case ListLevel.WAYPOINTS:
return this.waypointIndex;
default:
return undefined;
}
}
getFileId(): string {
return this.fileId;
}
getWaypointIndex(): number {
return this.waypointIndex;
}
getParent(): ListItem {
return new ListWaypointsItem(this.fileId);
}
extend(): ListWaypointItem {
return this;
}
}
export function sortItems(items: ListItem[], reverse: boolean = false) {
items.sort((a, b) => {
if (a instanceof ListTrackItem && b instanceof ListTrackItem) {
return a.getTrackIndex() - b.getTrackIndex();
} else if (a instanceof ListTrackSegmentItem && b instanceof ListTrackSegmentItem) {
return a.getSegmentIndex() - b.getSegmentIndex();
} else if (a instanceof ListWaypointItem && b instanceof ListWaypointItem) {
return a.getWaypointIndex() - b.getWaypointIndex();
}
return a.level - b.level;
});
if (reverse) {
items.reverse();
}
}
@@ -1,18 +1,19 @@
import { isMac } from '$lib/utils';
import Sortable, { type Direction } from 'sortablejs/Sortable';
import { ListLevel } from './file-list';
import { get, writable } from 'svelte/store';
import { tick } from 'svelte';
import { engine, type MoveTarget, type Selection } from '$lib/engine';
import { isSelected, type FileTreeNode } from '$lib/selection-helpers';
/** What a list holds: the files, the tracks of a file, the segments of a track, or the waypoints. */
export type ListLevel = 'file' | 'track' | 'segment' | 'waypoints' | 'waypoint';
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
file: ['file'],
track: ['file', 'track'],
segment: ['file', 'track', 'segment'],
waypoints: ['waypoints'],
waypoint: ['waypoints', 'waypoint'],
};
export const dragging = writable<ListLevel | null>(null);
@@ -84,11 +85,11 @@ export class SortableFileList {
childNode(id: string): FileTreeNode | undefined {
const parent = this._parent;
switch (this._sortableLevel) {
case ListLevel.FILE:
case 'file':
return { type: 'file', fileId: id };
case ListLevel.TRACK:
case 'track':
return parent ? { type: 'track', fileId: parent.fileId, trackId: id } : undefined;
case ListLevel.SEGMENT:
case 'segment':
return parent?.type === 'track'
? {
type: 'segment',
@@ -97,9 +98,9 @@ export class SortableFileList {
segmentId: id,
}
: undefined;
case ListLevel.WAYPOINTS:
case 'waypoints':
return parent ? { type: 'waypoints', fileId: parent.fileId } : undefined;
case ListLevel.WAYPOINT:
case 'waypoint':
return parent
? { type: 'waypoint', fileId: parent.fileId, waypointId: id }
: undefined;
@@ -110,11 +111,11 @@ export class SortableFileList {
private selectionOf(ids: string[]): Selection | undefined {
const parent = this._parent;
switch (this._sortableLevel) {
case ListLevel.FILE:
case 'file':
return { type: 'file', fileIds: ids };
case ListLevel.TRACK:
case 'track':
return parent ? { type: 'track', fileId: parent.fileId, trackIds: ids } : undefined;
case ListLevel.SEGMENT:
case 'segment':
return parent?.type === 'track'
? {
type: 'segment',
@@ -123,9 +124,9 @@ export class SortableFileList {
segmentIds: ids,
}
: undefined;
case ListLevel.WAYPOINTS:
case 'waypoints':
return parent ? { type: 'waypoints', fileId: parent.fileId } : undefined;
case ListLevel.WAYPOINT:
case 'waypoint':
return parent
? { type: 'waypoint', fileId: parent.fileId, waypointIds: ids }
: undefined;
@@ -136,20 +137,20 @@ export class SortableFileList {
private targetAt(index: number): MoveTarget | undefined {
const parent = this._parent;
switch (this._sortableLevel) {
case ListLevel.FILE:
case 'file':
return { type: 'files', index };
case ListLevel.TRACK:
case 'track':
return parent ? { type: 'tracks', fileId: parent.fileId, index } : undefined;
case ListLevel.SEGMENT:
case 'segment':
return parent?.type === 'track'
? { type: 'segments', fileId: parent.fileId, trackId: parent.trackId, index }
: undefined;
case ListLevel.WAYPOINTS:
case 'waypoints':
// the list holds the node standing for the waypoints: they go after the others
return parent
? { type: 'waypoints', fileId: parent.fileId, index: Infinity }
: undefined;
case ListLevel.WAYPOINT:
case 'waypoint':
return parent ? { type: 'waypoints', fileId: parent.fileId, index } : undefined;
}
}
@@ -161,7 +162,7 @@ export class SortableFileList {
return;
}
if (from === to && this._sortableLevel === ListLevel.FILE) {
if (from === to && this._sortableLevel === 'file') {
// the order of the files is the one of the list
this.updateToFileOrder();
return;
@@ -169,7 +170,7 @@ export class SortableFileList {
// The event is triggered on the source and on the destination list: handle it once.
// Nothing to move in the list of the single node of the waypoints.
if (to !== this || (from === to && this._sortableLevel === ListLevel.WAYPOINTS)) {
if (to !== this || (from === to && this._sortableLevel === 'waypoints')) {
return;
}
@@ -245,19 +246,19 @@ export class SortableFileList {
engine.select([]);
} else {
switch (this._sortableLevel) {
case ListLevel.FILE:
case 'file':
engine.select(selected);
break;
case ListLevel.TRACK:
case 'track':
engine.selectTracks(parent!.fileId, selected);
break;
case ListLevel.SEGMENT:
case 'segment':
engine.selectSegments(parent!.fileId, (parent as any).trackId, selected);
break;
case ListLevel.WAYPOINTS:
case 'waypoints':
engine.selectWaypointGroup(parent!.fileId);
break;
case ListLevel.WAYPOINT:
case 'waypoint':
engine.selectWaypoints(parent!.fileId, selected);
break;
}
@@ -266,7 +267,7 @@ export class SortableFileList {
}
updateFromFileOrder() {
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
if (!this._sortable || this._sortableLevel !== 'file') {
return;
}
@@ -282,7 +283,7 @@ export class SortableFileList {
}
updateToFileOrder() {
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
if (!this._sortable || this._sortableLevel !== 'file') {
return;
}
@@ -304,7 +305,7 @@ export class SortableFileList {
if (element instanceof HTMLElement) {
let attr = element.getAttribute('data-id');
if (attr) {
if (this._sortableLevel === ListLevel.FILE && !files.has(attr)) {
if (this._sortableLevel === 'file' && !files.has(attr)) {
element.remove();
} else {
this._elements[attr] = element;