mirror of
https://github.com/SteamFramelyHomebrew/framely.git
synced 2026-10-23 09:01:46 +02:00
Add mobile manager navigation and touch-first page sheets
This commit is contained in:
1 parent
418b0e0380
commit
c2d457ed84
6 files changed
+178
-4
No files matched your search
@@ -1014,6 +1014,22 @@
|
||||
"确认后会替换同名文件,不删除目录中的其他文件。": "Confirmation replaces matching files without deleting other files in the directory.",
|
||||
"已上传 {0} / {1} 个文件": "Uploaded {0} / {1} files",
|
||||
"暂无上传任务": "No uploads yet",
|
||||
"选择页面": "Choose a page",
|
||||
"关闭文件导航": "Close folder navigation",
|
||||
"浏览文件夹": "Browse folders",
|
||||
"新建与上传": "Create and upload",
|
||||
"筛选与显示": "Filters and view",
|
||||
"取消选择": "Cancel selection",
|
||||
"终端快捷键": "Terminal shortcuts",
|
||||
"请先选择要复制的终端文本。": "Select terminal text before copying.",
|
||||
"粘贴到终端": "Paste into terminal",
|
||||
"粘贴文本": "Paste text",
|
||||
"在这里使用手机的粘贴功能": "Use your phone’s paste action here",
|
||||
"发送到终端": "Send to terminal",
|
||||
"向上": "Up",
|
||||
"向下": "Down",
|
||||
"向左": "Left",
|
||||
"向右": "Right",
|
||||
"上传任务队列": "Upload queue",
|
||||
"同时上传文件数": "Simultaneous file uploads",
|
||||
"清除已结束记录": "Clear finished tasks",
|
||||
|
||||
@@ -1014,6 +1014,22 @@
|
||||
"确认后会替换同名文件,不删除目录中的其他文件。": "确认后会替换同名文件,不删除目录中的其他文件。",
|
||||
"已上传 {0} / {1} 个文件": "已上传 {0} / {1} 个文件",
|
||||
"暂无上传任务": "暂无上传任务",
|
||||
"选择页面": "选择页面",
|
||||
"关闭文件导航": "关闭文件导航",
|
||||
"浏览文件夹": "浏览文件夹",
|
||||
"新建与上传": "新建与上传",
|
||||
"筛选与显示": "筛选与显示",
|
||||
"取消选择": "取消选择",
|
||||
"终端快捷键": "终端快捷键",
|
||||
"请先选择要复制的终端文本。": "请先选择要复制的终端文本。",
|
||||
"粘贴到终端": "粘贴到终端",
|
||||
"粘贴文本": "粘贴文本",
|
||||
"在这里使用手机的粘贴功能": "在这里使用手机的粘贴功能",
|
||||
"发送到终端": "发送到终端",
|
||||
"向上": "向上",
|
||||
"向下": "向下",
|
||||
"向左": "向左",
|
||||
"向右": "向右",
|
||||
"上传任务队列": "上传任务队列",
|
||||
"同时上传文件数": "同时上传文件数",
|
||||
"清除已结束记录": "清除已结束记录",
|
||||
|
||||
+5
-3
@@ -1,3 +1,4 @@
|
||||
import {MobilePagePicker,managerPages,useMobileManager,useMobileViewport} from './mobile-manager';
|
||||
import {FileManager} from './file-manager';
|
||||
import {TerminalManager} from './terminal';
|
||||
import {FrameFilePicker} from './file-picker';
|
||||
@@ -22,7 +23,7 @@ import {InstallReview} from './install-review';
|
||||
import {MemoryLimit} from './memory-limit';
|
||||
import {installScrollbars} from '../../sdk/src/scrollbars';
|
||||
installScrollbars();
|
||||
import React,{useEffect,useRef,useState} from 'react';import{createRoot}from'react-dom/client';import{uploadChunk,api,viewKey,exportDiagnosticLogs}from'./api';import {Icon,Brand} from './icons';import {Store,Sources,PluginImage,PluginLinks,PluginAuthor,installedIcon,runJob,Transfer,Job,Presentation,compareVersions} from './store';import {SystemUpdates} from './updates';import './style.css';if(location.pathname==='/launcher')document.documentElement.classList.add('launcher-view');if(location.pathname==='/notifications')document.documentElement.classList.add('notification-view');
|
||||
import React,{useEffect,useRef,useState} from 'react';import{createRoot}from'react-dom/client';import{uploadChunk,api,viewKey,exportDiagnosticLogs}from'./api';import {Icon,Brand} from './icons';import {Store,Sources,PluginImage,PluginLinks,PluginAuthor,installedIcon,runJob,Transfer,Job,Presentation,compareVersions} from './store';import {SystemUpdates} from './updates';import './style.css';import './mobile-manager.css';if(location.pathname==='/launcher')document.documentElement.classList.add('launcher-view');if(location.pathname==='/notifications')document.documentElement.classList.add('notification-view');
|
||||
export type Manifest=Presentation&{id:string;name:string;version:string;author:string;description:string;backend?:{runAs:string;autostart:boolean;memoryLimitMiB?:number};lifecycle?:{runAs?:string};dependencies?:Record<string,unknown>;optionalDependencies?:Record<string,unknown>;conflicts?:Record<string,string>;exclusiveResources?:string[];engines?:{framely?:string};ui:{quickPage?:string;windows:Record<string,unknown>;launch?:Record<string,{type:string;window?:string}>;launcherActions?:{id:string;label:string;target:{type:string;entry?:string;window?:string}}[]}};
|
||||
export type Plugin={manifest:Manifest;enabled:boolean;favorite:boolean;order:number;source?:string;error?:string};
|
||||
type Source={id:string;name:string;url:string;enabled:boolean;allowHttp:boolean};
|
||||
@@ -31,6 +32,7 @@ const managerMode=location.pathname==='/manager';
|
||||
const empty:Status={version:'0.4.0',database:{plugins:{},sources:[],safeMode:false},notifications:[],running:[]};
|
||||
|
||||
function App(){
|
||||
const mobile=useMobileManager();useMobileViewport(managerMode&&mobile);
|
||||
const[catalogTarget,setCatalogTarget]=useState<{plugin:string;source?:string}|null>(null);const runtimeLabels:Record<string,string>={starting:t("初始化中"),running:t("运行中"),stopping:t("停止中"),stopped:t("已停止"),recovering:t("等待恢复"),failed:t("失败"),crashed:t("异常退出"),ready:t("已就绪"),installed:t("已安装"),'waiting-dependency':t("等待依赖")};const [status,setStatus]=useState<Status>(empty),[error,setError]=useState(''),[tab,setTab]=useState(managerMode?managerTab(location.hash):'favorites'),[page,setPage]=useState<string|null>(!managerMode&&location.hash.startsWith('#plugin:')?location.hash.slice(8):null),[busy,setBusy]=useState(false),[query,setQuery]=useState(''),[manage,setManage]=useState<Plugin|null>(null),[install,setInstall]=useState<any>(null),[confirmRunAs,setConfirmRunAs]=useState(false),[logs,setLogs]=useState<string|null>(null),[importing,setImporting]=useState(false),[transfer,setTransfer]=useState<Job|null>(null),[downloadRequest,setDownloadRequest]=useState<any>(null),[relationAction,setRelationAction]=useState<any>(null),[reviewAgreement,setReviewAgreement]=useState(false),[agreementDeclined,setAgreementDeclined]=useState(false),[languageFiles,setLanguageFiles]=useState<string[]>([]),[logExport,setLogExport]=useState(''),[exportingLogs,setExportingLogs]=useState(false);
|
||||
useEffect(()=>{const ticket=install?.ticket;return()=>{if(ticket)void api('job.cancel',{job:ticket}).catch(()=>{});};},[install?.ticket]);
|
||||
const localePacks=useRef<LanguagePack[]|null>(null);const localePreference=useRef<string|null>(null);
|
||||
@@ -89,8 +91,8 @@ function App(){
|
||||
if(parts[1]==='launcher')return <Launcher plugins={status.database.plugins} safeMode={status.database.safeMode} config={status.database.launcher} favorites={status.database.steamFavorites??[]} localFavorites={status.database.launcherFavorites??[]} order={status.database.launcherOrder??[]} refresh={refresh}/>;
|
||||
if(parts[1]==='notifications')return <NotificationCenter items={status.notifications} plugins={status.database.plugins} safeMode={status.database.safeMode} refresh={refresh}/>;
|
||||
if(windowPlugin){const p=status.database.plugins[windowPlugin];return <div className="app plugin-window">{p?.enabled&&!status.database.safeMode?<PluginFrame plugin={windowPlugin} entry={parts[3]}/>:<div className="empty"><p>{t("插件已停用或正在启动。")}</p><button onClick={()=>void action(()=>api('window.close',{plugin:windowPlugin,window:parts[3]}))}>{t("关闭")}</button></div>}{error&&<p className="error">{error}</p>}</div>;}
|
||||
return <div className={`app ${managerMode?'manager':'quick'}`}><header className="shell-header"><strong className="brand-title"><Brand wordmark/></strong>{managerMode?<>{managerSection==='plugins'&&<button className="primary manager-install-button" onClick={()=>setImporting(true)}><Icon name="download" size={20}/>{t("安装插件")}</button>}<ManagerSwitcher section={managerSection} onChange={switchManager}/><button className="icon-button" aria-label={t("关闭管理窗口")} onClick={()=>void api('host.manager.close')}><Icon name="close"/></button></>:<><button className="icon-button quick-library-button" aria-label={t("插件库")} onClick={()=>void action(()=>api('host.manager.open',{page:'catalog'}))}><Icon name="store"/><span>{t("插件库")}</span></button><button className="icon-button" aria-label={t("关闭菜单")} onClick={()=>void api('host.menu.close')}><Icon name="close"/></button></>}</header>{error&&<p className="error" role="alert">{error}<button onClick={()=>setError('')}>×</button></p>}{status.database.safeMode&&<p className="banner">{t("安全模式:第三方插件暂停运行。")}</p>}
|
||||
{selected?<><div className="page-title"><button onClick={()=>setPage(null)}>{t("‹ 返回")}</button><strong>{selected.manifest.name}</strong><button onClick={()=>void action(()=>api('plugin.favorite',{plugin:page,favorite:!selected.favorite}))}>{selected.favorite?'★':'☆'}</button>{managerMode&&<button onClick={()=>setManage(selected)}>{t("管理")}</button>}</div>{selected.enabled&&!status.database.safeMode&&selected.manifest.ui.quickPage?<PluginFrame plugin={page!}/>:<p className="empty">{t("插件已停用、处于安全模式或没有快捷页面。")}</p>}</>:<div className={`workspace ${managerMode&&(tab==='terminal'||tab==='files')?'tools-workspace':''}`}><nav aria-label={t("主导航")}>{(managerMode?(managerSection==='terminal'||managerSection==='files'?[]:managerSection==='settings'?[['settings',t('通用'),'settings'],['launcher-settings',t('启动台'),'launcher'],['updates',t('更新'),'refresh'],['about',t('关于'),'info']]:apkPanel?[['apk',t('应用'),'download'],['apk-containers',t('容器'),'plugins'],['apk-cleanup',t('数据清理'),'settings'],['apk-settings',t('设置'),'settings']]:[['installed',t("已安装"),'plugins'],['catalog',t("插件库"),'download'],['sources',t("插件源导航"),'link'],['notification-settings',t("通知设置导航"),'bell']]):[['favorites',t("常用"),'star'],['installed',t("已安装"),'plugins'],['notifications',t("通知"),'bell'],['settings',t("设置"),'settings']]).map(([key,label,icon])=><button className={tab===key?'active':''} aria-current={tab===key?'page':undefined} key={key} onClick={()=>key==='catalog'?void loadCatalog():(setTab(key),history.replaceState(null,'',location.pathname+location.search+'#'+key))}>{icon==='bell'?<IconBell size={24} stroke={1.8}/>:icon==='launcher'?<IconLayoutGrid size={24} stroke={1.8}/>:<Icon name={icon as any}/>}<span>{label}{key==='notifications'&&!!status.notificationBadge&&<small className="notification-count">{status.notificationBadge!>99?'99+':status.notificationBadge}</small>}</span></button>)}{managerMode&&<div className="nav-foot"><span>{apkPanel?t("Lepton 侧载管理"):<>{plugins.length}{t("个已安装插件")}</>}</span><small>{status.version}</small></div>}</nav><main className={`content ${managerMode&&tab==='files'?'file-content':managerMode&&tab==='terminal'?'terminal-content':''}`}>
|
||||
return <div className={`app ${managerMode?'manager':'quick'}`}><header className="shell-header"><strong className="brand-title"><Brand wordmark/></strong>{managerMode?<>{managerSection==='plugins'&&<button className="primary manager-install-button" onClick={()=>setImporting(true)}><Icon name="download" size={20}/>{t("安装插件")}</button>}<ManagerSwitcher section={managerSection} onChange={switchManager}/>{mobile&&<MobilePagePicker section={managerSection} tab={tab} onChange={key=>key==='catalog'?void loadCatalog():(setPage(null),setTab(key),history.replaceState(null,'',location.pathname+location.search+'#'+key))}/> }<button className="icon-button" aria-label={t("关闭管理窗口")} onClick={()=>void api('host.manager.close')}><Icon name="close"/></button></>:<><button className="icon-button quick-library-button" aria-label={t("插件库")} onClick={()=>void action(()=>api('host.manager.open',{page:'catalog'}))}><Icon name="store"/><span>{t("插件库")}</span></button><button className="icon-button" aria-label={t("关闭菜单")} onClick={()=>void api('host.menu.close')}><Icon name="close"/></button></>}</header>{error&&<p className="error" role="alert">{error}<button onClick={()=>setError('')}>×</button></p>}{status.database.safeMode&&<p className="banner">{t("安全模式:第三方插件暂停运行。")}</p>}
|
||||
{selected?<><div className="page-title"><button onClick={()=>setPage(null)}>{t("‹ 返回")}</button><strong>{selected.manifest.name}</strong><button onClick={()=>void action(()=>api('plugin.favorite',{plugin:page,favorite:!selected.favorite}))}>{selected.favorite?'★':'☆'}</button>{managerMode&&<button onClick={()=>setManage(selected)}>{t("管理")}</button>}</div>{selected.enabled&&!status.database.safeMode&&selected.manifest.ui.quickPage?<PluginFrame plugin={page!}/>:<p className="empty">{t("插件已停用、处于安全模式或没有快捷页面。")}</p>}</>:<div className={`workspace ${managerMode&&(tab==='terminal'||tab==='files')?'tools-workspace':''}`}><nav aria-label={t("主导航")}>{(managerMode?managerPages(managerSection):[['favorites',t("常用"),'star'],['installed',t("已安装"),'plugins'],['notifications',t("通知"),'bell'],['settings',t("设置"),'settings']]).map(([key,label,icon])=><button className={tab===key?'active':''} aria-current={tab===key?'page':undefined} key={key} onClick={()=>key==='catalog'?void loadCatalog():(setTab(key),history.replaceState(null,'',location.pathname+location.search+'#'+key))}>{icon==='bell'?<IconBell size={24} stroke={1.8}/>:icon==='launcher'?<IconLayoutGrid size={24} stroke={1.8}/>:<Icon name={icon as any}/>}<span>{label}{key==='notifications'&&!!status.notificationBadge&&<small className="notification-count">{status.notificationBadge!>99?'99+':status.notificationBadge}</small>}</span></button>)}{managerMode&&<div className="nav-foot"><span>{apkPanel?t("Lepton 侧载管理"):<>{plugins.length}{t("个已安装插件")}</>}</span><small>{status.version}</small></div>}</nav><main className={`content ${managerMode&&tab==='files'?'file-content':managerMode&&tab==='terminal'?'terminal-content':''}`}>
|
||||
{(tab==='favorites'||tab==='installed')&&<>
|
||||
{managerMode&&<div className="list-heading"><div><h1>{t("已安装")}</h1><p>{plugins.length}{t("个插件,")}{plugins.filter(p=>p.enabled).length}{t("个已启用")}</p></div><div className="search-field"><Icon name="search"/><input aria-label={t("搜索已安装插件")} placeholder={t("搜索插件")} value={query} onChange={e=>setQuery(e.target.value)}/></div></div>}
|
||||
{managerMode&&<div className="list-columns"><span>{t("插件")}</span><span>{t("状态")}</span><span>{t("版本")}</span><span>{t("操作")}</span></div>}<div className="plugin-list">{listedPlugins.map(p=><article className={`plugin-row ${!p.enabled?'disabled':''}`} key={p.manifest.id}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import {IconPuzzle,IconBrandAndroid,IconTerminal2,IconFolder,IconSettings} from '@tabler/icons-react';
|
||||
import {t} from './i18n';
|
||||
export type ManagerSection='plugins'|'apk'|'terminal'|'files'|'settings';
|
||||
export function sectionFor(tab:string):ManagerSection {
|
||||
@@ -9,6 +10,6 @@ export function sectionFor(tab:string):ManagerSection {
|
||||
export function managerTab(hash:string){const key=hash.slice(1);return ['installed','catalog','sources','notification-settings','settings','launcher-settings','about','updates','terminal','files','apk','apk-containers','apk-cleanup','apk-settings'].includes(key)?key:'installed';}
|
||||
export function ManagerSwitcher({section,onChange}:{section:ManagerSection;onChange:(section:ManagerSection)=>void}) {
|
||||
return <div className="manager-switcher" role="group" aria-label={t('切换管理面板')}>
|
||||
{([['plugins','插件'],['apk','APK'],['terminal','终端'],['files','文件'],['settings','设置']] as const).map(([key,label])=><button key={key} className={section===key?'active':''} aria-pressed={section===key} onClick={()=>onChange(key)}>{t(label)}</button>)}
|
||||
{([['plugins','插件'],['apk','APK'],['terminal','终端'],['files','文件'],['settings','设置']] as const).map(([key,label])=><button key={key} className={section===key?'active':''} aria-pressed={section===key} onClick={()=>onChange(key)}><span className="manager-section-icon">{React.createElement(({plugins:IconPuzzle,apk:IconBrandAndroid,terminal:IconTerminal2,files:IconFolder,settings:IconSettings})[key],{size:22})}</span><span>{t(label)}</span></button>)}
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/* Mobile product navigation: app chrome 30, contextual drawers 70, sheets 80.
|
||||
Preserve the existing neutral theme. 12px controls, 20px sheet tops, pill nav items. */
|
||||
.manager-section-icon,.mobile-page-picker,.file-mobile-tools,.terminal-touch-keys{display:none}
|
||||
.mobile-sheet-backdrop{position:absolute;inset:0;z-index:80;background:#09090b99;display:flex;align-items:flex-end}
|
||||
.mobile-sheet{display:flex;flex-direction:column;width:100%;max-height:90%;background:#242426;border:1px solid #ffffff20;border-bottom:0;border-radius:20px 20px 0 0;padding-bottom:env(safe-area-inset-bottom,0px);overflow:hidden;color:#ededf0}
|
||||
.mobile-sheet>header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;flex:none;border-bottom:1px solid #ffffff10}
|
||||
.mobile-sheet>header h2{font-size:18px;font-weight:600;margin:0}
|
||||
.mobile-sheet>header button{background:transparent;border:0;min-width:44px;min-height:44px;padding:8px;color:#b2b2ba}
|
||||
.mobile-sheet-body{overflow:auto;overscroll-behavior:contain;padding:12px 16px 20px;min-height:0}
|
||||
.mobile-page-options{display:grid;gap:6px}
|
||||
.mobile-page-options button{min-height:52px;background:transparent;border:0;display:flex;justify-content:space-between;padding:12px 14px;border-radius:12px;color:#ededf0;text-align:left;font-size:16px}
|
||||
.mobile-page-options button[aria-current]{background:#3a3a3e}
|
||||
@media(max-width:767px){
|
||||
html,body,#root{height:100%;height:100dvh;overflow:hidden}
|
||||
.app.manager{height:var(--manager-mobile-height,100dvh);border-radius:0;clip-path:none;padding-top:env(safe-area-inset-top,0px);padding-bottom:calc(68px + env(safe-area-inset-bottom,0px))}
|
||||
.manager .shell-header{min-height:58px;height:auto;padding:8px 14px;gap:8px;flex-wrap:nowrap;position:relative;z-index:30;background:var(--manager-chrome-tint)}
|
||||
.manager .shell-header .brand-title{flex:none;margin-right:auto}
|
||||
.manager .brand-wordmark{width:110px;height:28px}
|
||||
.manager .shell-header>.icon-button{display:none}
|
||||
.manager .shell-header .manager-install-button{font-size:0;min-height:44px;width:44px;padding:10px;border-radius:12px;order:3}
|
||||
.manager .shell-header .manager-install-button svg{width:22px;height:22px}
|
||||
.manager .shell-header .manager-switcher{position:fixed;z-index:30;left:0;right:0;bottom:0;width:100%;max-width:none;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));border:0;border-top:1px solid #ffffff18;border-radius:0;background:#202022f5;gap:0;padding:6px 6px calc(6px + env(safe-area-inset-bottom,0px));overflow:visible}
|
||||
.manager .manager-switcher button{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:56px;padding:5px 2px;gap:3px;font-size:11px;border-radius:12px;color:#b2b2ba}
|
||||
.manager .manager-switcher button.active{background:transparent;color:#a2c7df}
|
||||
.manager-section-icon{display:flex;padding:3px 12px;border-radius:999px}
|
||||
.manager-switcher button.active .manager-section-icon{background:#a2c7df18}
|
||||
.mobile-page-picker{display:block;min-width:0;max-width:55%}
|
||||
.mobile-page-picker>button{border:0;border-radius:12px;background:#ffffff08;color:var(--text);font-size:14px;min-height:42px;padding:8px 10px;gap:7px;white-space:nowrap}
|
||||
.mobile-page-picker>button:disabled{opacity:1}
|
||||
.mobile-page-picker .mobile-sheet-backdrop{position:fixed}
|
||||
.manager .workspace{display:flex;min-width:0;min-height:0}
|
||||
.manager .workspace>nav{display:none}
|
||||
.manager .content{padding:18px 16px 24px;min-width:0;overscroll-behavior:contain}
|
||||
.manager h1{font-size:24px;letter-spacing:-.4px}
|
||||
.manager h2{font-size:19px}
|
||||
.manager p,.manager .setting p,.manager .store-copy p{font-size:14px;line-height:1.55}
|
||||
.manager button{touch-action:manipulation}
|
||||
.manager .content button{min-height:44px;font-size:14px}
|
||||
.manager input:not([type=checkbox]):not([type=file]),.manager textarea{font-size:16px!important;min-width:0;max-width:100%}
|
||||
.manager .list-heading{gap:14px;margin-bottom:18px;align-items:flex-start}
|
||||
.manager .list-heading>div{min-width:0}
|
||||
.manager .list-heading .search-field,.manager .search-field{min-width:0;width:100%}
|
||||
.manager .list-heading p{font-size:14px}
|
||||
.manager .setting{padding:18px 0;margin:0;width:100%}
|
||||
.manager .setting h2{font-size:18px}
|
||||
.manager .setting label{font-size:14px}
|
||||
.manager .switch-row{gap:14px;width:100%;min-width:0}
|
||||
.manager .switch-row b{font-size:15px}
|
||||
.manager .switch-row small{font-size:13px;line-height:1.5}
|
||||
.manager .ui-switch{flex:none;min-width:50px!important;min-height:30px!important}
|
||||
.manager .plugin-row{padding:16px 0;gap:12px;align-items:flex-start;flex-wrap:wrap}
|
||||
.manager .plugin-copy{min-width:0;flex:1}
|
||||
.manager .plugin-copy h2{font-size:17px}
|
||||
.manager .plugin-copy small{font-size:12px}
|
||||
.manager .plugin-copy p{font-size:14px}
|
||||
.manager .plugin-actions{width:100%;display:flex;justify-content:flex-end}
|
||||
.manager .source-row{padding:16px 0;gap:12px}
|
||||
.manager .store-filters{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
|
||||
.manager .store-filters .search-field{grid-column:1/-1;min-width:0}
|
||||
.manager .store-filters .framely-select{min-width:0;max-width:100%}
|
||||
.manager .store-card{min-width:0}
|
||||
.manager .store-copy h2{font-size:18px}
|
||||
.manager .store-card>.plugin-avatar{width:52px!important;height:52px!important}
|
||||
.manager .detail-heading{gap:12px}
|
||||
.manager .modal{position:absolute;left:0!important;right:0!important;bottom:0!important;top:auto!important;transform:none!important;width:100%;max-width:none;max-height:92%;border-radius:20px 20px 0 0;padding:20px 18px calc(20px + env(safe-area-inset-bottom,0px));box-shadow:0 0 0 2000px #09090b99;border:1px solid #ffffff20;z-index:80;overflow:auto}
|
||||
.manager .modal h2{font-size:21px}
|
||||
.manager .modal footer{display:flex;gap:10px;position:sticky;bottom:0;background:#242426;padding-top:14px}
|
||||
.manager .modal footer button{flex:1;min-height:46px;font-size:15px}
|
||||
.manager .modal.install-review{padding:0}
|
||||
.manager .install-review-body{padding:16px 18px}
|
||||
.manager .install-review-heading{padding:20px 18px}
|
||||
.manager .install-review-footer{padding:14px 18px;flex-direction:column}
|
||||
.manager .install-review-actions{display:grid;grid-template-columns:1fr 1fr;width:100%}
|
||||
.manager .install-review .install-review-actions .primary{grid-row:auto}
|
||||
.manager .app>.error,.manager .app>.banner{margin:8px 14px;font-size:13px}
|
||||
.manager-keyboard-open .manager .shell-header .manager-switcher{display:none}
|
||||
.manager-keyboard-open .app.manager{padding-bottom:0}
|
||||
.manager .apk-row{display:grid;grid-template-columns:52px minmax(0,1fr);gap:12px;padding:16px 0}
|
||||
.manager .apk-row>.plugin-avatar{width:52px!important;height:52px!important}
|
||||
.manager .apk-row .apk-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;width:100%}
|
||||
.manager .apk-copy{min-width:0}
|
||||
.manager .apk-copy h2{font-size:17px;display:flex;flex-wrap:wrap;gap:6px}
|
||||
.manager .apk-copy code,.manager .apk-container code{font-size:12px;overflow-wrap:anywhere}
|
||||
.manager .apk-copy p{font-size:13px}
|
||||
.manager .apk-container{display:flex;flex-direction:column;gap:12px;padding:16px 0}
|
||||
.manager .apk-actions{flex-wrap:wrap;gap:8px}
|
||||
.manager .apk-actions button{flex:1;min-width:0;white-space:normal}
|
||||
.manager .apk-filters{display:grid;grid-template-columns:1fr;gap:10px}
|
||||
.manager .apk-filters>*{width:100%;min-width:0}
|
||||
.manager .apk-cleanup-row{gap:12px;flex-wrap:wrap}
|
||||
.manager .notification-plugin-row,.manager .notification-plugin-columns{grid-template-columns:minmax(0,1fr) 50px 50px}
|
||||
.manager .notification-plugin-name b{font-size:14px}
|
||||
.manager .notification-option p{font-size:13px}
|
||||
}
|
||||
@media(max-width:767px) and (prefers-reduced-motion:no-preference){.mobile-sheet{animation:manager-sheet-in .18s ease-out} @keyframes manager-sheet-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}}
|
||||
@media(max-width:767px){
|
||||
.manager .plugin-row{display:grid;grid-template-columns:minmax(0,1fr) 44px 44px;gap:8px}
|
||||
.manager .plugin-main{gap:12px;padding:4px 0;grid-column:1/-1;min-height:62px;width:100%}
|
||||
.manager .plugin-copy b{font-size:17px;white-space:normal}
|
||||
.manager .plugin-copy small{font-size:13px}
|
||||
.manager .plugin-meta{display:flex;font-size:12px}
|
||||
.manager .plugin-switch{display:block;grid-column:1;justify-self:start;padding:0!important}
|
||||
.manager .plugin-switch.ui-switch{width:50px;height:30px}
|
||||
.manager .plugin-row>.icon-button{min-width:44px;min-height:44px;padding:10px}
|
||||
.manager .plugin-row .row-state,.manager .plugin-row .row-version,.manager .list-columns{display:none}
|
||||
.manager .proxy-fields,.manager .manage-grid{grid-template-columns:minmax(0,1fr)}
|
||||
.manager .framely-select button[aria-haspopup]{white-space:normal;overflow-wrap:anywhere}
|
||||
.manager .framely-select [role=listbox]{background:#242426!important;border-color:#55555c!important;max-width:calc(100vw - 32px)!important}
|
||||
.manager .framely-select [role=option]{min-height:44px!important;color:#ededf0}
|
||||
.manager .framely-select [aria-selected=true]{background:#3a3a3e!important}
|
||||
}
|
||||
@media(max-width:767px){.manager .file-mobile-tools{display:flex}.manager .file-select-mode .file-status-bar>span{visibility:hidden}}
|
||||
@media(max-width:767px){
|
||||
.manager .apk-manager>.list-heading>.apk-actions{width:100%;display:grid;grid-template-columns:1fr 1fr}
|
||||
.manager .apk-manager>.list-heading>.apk-actions button{white-space:nowrap;min-height:46px;padding:10px 12px}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React,{useEffect,useRef,useState} from 'react';
|
||||
import {IconChevronDown,IconCheck,IconX} from '@tabler/icons-react';
|
||||
import {t} from './i18n';
|
||||
import type {ManagerSection} from './manager-switcher';
|
||||
export function useMobileManager(){
|
||||
const [mobile,setMobile]=useState(()=>matchMedia('(max-width: 767px)').matches);
|
||||
useEffect(()=>{const query=matchMedia('(max-width: 767px)'),update=()=>setMobile(query.matches);query.addEventListener('change',update);return()=>query.removeEventListener('change',update);},[]);return mobile;
|
||||
}
|
||||
export function MobileSheet({title,onClose,children}:{title:string;onClose:()=>void;children:React.ReactNode}){
|
||||
const ref=useRef<HTMLDivElement>(null),closeRef=useRef(onClose);closeRef.current=onClose;
|
||||
useEffect(()=>{const previous=document.activeElement as HTMLElement|null;const items=()=>Array.from(ref.current?.querySelectorAll<HTMLElement>('button:not(:disabled),input:not(:disabled),textarea,[tabindex="0"]')??[]).filter(el=>el.getClientRects().length);items()[0]?.focus();
|
||||
const key=(e:KeyboardEvent)=>{if(e.key==='Escape'){if((e.target as HTMLElement)?.closest('.framely-select')?.querySelector('[aria-expanded="true"]'))return;e.preventDefault();e.stopPropagation();closeRef.current();}if(e.key==='Tab'){const list=items(),first=list[0],last=list.at(-1);if(e.shiftKey&&document.activeElement===first){e.preventDefault();last?.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first?.focus();}}};document.addEventListener('keydown',key,true);return()=>{document.removeEventListener('keydown',key,true);if(previous?.isConnected&&!document.querySelector('.modal[aria-modal="true"]'))previous.focus();};},[]);
|
||||
return <div className="mobile-sheet-backdrop" onClick={e=>{if(e.target===e.currentTarget)onClose();}}><div className="mobile-sheet" ref={ref} role="dialog" aria-modal="true" aria-label={title}><header><h2>{title}</h2><button aria-label={t('关闭')} onClick={onClose}><IconX size={22}/></button></header><div className="mobile-sheet-body">{children}</div></div></div>;
|
||||
}
|
||||
export const managerPages=(section:ManagerSection):[string,string,string][]=>section==='settings'?[['settings',t('通用'),'settings'],['launcher-settings',t('启动台'),'launcher'],['updates',t('更新'),'refresh'],['about',t('关于'),'info']]:section==='apk'?[['apk',t('应用'),'download'],['apk-containers',t('容器'),'plugins'],['apk-cleanup',t('数据清理'),'settings'],['apk-settings',t('设置'),'settings']]:section==='plugins'?[['installed',t('已安装'),'plugins'],['catalog',t('插件库'),'download'],['sources',t('插件源导航'),'link'],['notification-settings',t('通知设置导航'),'bell']]:[];
|
||||
export function MobilePagePicker({section,tab,onChange}:{section:ManagerSection;tab:string;onChange:(tab:string)=>void}){
|
||||
const [open,setOpen]=useState(false),pages=managerPages(section),label=pages.find(p=>p[0]===tab)?.[1]??t(section==='terminal'?'终端':'文件');
|
||||
useEffect(()=>setOpen(false),[section,tab]);
|
||||
return <div className="mobile-page-picker"><button disabled={!pages.length} aria-expanded={open} onClick={()=>setOpen(v=>!v)}>{label}{pages.length>0&&<IconChevronDown size={18}/>}</button>{open&&<MobileSheet title={t('选择页面')} onClose={()=>setOpen(false)}><div className="mobile-page-options">{pages.map(([key,label])=><button key={key} aria-current={tab===key?'page':undefined} onClick={()=>{onChange(key);setOpen(false);}}><span>{label}</span>{tab===key&&<IconCheck size={20}/>}</button>)}</div></MobileSheet>}</div>;
|
||||
}
|
||||
export function useMobileViewport(enabled:boolean){
|
||||
useEffect(()=>{if(!enabled)return;const viewport=window.visualViewport;const update=()=>{const keyboard=window.innerHeight-(viewport?.height??window.innerHeight)>140;document.documentElement.style.setProperty('--manager-mobile-height',`${viewport?.height??window.innerHeight}px`);document.documentElement.classList.toggle('manager-keyboard-open',keyboard);};update();viewport?.addEventListener('resize',update);window.addEventListener('resize',update);return()=>{viewport?.removeEventListener('resize',update);window.removeEventListener('resize',update);document.documentElement.style.removeProperty('--manager-mobile-height');document.documentElement.classList.remove('manager-keyboard-open');};},[enabled]);
|
||||
}
|
||||
Reference in new issue
Block a user