diff --git a/docs/user-guide/apk-manager.md b/docs/user-guide/apk-manager.md index 8d73bf3..5981f77 100644 --- a/docs/user-guide/apk-manager.md +++ b/docs/user-guide/apk-manager.md @@ -150,3 +150,5 @@ Steam 入口显示 APK 的应用名称和图标,并根据图标生成对应的 名称、图片和启动适配通过 Steam 已有的本地库界面 API 配置,连接其 8080 端口的本地调试接口。Framely 不开启该接口,也不为此修改 Steam 启动设置。API 不可用或快捷方式目标验证失败时,注册显示错误并重试,不直接修改运行中的快捷方式数据库,也不悄悄切换为无数据保护的启动。此 Steam 界面接口可能随 Steam 更新变化。 关闭原生 Steam 应用后,才能关闭“通过 Steam 启动”;关闭开关只移除 Framely 的入口。卸载 APK 时先停止其原生容器,再移除入口。只有明确选择的卸载或数据清理操作才删除应用数据。用户会话服务继续核对进程及容器启动标识,防止旧占用记录停止新的应用。 + +Container list headings show installed application names; the data path identifies the container. An APK update stops its container before backing up data. If stopping fails or a native Steam launch still owns it, the installation dialog offers **Close container** with confirmation. Closing interrupts running apps and retains their saves. The reviewed APK remains available so installation can be retried after closing. diff --git a/docs/zh-CN/user-guide/apk-manager.md b/docs/zh-CN/user-guide/apk-manager.md index 023fcb4..a7fc4bc 100644 --- a/docs/zh-CN/user-guide/apk-manager.md +++ b/docs/zh-CN/user-guide/apk-manager.md @@ -138,3 +138,5 @@ Steam 入口显示 APK 的应用名称和图标,并根据图标生成对应的 名称、图片和启动适配通过 Steam 已有的本地库界面 API 配置,连接其 8080 端口的本地调试接口。Framely 不开启该接口,也不为此修改 Steam 启动设置。API 不可用或快捷方式目标验证失败时,注册显示错误并重试,不直接修改运行中的快捷方式数据库,也不悄悄切换为无数据保护的启动。此 Steam 界面接口可能随 Steam 更新变化。 关闭原生 Steam 应用后,才能关闭“通过 Steam 启动”;关闭开关只移除 Framely 的入口。卸载 APK 时先停止其原生容器,再移除入口。只有明确选择的卸载或数据清理操作才删除应用数据。用户会话服务继续核对进程及容器启动标识,防止旧占用记录停止新的应用。 + +容器列表标题显示已安装应用名称,数据路径用于识别容器。更新 APK 会先停止容器再备份数据。如果停止失败,或原生 Steam 启动进程尚未释放容器,安装确认中提供 **关闭容器** 按钮并要求确认。关闭会中断运行中的应用,保留存档;已检查的 APK 会保留,可关闭后重试安装。 diff --git a/ui/locales/en-US.json b/ui/locales/en-US.json index 7d95e3a..7bfd022 100644 --- a/ui/locales/en-US.json +++ b/ui/locales/en-US.json @@ -921,6 +921,9 @@ "通过 Steam 启动": "Launch through Steam", "保存 Steam 启动设置": "Save Steam launch settings", "默认开启,自动包装已有和新安装的侧载应用。注册不启动应用或重启容器;首次通过 Steam 启动前请先关闭运行中的容器。": "Enabled by default. Existing and newly installed sideloaded apps are wrapped automatically. Registration does not start apps or restart containers. Close a running container before its first Steam launch.", - "等待 Steam 注册,Steam 就绪后会自动重试。": "Waiting for Steam registration. It will retry automatically when Steam is ready." + "等待 Steam 注册,Steam 就绪后会自动重试。": "Waiting for Steam registration. It will retry automatically when Steam is ready.", + "空容器": "Empty container", + "关闭容器": "Close container", + "安装前需要关闭容器,以安全备份应用数据。关闭会中断其中正在运行的应用,保留存档和设置。": "Close the container before installation to safely back up app data. This interrupts apps running inside it and retains saves and settings." } } diff --git a/ui/src/apk-manager.tsx b/ui/src/apk-manager.tsx index 6957f98..857ee61 100644 --- a/ui/src/apk-manager.tsx +++ b/ui/src/apk-manager.tsx @@ -43,6 +43,8 @@ export function ApkManager({view:externalView,onViewChange}:{view?:string;onView const listed=data.apps.filter(a=>(filter==='all'||(filter==='installed'?(a.installed||a.stateKnown===false):(!a.installed&&a.stateKnown!==false)))&&!a.steam&&`${a.metadata.name} ${a.metadata.package} ${a.context}`.toLowerCase().includes(query.toLowerCase())); const contextFor=(id:string)=>data.containers.find(c=>c.id===id); function containerAsk(c:Container,operation:string){setConfirm({operation,container:c,title:t(operation==='container.delete'?'删除容器':operation==='container.stop'?'停止容器':'启动容器'),description:`${t('此操作影响以下应用:')} ${data.apps.filter(a=>a.context===c.id).map(a=>a.metadata.name).join(', ')||c.name}${operation==='container.delete'?`\n${t('容器内全部数据将永久删除。')}`:''}`});} + const containerTitle=(c:Container)=>data.apps.filter(a=>a.context===c.id&&a.installed).map(a=>a.metadata.name).join(', ')||t('空容器'); + const installContainer=review?contextFor(target||updating?.context||data.apps.find(a=>!a.steam&&a.metadata.package===review.metadata.package)?.context||''):undefined; const deletesData=!!confirm&&(['purge','clear','cleanup','container.delete'].includes(confirm.operation)||(confirm.operation==='uninstall'&&!!confirm.purge)); const dialog=(children:React.ReactNode,label:string)=>createPortal(
{error&&!review&&!confirm&&

{error}

}{children}
,document.body); return
@@ -53,11 +55,11 @@ export function ApkManager({view:externalView,onViewChange}:{view?:string;onView {!externalView&&
{[['apps','应用'],['containers','容器'],['cleanup','数据清理'],['settings','设置']].map(([key,label])=>)}
} {view==='settings'&&<>void perform('lifecycle.settings',{autoStopContainer,stopContainerOnClose:data.stopContainerOnClose??true})}/>void perform('lifecycle.settings',{autoStopContainer:!!data.autoStopContainer,stopContainerOnClose})}/>void perform('gamepad.settings',{enabled})}/>} {view==='apps'&&<>
setQuery(e.target.value)}/>setPath(e.target.value)}/>setContextName(e.target.value)}/>
{data.roots.map(p=>
{p}
)}
} + {view==='containers'&&<>
{data.containers.map(c=>

{containerTitle(c)}

{c.baked}

{t(c.steam?'Steam 管理':c.managed?'Framely 管理':'已有侧载容器')} · {t(c.running?'运行中':'已停止')}

{!data.apps.some(a=>a.context===c.id&&a.installed)&&

{t('未发现已安装的应用,启动容器不会安装 APK。')}

}
{!c.steam&&<>}
)}

{t('其他数据位置')}

{t('添加其他工具使用的 baked 数据目录,不移动原有应用。')}

setPath(e.target.value)}/>setContextName(e.target.value)}/>
{data.roots.map(p=>
{p}
)}
} {view==='cleanup'&&<>

{t('数据清理')}

{t('存档、设置与恢复备份默认不选中。未知归属的数据不会自动删除。')}

{cleanup.map(i=>
setSelected(s=>checked?[...s,i.id]:s.filter(v=>v!==i.id))}/>
{i.name}

{t(i.kind==='unknown'?'未知归属的数据':i.kind==='data'?'保留的应用数据':i.kind==='backups'?'恢复备份':(i.kind==='reviews'||i.kind==='uploads')?'过期上传文件':'旧 APK')} · {bytes(i.bytes)}

{i.unknown?{t('无法确认归属,不提供自动删除。')}:i.sensitive&&{t('删除后不可恢复')}}
{i.restorable&&}
)}{!cleanup.length&&

{t('没有可清理的数据。')}

}} {!!data.warnings.length&&
{t('部分容器无法读取')}{data.warnings.map((w,i)=>

{w}

)}
} {details&&!confirm&&!review&&!log&&!job&&dialog(<>

{details.metadata.name}

{details.metadata.package}
{t('版本')}
{details.metadata.version} ({details.metadata.versionCode})
{t('容器')}
{contextFor(details.context)?.name??details.context}
{t('空间占用')}
{bytes(details.size)}
{details.pending&&<>

{t(details.pending)}

}
{details.installed&&details.stateKnown!==false&&!details.steam&&<>}{!details.installed&&details.stateKnown!==false&&!details.steam&&}
{details.installed&&details.stateKnown!==false&&!details.steam&&
{t('高级设置')}void perform('steam.settings',{app:details.id,enabled})}/>{(details.steamLaunch??true)&&!details.steamRegistered&&

{t('等待 Steam 注册,Steam 就绪后会自动重试。')}

}{details.steamRegistrationError&&

{details.steamRegistrationError}

}

{t('默认自动处理窗口显示。此开关不改变应用的二维或 VR 类型。')}

void perform('settings',{app:details.id,showWindow:checked?null:true})}/>{details.showWindow!=null&&void perform('settings',{app:details.id,showWindow:checked})}/>}

{t('窗口设置将在下次打开应用时应用;必要时自动重启容器。')}

},t('管理应用'))} - {review&&!job&&dialog(<>

{t(updating?'确认更新 APK':'确认安装 APK')}

{review.metadata.name}

{review.metadata.package}
{t('版本')}
{updating&&`${updating.metadata.version} → `}{review.metadata.version} ({review.metadata.versionCode})
{t('文件大小')}
{bytes(review.bytes)}
{t('最低 Android SDK')}
{review.metadata.minSdk||t('未声明')}
{t('启动入口')}
{review.metadata.activities.length||t('未发现启动入口')}
{!updating&&

{t('每个新应用使用独立容器,更新保留原容器。')}

}{target&&

{t('操作可能需要停止目标容器,影响其中正在运行的应用。')}

}{updating&&

{t('同包名更新保留数据;签名不匹配或降级不会自动卸载重装。')}

}{updating&&

{t('共享容器的备份需手动恢复;独立侧载容器支持一键恢复。')}

}{error&&

{error}

},t('安装 APK'))} + {review&&!confirm&&!job&&dialog(<>

{t(updating?'确认更新 APK':'确认安装 APK')}

{review.metadata.name}

{review.metadata.package}
{t('版本')}
{updating&&`${updating.metadata.version} → `}{review.metadata.version} ({review.metadata.versionCode})
{t('文件大小')}
{bytes(review.bytes)}
{t('最低 Android SDK')}
{review.metadata.minSdk||t('未声明')}
{t('启动入口')}
{review.metadata.activities.length||t('未发现启动入口')}
{!updating&&

{t('每个新应用使用独立容器,更新保留原容器。')}

}{installContainer?.running&&!installContainer.steam&&

{t('安装前需要关闭容器,以安全备份应用数据。关闭会中断其中正在运行的应用,保留存档和设置。')}

}{updating&&

{t('同包名更新保留数据;签名不匹配或降级不会自动卸载重装。')}

}{updating&&

{t('共享容器的备份需手动恢复;独立侧载容器支持一键恢复。')}

}{error&&

{error}

{installContainer&&!installContainer.steam&&!installContainer.running&&/container|backup|容器|备份/i.test(error)&&}
},t('安装 APK'))} {confirm&&!job&&dialog(<>

{confirm.title}

{confirm.description}

{confirm.app&&

{confirm.app.metadata.name}
{confirm.app.metadata.package}

}{confirm.operation==='uninstall'&&setConfirm(c=>c?{...c,purge}:c)}/>}{deletesData&&

{t('永久删除的数据无法撤销。')}

}{error&&

{error}

},confirm.title)} {job&&dialog(<>

{t(job.phase==='preparing'&&job.operation?operationLabels[job.operation]??phases[job.phase]:phases[job.phase]??job.phase)}

{t('操作进行中,请等待实际结果。')}

{job.phase==='uploading'&&

{bytes(job.received)} / {bytes(job.total??0)}

}{job.cancellable===false?

{t('操作已开始,请等待完成;此阶段无法取消。')}

:},t('APK 操作进度'))} {log&&!job&&dialog(<>

{t('APK 日志')}

{log.text||t('暂无日志')}
,t('APK 日志'))}