Fix #63: decide layout from compile-time platform, not the webview user-agent

This commit is contained in:
Yuri Karamian
2026-06-23 13:12:36 +02:00
parent ac5ab2a27a
commit eeea16531a
12 changed files with 40 additions and 29 deletions
+1 -2
View File
@@ -58,9 +58,8 @@
const appWindow = getCurrentWindow();
const isMac = navigator.platform.startsWith('Mac');
const isMobile = $derived($platformIsMobile);
const isAndroid = /android/i.test(navigator.userAgent);
import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, setTaskDone, setTaskPriority, setTaskDue } from '$lib/api';
import { darkThemes } from '$lib/platform';
import { darkThemes, isAndroid } from '$lib/platform';
import { debounce } from '$lib/utils/debounce';
import { openNoteWindow } from '$lib/utils/window';
import { get } from 'svelte/store';
+1 -2
View File
@@ -49,9 +49,9 @@
import { wrapTextareaSelection } from '$lib/editor/source/selectionPairs';
import GraphView from './GraphView.svelte';
import TagSuggestInput from './TagSuggestInput.svelte';
import { isMobile, isAndroid } from '$lib/platform';
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
// Track virtual keyboard height on mobile via visualViewport
let keyboardHeight = $state(0);
@@ -1028,7 +1028,6 @@
function addToolbar(container: HTMLElement, source: string) {
const toolbar = document.createElement('div');
toolbar.className = 'mermaid-render-toolbar';
const isAndroid = /android/i.test(navigator.userAgent);
if (!isAndroid) {
const copyBtn = document.createElement('button');
+1 -2
View File
@@ -2,14 +2,13 @@
import { onDestroy } from 'svelte';
import { getGraphData } from '$lib/api';
import { activeNotePath, appConfig } from '$lib/stores/app';
import { isMobile } from '$lib/platform';
let { onclose, onnavigate }: {
onclose: () => void;
onnavigate: (path: string, title: string) => void;
} = $props();
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
let canvas = $state<HTMLCanvasElement>(null!);
let loading = $state(true);
let searchQuery = $state('');
+1 -1
View File
@@ -15,9 +15,9 @@
bindingsEqual,
type ActionDef,
} from '$lib/keybindings';
import { isMobile } from '$lib/platform';
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
// Customizable shortcuts, grouped for display.
const actionGroups: { title: string; actions: ActionDef[] }[] = [
+1 -2
View File
@@ -43,6 +43,7 @@
import type { NoteEntry, TrashNotebookEntry, SortMode, TaskItem } from '$lib/types';
import TasksView from './TasksView.svelte';
import TagSuggestInput from './TagSuggestInput.svelte';
import { isMobile, isAndroid } from '$lib/platform';
let { onNoteSelected = (_path: string, _content: string) => {}, onNoteMoved = () => {}, onBeforeNoteSwitch = () => {}, onNoteCreated = () => {}, onToggleTask = async (_t: TaskItem) => {}, onSetTaskPriority = async (_t: TaskItem, _p: string | null) => {}, onSetTaskDue = async (_t: TaskItem, _d: string | null) => {} }: {
onNoteSelected?: (path: string, content: string) => void;
@@ -55,8 +56,6 @@
} = $props();
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
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[]>([]);
let trashBusy = $state<string | null>(null);
+1 -2
View File
@@ -3,6 +3,7 @@
import { searchNotes, readNote } from '$lib/api';
import { debounce } from '$lib/utils/debounce';
import type { SearchResult } from '$lib/types';
import { isMobile } from '$lib/platform';
let query = $state('');
let results = $state<SearchResult[]>([]);
@@ -83,8 +84,6 @@
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
async function openResult(result: SearchResult) {
try {
const content = await readNote(result.path);
+1 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { showSettings, theme, appConfig, updateAvailable as globalUpdateAvailable, updateObj as globalUpdateObj, installType, settingsTab, vaultReady, androidApkUrl, checkForUpdateMobile, notebookSortMode, isManagedInstall } from '$lib/stores/app';
import { setTheme, setAccentColor, setFontSize, setFontFamily, setLineHeight, setUiScale, setContentWidth, setGeneralSettings, importObsidian, createBackup, listBackups, restoreBackup, deleteBackup, setBackupSettings, setAiSettings, testAiConnection, setSyncSettings, testSyncConnection, syncNow } from '$lib/api';
import { darkThemes } from '$lib/platform';
import { darkThemes, isMobile } from '$lib/platform';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import { listen } from '@tauri-apps/api/event';
import { getVersion } from '@tauri-apps/api/app';
@@ -9,7 +9,6 @@
import { openUrl } from '$lib/api';
import type { ImportResult, BackupEntry } from '$lib/types';
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
type Tab = 'general' | 'editor' | 'styling' | 'import' | 'backup' | 'ai' | 'sync' | 'updates';
+1 -1
View File
@@ -26,13 +26,13 @@
import { readFile } from '@tauri-apps/plugin-fs';
import { convertFileSrc } from '@tauri-apps/api/core';
import type { NotebookEntry } from '$lib/types';
import { isMobile } from '$lib/platform';
let { onViewChanged = () => {} }: {
onViewChanged?: () => void;
} = $props();
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
// Whether any top nav item (All Notes, Quick Access, Tasks, Daily Notes, Trash) is visible.
// When all are hidden we drop the empty <nav> and its divider so the tree sits flush.
+14 -5
View File
@@ -1,10 +1,19 @@
// Centralized platform detection. iOS WebViews report iPhone/iPad/iPod in the user
// agent (not the literal "ios"), so they must be matched explicitly.
// Platform is the compile-time build target, injected by the backend into
// window.__HELIX_PLATFORM__ before app scripts run (see src-tauri/src/lib.rs). The UA is only a
// fallback when that global is absent (SSR/prerender, plain-browser dev): some desktop WebKitGTK
// builds report a mobile-looking user-agent. (#63)
type HelixPlatform = { mobile: boolean; android: boolean; ios: boolean };
const injected: HelixPlatform | undefined =
typeof window !== 'undefined'
? (window as unknown as { __HELIX_PLATFORM__?: HelixPlatform }).__HELIX_PLATFORM__
: undefined;
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;
export const isAndroid = injected ? injected.android : /android/i.test(ua);
export const isIOS = injected ? injected.ios : /iphone|ipad|ipod/i.test(ua);
export const isMobile = injected ? injected.mobile : isAndroid || isIOS;
// Themes that use the dark color scheme. Used by applyTheme() to toggle the
// `dark` class on the root element. Keep this in sync when adding new themes.
+3 -5
View File
@@ -1,4 +1,5 @@
import { writable, derived, get } from "svelte/store";
import { isMobile } from "$lib/platform";
import type {
AppConfig,
NoteEntry,
@@ -87,11 +88,8 @@ export const updateAvailable = writable<{
} | null>(null);
export const updateObj = writable<any>(null);
export const installType = writable<string>("native");
// True only on the Android/iOS build. Defaults to a user-agent guess for the first paint, then
// gets the authoritative compile-time value from the backend at startup (see +layout). (#63)
export const platformIsMobile = writable<boolean>(
typeof navigator !== "undefined" && /android|iphone|ipad|ipod/i.test(navigator.userAgent),
);
// True only on the Android/iOS build. Reconfirmed from the backend at startup (see +layout). (#63)
export const platformIsMobile = writable<boolean>(isMobile);
export const androidApkUrl = writable<string | null>(null);
// Install types that handle their own updates (in-app auto-updater, or a