Files
gpx.studio/website/src/lib/logic/selection.ts

319 lines
11 KiB
TypeScript
Raw Normal View History

2025-10-17 23:54:45 +02:00
import {
ListFileItem,
ListItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListLevel,
sortItems,
ListWaypointsItem,
} from '$lib/components/file-list/file-list';
2025-10-18 00:46:59 +02:00
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
2025-10-17 23:54:45 +02:00
import { settings } from '$lib/logic/settings';
import type { GPXFile } from 'gpx';
2025-10-18 00:31:14 +02:00
import { get, writable, type Readable, type Writable } from 'svelte/store';
2025-10-17 23:54:45 +02:00
import { SelectionTreeType } from '$lib/logic/selection-tree';
export class Selection {
private _selection: Writable<SelectionTreeType>;
private _copied: Writable<ListItem[] | undefined>;
private _cut: Writable<boolean>;
constructor() {
this._selection = writable(new SelectionTreeType(new ListRootItem()));
this._copied = writable(undefined);
this._cut = writable(false);
2025-10-05 19:34:05 +02:00
}
2025-10-17 23:54:45 +02:00
subscribe(
run: (value: SelectionTreeType) => void,
invalidate?: (value?: SelectionTreeType) => void
) {
return this._selection.subscribe(run, invalidate);
2025-10-05 19:34:05 +02:00
}
2025-10-25 17:44:41 +02:00
update(updater: (value: SelectionTreeType) => SelectionTreeType) {
this._selection.update(updater);
}
2025-10-17 23:54:45 +02:00
selectItem(item: ListItem) {
this._selection.update(($selection) => {
$selection.clear();
$selection.set(item, true);
return $selection;
});
2025-10-05 19:34:05 +02:00
}
2025-10-17 23:54:45 +02:00
selectFile(fileId: string) {
this.selectItem(new ListFileItem(fileId));
2025-10-05 19:34:05 +02:00
}
2025-10-17 23:54:45 +02:00
addSelectItem(item: ListItem) {
this._selection.update(($selection) => {
$selection.toggle(item);
return $selection;
});
2025-10-05 19:34:05 +02:00
}
2025-10-17 23:54:45 +02:00
addSelectFile(fileId: string) {
this.addSelectItem(new ListFileItem(fileId));
2025-10-05 19:34:05 +02:00
}
2025-10-17 23:54:45 +02:00
selectAll() {
let item: ListItem = new ListRootItem();
get(this._selection).forEach((i) => {
item = i;
});
this._selection.update(($selection) => {
$selection.clear();
if (item instanceof ListRootItem || item instanceof ListFileItem) {
fileStateCollection.forEach((fileId, file) => {
$selection.set(new ListFileItem(fileId), true);
});
} else if (item instanceof ListTrackItem) {
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
file.trk.forEach((_track, trackId) => {
$selection.set(new ListTrackItem(item.getFileId(), trackId), true);
});
2025-10-05 19:34:05 +02:00
}
2025-10-17 23:54:45 +02:00
} else if (item instanceof ListTrackSegmentItem) {
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(
new ListTrackSegmentItem(
item.getFileId(),
item.getTrackIndex(),
segmentId
),
true
);
});
}
} else if (item instanceof ListWaypointItem) {
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
file.wpt.forEach((_waypoint, waypointId) => {
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
});
2025-10-05 19:34:05 +02:00
}
}
2025-10-17 23:54:45 +02:00
return $selection;
});
}
2025-10-18 16:10:08 +02:00
selectFileWhenLoaded(fileId: string) {
const unsubscribe = fileStateCollection.subscribe((files) => {
if (files.has(fileId)) {
this.selectFile(fileId);
unsubscribe();
}
});
}
2025-10-17 23:54:45 +02:00
set(items: ListItem[]) {
this._selection.update(($selection) => {
$selection.clear();
items.forEach((item) => {
$selection.set(item, true);
});
return $selection;
});
}
2025-10-25 17:44:41 +02:00
updateFiles(updatedFiles: GPXFile[], deletedFileIds: string[]) {
2025-10-18 00:46:59 +02:00
let removedItems: ListItem[] = [];
applyToOrderedItemsFromFile(get(this._selection).getSelected(), (fileId, level, items) => {
let file = updatedFiles.find((file) => file._data.id === fileId);
if (file) {
items.forEach((item) => {
if (item instanceof ListTrackItem) {
let newTrackIndex = file.trk.findIndex(
(track) => track._data.trackIndex === item.getTrackIndex()
);
if (newTrackIndex === -1) {
removedItems.push(item);
}
} else if (item instanceof ListTrackSegmentItem) {
let newTrackIndex = file.trk.findIndex(
(track) => track._data.trackIndex === item.getTrackIndex()
);
if (newTrackIndex === -1) {
removedItems.push(item);
} else {
let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex(
(segment) => segment._data.segmentIndex === item.getSegmentIndex()
);
if (newSegmentIndex === -1) {
removedItems.push(item);
}
}
} else if (item instanceof ListWaypointItem) {
let newWaypointIndex = file.wpt.findIndex(
(wpt) => wpt._data.index === item.getWaypointIndex()
);
if (newWaypointIndex === -1) {
removedItems.push(item);
}
}
});
} else if (deletedFileIds.includes(fileId)) {
items.forEach((item) => {
removedItems.push(item);
});
}
});
if (removedItems.length > 0) {
this._selection.update(($selection) => {
removedItems.forEach((item) => {
if (item instanceof ListFileItem) {
$selection.deleteChild(item.getFileId());
} else {
$selection.set(item, false);
}
});
return $selection;
});
}
2025-10-17 23:54:45 +02:00
}
getOrderedSelection(reverse: boolean = false): ListItem[] {
let selected: ListItem[] = [];
this.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
selected.push(...items);
}, reverse);
return selected;
}
2025-11-11 14:03:07 +01:00
applyToSelectedItemsFromFile(
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void
) {
let selectedItems = get(this._selection).getSelected();
get(fileStateCollection).forEach((_, fileId) => {
let level: ListLevel | undefined = undefined;
let items: ListItem[] = [];
selectedItems.forEach((item) => {
if (item.getFileId() === fileId) {
level = item.level;
if (
item instanceof ListFileItem ||
item instanceof ListTrackItem ||
item instanceof ListTrackSegmentItem ||
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem
) {
items.push(item);
}
}
});
if (items.length > 0) {
callback(fileId, level, items);
}
});
}
2025-10-17 23:54:45 +02:00
applyToOrderedSelectedItemsFromFile(
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true
) {
applyToOrderedItemsFromFile(get(this._selection).getSelected(), callback, reverse);
}
copySelection(): boolean {
let selected = get(this._selection).getSelected();
if (selected.length > 0) {
this._copied.set(selected);
this._cut.set(false);
return true;
2025-10-05 19:34:05 +02:00
}
return false;
}
2025-10-17 23:54:45 +02:00
cutSelection() {
if (this.copySelection()) {
this._cut.set(true);
2025-10-05 19:34:05 +02:00
}
}
2025-10-17 23:54:45 +02:00
resetCopied() {
this._copied.set(undefined);
this._cut.set(false);
2025-10-05 19:34:05 +02:00
}
2025-10-18 00:31:14 +02:00
get copied(): Readable<ListItem[] | undefined> {
2025-10-17 23:54:45 +02:00
return this._copied;
2025-10-05 19:34:05 +02:00
}
2025-10-18 00:31:14 +02:00
get cut(): Readable<boolean> {
2025-10-17 23:54:45 +02:00
return this._cut;
2025-10-05 19:34:05 +02:00
}
}
2025-10-17 23:54:45 +02:00
export const selection = new Selection();
2025-10-18 09:36:55 +02:00
export const copied = selection.copied;
export const cut = selection.cut;
2025-10-17 23:54:45 +02:00
export function applyToOrderedItemsFromFile(
selectedItems: ListItem[],
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true
) {
2025-10-18 00:31:14 +02:00
get(settings.fileOrder).forEach((fileId) => {
2025-10-17 23:54:45 +02:00
let level: ListLevel | undefined = undefined;
let items: ListItem[] = [];
selectedItems.forEach((item) => {
if (item.getFileId() === fileId) {
level = item.level;
if (
item instanceof ListFileItem ||
item instanceof ListTrackItem ||
item instanceof ListTrackSegmentItem ||
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem
) {
items.push(item);
}
}
});
if (items.length > 0) {
sortItems(items, reverse);
callback(fileId, level, items);
}
});
}
2025-10-19 13:45:05 +02:00
export class SelectedGPXFilesObserver {
private _fileStateCollectionObserver: GPXFileStateCollectionObserver;
private _unsubscribes: Map<string, () => void> = new Map();
constructor(onSelectedFileChange: () => void) {
this._unsubscribes = new Map();
this._fileStateCollectionObserver = new GPXFileStateCollectionObserver(
2025-10-19 16:14:05 +02:00
(newFiles) => {
newFiles.forEach((fileState, fileId) => {
this._unsubscribes.set(
fileId,
fileState.subscribe(() => {
if (get(selection).hasAnyChildren(new ListFileItem(fileId))) {
onSelectedFileChange();
}
})
);
});
2025-10-19 13:45:05 +02:00
},
(fileId) => {
this._unsubscribes.get(fileId)?.();
this._unsubscribes.delete(fileId);
},
() => {
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
this._unsubscribes.clear();
}
);
selection.subscribe(() => onSelectedFileChange());
}
}