mirror of
https://github.com/SteamFramelyHomebrew/framely.git
synced 2026-10-18 11:01:26 +02:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4108d4e5be | ||
|
|
1c6e36e63b | ||
|
|
799433e041 |
No files matched your search
Generated
+1
-1
@@ -291,7 +291,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "framely"
|
||||
version = "0.4.2-preview.10"
|
||||
version = "0.4.2-preview.12"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "framely"
|
||||
version = "0.4.2-preview.10"
|
||||
version = "0.4.2-preview.12"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
|
||||
@@ -117,3 +117,7 @@ Development preview simulates windows, notifications and some bridge operations.
|
||||
`framely.ui.getVisibility()` returns `{ known, captureObscured, pageVisible, sequence, sessionId }`. `framely.ui.onVisibilityChanged(callback, onError?)` subscribes and immediately queries the current snapshot; it returns an unsubscribe function. `useVisibility()` provides the same fields and `error`. Capture visibility combines the actual visibility of the quick panel, manager and every plugin window, excluding notifications and dock icons. Remote browser pages have `pageVisible: false` and cannot report headset visibility.
|
||||
|
||||
Declare `backend.uiVisibilityEvents: true` to receive the reserved JSON-line RPC `framely.ui.visibility` at startup and on changes, independently of mounted pages. Reply with the request id. The backend snapshot includes `known`, `captureObscured`, `sequence` and `views`; `pageVisible` is frontend-only. A native heartbeat missing for more than two seconds makes `known` false. Capture backends should pause on `!known || captureObscured`. Existing backends do not receive this RPC without opting in. Node/TS can use `registerVisibility` from `@framely/sdk/visibility`; Python supports `serve(dispatch, visibility=callback)`.
|
||||
|
||||
`framely.ui.close()` closes the current native surface: the quick panel or the current plugin window. It does not stop the backend or recording. Requires a host with this bridge method.
|
||||
|
||||
Standalone plugin windows display the plugin page without a host title bar. Plugins provide their own title, navigation and close button using `framely.ui.close()` or `framely.windows.close(id)`. Quick panels retain host navigation.
|
||||
+3
-3
@@ -38,7 +38,7 @@ Deleted `/home` data cannot be recovered. OS updates may require manual interven
|
||||
|
||||
Device and installer share the repository with independent versions/tags/workflows:
|
||||
|
||||
- Device: root Cargo version, `v<version>` (currently `v0.4.2-preview.10`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF.
|
||||
- Device: root Cargo version, `v<version>` (currently `v0.4.2-preview.12`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF.
|
||||
- Installer: `installer/Cargo.toml`, `installer-v<version>` (currently `installer-v0.4.1-preview.8`), `.github/workflows/installer-release.yml`, Linux x64/ARM64, Windows x64, macOS Intel/Apple Silicon, pinned GPUI Kit.
|
||||
|
||||
Versions need not match or ship together. Update the relevant Cargo.lock with version changes. Installer package/macOS versions use the installer version.
|
||||
@@ -47,8 +47,8 @@ Both workflows cache dependencies/builds by product/platform/toolchain/configura
|
||||
|
||||
```bash
|
||||
# After committing and pushing the intended source:
|
||||
git tag v0.4.2-preview.10
|
||||
git push origin v0.4.2-preview.10
|
||||
git tag v0.4.2-preview.12
|
||||
git push origin v0.4.2-preview.12
|
||||
# Independent installer release:
|
||||
git tag installer-v0.4.1-preview.8
|
||||
git push origin installer-v0.4.1-preview.8
|
||||
|
||||
@@ -124,3 +124,7 @@ const off = framely.ui.onVisibilityChanged(state => {
|
||||
});
|
||||
// 页面卸载时调用 off()
|
||||
```
|
||||
|
||||
`framely.ui.close()` 关闭当前原生界面:快捷页关闭 Framely 面板,插件窗口关闭自身;不会停止插件后端或当前录制。需要包含此 bridge 方法的新版宿主。
|
||||
|
||||
独立插件窗口直接展示插件页面,宿主不添加标题栏。插件应自行提供标题、导航和关闭按钮,可使用 `framely.ui.close()` 或 `framely.windows.close(id)`。快捷面板仍保留宿主导航。
|
||||
@@ -48,7 +48,7 @@ CEF 按内容哈希保存在 `/home/.framely/cef/<运行库标识>`,更新时
|
||||
|
||||
Framely 和桌面安装器保留在同一仓库,使用独立的版本号、标签和发布工作流:
|
||||
|
||||
- Framely:版本由根 `Cargo.toml` 定义,标签为 `v<版本>`(当前 `v0.4.2-preview.10`);`.github/workflows/release.yml` 只构建 Frame Linux ARM64 发行,使用锁定的 CEF 并执行自动检查。
|
||||
- Framely:版本由根 `Cargo.toml` 定义,标签为 `v<版本>`(当前 `v0.4.2-preview.12`);`.github/workflows/release.yml` 只构建 Frame Linux ARM64 发行,使用锁定的 CEF 并执行自动检查。
|
||||
- 安装器:版本由 `installer/Cargo.toml` 定义,标签为 `installer-v<版本>`(当前 `installer-v0.4.1-preview.8`);`.github/workflows/installer-release.yml` 只构建 Linux x64/ARM64、Windows x64、macOS x64/ARM64 的原生安装器,使用锁定的 GPUI Kit。
|
||||
|
||||
两个版本不需要相同,也不需要同时发布。修改各自版本时同步对应的 `Cargo.lock`;安装器包名及 macOS 应用版本使用安装器版本。
|
||||
@@ -59,8 +59,8 @@ Framely 和桌面安装器保留在同一仓库,使用独立的版本号、标
|
||||
|
||||
```bash
|
||||
# 提交并推送代码后,选择需要发布的产品
|
||||
git tag v0.4.2-preview.10
|
||||
git push origin v0.4.2-preview.10
|
||||
git tag v0.4.2-preview.12
|
||||
git push origin v0.4.2-preview.12
|
||||
|
||||
# 独立发布安装器,不触发 Framely 构建
|
||||
git tag installer-v0.4.1-preview.8
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ export interface Notification {id:string;title:string;body:string;image?:string;
|
||||
type Bridge={request:(op:string,p?:unknown)=>Promise<any>;subscribe:(f:(e:unknown)=>void)=>()=>void};
|
||||
function bridge():Bridge{const b=(window as any).__framelyBridge;if(!b)throw new Error('This page requires the Framely plugin host');return b;}
|
||||
export const framely={
|
||||
ui:createVisibilityApi(bridge),
|
||||
ui:{...createVisibilityApi(bridge),close:():Promise<unknown>=>bridge().request('ui.close')},
|
||||
language:{get:():Promise<{preference:string;language:string}>=>bridge().request('language.get')},
|
||||
call:<T=unknown>(method:string,params:unknown={}):Promise<T>=>bridge().request('call',{method,params}),
|
||||
windows:{open:(window:string)=>bridge().request('window.open',{window}),close:(window:string)=>bridge().request('window.close',{window})},
|
||||
|
||||
@@ -44,10 +44,19 @@ async function choose(label:string){(document.querySelector('.framely-select but
|
||||
if(!document.querySelector('.framely-select button')?.textContent?.includes('正式版'))throw Error('legacy source did not default to stable');
|
||||
button('检查更新').click();await until(()=>!!document.querySelector('.release-card'));
|
||||
if(!document.querySelector('.release-card')?.textContent?.includes('较旧版本'))throw Error('channel downgrade not identified');
|
||||
button('下载并安装').click();await wait();
|
||||
if(downloads!==0||attempts!==0||!document.querySelector('.confirm-box'))throw Error('update bypassed confirmation');
|
||||
button('下载并安装').focus();button('下载并安装').click();await wait();
|
||||
const dialog=document.querySelector('.update-confirmation') as HTMLDialogElement|null;
|
||||
if(downloads!==0||attempts!==0||!dialog?.open||!dialog.matches(':modal'))throw Error('update bypassed modal confirmation');
|
||||
if(!dialog.getAttribute('aria-labelledby')||!dialog.getAttribute('aria-describedby')||document.activeElement!==button('取消'))throw Error('confirmation lacked accessible focus or description');
|
||||
const bounds=dialog.getBoundingClientRect();
|
||||
if(bounds.width<=0||bounds.height<=0||bounds.left<0||bounds.top<0||bounds.right>innerWidth||bounds.bottom>innerHeight)throw Error('confirmation outside viewport');
|
||||
button('检查更新').focus();
|
||||
if(!dialog.contains(document.activeElement))throw Error('background took focus during confirmation');
|
||||
button('取消').click();await wait();
|
||||
if(downloads!==0||attempts!==0)throw Error('cancelled update performed work');
|
||||
if(downloads!==0||attempts!==0||document.querySelector('.update-confirmation')||document.activeElement!==button('下载并安装'))throw Error('cancelled update performed work or did not restore focus');
|
||||
button('下载并安装').click();await wait();
|
||||
document.querySelector('.update-confirmation')!.dispatchEvent(new Event('cancel',{cancelable:true}));await wait();
|
||||
if(document.querySelector('.update-confirmation')||downloads!==0||attempts!==0)throw Error('dialog cancellation performed work');
|
||||
failDownload=true;button('下载并安装').click();await wait();button('确认下载并安装').click();
|
||||
await until(()=>!!document.querySelector('[role=alert]'));
|
||||
if(attempts!==0||!document.querySelector('[role=alert]')?.textContent?.includes('checksum failed'))throw Error('failed verification did not stop installation');
|
||||
@@ -73,7 +82,7 @@ async function choose(label:string){(document.querySelector('.framely-select but
|
||||
if(verification.value!==512*1024||verification.max!==1024*1024||attempts!==2)throw Error('verification byte progress or installation gate incorrect');
|
||||
console.log('FRAMELY_PREVIEW_INSTALL_UPDATE_TESTING');
|
||||
hold=false;await until(()=>installs===1);
|
||||
if(attempts!==3||document.querySelector('.confirm-box'))throw Error('verified download required a second confirmation');
|
||||
if(attempts!==3||document.querySelector('.update-confirmation'))throw Error('verified download required a second confirmation');
|
||||
remount();await wait();
|
||||
failCheck=true;button('检查更新').click();await until(()=>!!document.querySelector('[role=alert]'));
|
||||
if(document.querySelector('.release-card'))throw Error('failed recheck retained stale install');
|
||||
@@ -83,7 +92,7 @@ async function choose(label:string){(document.querySelector('.framely-select but
|
||||
if(!document.querySelector('.framely-select button')?.textContent?.includes('Stable'))throw Error('English channel untranslated');
|
||||
await choose('Testing');button('Check for updates').click();await until(()=>!!document.querySelector('.release-card'));
|
||||
button('Download and install').click();await wait();
|
||||
if(!document.querySelector('.confirm-box')?.textContent?.includes('installed automatically'))throw Error('English update flow untranslated');
|
||||
if(!document.querySelector('.update-confirmation')?.textContent?.includes('installed automatically'))throw Error('English update flow untranslated');
|
||||
hold=true;button('Confirm download and installation').click();
|
||||
await until(()=>!!document.querySelector('progress[aria-label="Verification progress"]'));
|
||||
if(!document.body.textContent?.includes('Download complete. Verifying')||!document.body.textContent?.includes('Verified 0.5 / 1.0 MiB'))throw Error('English verification progress untranslated');
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"管理 {0}": "Manage {0}",
|
||||
"未找到插件": "No plugins found",
|
||||
"没有收藏的插件": "No favorite plugins",
|
||||
"没有已启用的插件": "No enabled plugins",
|
||||
"打开插件管理以启用插件。": "Open the manager to enable plugins.",
|
||||
"还没有安装插件": "No plugins installed yet",
|
||||
"换一个关键词再试。": "Try another search term.",
|
||||
"点击插件旁的星标,将它加入常用。": "Click a plugin's star to add it to your favorites.",
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"管理 {0}": "管理 {0}",
|
||||
"未找到插件": "未找到插件",
|
||||
"没有收藏的插件": "没有收藏的插件",
|
||||
"没有已启用的插件": "没有已启用的插件",
|
||||
"打开插件管理以启用插件。": "打开插件管理以启用插件。",
|
||||
"还没有安装插件": "还没有安装插件",
|
||||
"换一个关键词再试。": "换一个关键词再试。",
|
||||
"点击插件旁的星标,将它加入常用。": "点击插件旁的星标,将它加入常用。",
|
||||
|
||||
+11
-3
@@ -32,6 +32,12 @@ function PluginFrame({plugin,entry='quick'}:{plugin:string;entry?:string}){
|
||||
const {id,op,params}=e.data;if(!Number.isSafeInteger(id)||typeof op!=='string')return;
|
||||
try{let result;
|
||||
if(op==='language.get')result={preference:pluginLanguagePreference,language:currentLanguage()};
|
||||
else if(op==='ui.close'){
|
||||
const view=viewKey();
|
||||
if(view==='menu')result=await api('host.menu.close');
|
||||
else if(view==='framely.manager')result=await api('host.manager.close');
|
||||
else result=await api('window.close',{plugin,window:entry});
|
||||
}
|
||||
else if(op==='ui.visibility.get')result=await api('ui.visibility.get',{view:viewKey()});
|
||||
else if(op==='haptic')result=await api('host.haptic',{view:viewKey()});
|
||||
else if(op==='keyboard')result=await api('host.keyboard',{...params,view:viewKey()});
|
||||
@@ -89,6 +95,8 @@ function App(){
|
||||
useEffect(()=>{if(!managerMode)return;const navigate=()=>{if(location.hash==='#catalog'){setPage(null);setCatalogTarget(null);setTab('catalog');}};window.addEventListener('hashchange',navigate);return()=>window.removeEventListener('hashchange',navigate);},[]);
|
||||
async function loadCatalog(){setCatalogTarget(null);setTab('catalog');}
|
||||
const plugins=Object.values(status.database.plugins).sort((a,b)=>a.order-b.order);
|
||||
const listedPlugins=plugins.filter(p=>(tab==='installed'||p.favorite)&&(managerMode||tab!=='installed'||p.enabled)&&`${p.manifest.name} ${p.manifest.description}`.toLowerCase().includes(query.toLowerCase()));
|
||||
const onlyDisabledInstalled=!managerMode&&tab==='installed'&&plugins.length>0&&listedPlugins.length===0&&!query;
|
||||
const selected=page?status.database.plugins[page]:null;
|
||||
const installedVersion=install?.info.installedVersion;
|
||||
const installDifference=installedVersion?compareVersions(install.info.manifest.version,installedVersion):null;
|
||||
@@ -100,17 +108,17 @@ function App(){
|
||||
if(!status.agreement.accepted)return <AgreementGate language={{value:status.database.language??'auto',packs:localePacks.current??[]}} key="required" initialDeclined={agreementDeclined} agreement={status.agreement} refresh={refresh}/>;
|
||||
if(reviewAgreement)return <AgreementGate language={{value:status.database.language??'auto',packs:localePacks.current??[]}} key="review" agreement={status.agreement} refresh={refresh} onClose={()=>setReviewAgreement(false)} onRevoked={closed=>{setAgreementDeclined(!closed);setReviewAgreement(false);setPage(null);setManage(null);}}/>;
|
||||
if(parts[1]==='notifications')return <Notifications status={status} error={error} action={action}/>;
|
||||
if(windowPlugin){const p=status.database.plugins[windowPlugin];return <div className="app"><header className="shell-header"><strong>{p?.manifest.name??t("插件窗口")}</strong><button onClick={()=>void action(()=>api('window.close',{plugin:windowPlugin,window:parts[3]}))}>{t("关闭")}</button></header>{p?.enabled&&!status.database.safeMode?<PluginFrame plugin={windowPlugin} entry={parts[3]}/>:<p className="empty">{t("插件已停用或正在启动。")}</p>}{error&&<p className="error">{error}</p>}</div>;}
|
||||
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?<><button className="primary" onClick={()=>setImporting(true)}><Icon name="download"/>{t("安装插件")}</button><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"><nav aria-label={t("主导航")}>{(managerMode?[['installed',t("已安装"),'plugins'],['catalog',t("插件库"),'download'],['sources',t("插件源导航"),'link'],['settings',t("设置"),'settings'],['about',t("关于"),'info']]:[['favorites',t("常用"),'star'],['installed',t("已安装"),'plugins'],['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)}><Icon name={icon as any}/><span>{label}</span></button>)}{managerMode&&<div className="nav-foot"><span>{plugins.length}{t("个已安装插件")}</span><small>{status.version}</small></div>}</nav><main className="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">{plugins.filter(p=>(tab==='installed'||p.favorite)&&`${p.manifest.name} ${p.manifest.description}`.toLowerCase().includes(query.toLowerCase())).map(p=><article className={`plugin-row ${!p.enabled?'disabled':''}`} key={p.manifest.id}>
|
||||
{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}>
|
||||
<button className="plugin-main" onClick={()=>setPage(p.manifest.id)}><PluginImage src={installedIcon(p)} name={p.manifest.name} size={48}/><span className="plugin-copy"><b>{p.manifest.name}</b>{managerMode&&<small>{p.manifest.description}</small>}<span className="plugin-meta"><i className={`status-dot ${p.error?'failed':!p.enabled?'off':''}`}/>{p.error?t("异常"):!p.enabled?t("已停用"):runtimeLabels[status.runtime?.[p.manifest.id]?.phase??'']??(status.running.includes(p.manifest.id)?t("运行中"):t("已启用"))}{!managerMode&&<span className="version">{p.manifest.version}</span>}</span></span></button>
|
||||
{managerMode&&<><span className="row-state">{p.error?t("异常"):!p.enabled?t("已停用"):runtimeLabels[status.runtime?.[p.manifest.id]?.phase??'']??(status.running.includes(p.manifest.id)?t("运行中"):t("已启用"))}</span><span className="row-version">{p.manifest.version}</span><button className={`plugin-switch ${p.enabled?'on':''}`} role="switch" aria-checked={p.enabled} aria-label={t("启用 {0}", {"0": p.manifest.name})} disabled={busy} onClick={()=>void action(()=>changePlugin(p,'enable'))}><span/></button></>}<button className={`icon-button favorite ${p.favorite?'selected':''}`} aria-label={t("收藏 {0}", {"0": p.manifest.name})} onClick={()=>void action(()=>api('plugin.favorite',{plugin:p.manifest.id,favorite:!p.favorite}))}><Icon name="star" size={23} filled={p.favorite}/></button>
|
||||
{managerMode&&<button className="icon-button" aria-label={t("管理 {0}", {"0": p.manifest.name})} onClick={()=>setManage(p)}><Icon name="more"/></button>}
|
||||
</article>)}</div>
|
||||
{plugins.filter(p=>(tab==='installed'||p.favorite)&&`${p.manifest.name} ${p.manifest.description}`.toLowerCase().includes(query.toLowerCase())).length===0&&<div className="empty-state"><span className="empty-symbol"><Icon name={tab==='favorites'?'star':'plugins'} size={46}/></span><h2>{query?t("未找到插件"):tab==='favorites'?t("没有收藏的插件"):t("还没有安装插件")}</h2><p>{query?t("换一个关键词再试。"):tab==='favorites'?t("点击插件旁的星标,将它加入常用。"):t("打开插件管理以安装插件。")}</p><button className="primary" onClick={()=>query?setQuery(''):tab==='favorites'?setTab('installed'):managerMode?setImporting(true):void api('host.manager.open')}>{query?t("清除搜索"):tab==='favorites'?t("查看已安装"):managerMode?t("安装插件"):t("打开插件管理")}</button></div>}
|
||||
{listedPlugins.length===0&&<div className="empty-state"><span className="empty-symbol"><Icon name={tab==='favorites'?'star':'plugins'} size={46}/></span><h2>{query?t("未找到插件"):tab==='favorites'?t("没有收藏的插件"):onlyDisabledInstalled?t("没有已启用的插件"):t("还没有安装插件")}</h2><p>{query?t("换一个关键词再试。"):tab==='favorites'?t("点击插件旁的星标,将它加入常用。"):onlyDisabledInstalled?t("打开插件管理以启用插件。"):t("打开插件管理以安装插件。")}</p><button className="primary" onClick={()=>query?setQuery(''):tab==='favorites'?setTab('installed'):managerMode?setImporting(true):void api('host.manager.open')}>{query?t("清除搜索"):tab==='favorites'?t("查看已安装"):managerMode?t("安装插件"):t("打开插件管理")}</button></div>}
|
||||
</>}
|
||||
{tab==='sources'&&managerMode&&<><div className="list-heading"><div><h1>{t("插件源")}</h1><p>{t("管理插件目录及订阅。")}</p></div></div><section className="settings-card"><Sources origins={status.database.sourceOrigins} sources={status.database.sources} save={async s=>{await api('sources.save',{sources:s});await refresh();}}/></section><Subscriptions subscriptions={status.database.subscriptions??[]} refresh={refresh}/></>}
|
||||
{tab==='about'&&managerMode&&<><div className="list-heading"><div><h1>{t("关于 Framely")}</h1></div></div><SystemUpdates status={status} refresh={refresh}/><section className="settings-card"><h2>{t("用户协议与隐私声明")}</h2><div className="row"><button onClick={()=>setReviewAgreement(true)}>{t("查看协议与隐私声明")}</button></div></section></>}
|
||||
|
||||
@@ -9,6 +9,7 @@ input:not([type=checkbox]):not([type=file]),textarea,select{width:100%;padding:1
|
||||
.settings-card{border-top:1px solid var(--line);padding:26px 0;margin:0 0 16px}.settings-card h2{color:#e5e7eb;font-size:24px;font-weight:550}.settings-card>p,.source-form,.settings-card>label{max-width:860px}.settings-card .sub{line-height:1.6}.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:16px}.row h2{flex:1}.source-row{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;padding:24px 0;border-bottom:1px solid var(--line)}.source-row>div:first-child{flex:1;min-width:0}.source-row .row{margin-top:0;max-width:470px}.source-row b{margin-right:14px;font-weight:550}.source-row p{margin:8px 0}.source-row small{font-size:16px}.source-url{display:block;margin:8px 0;color:var(--muted);font-size:17px;overflow-wrap:anywhere}.source-form{padding-top:28px}.confirm-box,.release-card{padding:22px;border:1px solid #565d68;background:#202328;margin-top:24px;border-radius:4px}.confirm-box p{color:#c9cfd9}.settings-card details{font-size:18px;margin-top:10px}.settings-card summary{cursor:pointer;padding:8px 0}.plugin{padding:18px 0;border-bottom:1px solid var(--line)}.plugin>small{display:block;overflow-wrap:anywhere}
|
||||
.empty-state{min-height:320px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:38px;text-align:center;gap:16px}.empty-state h2{font-size:26px;margin:0}.empty-state p{color:var(--muted);font-size:19px;max-width:520px;margin:0}.empty-symbol{color:#808994}.empty{padding:36px;text-align:center;color:var(--muted)}.page-title{padding:16px 24px;display:flex;align-items:center;gap:12px;flex:none;border-bottom:1px solid var(--line)}.page-title strong{flex:1;font-size:24px}.quick:has(.page-title) .shell-header{display:none}iframe{width:100%;flex:1;min-height:0;border:0;background:var(--bg)}.error,.banner{padding:14px 16px;margin:12px 0;border-radius:3px;line-height:1.5;overflow-wrap:anywhere;font-size:18px;flex:none;border-left:3px solid #929ca9;background:#252930;color:#d5dbe4}.error{border-color:#db9797;background:#3a292d;color:#f1b8b8}.app>.error,.app>.banner{margin:12px 26px}.error button{float:right;min-height:30px;padding:0 8px;background:transparent;border:0}.banner p{margin:8px 0}
|
||||
.modal{position:absolute;top:50%;bottom:auto;transform:translateY(-50%);max-height:calc(100% - 56px);left:max(24px,calc((100% - 780px)/2));right:max(24px,calc((100% - 780px)/2));padding:30px;background:#202328;border:1px solid #626974;border-radius:6px;overflow:auto;box-shadow:0 0 0 2000px #000a;z-index:20}.modal h2{font-size:28px;margin-bottom:16px}.modal h3{margin-top:24px}.modal button{margin:8px 8px 8px 0}.modal .source-row{display:block}.modal .row{margin:12px 0}.modal code{display:block;font-size:17px;overflow-wrap:anywhere;background:#141619;padding:16px;border-radius:3px}.modal pre{font:17px/1.6 monospace;white-space:pre-wrap;overflow-wrap:anywhere}.modal-close{float:right}.transfer{z-index:30;display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:16px}.transfer>p{margin:0}progress{width:100%;height:10px;accent-color:var(--blue)}.download-progress{margin:24px 0}.system-updates>h2{display:none}.system-updates .error small{display:block;margin-top:8px;color:inherit}.manage-dialog>button{min-width:150px}.manage-dialog>.danger{display:block}
|
||||
.update-confirmation{position:fixed;margin:0;width:auto;color:var(--text);box-shadow:none}.update-confirmation::backdrop{background:#000a}
|
||||
html.notification-view,html.notification-view body{background:transparent}.toasts{height:100%;overflow:auto;padding:8px;background:transparent;font-size:18px}.toast{padding:16px;background:#202328;border:1px solid #454b55;border-radius:8px;margin-bottom:8px}.toast-heading{display:flex;align-items:center;gap:10px}.toast-heading>div{flex:1;min-width:0}.toast-heading small{font-size:14px}.toast h2{font-size:21px;line-height:1.3;margin:2px 0 0;overflow-wrap:anywhere}.toast-close{min-width:36px;min-height:36px;padding:6px;border:0;background:transparent;color:var(--muted)}.toast-content{display:flex;align-items:flex-start;gap:12px;margin-top:12px}.toast-content img{width:80px;height:64px;object-fit:contain;flex:none}.toast-content p{margin:0;line-height:1.45;max-height:104px;overflow:auto;overflow-wrap:anywhere;flex:1}.toast-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:12px}.toast-actions button{min-width:44px;min-height:40px;padding:6px 10px;font-size:20px;border-radius:4px}
|
||||
@media(max-width:1000px){.manager .workspace{grid-template-columns:186px minmax(0,1fr)}.manager nav{padding:20px 8px}.manager nav button{padding:12px 10px;font-size:18px}.content{padding:24px}.row-state{width:90px}.row-version{width:85px}.list-columns{grid-template-columns:minmax(0,1fr) 90px 85px 184px}.list-heading{flex-wrap:wrap}.manager .plugin-copy small{white-space:normal}.source-row{flex-direction:column}.store-version{display:none}}
|
||||
@media(max-width:700px){.manager .workspace{display:flex}.manager nav{flex-direction:row;padding:8px;overflow:auto;border-right:0;border-bottom:1px solid var(--line)}.manager nav button{white-space:nowrap}.nav-foot{display:none}.shell-header{padding:14px 18px}.header-label{display:none}.shell-header .primary{font-size:18px}.content{padding:18px}.list-columns,.row-state,.row-version,.manager .plugin-switch{display:none}.manager .plugin-meta{display:flex}.list-heading .search-field{width:100%}.store-card{gap:14px;padding:18px 0}.store-card>.badge{min-width:65px;font-size:15px}.store-card>.plugin-avatar{width:64px!important;height:64px!important}.store-card>svg{display:none}.store-copy p{font-size:17px}.store-copy h2{font-size:22px}.store-filters .search-field{min-width:100%}.detail-heading{flex-wrap:wrap}.detail-heading .primary{width:100%}.modal{left:14px;right:14px;max-height:calc(100% - 28px);padding:22px}}
|
||||
|
||||
+14
-5
@@ -1,5 +1,5 @@
|
||||
import {t} from './i18n';
|
||||
import React,{useEffect,useState} from 'react';
|
||||
import React,{useEffect,useId,useLayoutEffect,useRef,useState} from 'react';
|
||||
import {api} from './api';
|
||||
import {runJob,Job,compareVersions} from './store';
|
||||
import {Select} from './localized-select';
|
||||
@@ -9,6 +9,13 @@ export function SystemUpdates({status,refresh}:{status:any;refresh:()=>Promise<v
|
||||
const source:UpdateSource|null=status.database.updateSource??null;
|
||||
const channel:UpdateChannel=status.database.updateChannel??'stable';
|
||||
const[error,setError]=useState(''),[busy,setBusy]=useState(false),[job,setJob]=useState<Job|null>(null),[release,setRelease]=useState<any>(null),[ready,setReady]=useState(false),[checked,setChecked]=useState(false),[confirm,setConfirm]=useState<'apply'|'rollback'|null>(null),[installing,setInstalling]=useState(false);
|
||||
const confirmation=useRef<HTMLDialogElement>(null),cancelConfirmation=useRef<HTMLButtonElement>(null),confirmationTitle=useId(),confirmationDescription=useId();
|
||||
useLayoutEffect(()=>{
|
||||
const dialog=confirmation.current;if(!confirm||!dialog)return;
|
||||
const previous=document.activeElement as HTMLElement|null;
|
||||
dialog.showModal();cancelConfirmation.current?.focus();
|
||||
return()=>{dialog.close();if(previous?.isConnected)previous.focus();};
|
||||
},[confirm]);
|
||||
function reset(){setRelease(null);setReady(false);setChecked(false);setConfirm(null);}
|
||||
useEffect(reset,[source?.url,channel]);
|
||||
const progress=job?.kind==='system.download'?job:status.systemUpdate;
|
||||
@@ -71,15 +78,17 @@ export function SystemUpdates({status,refresh}:{status:any;refresh:()=>Promise<v
|
||||
</div>}
|
||||
{error&&<p className="error" role="alert">{error}</p>}
|
||||
{installing&&<p className="banner">{t('正在切换版本。Framely 入口会在服务恢复后重新出现。')}</p>}
|
||||
{confirm&&<div className="confirm-box">
|
||||
<h3>{confirm==='apply'?t('安装 Framely {0}?',{'0':release.version}):t('回滚 Framely?')}</h3>
|
||||
{confirm&&<dialog ref={confirmation} className="modal update-confirmation" aria-labelledby={confirmationTitle} aria-describedby={confirmationDescription} onCancel={e=>{e.preventDefault();setConfirm(null);}} onKeyDown={e=>{if(e.key==='Escape'){e.stopPropagation();setConfirm(null);}}}>
|
||||
<h2 id={confirmationTitle}>{confirm==='apply'?t('安装 Framely {0}?',{'0':release.version}):t('回滚 Framely?')}</h2>
|
||||
<div id={confirmationDescription}>
|
||||
{confirm==='apply'&&!ready&&<p>{t('确认后将下载并校验发行包,校验通过后自动安装。')}</p>}
|
||||
<p>{t('插件和数据会保留,Framely 界面会暂时关闭。SteamVR 不会被重启。')}</p>
|
||||
{confirm==='apply'&&older&&<p className="banner">{t('所选版本早于当前版本,安装将切换到较旧版本。')}</p>}
|
||||
</div>
|
||||
<div className="row">
|
||||
<button className="primary" disabled={installing||busy} onClick={()=>void confirmUpdate()}>{confirm==='apply'?(ready?t('确认安装'):t('确认下载并安装')):t('确认回滚')}</button>
|
||||
<button disabled={installing} onClick={()=>setConfirm(null)}>{t('取消')}</button>
|
||||
<button ref={cancelConfirmation} disabled={installing} onClick={()=>setConfirm(null)}>{t('取消')}</button>
|
||||
</div>
|
||||
</div>}
|
||||
</dialog>}
|
||||
</section>;
|
||||
}
|
||||
Reference in new issue
Block a user