diff --git a/Cargo.lock b/Cargo.lock index 2821f16..75795cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,7 +291,7 @@ dependencies = [ [[package]] name = "framely" -version = "0.4.2-preview.11" +version = "0.4.2-preview.12" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 00ac396..316d2a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "framely" -version = "0.4.2-preview.11" +version = "0.4.2-preview.12" edition = "2021" license = "AGPL-3.0-only" diff --git a/docs/releases.md b/docs/releases.md index db5c48e..03d0457 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -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` (currently `v0.4.2-preview.11`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF. +- Device: root Cargo version, `v` (currently `v0.4.2-preview.12`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF. - Installer: `installer/Cargo.toml`, `installer-v` (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.11 -git push origin v0.4.2-preview.11 +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 diff --git a/docs/zh-CN/releases.md b/docs/zh-CN/releases.md index 2c5c6f1..94c0fda 100644 --- a/docs/zh-CN/releases.md +++ b/docs/zh-CN/releases.md @@ -48,7 +48,7 @@ CEF 按内容哈希保存在 `/home/.framely/cef/<运行库标识>`,更新时 Framely 和桌面安装器保留在同一仓库,使用独立的版本号、标签和发布工作流: -- Framely:版本由根 `Cargo.toml` 定义,标签为 `v<版本>`(当前 `v0.4.2-preview.11`);`.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.11 -git push origin v0.4.2-preview.11 +git tag v0.4.2-preview.12 +git push origin v0.4.2-preview.12 # 独立发布安装器,不触发 Framely 构建 git tag installer-v0.4.1-preview.8 diff --git a/tests/system_updates_fixture.tsx b/tests/system_updates_fixture.tsx index 9e8d11c..1f31e00 100644 --- a/tests/system_updates_fixture.tsx +++ b/tests/system_updates_fixture.tsx @@ -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'); diff --git a/ui/src/style.css b/ui/src/style.css index 12e50d1..8222007 100644 --- a/ui/src/style.css +++ b/ui/src/style.css @@ -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}} diff --git a/ui/src/updates.tsx b/ui/src/updates.tsx index 19b6e7c..b5501c7 100644 --- a/ui/src/updates.tsx +++ b/ui/src/updates.tsx @@ -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(null),[release,setRelease]=useState(null),[ready,setReady]=useState(false),[checked,setChecked]=useState(false),[confirm,setConfirm]=useState<'apply'|'rollback'|null>(null),[installing,setInstalling]=useState(false); + const confirmation=useRef(null),cancelConfirmation=useRef(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} {error&&

{error}

} {installing&&

{t('正在切换版本。Framely 入口会在服务恢复后重新出现。')}

} - {confirm&&
-

{confirm==='apply'?t('安装 Framely {0}?',{'0':release.version}):t('回滚 Framely?')}

+ {confirm&&{e.preventDefault();setConfirm(null);}} onKeyDown={e=>{if(e.key==='Escape'){e.stopPropagation();setConfirm(null);}}}> +

{confirm==='apply'?t('安装 Framely {0}?',{'0':release.version}):t('回滚 Framely?')}

+
{confirm==='apply'&&!ready&&

{t('确认后将下载并校验发行包,校验通过后自动安装。')}

}

{t('插件和数据会保留,Framely 界面会暂时关闭。SteamVR 不会被重启。')}

{confirm==='apply'&&older&&

{t('所选版本早于当前版本,安装将切换到较旧版本。')}

} +
- +
-
} + } ; }