Files
gpx.studio/website/src/lib/db.ts

524 lines
21 KiB
TypeScript
Raw Normal View History

2024-05-02 19:51:08 +02:00
import Dexie, { liveQuery } from 'dexie';
2024-05-24 16:37:26 +02:00
import { GPXFile, GPXStatistics, Track, type AnyGPXTreeElement } from 'gpx';
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, castDraft, freeze, produceWithPatches, original, produce } from 'immer';
2024-05-02 19:51:08 +02:00
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
2024-05-22 16:05:31 +02:00
import { initTargetMapBounds, updateTargetMapBounds } from './stores';
2024-05-04 15:10:30 +02:00
import { mode } from 'mode-watcher';
2024-05-05 18:59:09 +02:00
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays } from './assets/layers';
2024-05-23 16:35:20 +02:00
import { applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection';
2024-05-24 16:37:26 +02:00
import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList';
2024-05-23 12:57:24 +02:00
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
2024-05-02 19:51:08 +02:00
2024-05-07 18:14:47 +02:00
enableMapSet();
enablePatches();
2024-05-16 13:27:12 +02:00
2024-05-02 19:51:08 +02:00
class Database extends Dexie {
2024-05-03 15:59:34 +02:00
fileids!: Dexie.Table<string, string>;
2024-05-07 18:14:47 +02:00
files!: Dexie.Table<GPXFile, string>;
2024-05-07 13:19:02 +02:00
patches!: Dexie.Table<{ patch: Patch[], inversePatch: Patch[], index: number }, number>;
2024-05-02 19:51:08 +02:00
settings!: Dexie.Table<any, string>;
constructor() {
2024-05-03 15:59:34 +02:00
super("Database", {
cache: 'immutable'
});
2024-05-02 19:51:08 +02:00
this.version(1).stores({
2024-05-03 15:59:34 +02:00
fileids: ',&fileid',
files: '',
patches: ',patch',
settings: ''
2024-05-02 19:51:08 +02:00
});
2024-05-03 15:59:34 +02:00
this.files.add
2024-05-02 19:51:08 +02:00
}
}
const db = new Database();
2024-05-05 18:59:09 +02:00
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, and updates to the store are pushed to the DB
2024-05-21 22:37:52 +02:00
function dexieSettingStore<T>(setting: string, initial: T): Writable<T> {
2024-05-05 18:59:09 +02:00
let store = writable(initial);
liveQuery(() => db.settings.get(setting)).subscribe(value => {
if (value !== undefined) {
store.set(value);
}
});
return {
subscribe: store.subscribe,
set: (value: any) => db.settings.put(value, setting),
update: (callback: (value: any) => any) => {
let newValue = callback(get(store));
db.settings.put(newValue, setting);
}
};
}
2024-05-06 15:52:11 +02:00
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, and updates to the store are pushed to the DB
function dexieUninitializedSettingStore(setting: string, initial: any): Writable<any> {
let store = writable(undefined);
liveQuery(() => db.settings.get(setting)).subscribe(value => {
if (value !== undefined) {
store.set(value);
} else {
store.set(initial);
}
});
return {
subscribe: store.subscribe,
set: (value: any) => db.settings.put(value, setting),
update: (callback: (value: any) => any) => {
let newValue = callback(get(store));
db.settings.put(newValue, setting);
}
};
}
2024-05-05 18:59:09 +02:00
export const settings = {
2024-05-22 16:05:31 +02:00
distanceUnits: dexieSettingStore<'metric' | 'imperial'>('distanceUnits', 'metric'),
2024-05-05 18:59:09 +02:00
velocityUnits: dexieSettingStore('velocityUnits', 'speed'),
temperatureUnits: dexieSettingStore('temperatureUnits', 'celsius'),
2024-05-22 16:05:31 +02:00
verticalFileView: dexieSettingStore<boolean>('fileView', false),
2024-05-05 18:59:09 +02:00
mode: dexieSettingStore('mode', (() => {
let currentMode: string | undefined = get(mode);
if (currentMode === undefined) {
currentMode = 'system';
}
return currentMode;
})()),
routing: dexieSettingStore('routing', true),
routingProfile: dexieSettingStore('routingProfile', 'bike'),
privateRoads: dexieSettingStore('privateRoads', false),
currentBasemap: dexieSettingStore('currentBasemap', defaultBasemap),
previousBasemap: dexieSettingStore('previousBasemap', defaultBasemap),
selectedBasemapTree: dexieSettingStore('selectedBasemapTree', defaultBasemapTree),
2024-05-06 15:52:11 +02:00
currentOverlays: dexieUninitializedSettingStore('currentOverlays', defaultOverlays),
2024-05-05 18:59:09 +02:00
previousOverlays: dexieSettingStore('previousOverlays', defaultOverlays),
selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree),
2024-05-08 12:35:31 +02:00
directionMarkers: dexieSettingStore('directionMarkers', false),
distanceMarkers: dexieSettingStore('distanceMarkers', false),
2024-05-21 22:37:52 +02:00
fileOrder: dexieSettingStore<string[]>('fileOrder', []),
2024-05-05 18:59:09 +02:00
};
2024-05-04 14:27:12 +02:00
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber
function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T> {
let store = writable<T>(initial);
2024-05-03 15:59:34 +02:00
liveQuery(querier).subscribe(value => {
if (value !== undefined) {
2024-05-04 14:27:12 +02:00
store.set(value);
2024-05-03 15:59:34 +02:00
}
});
2024-05-02 19:51:08 +02:00
return {
2024-05-04 14:27:12 +02:00
subscribe: store.subscribe,
2024-05-03 15:59:34 +02:00
};
}
2024-05-22 16:05:31 +02:00
export class GPXStatisticsTree {
level: ListLevel;
statistics: {
[key: number]: GPXStatisticsTree | GPXStatistics;
} = {};
constructor(element: GPXFile | Track) {
if (element instanceof GPXFile) {
2024-05-24 13:16:41 +02:00
this.level = ListLevel.FILE;
2024-05-22 16:05:31 +02:00
element.children.forEach((child, index) => {
this.statistics[index] = new GPXStatisticsTree(child);
});
} else {
2024-05-24 13:16:41 +02:00
this.level = ListLevel.TRACK;
2024-05-22 16:05:31 +02:00
element.children.forEach((child, index) => {
this.statistics[index] = child.getStatistics();
});
}
}
getStatisticsFor(item: ListItem): GPXStatistics {
let statistics = new GPXStatistics();
let id = item.getIdAtLevel(this.level);
if (id === undefined || id === 'waypoints') {
Object.keys(this.statistics).forEach(key => {
if (this.statistics[key] instanceof GPXStatistics) {
statistics.mergeWith(this.statistics[key]);
} else {
statistics.mergeWith(this.statistics[key].getStatisticsFor(item));
}
});
} else {
let child = this.statistics[id];
if (child instanceof GPXStatistics) {
statistics.mergeWith(child);
} else if (child !== undefined) {
statistics.mergeWith(child.getStatisticsFor(item));
}
}
return statistics;
}
};
export type GPXFileWithStatistics = { file: GPXFile, statistics: GPXStatisticsTree };
2024-05-08 21:31:54 +02:00
2024-05-04 14:27:12 +02:00
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, also takes care of the conversion to a GPXFile object
2024-05-15 11:47:42 +02:00
function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { destroy: () => void } {
2024-05-08 21:31:54 +02:00
let store = writable<GPXFileWithStatistics>(undefined);
2024-05-15 11:47:42 +02:00
let query = liveQuery(() => db.files.get(id)).subscribe(value => {
2024-05-03 15:59:34 +02:00
if (value !== undefined) {
2024-05-04 14:27:12 +02:00
let gpx = new GPXFile(value);
2024-05-23 12:57:24 +02:00
updateAnchorPoints(gpx);
2024-05-22 16:05:31 +02:00
let statistics = new GPXStatisticsTree(gpx);
2024-05-15 11:47:42 +02:00
if (!fileState.has(id)) { // Update the map bounds for new files
2024-05-22 16:05:31 +02:00
updateTargetMapBounds(statistics.getStatisticsFor(new ListFileItem(id)).global.bounds);
2024-05-08 21:31:54 +02:00
}
2024-05-22 16:05:31 +02:00
2024-05-15 11:47:42 +02:00
fileState.set(id, gpx);
2024-05-08 21:31:54 +02:00
store.set({
file: gpx,
statistics
});
2024-05-02 19:51:08 +02:00
}
2024-05-03 15:59:34 +02:00
});
return {
2024-05-09 00:02:27 +02:00
subscribe: store.subscribe,
2024-05-15 11:47:42 +02:00
destroy: () => {
fileState.delete(id);
query.unsubscribe();
}
2024-05-03 15:59:34 +02:00
};
}
2024-05-04 14:27:12 +02:00
// Add/update the files to the database
2024-05-07 18:14:47 +02:00
function updateDbFiles(files: (GPXFile | undefined)[], add: boolean = false) {
let filteredFiles = files.filter(file => file !== undefined) as GPXFile[];
2024-05-03 15:59:34 +02:00
let fileIds = filteredFiles.map(file => file._data.id);
if (add) {
return db.transaction('rw', db.fileids, db.files, async () => {
await db.fileids.bulkAdd(fileIds, fileIds);
await db.files.bulkAdd(filteredFiles, fileIds);
});
} else {
return db.files.bulkPut(filteredFiles, fileIds);
2024-05-02 19:51:08 +02:00
}
}
2024-05-04 14:27:12 +02:00
// Delete the files with the given ids from the database
function deleteDbFiles(fileIds: string[]) {
2024-05-03 15:59:34 +02:00
return db.transaction('rw', db.fileids, db.files, async () => {
await db.fileids.bulkDelete(fileIds);
await db.files.bulkDelete(fileIds);
});
}
2024-05-04 14:27:12 +02:00
// Commit the changes to the file state to the database
2024-05-07 18:14:47 +02:00
function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch: Patch[]) {
2024-05-03 15:59:34 +02:00
if (newFileState.size > fileState.size) {
2024-05-04 14:27:12 +02:00
return updateDbFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)), true);
2024-05-03 15:59:34 +02:00
} else if (newFileState.size === fileState.size) {
2024-05-04 14:27:12 +02:00
return updateDbFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)));
2024-05-03 15:59:34 +02:00
} else {
2024-05-04 14:27:12 +02:00
return deleteDbFiles(getChangedFileIds(patch));
2024-05-03 15:59:34 +02:00
}
2024-05-02 19:51:08 +02:00
}
2024-05-09 00:02:27 +02:00
export const fileObservers: Writable<Map<string, Readable<GPXFileWithStatistics | undefined> & { destroy: () => void }>> = writable(new Map());
2024-05-03 17:37:34 +02:00
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
2024-05-02 19:51:08 +02:00
2024-05-04 14:27:12 +02:00
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
2024-05-03 15:59:34 +02:00
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
2024-05-02 19:51:08 +02:00
// Find new files to observe
2024-05-07 12:36:54 +02:00
let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id)).sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
2024-05-02 19:51:08 +02:00
// Find deleted files to stop observing
2024-05-03 15:59:34 +02:00
let deletedFiles = Array.from(get(fileObservers).keys()).filter(id => !dbFileIds.find(fileId => fileId === id));
2024-05-08 21:31:54 +02:00
2024-05-02 19:51:08 +02:00
// Update the store
if (newFiles.length > 0 || deletedFiles.length > 0) {
fileObservers.update($files => {
2024-05-23 16:35:20 +02:00
if (newFiles.length > 0) { // Reset the target map bounds when new files are added
initTargetMapBounds($files.size === 0);
}
2024-05-02 19:51:08 +02:00
newFiles.forEach(id => {
2024-05-15 11:47:42 +02:00
$files.set(id, dexieGPXFileStore(id));
2024-05-02 19:51:08 +02:00
});
deletedFiles.forEach(id => {
2024-05-09 00:02:27 +02:00
$files.get(id)?.destroy();
2024-05-02 19:51:08 +02:00
$files.delete(id);
});
return $files;
});
2024-05-23 16:35:20 +02:00
if (deletedFiles.length > 0) {
selection.update(($selection) => {
deletedFiles.forEach((fileId) => {
$selection.deleteChild(fileId);
});
return $selection;
});
}
2024-05-02 19:51:08 +02:00
}
});
2024-05-03 15:59:34 +02:00
const patchIndex: Readable<number> = dexieStore(() => db.settings.get('patchIndex'), -1);
2024-05-07 15:09:44 +02:00
const patchMinMaxIndex: Readable<{ min: number, max: number }> = dexieStore(() => db.patches.orderBy(':id').keys().then(keys => {
if (keys.length === 0) {
return { min: 0, max: 0 };
} else {
return { min: keys[0], max: keys[keys.length - 1] + 1 };
}
}), { min: 0, max: 0 });
2024-05-07 13:19:02 +02:00
export const canUndo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex >= $patchMinMaxIndex.min);
2024-05-07 15:09:44 +02:00
export const canRedo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex < $patchMinMaxIndex.max - 1);
2024-05-02 19:51:08 +02:00
2024-05-04 14:27:12 +02:00
// Helper function to apply a callback to the global file state
function applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
2024-05-23 12:57:24 +02:00
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, callback);
2024-05-02 19:51:08 +02:00
2024-05-04 14:27:12 +02:00
storePatches(patch, inversePatch);
2024-05-02 19:51:08 +02:00
2024-05-03 15:59:34 +02:00
return commitFileStateChange(newFileState, patch);
2024-05-02 19:51:08 +02:00
}
2024-05-04 14:27:12 +02:00
// Helper function to apply a callback to multiple files
2024-05-15 15:30:02 +02:00
function applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>) => GPXFile) {
2024-05-23 12:57:24 +02:00
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
2024-05-02 19:51:08 +02:00
fileIds.forEach((fileId) => {
2024-05-03 15:59:34 +02:00
let file = draft.get(fileId);
if (file) {
2024-05-15 15:30:02 +02:00
draft.set(fileId, castDraft(callback(file)));
2024-05-03 15:59:34 +02:00
}
2024-05-02 19:51:08 +02:00
});
});
2024-05-04 14:27:12 +02:00
storePatches(patch, inversePatch);
2024-05-02 19:51:08 +02:00
2024-05-03 15:59:34 +02:00
return commitFileStateChange(newFileState, patch);
2024-05-02 19:51:08 +02:00
}
2024-05-07 15:09:44 +02:00
const MAX_PATCHES = 100;
2024-05-04 14:27:12 +02:00
// Store the new patches in the database
async function storePatches(patch: Patch[], inversePatch: Patch[]) {
2024-05-03 15:59:34 +02:00
if (get(patchIndex) !== undefined) {
2024-05-07 13:19:02 +02:00
db.patches.where(':id').above(get(patchIndex)).delete(); // Delete all patches after the current patch to avoid redoing them
2024-05-07 15:09:44 +02:00
let minmax = get(patchMinMaxIndex);
if (minmax.max - minmax.min + 1 > MAX_PATCHES) {
db.patches.where(':id').belowOrEqual(get(patchMinMaxIndex).max - MAX_PATCHES).delete();
}
2024-05-03 15:59:34 +02:00
}
db.transaction('rw', db.patches, db.settings, async () => {
2024-05-07 13:19:02 +02:00
let index = get(patchIndex) + 1;
2024-05-03 15:59:34 +02:00
await db.patches.put({
patch,
2024-05-07 13:19:02 +02:00
inversePatch,
index
}, index);
await db.settings.put(index, 'patchIndex');
2024-05-02 19:51:08 +02:00
});
}
2024-05-04 14:27:12 +02:00
// Apply a patch to the file state
2024-05-02 19:51:08 +02:00
function applyPatch(patch: Patch[]) {
let newFileState = applyPatches(fileState, patch);
2024-05-03 15:59:34 +02:00
return commitFileStateChange(newFileState, patch);
}
2024-05-04 14:27:12 +02:00
// Get the file ids of the files that have changed in the patch
2024-05-07 18:14:47 +02:00
function getChangedFileIds(patch: Patch[]): string[] {
2024-05-15 11:47:42 +02:00
let changedFileIds = new Set<string>();
2024-05-02 19:51:08 +02:00
for (let p of patch) {
2024-05-07 18:14:47 +02:00
changedFileIds.add(p.path[0]);
2024-05-02 19:51:08 +02:00
}
2024-05-07 18:14:47 +02:00
return Array.from(changedFileIds);
2024-05-02 19:51:08 +02:00
}
2024-05-04 14:27:12 +02:00
// Generate unique file ids, different from the ones in the database
2024-05-03 15:59:34 +02:00
function getFileIds(n: number) {
let ids = [];
for (let index = 0; ids.length < n; index++) {
2024-05-02 19:51:08 +02:00
let id = `gpx-${index}`;
if (!get(fileObservers).has(id)) {
2024-05-03 15:59:34 +02:00
ids.push(id);
2024-05-02 19:51:08 +02:00
}
}
2024-05-03 15:59:34 +02:00
return ids;
2024-05-02 19:51:08 +02:00
}
2024-05-04 14:27:12 +02:00
// Helper functions for file operations
2024-05-02 19:51:08 +02:00
export const dbUtils = {
add: (file: GPXFile) => {
2024-05-03 15:59:34 +02:00
file._data.id = getFileIds(1)[0];
return applyGlobal((draft) => {
2024-05-23 12:57:24 +02:00
draft.set(file._data.id, freeze(file));
2024-05-02 19:51:08 +02:00
});
},
addMultiple: (files: GPXFile[]) => {
2024-05-03 15:59:34 +02:00
return applyGlobal((draft) => {
let ids = getFileIds(files.length);
files.forEach((file, index) => {
file._data.id = ids[index];
2024-05-23 12:57:24 +02:00
draft.set(file._data.id, freeze(file));
2024-05-02 19:51:08 +02:00
});
});
},
2024-05-15 15:30:02 +02:00
applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
2024-05-02 19:51:08 +02:00
applyToFiles([id], callback);
},
2024-05-24 16:37:26 +02:00
applyToSelection: (callback: (file: WritableDraft<AnyGPXTreeElement>) => AnyGPXTreeElement) => {
2024-05-23 16:35:20 +02:00
if (get(selection).size === 0) {
return;
}
2024-05-02 19:51:08 +02:00
applyGlobal((draft) => {
2024-05-24 16:37:26 +02:00
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = draft.get(fileId);
if (file) {
for (let item of items) {
if (item instanceof ListFileItem) {
callback(castDraft(file));
} else if (item instanceof ListTrackItem) {
let trackIndex = item.getTrackIndex();
file = produce(file, (fileDraft) => {
callback(fileDraft.trk[trackIndex]);
});
} else if (item instanceof ListTrackSegmentItem) {
let trackIndex = item.getTrackIndex();
let segmentIndex = item.getSegmentIndex();
file = produce(file, (fileDraft) => {
callback(fileDraft.trk[trackIndex].trkseg[segmentIndex]);
});
}
}
draft.set(fileId, freeze(file));
}
});
});
},
2024-05-24 17:23:26 +02:00
duplicateSelection: () => {
if (get(selection).size === 0) {
return;
}
applyGlobal((draft) => {
let ids = getFileIds(get(settings.fileOrder).length);
let index = 0;
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = original(draft)?.get(fileId);
if (file) {
let newFile = file;
if (level === ListLevel.FILE) {
newFile = file.clone();
newFile._data.id = ids[index++];
} else if (level === ListLevel.TRACK) {
for (let item of items) {
let trackIndex = (item as ListTrackItem).getTrackIndex();
newFile = newFile.replaceTracks(trackIndex + 1, trackIndex, [file.trk[trackIndex].clone()]);
}
} else if (level === ListLevel.SEGMENT) {
for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
newFile = newFile.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]);
}
} else if (level === ListLevel.WAYPOINT) {
for (let item of items) {
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
newFile = newFile.replaceWaypoints(waypointIndex + 1, waypointIndex, [file.wpt[waypointIndex].clone()]);
}
}
draft.set(newFile._data.id, freeze(newFile));
}
});
});
},
2024-05-24 16:37:26 +02:00
reverseSelection: () => {
if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
return;
}
applyGlobal((draft) => {
2024-05-23 16:35:20 +02:00
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
2024-05-23 14:44:07 +02:00
let file = original(draft)?.get(fileId);
if (file) {
let newFile = file;
2024-05-24 13:16:41 +02:00
if (level === ListLevel.FILE) {
2024-05-24 16:37:26 +02:00
newFile = file.reverse();
2024-05-24 13:16:41 +02:00
} else if (level === ListLevel.TRACK) {
2024-05-23 14:44:07 +02:00
for (let item of items) {
let trackIndex = (item as ListTrackItem).getTrackIndex();
2024-05-24 16:37:26 +02:00
newFile = newFile.reverseTrack(trackIndex);
2024-05-23 14:44:07 +02:00
}
2024-05-24 13:16:41 +02:00
} else if (level === ListLevel.SEGMENT) {
2024-05-23 14:44:07 +02:00
for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
2024-05-24 16:37:26 +02:00
newFile = newFile.reverseTrackSegment(trackIndex, segmentIndex);
2024-05-23 14:44:07 +02:00
}
2024-05-02 19:51:08 +02:00
}
2024-05-23 16:35:20 +02:00
draft.set(newFile._data.id, freeze(newFile));
2024-05-02 19:51:08 +02:00
}
});
});
},
2024-05-22 16:05:31 +02:00
deleteSelection: () => {
2024-05-23 16:35:20 +02:00
if (get(selection).size === 0) {
return;
}
2024-05-02 19:51:08 +02:00
applyGlobal((draft) => {
2024-05-23 16:35:20 +02:00
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
2024-05-24 13:16:41 +02:00
if (level === ListLevel.FILE) {
2024-05-23 16:35:20 +02:00
draft.delete(fileId);
} else {
let file = original(draft)?.get(fileId);
if (file) {
let newFile = file;
2024-05-24 13:16:41 +02:00
if (level === ListLevel.TRACK) {
2024-05-23 16:35:20 +02:00
for (let item of items) {
let trackIndex = (item as ListTrackItem).getTrackIndex();
newFile = newFile.replaceTracks(trackIndex, trackIndex, []);
}
2024-05-24 13:16:41 +02:00
} else if (level === ListLevel.SEGMENT) {
2024-05-23 16:35:20 +02:00
for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
newFile = newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []);
}
2024-05-24 13:16:41 +02:00
} else if (level === ListLevel.WAYPOINTS) {
2024-05-23 16:35:20 +02:00
newFile = newFile.replaceWaypoints(0, newFile.wpt.length - 1, []);
2024-05-24 13:16:41 +02:00
} else if (level === ListLevel.WAYPOINT) {
2024-05-23 16:35:20 +02:00
for (let item of items) {
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
newFile = newFile.replaceWaypoints(waypointIndex, waypointIndex, []);
}
}
draft.set(newFile._data.id, freeze(newFile));
2024-05-23 11:21:57 +02:00
}
2024-05-23 16:35:20 +02:00
}
2024-05-02 19:51:08 +02:00
});
});
2024-05-24 17:23:26 +02:00
selection.update(($selection) => {
$selection.clear();
return $selection;
});
2024-05-02 19:51:08 +02:00
},
deleteAllFiles: () => {
applyGlobal((draft) => {
draft.clear();
});
},
2024-05-04 14:27:12 +02:00
// undo-redo
undo: () => {
if (get(canUndo)) {
let index = get(patchIndex);
db.patches.get(index).then(patch => {
if (patch) {
applyPatch(patch.inversePatch);
db.settings.put(index - 1, 'patchIndex');
}
});
}
},
redo: () => {
if (get(canRedo)) {
let index = get(patchIndex) + 1;
db.patches.get(index).then(patch => {
if (patch) {
applyPatch(patch.patch);
db.settings.put(index, 'patchIndex');
}
});
}
}
2024-05-05 18:59:09 +02:00
}