From 5e2663b26b257fc0154f70934df4bc4f5f17b1f7 Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Sat, 13 Jun 2026 23:13:39 +0800 Subject: [PATCH 1/2] feat(ui): add mobile portrait shell with bottom nav and responsive pages Replace fixed sidebar on narrow/Android with top bar, bottom tabs, and more sheet; adapt Overview, History, Vocab, and Settings for portrait; add android-ui-verify.ps1. Co-authored-by: Cursor --- .../app/scripts/android-ui-verify.ps1 | 86 ++++++ .../app/src/components/FloatingShell.tsx | 255 ++++++++++++++++-- openless-all/app/src/components/Icon.tsx | 1 + .../app/src/components/MobileMoreSheet.tsx | 129 +++++++++ .../app/src/components/SettingsModal.tsx | 152 +++++++++-- .../app/src/components/WindowChrome.tsx | 2 +- openless-all/app/src/i18n/en.ts | 2 + openless-all/app/src/i18n/ja.ts | 2 + openless-all/app/src/i18n/ko.ts | 2 + openless-all/app/src/i18n/zh-CN.ts | 2 + openless-all/app/src/i18n/zh-TW.ts | 2 + openless-all/app/src/pages/History.tsx | 25 +- openless-all/app/src/pages/Overview.tsx | 8 +- openless-all/app/src/pages/Vocab.tsx | 8 +- openless-all/app/src/pages/_atoms.tsx | 13 +- 15 files changed, 634 insertions(+), 55 deletions(-) create mode 100644 openless-all/app/scripts/android-ui-verify.ps1 create mode 100644 openless-all/app/src/components/MobileMoreSheet.tsx diff --git a/openless-all/app/scripts/android-ui-verify.ps1 b/openless-all/app/scripts/android-ui-verify.ps1 new file mode 100644 index 00000000..9a125368 --- /dev/null +++ b/openless-all/app/scripts/android-ui-verify.ps1 @@ -0,0 +1,86 @@ +# android-ui-verify.ps1 — Trigger CI Android debug APK build (optional), install via adb, +# capture logcat, and save UI screenshots. Artifacts stay local; do not commit outputs. +param( + [string]$Repo = "HKLHaoBin/openless", + [string]$Branch = "feat/mobile-portrait-ui", + [string]$Workflow = "Android APK (debug)", + [string]$ArtifactName = "openless-android-debug-arm64-v8a", + [switch]$SkipBuild, + [int]$StartupWaitSeconds = 8, + [string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\\..\\..")).Path +) + +$ErrorActionPreference = "Stop" +Set-Location $ProjectRoot + +function Require-Command($name) { + if (-not (Get-Command $name -ErrorAction SilentlyContinue)) { + throw "Required command not found: $name" + } +} + +Require-Command adb +if (-not $SkipBuild) { + Require-Command gh +} + +$devices = adb devices | Select-String "device$" +if (-not $devices) { + throw "No adb device in 'device' state. Connect phone and enable USB debugging." +} + +$ciDir = Join-Path $ProjectRoot "ci-artifacts" +$shotDir = Join-Path $ProjectRoot "android-screenshots" +New-Item -ItemType Directory -Force -Path $ciDir, $shotDir | Out-Null + +if (-not $SkipBuild) { + Write-Host "Triggering workflow '$Workflow' on $Repo ref $Branch ..." + gh workflow run $Workflow --repo $Repo --ref $Branch + Start-Sleep -Seconds 5 + $runLine = gh run list --repo $Repo --workflow $Workflow --limit 1 --json databaseId,status --jq '.[0]' + $run = $runLine | ConvertFrom-Json + if (-not $run.databaseId) { + throw "Could not resolve latest workflow run id." + } + $runId = $run.databaseId + Write-Host "Watching run $runId ..." + gh run watch $runId --repo $Repo --exit-status + if (Test-Path $ciDir) { + Get-ChildItem $ciDir -File | Remove-Item -Force + } + gh run download $runId --repo $Repo --name $ArtifactName --dir $ciDir +} + +$apk = Get-ChildItem -Path $ciDir -Filter "OpenLess-android-debug-*.apk" -File | Select-Object -First 1 +if (-not $apk) { + throw "No APK found in $ciDir. Run without -SkipBuild or place an APK there." +} + +Write-Host "Installing $($apk.FullName) ..." +adb logcat -c +adb install -r $apk.FullName + +Write-Host "Starting MainActivity ..." +adb shell am start -n com.openless.app/.MainActivity +Start-Sleep -Seconds $StartupWaitSeconds + +$stamp = Get-Date -Format "yyyyMMdd-HHmmss" +$crashOnly = Join-Path $ProjectRoot "android-crash-only.log" +$fullLog = Join-Path $ProjectRoot "android-crash.log" +adb logcat -b crash -d -v time | Set-Content -Path $crashOnly -Encoding utf8 +adb logcat -d -v time | Set-Content -Path $fullLog -Encoding utf8 +adb shell dumpsys package com.openless.app | Set-Content -Path (Join-Path $ProjectRoot "openless-package.txt") -Encoding utf8 + +function Capture-Screen([string]$label) { + $out = Join-Path $shotDir "openless-screenshot-$label-$stamp.png" + $bytes = adb exec-out screencap -p + if ($bytes) { + [System.IO.File]::WriteAllBytes($out, $bytes) + Write-Host "Screenshot: $out" + } +} + +Capture-Screen "overview" +Write-Host "Capture additional screens manually or extend script with adb input tap coordinates." +Write-Host "Logs: $fullLog" +Write-Host "Filter: Select-String -Path '$fullLog' -Pattern 'FATAL EXCEPTION','panic','UnsatisfiedLinkError'" diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index 13adbf90..c397ba8f 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -4,7 +4,7 @@ // // Ported verbatim from design_handoff_openless/variants.jsx::FloatingShell. -import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType, type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from './Icon'; import { WindowChrome, detectOS, type OS } from './WindowChrome'; @@ -32,8 +32,12 @@ import { shouldShowProviderSetupPrompt, } from '../lib/providerSetup'; import { type SettingsSectionId } from './SettingsModal'; +import { MobileMoreSheet } from './MobileMoreSheet'; +import { useMobileLayout } from '../lib/useMobileLayout'; import { useAppState, type AppTab } from '../state/useAppState'; +const MORE_TAB_IDS: AppTab[] = ['vocab', 'translation', 'selectionAsk']; + interface NavItem { id: AppTab; name: string; @@ -67,10 +71,12 @@ export function FloatingShell({ os: osProp, initialTab = 'overview', initialSett function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initialTab: AppTab; initialSettings: boolean }) { const { t } = useTranslation(); + const mobile = useMobileLayout(); const { currentTab, setCurrentTab, settingsOpen, setSettingsOpen } = useAppState(initialTab, initialSettings); const [settingsInitialSection, setSettingsInitialSection] = useState(); const [providerPromptOpen, setProviderPromptOpen] = useState(false); const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false); + const [moreOpen, setMoreOpen] = useState(false); // tab 切换的 cross-fade:旧页 blur+fade out(180ms),结束后挂载新页(走 ol-page-slide enter)。 // displayTab 是实际渲染的 tab,currentTab 是用户点中的目标 tab。 @@ -155,6 +161,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia const openSettings = (section?: SettingsSectionId) => { setSettingsInitialSection(section); setSettingsOpen(true); + setMoreOpen(false); }; // ⌘, 打开设置页面 @@ -180,8 +187,22 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia openSettings('general'); }; + const mobileTitle = settingsOpen + ? t('shell.footer.settings') + : (NAV.find(n => n.id === currentTab)?.name ?? t('nav.overview')); + const moreTabActive = MORE_TAB_IDS.includes(currentTab); + const useOpaqueMain = mobile || os === 'linux'; + return ( -
+
+ + {mobile && ( + openSettings()} + settingsActive={settingsOpen} + /> + )} {/* Main shell — flush with the frosted backplate (no separate float). */}
- {/* Sidebar — 透明地坐在外层磨砂底板上,让 LOGO/导航/快捷键/BETA/footer 共用同一片磨砂玻璃 */} + {/* Sidebar — desktop / wide only */} + {!mobile && (
+ )} - {/* Main content — Linux 禁用透明窗口后使用不透明面;其他平台保留玻璃层。 - 悬浮台到右边 / 下边的间距相等(都 8px),左侧贴 sidebar(0)。 */} -
+ {/* Main content — Linux 禁用透明窗口后使用不透明面;mobile 全宽无玻璃层。 */} +
+ {mobile && ( + <> + { + setMoreOpen(false); + setCurrentTab(id); + }} + onOpenMore={() => setMoreOpen(true)} + /> + setMoreOpen(false)} + onSelectTab={setCurrentTab} + onOpenSettings={() => openSettings()} + /> + + )} + {/* Settings modal — rendered inside this window */} {settingsOpen && = [ + { id: 'overview', icon: 'overview' }, + { id: 'history', icon: 'history' }, + { id: 'style', icon: 'style' }, +]; + +function MobileTopBar({ + title, + onOpenSettings, + settingsActive, +}: { + title: string; + onOpenSettings: () => void; + settingsActive: boolean; +}) { + const { t } = useTranslation(); + return ( +
+
+ + + {title} + +
+ +
+ ); +} + +function MobileBottomNav({ + currentTab, + moreOpen, + moreTabActive, + settingsOpen, + onSelectTab, + onOpenMore, +}: { + currentTab: AppTab; + moreOpen: boolean; + moreTabActive: boolean; + settingsOpen: boolean; + onSelectTab: (tab: AppTab) => void; + onOpenMore: () => void; +}) { + const { t } = useTranslation(); + const moreActive = moreOpen || moreTabActive; + + return ( + + ); +} + +const mobileNavBtnStyle: CSSProperties = { + flex: 1, + minWidth: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: 4, + padding: '6px 4px', + border: 0, + borderRadius: 10, + background: 'transparent', + fontFamily: 'inherit', + cursor: 'default', +}; + function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; onOpenSettings: () => void }) { const { t } = useTranslation(); return ( diff --git a/openless-all/app/src/components/Icon.tsx b/openless-all/app/src/components/Icon.tsx index cb0705f4..e0117fde 100644 --- a/openless-all/app/src/components/Icon.tsx +++ b/openless-all/app/src/components/Icon.tsx @@ -51,6 +51,7 @@ export const ICONS: Record = { shield: 'M12 22s8-3 8-9V5l-8-3-8 3v8c0 6 8 9 8 9z', // Shield (privacy) external: 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3', // External link close: 'M18 6L6 18M6 6l12 12', // Close / X + more: 'M5 12h.01M12 12h.01M19 12h.01', // More (horizontal dots) play: 'M5 3l14 9-14 9V3z', // Play download: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3', // Download }; diff --git a/openless-all/app/src/components/MobileMoreSheet.tsx b/openless-all/app/src/components/MobileMoreSheet.tsx new file mode 100644 index 00000000..286ec455 --- /dev/null +++ b/openless-all/app/src/components/MobileMoreSheet.tsx @@ -0,0 +1,129 @@ +import type { CSSProperties } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Icon } from './Icon'; +import type { AppTab } from '../state/useAppState'; + +const MORE_TABS: Array<{ id: AppTab; icon: string }> = [ + { id: 'vocab', icon: 'vocab' }, + { id: 'translation', icon: 'translate' }, + { id: 'selectionAsk', icon: 'selectionAsk' }, +]; + +interface MobileMoreSheetProps { + open: boolean; + currentTab: AppTab; + onClose: () => void; + onSelectTab: (tab: AppTab) => void; + onOpenSettings: () => void; +} + +export function MobileMoreSheet({ + open, + currentTab, + onClose, + onSelectTab, + onOpenSettings, +}: MobileMoreSheetProps) { + const { t } = useTranslation(); + if (!open) return null; + + return ( +
+
e.stopPropagation()} + style={{ + background: 'var(--ol-surface)', + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + border: '0.5px solid var(--ol-line)', + padding: '12px 12px calc(12px + env(safe-area-inset-bottom, 0px))', + boxShadow: '0 -8px 32px -8px rgba(15,17,22,0.18)', + animation: 'ol-mobile-sheet-up 0.26s var(--ol-motion-spring)', + }} + > +
+ {t('nav.more')} + +
+
+ {MORE_TABS.map(item => { + const active = currentTab === item.id; + return ( + + ); + })} + +
+
+
+ ); +} + +const iconBtnStyle: CSSProperties = { + width: 32, + height: 32, + border: 0, + borderRadius: 999, + background: 'transparent', + color: 'var(--ol-ink-3)', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'default', +}; + +const rowBtnStyle: CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '12px 14px', + borderRadius: 10, + border: 0, + background: 'transparent', + fontFamily: 'inherit', + fontSize: 14, + cursor: 'default', + textAlign: 'left', +}; diff --git a/openless-all/app/src/components/SettingsModal.tsx b/openless-all/app/src/components/SettingsModal.tsx index 7fe71d9e..01cf352c 100644 --- a/openless-all/app/src/components/SettingsModal.tsx +++ b/openless-all/app/src/components/SettingsModal.tsx @@ -7,12 +7,13 @@ // 设计原则:每个可见控件都必须可用。没有后端支撑的占位(账号 / 主题切换 等) // 不在此弹窗出现。 -import { useLayoutEffect, useRef, useState } from 'react'; +import { useLayoutEffect, useRef, useState, type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from './Icon'; import { SavedToast } from './SavedToast'; import { useSavedToastListener } from '../lib/savedEvent'; import { openExternal } from '../lib/ipc'; +import { useMobileLayout } from '../lib/useMobileLayout'; import type { OS } from './WindowChrome'; import { GeneralTab, ServicesTab, PrivacyTab, AdvancedTab } from '../pages/settings/tabs'; import { AboutSection } from '../pages/settings/AboutSection'; @@ -56,47 +57,97 @@ const LINK_ITEMS: ModalNavItem[] = [ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: SettingsModalProps) { const { t } = useTranslation(); + const mobile = useMobileLayout(); const [section, setSection] = useState(initialSettingsSection ?? 'general'); const savedToast = useSavedToastListener(); - // 与 sidebar nav 一致的滑动指示器:仅 tab 组有 pill;外链组永远不画 pill。 + // 与 sidebar nav 一致的滑动指示器:仅 tab 组有 pill;外链组永远不画 pill(desktop)。 const tabRefs = useRef>([]); const [pillRect, setPillRect] = useState<{ top: number; height: number } | null>(null); useLayoutEffect(() => { + if (mobile) { + setPillRect(null); + return; + } const idx = TAB_ITEMS.findIndex(it => it.id === section); const el = tabRefs.current[idx]; if (!el) return; setPillRect({ top: el.offsetTop, height: el.offsetHeight }); - }, [section]); + }, [section, mobile]); return (
e.stopPropagation()} style={{ - width: '100%', maxWidth: 880, height: '100%', maxHeight: 600, + width: '100%', + maxWidth: mobile ? undefined : 880, + height: '100%', + maxHeight: mobile ? undefined : 600, background: 'var(--ol-surface)', - borderRadius: 14, - border: '0.5px solid rgba(0,0,0,.08)', - boxShadow: '0 30px 80px -20px rgba(15,17,22,.35), 0 0 0 0.5px rgba(0,0,0,.06)', - display: 'flex', overflow: 'hidden', - animation: 'ol-modal-card-in 0.24s var(--ol-motion-spring)', + borderRadius: mobile ? 0 : 14, + border: mobile ? 'none' : '0.5px solid rgba(0,0,0,.08)', + boxShadow: mobile ? 'none' : '0 30px 80px -20px rgba(15,17,22,.35), 0 0 0 0.5px rgba(0,0,0,.06)', + display: 'flex', + flexDirection: mobile ? 'column' : 'row', + overflow: 'hidden', + animation: mobile ? undefined : 'ol-modal-card-in 0.24s var(--ol-motion-spring)', position: 'relative', }}> - {/* ─── 单层侧栏 ────────────────────────────────────────────── */} + {mobile ? ( +
+ +
+ {TAB_ITEMS.map(it => { + const active = section === it.id; + return ( + + ); + })} +
+
+ ) : ( + )} - {/* ─── 内容区 ────────────────────────────────────────────── - 父容器 overflow:hidden + 列向 flex;关闭按钮、section 标题固定在头部, - 只有最里层的 scroll wrapper 真正滚动。 */} + {/* ─── 内容区 ────────────────────────────────────────────── */}
- {/* "已保存" toast:right:54 避开 28×28 关闭按钮 + 12px gap。 */} + {!mobile && ( + )} + {!mobile && (

{t(`modal.sections.${section}`)}

+ )}
+ style={{ + flex: 1, + minHeight: 0, + overflow: 'auto', + padding: mobile ? '12px 16px calc(16px + env(safe-area-inset-bottom, 0px))' : '10px 28px 28px', + }}> {/* key=section 让切 tab 时整块重挂载,ol-tab-fade 轻微淡入。 */}
} {section === 'about' && }
+ {mobile && ( +
+ {LINK_ITEMS.map(it => ( + + ))} +
+ )}
@@ -209,6 +284,35 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett ); } +const mobileHeaderBtnStyle: CSSProperties = { + width: 36, + height: 36, + flexShrink: 0, + border: 0, + borderRadius: 10, + background: 'transparent', + color: 'var(--ol-ink-3)', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'default', +}; + +function mobileTabChipStyle(active: boolean): CSSProperties { + return { + flexShrink: 0, + padding: '6px 12px', + borderRadius: 999, + border: active ? '0.5px solid var(--ol-ink)' : '0.5px solid var(--ol-line-strong)', + background: active ? 'var(--ol-ink)' : 'transparent', + color: active ? '#fff' : 'var(--ol-ink-3)', + fontFamily: 'inherit', + fontSize: 12, + fontWeight: active ? 600 : 500, + cursor: 'default', + }; +} + const navBtnStyle = { display: 'flex', alignItems: 'center', gap: 10, padding: '7px 10px', diff --git a/openless-all/app/src/components/WindowChrome.tsx b/openless-all/app/src/components/WindowChrome.tsx index 4e75e03a..dca45a75 100644 --- a/openless-all/app/src/components/WindowChrome.tsx +++ b/openless-all/app/src/components/WindowChrome.tsx @@ -46,7 +46,7 @@ export function WindowChrome({ radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%), linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%) `; - const useSolidSurface = os === 'linux'; + const useSolidSurface = os === 'linux' || os === 'android'; return (
{ setLoading(true); @@ -176,7 +179,8 @@ export function History() {
} /> -
+
+ {( !mobile || !mobileDetailOpen) && (
(
+ )} + {(!mobile || mobileDetailOpen) && ( {item ? ( <> -
+ {mobile && ( +
+ setMobileDetailOpen(false)}> + {t('history.backToList')} + +
+ )} +
{formatTime(item.createdAt)} {MODE_LABEL[item.mode]} @@ -278,7 +294,7 @@ export function History() { key={item.id} /> )} -
+
{t('history.rawLabel')}

@@ -315,6 +331,7 @@ export function History() {

)} + )}
); diff --git a/openless-all/app/src/pages/Overview.tsx b/openless-all/app/src/pages/Overview.tsx index 118ef703..946956ec 100644 --- a/openless-all/app/src/pages/Overview.tsx +++ b/openless-all/app/src/pages/Overview.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { Icon } from '../components/Icon'; import { formatComboLabel } from '../lib/hotkey'; import { getCredentials, listHistory } from '../lib/ipc'; +import { useMobileLayout } from '../lib/useMobileLayout'; import type { CredentialsStatus, DictationSession, PolishMode } from '../lib/types'; import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { Btn, Card, PageHeader, Pill } from './_atoms'; @@ -53,6 +54,7 @@ const LLM_NAME_KEY_BY_ID: Record = { export function Overview({ onOpenHistory }: OverviewProps) { const { t } = useTranslation(); + const mobile = useMobileLayout(); const modeLabel = useModeLabels(); const [history, setHistory] = useState([]); const [historyError, setHistoryError] = useState(false); @@ -172,7 +174,7 @@ export function Overview({ onOpenHistory }: OverviewProps) { <> -
+
-
+
0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} /> @@ -197,7 +199,7 @@ export function Overview({ onOpenHistory }: OverviewProps) { {/* 底部一行 = flex:1 撑满剩余高度(父 wrapper 是 display:flex/column)。 只有「最近识别」内部允许滚动;其他卡片按内容自然高度,不破裂底部圆角。 issue #243 follow-up:去掉外层 overflow 后底部圆角被裁的视觉问题。 */} -
+
{t('overview.weekTitle')} diff --git a/openless-all/app/src/pages/Vocab.tsx b/openless-all/app/src/pages/Vocab.tsx index ee29f798..f82f28fa 100644 --- a/openless-all/app/src/pages/Vocab.tsx +++ b/openless-all/app/src/pages/Vocab.tsx @@ -16,6 +16,7 @@ import { } from '../lib/ipc'; import type { CorrectionRule, DictionaryEntry, VocabPreset } from '../lib/types'; import { DEFAULT_VOCAB_PRESETS, loadVocabPresets, persistVocabPresets } from '../lib/vocabPresets'; +import { useMobileLayout } from '../lib/useMobileLayout'; import { Btn, Card, Collapsible, PageHeader } from './_atoms'; const NEW_PRESET_DRAFT_ID = '__new__'; @@ -35,6 +36,7 @@ function isSupportedCorrectionRule(pattern: string, replacement: string) { export function Vocab() { const { t } = useTranslation(); + const mobile = useMobileLayout(); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const inputRef = useRef(null); @@ -317,21 +319,21 @@ export function Vocab() { desc={t('vocab.corrections.tip')} >
-
+
setRulePatternDraft(e.target.value)} placeholder={t('vocab.corrections.patternPlaceholder')} style={{ height: 32, padding: '0 10px', border: '0.5px solid var(--ol-line-strong)', borderRadius: 8, background: 'var(--ol-surface-2)' }} /> - + {!mobile && } setRuleReplacementDraft(e.target.value)} placeholder={t('vocab.corrections.replacementPlaceholder')} style={{ height: 32, padding: '0 10px', border: '0.5px solid var(--ol-line-strong)', borderRadius: 8, background: 'var(--ol-surface-2)' }} /> - void onAddCorrectionRule()}>{t('common.add')} + void onAddCorrectionRule()} style={mobile ? { justifySelf: 'start' } : undefined}>{t('common.add')}
{correctionRules.length === 0 && ( diff --git a/openless-all/app/src/pages/_atoms.tsx b/openless-all/app/src/pages/_atoms.tsx index 9f1bc75d..d98ce7cb 100644 --- a/openless-all/app/src/pages/_atoms.tsx +++ b/openless-all/app/src/pages/_atoms.tsx @@ -4,6 +4,7 @@ import { useState, type CSSProperties, type ReactNode } from 'react'; import { Icon } from '../components/Icon'; +import { useMobileLayout } from '../lib/useMobileLayout'; interface PageHeaderProps { kicker?: string; @@ -14,14 +15,22 @@ interface PageHeaderProps { } export function PageHeader({ kicker, title, desc, right, titleRight }: PageHeaderProps) { + const mobile = useMobileLayout(); return ( -
+
{kicker && (
{kicker}
)}
-

{title}

+

{title}

{titleRight}
{desc &&

{desc}

} From 452dca0a13aff357b0a5d75d4c9cc3c69ddca188 Mon Sep 17 00:00:00 2001 From: HKLHaoBin Date: Sat, 13 Jun 2026 23:34:54 +0800 Subject: [PATCH 2/2] fix(scripts): improve android-ui-verify uninstall and screencap on Windows Co-authored-by: Cursor --- openless-all/app/scripts/android-ui-verify.ps1 | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openless-all/app/scripts/android-ui-verify.ps1 b/openless-all/app/scripts/android-ui-verify.ps1 index 9a125368..3f33a802 100644 --- a/openless-all/app/scripts/android-ui-verify.ps1 +++ b/openless-all/app/scripts/android-ui-verify.ps1 @@ -58,7 +58,11 @@ if (-not $apk) { Write-Host "Installing $($apk.FullName) ..." adb logcat -c -adb install -r $apk.FullName +adb shell pm uninstall --user 0 com.openless.app 2>$null +if ($LASTEXITCODE -ne 0) { + adb uninstall com.openless.app 2>$null +} +adb install $apk.FullName Write-Host "Starting MainActivity ..." adb shell am start -n com.openless.app/.MainActivity @@ -73,9 +77,11 @@ adb shell dumpsys package com.openless.app | Set-Content -Path (Join-Path $Proje function Capture-Screen([string]$label) { $out = Join-Path $shotDir "openless-screenshot-$label-$stamp.png" - $bytes = adb exec-out screencap -p - if ($bytes) { - [System.IO.File]::WriteAllBytes($out, $bytes) + $tmp = Join-Path $env:TEMP "openless-screencap-$label.png" + adb shell screencap -p /sdcard/openless-ui-$label.png + adb pull /sdcard/openless-ui-$label.png $tmp | Out-Null + if (Test-Path $tmp) { + Move-Item -Force $tmp $out Write-Host "Screenshot: $out" } }