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
+14
View File
@@ -38,7 +38,21 @@ pub fn run() {
let close_to_tray = config.close_to_tray && show_tray; let close_to_tray = config.close_to_tray && show_tray;
let app_state = AppState::new(config); let app_state = AppState::new(config);
// Inject the compile-time platform so the frontend never sniffs the (sometimes
// mobile-looking) WebKitGTK user-agent. (#63)
let platform_init = format!(
"window.__HELIX_PLATFORM__={{mobile:{},android:{},ios:{}}};",
cfg!(mobile),
cfg!(target_os = "android"),
cfg!(target_os = "ios"),
);
let mut builder = tauri::Builder::default() let mut builder = tauri::Builder::default()
.plugin(
tauri::plugin::Builder::<tauri::Wry>::new("helix-platform")
.js_init_script(platform_init)
.build(),
)
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
+1 -2
View File
@@ -58,9 +58,8 @@
const appWindow = getCurrentWindow(); const appWindow = getCurrentWindow();
const isMac = navigator.platform.startsWith('Mac'); const isMac = navigator.platform.startsWith('Mac');
const isMobile = $derived($platformIsMobile); 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 { 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 { debounce } from '$lib/utils/debounce';
import { openNoteWindow } from '$lib/utils/window'; import { openNoteWindow } from '$lib/utils/window';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
+1 -2
View File
@@ -49,9 +49,9 @@
import { wrapTextareaSelection } from '$lib/editor/source/selectionPairs'; import { wrapTextareaSelection } from '$lib/editor/source/selectionPairs';
import GraphView from './GraphView.svelte'; import GraphView from './GraphView.svelte';
import TagSuggestInput from './TagSuggestInput.svelte'; import TagSuggestInput from './TagSuggestInput.svelte';
import { isMobile, isAndroid } from '$lib/platform';
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl'; 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 // Track virtual keyboard height on mobile via visualViewport
let keyboardHeight = $state(0); let keyboardHeight = $state(0);
@@ -1028,7 +1028,6 @@
function addToolbar(container: HTMLElement, source: string) { function addToolbar(container: HTMLElement, source: string) {
const toolbar = document.createElement('div'); const toolbar = document.createElement('div');
toolbar.className = 'mermaid-render-toolbar'; toolbar.className = 'mermaid-render-toolbar';
const isAndroid = /android/i.test(navigator.userAgent);
if (!isAndroid) { if (!isAndroid) {
const copyBtn = document.createElement('button'); const copyBtn = document.createElement('button');
+1 -2
View File
@@ -2,14 +2,13 @@
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { getGraphData } from '$lib/api'; import { getGraphData } from '$lib/api';
import { activeNotePath, appConfig } from '$lib/stores/app'; import { activeNotePath, appConfig } from '$lib/stores/app';
import { isMobile } from '$lib/platform';
let { onclose, onnavigate }: { let { onclose, onnavigate }: {
onclose: () => void; onclose: () => void;
onnavigate: (path: string, title: string) => void; onnavigate: (path: string, title: string) => void;
} = $props(); } = $props();
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
let canvas = $state<HTMLCanvasElement>(null!); let canvas = $state<HTMLCanvasElement>(null!);
let loading = $state(true); let loading = $state(true);
let searchQuery = $state(''); let searchQuery = $state('');
+1 -1
View File
@@ -15,9 +15,9 @@
bindingsEqual, bindingsEqual,
type ActionDef, type ActionDef,
} from '$lib/keybindings'; } from '$lib/keybindings';
import { isMobile } from '$lib/platform';
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl'; const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
// Customizable shortcuts, grouped for display. // Customizable shortcuts, grouped for display.
const actionGroups: { title: string; actions: ActionDef[] }[] = [ const actionGroups: { title: string; actions: ActionDef[] }[] = [
+1 -2
View File
@@ -43,6 +43,7 @@
import type { NoteEntry, TrashNotebookEntry, SortMode, TaskItem } from '$lib/types'; import type { NoteEntry, TrashNotebookEntry, SortMode, TaskItem } from '$lib/types';
import TasksView from './TasksView.svelte'; import TasksView from './TasksView.svelte';
import TagSuggestInput from './TagSuggestInput.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) => {} }: { 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; onNoteSelected?: (path: string, content: string) => void;
@@ -55,8 +56,6 @@
} = $props(); } = $props();
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl'; 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 multiSelectMode = $state(false);
let trashNotebooks = $state<TrashNotebookEntry[]>([]); let trashNotebooks = $state<TrashNotebookEntry[]>([]);
let trashBusy = $state<string | null>(null); let trashBusy = $state<string | null>(null);
+1 -2
View File
@@ -3,6 +3,7 @@
import { searchNotes, readNote } from '$lib/api'; import { searchNotes, readNote } from '$lib/api';
import { debounce } from '$lib/utils/debounce'; import { debounce } from '$lib/utils/debounce';
import type { SearchResult } from '$lib/types'; import type { SearchResult } from '$lib/types';
import { isMobile } from '$lib/platform';
let query = $state(''); let query = $state('');
let results = $state<SearchResult[]>([]); let results = $state<SearchResult[]>([]);
@@ -83,8 +84,6 @@
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); 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) { async function openResult(result: SearchResult) {
try { try {
const content = await readNote(result.path); const content = await readNote(result.path);
+1 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <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 { 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 { 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 { open as openDialog } from '@tauri-apps/plugin-dialog';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { getVersion } from '@tauri-apps/api/app'; import { getVersion } from '@tauri-apps/api/app';
@@ -9,7 +9,6 @@
import { openUrl } from '$lib/api'; import { openUrl } from '$lib/api';
import type { ImportResult, BackupEntry } from '$lib/types'; import type { ImportResult, BackupEntry } from '$lib/types';
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl'; const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl';
type Tab = 'general' | 'editor' | 'styling' | 'import' | 'backup' | 'ai' | 'sync' | 'updates'; 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 { readFile } from '@tauri-apps/plugin-fs';
import { convertFileSrc } from '@tauri-apps/api/core'; import { convertFileSrc } from '@tauri-apps/api/core';
import type { NotebookEntry } from '$lib/types'; import type { NotebookEntry } from '$lib/types';
import { isMobile } from '$lib/platform';
let { onViewChanged = () => {} }: { let { onViewChanged = () => {} }: {
onViewChanged?: () => void; onViewChanged?: () => void;
} = $props(); } = $props();
const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl'; 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. // 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. // 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 // Platform is the compile-time build target, injected by the backend into
// agent (not the literal "ios"), so they must be matched explicitly. // 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 : ''; const ua = typeof navigator !== 'undefined' ? navigator.userAgent : '';
export const isAndroid = /android/i.test(ua); export const isAndroid = injected ? injected.android : /android/i.test(ua);
export const isIOS = /iphone|ipad|ipod/i.test(ua); export const isIOS = injected ? injected.ios : /iphone|ipad|ipod/i.test(ua);
export const isMobile = isAndroid || isIOS; export const isMobile = injected ? injected.mobile : isAndroid || isIOS;
// Themes that use the dark color scheme. Used by applyTheme() to toggle the // 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. // `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 { writable, derived, get } from "svelte/store";
import { isMobile } from "$lib/platform";
import type { import type {
AppConfig, AppConfig,
NoteEntry, NoteEntry,
@@ -87,11 +88,8 @@ export const updateAvailable = writable<{
} | null>(null); } | null>(null);
export const updateObj = writable<any>(null); export const updateObj = writable<any>(null);
export const installType = writable<string>("native"); export const installType = writable<string>("native");
// True only on the Android/iOS build. Defaults to a user-agent guess for the first paint, then // True only on the Android/iOS build. Reconfirmed from the backend at startup (see +layout). (#63)
// gets the authoritative compile-time value from the backend at startup (see +layout). (#63) export const platformIsMobile = writable<boolean>(isMobile);
export const platformIsMobile = writable<boolean>(
typeof navigator !== "undefined" && /android|iphone|ipad|ipod/i.test(navigator.userAgent),
);
export const androidApkUrl = writable<string | null>(null); export const androidApkUrl = writable<string | null>(null);
// Install types that handle their own updates (in-app auto-updater, or a // Install types that handle their own updates (in-app auto-updater, or a
+1 -5
View File
@@ -4,13 +4,11 @@
import { theme, appConfig, activeNote, activeNotePath, installType, platformIsMobile, checkForUpdate, checkForUpdateMobile, isManagedInstall } from '$lib/stores/app'; import { theme, appConfig, activeNote, activeNotePath, installType, platformIsMobile, checkForUpdate, checkForUpdateMobile, isManagedInstall } from '$lib/stores/app';
import { openFile, openUrl, readNote, getInstallType, isMobilePlatform } from '$lib/api'; import { openFile, openUrl, readNote, getInstallType, isMobilePlatform } from '$lib/api';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { darkThemes } from '$lib/platform'; import { darkThemes, isMobile, isAndroid } from '$lib/platform';
import ResizeHandles from '$lib/components/ResizeHandles.svelte'; import ResizeHandles from '$lib/components/ResizeHandles.svelte';
let { children } = $props(); let { children } = $props();
const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent);
// Reactively apply theme class to <html> whenever $theme changes // Reactively apply theme class to <html> whenever $theme changes
$effect(() => { $effect(() => {
applyTheme($theme); applyTheme($theme);
@@ -46,8 +44,6 @@
return resolved.join('/'); return resolved.join('/');
} }
const isAndroid = /android/i.test(navigator.userAgent);
function openLocalFile(path: string) { function openLocalFile(path: string) {
if (isAndroid) { if (isAndroid) {
const bridge = (window as any).Android; const bridge = (window as any).Android;