mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 08:42:31 +00:00
reorganize
This commit is contained in:
8
website/package-lock.json
generated
8
website/package-lock.json
generated
@@ -1893,9 +1893,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui": {
|
"node_modules/bits-ui": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.7.tgz",
|
||||||
"integrity": "sha512-EDGHWkxnlcV2fbXn2tMps3SfpS7k6bfX3BrQ4s/h79jT6yprBS8DdDficlDK0SDHmPYHBZ0hSy4OgQUDodS/6w==",
|
"integrity": "sha512-1PKp90ly1R6jexIiAUj1Dk4u2pln7ok+L8Vc0rHMY7pi7YZvadFNZvkp1G5BtmL8qh2xsn4MVNgKjPAQMCxW0A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@internationalized/date": "^3.5.1",
|
"@internationalized/date": "^3.5.1",
|
||||||
"@melt-ui/svelte": "0.76.2",
|
"@melt-ui/svelte": "0.76.2",
|
||||||
@@ -1905,7 +1905,7 @@
|
|||||||
"url": "https://github.com/sponsors/huntabyte"
|
"url": "https://github.com/sponsors/huntabyte"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^4.0.0"
|
"svelte": "^4.0.0 || ^5.0.0-next.118"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui/node_modules/nanoid": {
|
"node_modules/bits-ui/node_modules/nanoid": {
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { derived, get } from 'svelte/store';
|
import { derived, get } from 'svelte/store';
|
||||||
import { canUndo, canRedo, dbUtils, fileObservers, redo, undo } from '$lib/db';
|
import { canUndo, canRedo, dbUtils, fileObservers } from '$lib/db';
|
||||||
|
|
||||||
let showDistanceMarkers = false;
|
let showDistanceMarkers = false;
|
||||||
let showDirectionMarkers = false;
|
let showDirectionMarkers = false;
|
||||||
@@ -93,12 +93,12 @@
|
|||||||
<Menubar.Menu>
|
<Menubar.Menu>
|
||||||
<Menubar.Trigger>{$_('menu.edit')}</Menubar.Trigger>
|
<Menubar.Trigger>{$_('menu.edit')}</Menubar.Trigger>
|
||||||
<Menubar.Content class="border-none">
|
<Menubar.Content class="border-none">
|
||||||
<Menubar.Item on:click={undo} disabled={$undoDisabled}>
|
<Menubar.Item on:click={dbUtils.undo} disabled={$undoDisabled}>
|
||||||
<Undo2 size="16" class="mr-1" />
|
<Undo2 size="16" class="mr-1" />
|
||||||
{$_('menu.undo')}
|
{$_('menu.undo')}
|
||||||
<Shortcut key="Z" ctrl={true} />
|
<Shortcut key="Z" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item on:click={redo} disabled={$redoDisabled}>
|
<Menubar.Item on:click={dbUtils.redo} disabled={$redoDisabled}>
|
||||||
<Redo2 size="16" class="mr-1" />
|
<Redo2 size="16" class="mr-1" />
|
||||||
{$_('menu.redo')}
|
{$_('menu.redo')}
|
||||||
<Shortcut key="Z" ctrl={true} shift={true} />
|
<Shortcut key="Z" ctrl={true} shift={true} />
|
||||||
@@ -213,9 +213,9 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
|
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
redo();
|
dbUtils.redo();
|
||||||
} else {
|
} else {
|
||||||
undo();
|
dbUtils.undo();
|
||||||
}
|
}
|
||||||
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
|
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
|
@@ -27,7 +27,21 @@ class Database extends Dexie {
|
|||||||
|
|
||||||
const db = new Database();
|
const db = new Database();
|
||||||
|
|
||||||
function dexieFileStore(querier: () => FreezedObject<GPXFile> | undefined | Promise<FreezedObject<GPXFile> | undefined>): Readable<GPXFile> {
|
// 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);
|
||||||
|
liveQuery(querier).subscribe(value => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
store.set(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
function dexieGPXFileStore(querier: () => FreezedObject<GPXFile> | undefined | Promise<FreezedObject<GPXFile> | undefined>): Readable<GPXFile> {
|
||||||
let store = writable<GPXFile>(undefined);
|
let store = writable<GPXFile>(undefined);
|
||||||
liveQuery(querier).subscribe(value => {
|
liveQuery(querier).subscribe(value => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
@@ -41,19 +55,8 @@ function dexieFileStore(querier: () => FreezedObject<GPXFile> | undefined | Prom
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T> {
|
// Add/update the files to the database
|
||||||
let store = writable<T>(initial);
|
function updateDbFiles(files: (FreezedObject<GPXFile> | undefined)[], add: boolean = false) {
|
||||||
liveQuery(querier).subscribe(value => {
|
|
||||||
if (value !== undefined) {
|
|
||||||
store.set(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFiles(files: (FreezedObject<GPXFile> | undefined)[], add: boolean = false) {
|
|
||||||
let filteredFiles = files.filter(file => file !== undefined) as FreezedObject<GPXFile>[];
|
let filteredFiles = files.filter(file => file !== undefined) as FreezedObject<GPXFile>[];
|
||||||
let fileIds = filteredFiles.map(file => file._data.id);
|
let fileIds = filteredFiles.map(file => file._data.id);
|
||||||
if (add) {
|
if (add) {
|
||||||
@@ -66,26 +69,29 @@ function updateFiles(files: (FreezedObject<GPXFile> | undefined)[], add: boolean
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteFiles(fileIds: string[]) {
|
// Delete the files with the given ids from the database
|
||||||
|
function deleteDbFiles(fileIds: string[]) {
|
||||||
return db.transaction('rw', db.fileids, db.files, async () => {
|
return db.transaction('rw', db.fileids, db.files, async () => {
|
||||||
await db.fileids.bulkDelete(fileIds);
|
await db.fileids.bulkDelete(fileIds);
|
||||||
await db.files.bulkDelete(fileIds);
|
await db.files.bulkDelete(fileIds);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit the changes to the file state to the database
|
||||||
function commitFileStateChange(newFileState: ReadonlyMap<string, FreezedObject<GPXFile>>, patch: Patch[]) {
|
function commitFileStateChange(newFileState: ReadonlyMap<string, FreezedObject<GPXFile>>, patch: Patch[]) {
|
||||||
if (newFileState.size > fileState.size) {
|
if (newFileState.size > fileState.size) {
|
||||||
return updateFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)), true);
|
return updateDbFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)), true);
|
||||||
} else if (newFileState.size === fileState.size) {
|
} else if (newFileState.size === fileState.size) {
|
||||||
return updateFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)));
|
return updateDbFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)));
|
||||||
} else {
|
} else {
|
||||||
return deleteFiles(getChangedFileIds(patch));
|
return deleteDbFiles(getChangedFileIds(patch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileObservers: Writable<Map<string, Readable<GPXFile | undefined>>> = writable(new Map());
|
export const fileObservers: Writable<Map<string, Readable<GPXFile | undefined>>> = writable(new Map());
|
||||||
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
|
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
|
||||||
|
|
||||||
|
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
|
||||||
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
|
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
|
||||||
// Find new files to observe
|
// Find new files to observe
|
||||||
let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id));
|
let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id));
|
||||||
@@ -95,7 +101,7 @@ liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
|
|||||||
if (newFiles.length > 0 || deletedFiles.length > 0) {
|
if (newFiles.length > 0 || deletedFiles.length > 0) {
|
||||||
fileObservers.update($files => {
|
fileObservers.update($files => {
|
||||||
newFiles.forEach(id => {
|
newFiles.forEach(id => {
|
||||||
$files.set(id, dexieFileStore(() => db.files.get(id)));
|
$files.set(id, dexieGPXFileStore(() => db.files.get(id)));
|
||||||
});
|
});
|
||||||
deletedFiles.forEach(id => {
|
deletedFiles.forEach(id => {
|
||||||
$files.delete(id);
|
$files.delete(id);
|
||||||
@@ -111,14 +117,16 @@ const patchCount: Readable<number> = dexieStore(() => db.patches.count(), 0);
|
|||||||
export const canUndo: Readable<boolean> = derived(patchIndex, ($patchIndex) => $patchIndex >= 0);
|
export const canUndo: Readable<boolean> = derived(patchIndex, ($patchIndex) => $patchIndex >= 0);
|
||||||
export const canRedo: Readable<boolean> = derived([patchIndex, patchCount], ([$patchIndex, $patchCount]) => $patchIndex < $patchCount - 1);
|
export const canRedo: Readable<boolean> = derived([patchIndex, patchCount], ([$patchIndex, $patchCount]) => $patchIndex < $patchCount - 1);
|
||||||
|
|
||||||
export function applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
|
// Helper function to apply a callback to the global file state
|
||||||
|
function applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
|
||||||
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, callback);
|
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, callback);
|
||||||
|
|
||||||
appendPatches(patch, inversePatch);
|
storePatches(patch, inversePatch);
|
||||||
|
|
||||||
return commitFileStateChange(newFileState, patch);
|
return commitFileStateChange(newFileState, patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to apply a callback to multiple files
|
||||||
function applyToFiles(fileIds: string[], callback: (file: GPXFile) => void) {
|
function applyToFiles(fileIds: string[], callback: (file: GPXFile) => void) {
|
||||||
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
|
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
|
||||||
fileIds.forEach((fileId) => {
|
fileIds.forEach((fileId) => {
|
||||||
@@ -129,12 +137,13 @@ function applyToFiles(fileIds: string[], callback: (file: GPXFile) => void) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
appendPatches(patch, inversePatch);
|
storePatches(patch, inversePatch);
|
||||||
|
|
||||||
return commitFileStateChange(newFileState, patch);
|
return commitFileStateChange(newFileState, patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function appendPatches(patch: Patch[], inversePatch: Patch[]) {
|
// Store the new patches in the database
|
||||||
|
async function storePatches(patch: Patch[], inversePatch: Patch[]) {
|
||||||
if (get(patchIndex) !== undefined) {
|
if (get(patchIndex) !== undefined) {
|
||||||
db.patches.where(':id').above(get(patchIndex)).delete();
|
db.patches.where(':id').above(get(patchIndex)).delete();
|
||||||
}
|
}
|
||||||
@@ -147,11 +156,13 @@ async function appendPatches(patch: Patch[], inversePatch: Patch[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply a patch to the file state
|
||||||
function applyPatch(patch: Patch[]) {
|
function applyPatch(patch: Patch[]) {
|
||||||
let newFileState = applyPatches(fileState, patch);
|
let newFileState = applyPatches(fileState, patch);
|
||||||
return commitFileStateChange(newFileState, patch);
|
return commitFileStateChange(newFileState, patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the file ids of the files that have changed in the patch
|
||||||
function getChangedFileIds(patch: Patch[]) {
|
function getChangedFileIds(patch: Patch[]) {
|
||||||
let changedFileIds = [];
|
let changedFileIds = [];
|
||||||
for (let p of patch) {
|
for (let p of patch) {
|
||||||
@@ -163,6 +174,7 @@ function getChangedFileIds(patch: Patch[]) {
|
|||||||
return changedFileIds;
|
return changedFileIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate unique file ids, different from the ones in the database
|
||||||
function getFileIds(n: number) {
|
function getFileIds(n: number) {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
for (let index = 0; ids.length < n; index++) {
|
for (let index = 0; ids.length < n; index++) {
|
||||||
@@ -174,30 +186,7 @@ function getFileIds(n: number) {
|
|||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function undo() {
|
// Helper functions for file operations
|
||||||
if (get(canUndo)) {
|
|
||||||
let index = get(patchIndex);
|
|
||||||
db.patches.get(index).then(patch => {
|
|
||||||
if (patch) {
|
|
||||||
applyPatch(patch.inversePatch);
|
|
||||||
db.settings.put(index - 1, 'patchIndex');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function 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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dbUtils = {
|
export const dbUtils = {
|
||||||
add: (file: GPXFile) => {
|
add: (file: GPXFile) => {
|
||||||
file._data.id = getFileIds(1)[0];
|
file._data.id = getFileIds(1)[0];
|
||||||
@@ -247,4 +236,27 @@ export const dbUtils = {
|
|||||||
draft.clear();
|
draft.clear();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user