mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
Cross-platform groundwork: platform detection, iOS config, Windows drag-drop fix, Android 10 storage fix (#72), bundled fonts
This commit is contained in:
+43
@@ -5,6 +5,37 @@
|
||||
src: local("Inter");
|
||||
}
|
||||
|
||||
/* JetBrains Mono - self-hosted (see static/fonts/jetbrains-mono/, OFL-1.1).
|
||||
Used for code blocks, inline code, the raw source editor, and the "Mono" font option. */
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/fonts/jetbrains-mono/JetBrainsMono-Italic.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/fonts/jetbrains-mono/JetBrainsMono-Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Light theme */
|
||||
--bg-primary: #ffffff;
|
||||
@@ -32,6 +63,9 @@
|
||||
--sidebar-width: 220px;
|
||||
--notelist-width: 280px;
|
||||
--panel-resize-handle: 3px;
|
||||
/* Monospace / code font (self-hosted JetBrains Mono, see @font-face above) */
|
||||
--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@@ -155,6 +189,15 @@ body.resizing .ProseMirror {
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
/* Skip layout/paint of off-screen blocks in very large (math-heavy) notes. */
|
||||
.tiptap-wrapper.large-doc .ProseMirror > * {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 2rem;
|
||||
}
|
||||
.tiptap-wrapper.large-doc .ProseMirror > .math-block {
|
||||
contain-intrinsic-size: auto 3.5rem;
|
||||
}
|
||||
|
||||
body.resizing {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
const isMac = navigator.platform.startsWith('Mac');
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
const isAndroid = /android/i.test(navigator.userAgent);
|
||||
import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme } from '$lib/api';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
import GraphView from './GraphView.svelte';
|
||||
|
||||
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
|
||||
// Track virtual keyboard height on mobile via visualViewport
|
||||
let keyboardHeight = $state(0);
|
||||
@@ -59,6 +59,8 @@
|
||||
|
||||
let editorElement = $state<HTMLDivElement>(null!);
|
||||
let sourceElement = $state<HTMLTextAreaElement>(null!);
|
||||
const LARGE_DOC_CHARS = 100_000;
|
||||
let isLargeDoc = $state(false);
|
||||
let editor: Editor | null = null;
|
||||
let editorReady = $state(false);
|
||||
let sourceContent = $state('');
|
||||
@@ -245,6 +247,38 @@
|
||||
editor.chain().focus().insertContent({ type: nodeType, attrs: { tex: trimmed } }).run();
|
||||
}
|
||||
}
|
||||
const katexCache = new Map<string, string>();
|
||||
function renderKatex(tex: string, displayMode: boolean): string {
|
||||
const key = (displayMode ? 'B:' : 'I:') + tex;
|
||||
let html = katexCache.get(key);
|
||||
if (html === undefined) {
|
||||
try {
|
||||
html = katex.renderToString(tex, { displayMode, throwOnError: false });
|
||||
} catch {
|
||||
html = `<span class="katex-error">${tex}</span>`;
|
||||
}
|
||||
katexCache.set(key, html);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
let mathObserver: IntersectionObserver | null = null;
|
||||
const mathPending = new WeakMap<Element, () => void>();
|
||||
function observeMath(dom: HTMLElement, render: () => void) {
|
||||
if (!mathObserver) {
|
||||
const root = (editorElement?.closest('.editor-body') as Element) ?? null;
|
||||
mathObserver = new IntersectionObserver((entries) => {
|
||||
for (const e of entries) {
|
||||
if (!e.isIntersecting) continue;
|
||||
const fn = mathPending.get(e.target);
|
||||
if (fn) { mathPending.delete(e.target); mathObserver!.unobserve(e.target); fn(); }
|
||||
}
|
||||
}, { root, rootMargin: '1000px 0px' });
|
||||
}
|
||||
mathPending.set(dom, render);
|
||||
mathObserver.observe(dom);
|
||||
}
|
||||
|
||||
function renderMathPreview(tex: string, displayMode: boolean): string {
|
||||
if (!tex.trim()) return '';
|
||||
try {
|
||||
@@ -453,7 +487,7 @@
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
const tex = HTMLAttributes.tex || '';
|
||||
const rendered = katex.renderToString(tex, { displayMode: true, throwOnError: false });
|
||||
const rendered = renderKatex(tex, true);
|
||||
return ['div', { 'data-math-block': encodeURIComponent(tex), class: 'math-block', contenteditable: 'false' }, ['div', { innerHTML: rendered }]];
|
||||
},
|
||||
addNodeView() {
|
||||
@@ -462,14 +496,15 @@
|
||||
dom.classList.add('math-block');
|
||||
dom.contentEditable = 'false';
|
||||
dom.setAttribute('data-math-block', encodeURIComponent(node.attrs.tex));
|
||||
dom.innerHTML = katex.renderToString(node.attrs.tex, { displayMode: true, throwOnError: false });
|
||||
const render = () => { dom.innerHTML = renderKatex(node.attrs.tex, true); };
|
||||
if (isLargeDoc) { dom.textContent = node.attrs.tex; observeMath(dom, render); } else { render(); }
|
||||
dom.addEventListener('dblclick', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const pos = typeof getPos === 'function' ? getPos() : null;
|
||||
if (pos !== null && pos !== undefined) openMathEdit(pos, 'block', node.attrs.tex);
|
||||
});
|
||||
return { dom };
|
||||
return { dom, destroy() { mathObserver?.unobserve(dom); mathPending.delete(dom); } };
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -490,7 +525,7 @@
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
const tex = HTMLAttributes.tex || '';
|
||||
const rendered = katex.renderToString(tex, { displayMode: false, throwOnError: false });
|
||||
const rendered = renderKatex(tex, false);
|
||||
return ['span', { 'data-math-inline': encodeURIComponent(tex), class: 'math-inline', contenteditable: 'false' }, ['span', { innerHTML: rendered }]];
|
||||
},
|
||||
addNodeView() {
|
||||
@@ -499,14 +534,15 @@
|
||||
dom.classList.add('math-inline');
|
||||
dom.contentEditable = 'false';
|
||||
dom.setAttribute('data-math-inline', encodeURIComponent(node.attrs.tex));
|
||||
dom.innerHTML = katex.renderToString(node.attrs.tex, { displayMode: false, throwOnError: false });
|
||||
const render = () => { dom.innerHTML = renderKatex(node.attrs.tex, false); };
|
||||
if (isLargeDoc) { dom.textContent = node.attrs.tex; observeMath(dom, render); } else { render(); }
|
||||
dom.addEventListener('dblclick', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const pos = typeof getPos === 'function' ? getPos() : null;
|
||||
if (pos !== null && pos !== undefined) openMathEdit(pos, 'inline', node.attrs.tex);
|
||||
});
|
||||
return { dom };
|
||||
return { dom, destroy() { mathObserver?.unobserve(dom); mathPending.delete(dom); } };
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1912,6 +1948,7 @@
|
||||
loadedPath = path;
|
||||
lastSourceMode = $sourceMode;
|
||||
isLoadingNote = true;
|
||||
isLargeDoc = content.length > LARGE_DOC_CHARS;
|
||||
// Apply default view mode when switching notes - but new notes always open in edit mode.
|
||||
// Viewer mode (external file) always forces read-only.
|
||||
const isViewer = !!get(viewerNote);
|
||||
@@ -2633,9 +2670,7 @@
|
||||
if (!mathBlock) { mathBlock = []; continue; }
|
||||
const tex = mathBlock.join('\n').trim();
|
||||
mathBlock = null;
|
||||
try {
|
||||
outLines.push(`<div data-math-block="${encodeURIComponent(tex)}" class="math-block">${katex.renderToString(tex, { displayMode: true, throwOnError: false })}</div>`);
|
||||
} catch { outLines.push('$$', tex, '$$'); }
|
||||
outLines.push(`<div data-math-block="${encodeURIComponent(tex)}" class="math-block"></div>`);
|
||||
continue;
|
||||
}
|
||||
if (mathBlock) { mathBlock.push(line); continue; }
|
||||
@@ -2645,11 +2680,9 @@
|
||||
let offset = 0;
|
||||
for (const m of processed.matchAll(/(?<!\$)\$(?!\$)([^\n$]+?)(?<!\$)\$(?!\$)/g)) {
|
||||
const tex = m[1].trim();
|
||||
try {
|
||||
const html = `<span data-math-inline="${encodeURIComponent(tex)}" class="math-inline">${katex.renderToString(tex, { displayMode: false, throwOnError: false })}</span>`;
|
||||
result = result.slice(0, m.index! + offset) + html + result.slice(m.index! + m[0].length + offset);
|
||||
offset += html.length - m[0].length;
|
||||
} catch { /* leave as-is */ }
|
||||
const html = `<span data-math-inline="${encodeURIComponent(tex)}" class="math-inline"></span>`;
|
||||
result = result.slice(0, m.index! + offset) + html + result.slice(m.index! + m[0].length + offset);
|
||||
offset += html.length - m[0].length;
|
||||
}
|
||||
outLines.push(result);
|
||||
}
|
||||
@@ -2769,6 +2802,8 @@
|
||||
editor.destroy();
|
||||
editor = null;
|
||||
}
|
||||
mathObserver?.disconnect();
|
||||
mathObserver = null;
|
||||
editorReady = false;
|
||||
closeSlashMenu();
|
||||
}
|
||||
@@ -2777,6 +2812,7 @@
|
||||
if (!editorElement) return;
|
||||
destroyEditor();
|
||||
|
||||
isLargeDoc = content.length > LARGE_DOC_CHARS;
|
||||
const html = markdownToHtml(content);
|
||||
|
||||
editor = new Editor({
|
||||
@@ -4312,7 +4348,7 @@
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="tiptap-wrapper" style={$sourceMode ? 'display:none' : ''} spellcheck="false" bind:this={editorElement} onclick={(e) => { closeLinkContextMenu(); handleEditorClick(e); showOutline = false; }}></div>
|
||||
<div class="tiptap-wrapper" class:large-doc={isLargeDoc} style={$sourceMode ? 'display:none' : ''} spellcheck="false" bind:this={editorElement} onclick={(e) => { closeLinkContextMenu(); handleEditorClick(e); showOutline = false; }}></div>
|
||||
{:else}
|
||||
<!-- Desktop: conditional rendering with line numbers -->
|
||||
{#if $sourceMode}
|
||||
@@ -4420,7 +4456,7 @@
|
||||
></textarea>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="tiptap-wrapper" spellcheck="false" bind:this={editorElement} onclick={(e) => { closeLinkContextMenu(); handleEditorClick(e); showOutline = false; }} oncontextmenu={handleEditorContextMenu}></div>
|
||||
<div class="tiptap-wrapper" class:large-doc={isLargeDoc} spellcheck="false" bind:this={editorElement} onclick={(e) => { closeLinkContextMenu(); handleEditorClick(e); showOutline = false; }} oncontextmenu={handleEditorContextMenu}></div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
onnavigate: (path: string, title: string) => void;
|
||||
} = $props();
|
||||
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
|
||||
let canvas = $state<HTMLCanvasElement>(null!);
|
||||
let loading = $state(true);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { VaultStats } from '$lib/types';
|
||||
|
||||
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
|
||||
let stats = $state<VaultStats | null>(null);
|
||||
let activeTab = $state<'about' | 'shortcuts'>(isMobile ? 'about' : 'shortcuts');
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
} = $props();
|
||||
|
||||
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
const isAndroid = /android/i.test(navigator.userAgent);
|
||||
let multiSelectMode = $state(false);
|
||||
let trashNotebooks = $state<TrashNotebookEntry[]>([]);
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
|
||||
async function openResult(result: SearchResult) {
|
||||
try {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { openUrl } from '$lib/api';
|
||||
import type { ImportResult, BackupEntry } from '$lib/types';
|
||||
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
|
||||
type Tab = 'general' | 'editor' | 'styling' | 'import' | 'backup' | 'ai' | 'updates';
|
||||
let activeTab = $state<Tab>('styling');
|
||||
@@ -301,7 +301,7 @@
|
||||
];
|
||||
|
||||
const fontFamilyPresets = [
|
||||
{ name: 'System', value: 'system', stack: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' },
|
||||
{ name: 'System', value: 'system', stack: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' },
|
||||
{ name: 'Inter', value: 'inter', stack: '"Inter", -apple-system, BlinkMacSystemFont, sans-serif' },
|
||||
{ name: 'Georgia', value: 'georgia', stack: 'Georgia, "Times New Roman", serif' },
|
||||
{ name: 'Merriweather', value: 'merriweather', stack: '"Merriweather", Georgia, serif' },
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
} = $props();
|
||||
|
||||
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
|
||||
let editingNotebook = $state<string | null>(null);
|
||||
let editValue = $state('');
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { documentDir } from '@tauri-apps/api/path';
|
||||
import { openVault, getAppConfig } from '$lib/api';
|
||||
import { appConfig, vaultReady } from '$lib/stores/app';
|
||||
import { isAndroid, isIOS, isMobile } from '$lib/platform';
|
||||
import type { VaultConfig } from '$lib/types';
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
|
||||
let recentVaults: VaultConfig[] = $derived($appConfig?.vaults ?? []);
|
||||
let loading = $state(false);
|
||||
let error = $state('');
|
||||
let vaultName = $state('HelixNotes');
|
||||
let hasPermission = $state(!isMobile);
|
||||
let hasPermission = $state(!isAndroid);
|
||||
let selectedLocation = $state('Documents');
|
||||
let customPath = $state('');
|
||||
let iosBasePath = $state('');
|
||||
|
||||
const storageLocations = [
|
||||
{ label: 'Documents', path: '/storage/emulated/0/Documents' },
|
||||
@@ -22,7 +24,7 @@
|
||||
{ label: 'Internal Storage', path: '/storage/emulated/0' },
|
||||
];
|
||||
|
||||
if (isMobile) {
|
||||
if (isAndroid) {
|
||||
checkPermission();
|
||||
setTimeout(checkPermission, 500);
|
||||
(window as any).__storagePermissionGranted = () => { hasPermission = true; };
|
||||
@@ -30,6 +32,10 @@
|
||||
if (document.visibilityState === 'visible') setTimeout(checkPermission, 300);
|
||||
});
|
||||
}
|
||||
if (isIOS) {
|
||||
// iOS is sandboxed: the vault lives in the app's Documents dir (exposed via the Files app).
|
||||
documentDir().then((d) => { iosBasePath = d.replace(/\/+$/, ''); }).catch(() => {});
|
||||
}
|
||||
|
||||
function checkPermission() {
|
||||
try {
|
||||
@@ -56,10 +62,17 @@
|
||||
return storageLocations.find(l => l.label === selectedLocation)?.path || '/storage/emulated/0/Documents';
|
||||
}
|
||||
|
||||
let fullPath = $derived(isMobile ? `${getMobileBasePath()}/${vaultName.trim() || 'HelixNotes'}` : '');
|
||||
let fullPath = $derived(
|
||||
isIOS ? `${iosBasePath}/${vaultName.trim() || 'HelixNotes'}`
|
||||
: isAndroid ? `${getMobileBasePath()}/${vaultName.trim() || 'HelixNotes'}`
|
||||
: ''
|
||||
);
|
||||
|
||||
async function pickFolder() {
|
||||
if (isMobile) {
|
||||
if (isIOS) {
|
||||
const base = iosBasePath || (await documentDir()).replace(/\/+$/, '');
|
||||
await openSelectedVault(`${base}/${vaultName.trim() || 'HelixNotes'}`);
|
||||
} else if (isAndroid) {
|
||||
await openSelectedVault(fullPath);
|
||||
} else {
|
||||
const selected = await open({ directory: true, multiple: false, title: 'Choose Notes Folder' });
|
||||
@@ -73,7 +86,7 @@
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
if (isMobile) {
|
||||
if (isAndroid) {
|
||||
try {
|
||||
const bridge = (window as any).Android;
|
||||
bridge?.prepareVaultDir?.(path);
|
||||
@@ -84,7 +97,12 @@
|
||||
$appConfig = config;
|
||||
$vaultReady = true;
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
const msg = String(e);
|
||||
if (isAndroid && /os error 13|permission denied/i.test(msg)) {
|
||||
error = 'Storage permission is required. Enable file access for HelixNotes in your system settings, then try again.';
|
||||
} else {
|
||||
error = msg;
|
||||
}
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -118,15 +136,20 @@
|
||||
<h1>HelixNotes</h1>
|
||||
<p class="subtitle">Local markdown notes</p>
|
||||
{#if isMobile}
|
||||
<p class="description">Choose where to store your notes. Sync with Syncthing, Nextcloud, or any file sync app.</p>
|
||||
{#if isIOS}
|
||||
<p class="description">Your notes are saved in the app's folder and appear in the Files app under "On My iPhone → HelixNotes".</p>
|
||||
{:else}
|
||||
<p class="description">Choose where to store your notes. Sync with Syncthing, Nextcloud, or any file sync app.</p>
|
||||
{/if}
|
||||
|
||||
{#if !hasPermission}
|
||||
{#if isAndroid && !hasPermission}
|
||||
<button class="btn-permission" onclick={requestPermission}>
|
||||
Grant File Access
|
||||
</button>
|
||||
<p class="permission-hint">Required to access Documents, Downloads, and other shared folders.</p>
|
||||
{/if}
|
||||
|
||||
{#if isAndroid}
|
||||
<div class="location-selector">
|
||||
<label class="location-label">Location</label>
|
||||
<div class="location-options">
|
||||
@@ -147,6 +170,7 @@
|
||||
<input class="custom-path-input" type="text" bind:value={customPath} placeholder="/storage/emulated/0/..." />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="vault-name-input">
|
||||
<label for="vault-name">Vault name</label>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Centralized platform detection. iOS WebViews report iPhone/iPad/iPod in the user
|
||||
// agent (not the literal "ios"), so they must be matched explicitly.
|
||||
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : '';
|
||||
|
||||
export const isAndroid = /android/i.test(ua);
|
||||
export const isIOS = /iphone|ipad|ipod/i.test(ua);
|
||||
export const isMobile = isAndroid || isIOS;
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const isMobile = /android|ios/i.test(navigator.userAgent);
|
||||
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
|
||||
|
||||
// Reactively apply theme class to <html> whenever $theme changes
|
||||
$effect(() => {
|
||||
@@ -160,6 +160,25 @@
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// On Windows the native OS drag-drop handler is disabled (dragDropEnabled:false
|
||||
// in tauri.windows.conf.json) so HTML5 drag-and-drop works for reordering. With it
|
||||
// off, a file dropped outside a drop zone would make the webview navigate to / open
|
||||
// that file, replacing the app. Swallow any drag that bubbles up unhandled. Real drop
|
||||
// zones (editor, sidebar reordering) call preventDefault in their own handlers first;
|
||||
// these bubble-phase listeners only act as a fallback. On macOS/Linux OS file drops are
|
||||
// intercepted natively and never surface as HTML5 events, so this is a harmless no-op there.
|
||||
onMount(() => {
|
||||
function preventNavigate(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
window.addEventListener('dragover', preventNavigate);
|
||||
window.addEventListener('drop', preventNavigate);
|
||||
return () => {
|
||||
window.removeEventListener('dragover', preventNavigate);
|
||||
window.removeEventListener('drop', preventNavigate);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:document oncontextmenu={(e) => { if (!isMobile) e.preventDefault(); }} />
|
||||
|
||||
Reference in New Issue
Block a user