Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions apps/web/src/lib/boot/profile-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ describe('profile and deployment boundaries', () => {
expect(
profile.preinstall.every((entry) => OFFICIAL_PLUGINS.some((p) => p.id === entry.id))
).toBe(true);
expect(
profile.preinstall.some(
(entry) => entry.id === profile.defaultTheme.pluginId && entry.enabled !== false
)
).toBe(true);
expect(profile.preinstall.some((entry) => entry.id === profile.defaultTheme.pluginId)).toBe(
true
);
expect(profile.preinstall.some((entry) => entry.id === 'core-shell')).toBe(false);
expect(profile.preinstall.some((entry) => entry.id === 'theme-arknights')).toBe(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,15 @@
<Button variant="outlined" onclick={onInstall}>{hostT('plugins.action.update')}</Button>
{:else if installed}
<span
class="inline-flex items-center gap-1 rounded-full bg-primary-container/50 px-2.5 py-1 text-xs font-medium text-primary"
class="text-label-large inline-flex h-7 min-h-7 min-w-24 shrink-0 items-center justify-center gap-1 rounded-full border border-transparent bg-primary-container/50 px-2.5 text-primary"
>
<CheckCircleFill class="size-3.5" />
{hostT('plugins.badge.installed')}
</span>
{:else}
<Button
variant="filled"
class="text-label-small h-7 !min-h-7 shrink-0 px-2.5"
class="text-label-large h-7 !min-h-7 min-w-24 shrink-0 px-2.5"
onclick={onInstall}
>
{hostT('plugins.action.install')}
Expand Down
126 changes: 51 additions & 75 deletions apps/web/src/lib/components/plugins/PluginsScreen.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import { resolveLocaleMapText } from '@chronos/core';
import SegmentedControl from '$lib/components/ui/SegmentedControl.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Switch from '$lib/components/ui/Switch.svelte';
import Dialog from '$lib/components/ui/Dialog.svelte';
import BottomSheet from '$lib/components/ui/BottomSheet.svelte';
import LoadingIndicator from '$lib/components/ui/LoadingIndicator.svelte';
Expand Down Expand Up @@ -258,17 +257,13 @@
}
}

async function handleToggleEnabled(pluginId: string, enabled: boolean) {
async function handleConfirmCompatibility(pluginId: string) {
operatingPluginId = pluginId;
try {
if (enabled) {
await officialPlugins.enable(pluginId);
} else {
await officialPlugins.disable(pluginId);
}
await officialPlugins.confirmHostCompatibility(pluginId);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
snackbarKey('snackbar.toggle.failed', { message: msg });
snackbarKey('snackbar.confirmCompatibility.failed', { message: msg });
} finally {
operatingPluginId = null;
}
Expand Down Expand Up @@ -386,13 +381,10 @@
{@const isBusy = operatingPluginId === record.manifest.id}
{@const update = updateStatuses[record.manifest.id]}
<div
class={[
'flex flex-col gap-2 p-3 transition-colors hover:bg-surface-variant/30',
!record.enabled && 'opacity-60'
]}
class="flex items-center justify-between gap-3 p-3 transition-colors hover:bg-surface-variant/30"
>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<div class="flex min-w-0 flex-1 flex-col gap-2">
<div>
<div class="flex flex-wrap items-center gap-1.5">
<span class="text-body-medium line-clamp-1 font-medium text-on-surface">
{name}
Expand All @@ -414,33 +406,48 @@
</p>
{/if}
</div>
{#if officialPlugins.isPreinstalledPlugin(record.manifest.id)}
<span
class="text-label-small shrink-0 rounded-full bg-surface-variant px-2 py-0.5 text-on-surface-variant"
>{hostT('plugins.preinstall.label')}</span
>

{#if update && update.status !== 'ready'}
<p class="text-body-small text-on-surface-variant" title={update.error}>
{hostT(`plugins.update.${update.status}`)}
</p>
{#if update.status === 'confirmation-required'}
<Button
variant="outlined"
disabled={isBusy}
onclick={() => handleConfirmCompatibility(record.manifest.id)}
>{hostT('plugins.action.confirmCompatibility')}</Button
>
{:else if update.status !== 'downloading'}
<Button variant="outlined" onclick={() => officialPlugins.retryPendingUpdates()}
>{hostT('plugins.action.retry')}</Button
>
{/if}
{/if}
{#if record.manifest.configSchema}
<div class="flex items-center gap-1.5">
<Button
variant="outlined"
class="text-label-small h-7 px-2.5 font-normal"
disabled={isBusy || update?.status === 'confirmation-required'}
onclick={() =>
handleOpenConfig(record.manifest.id, name, record.manifest.configSchema)}
>
<TuneFill class="mr-0.5 size-3" />
{hostT('plugins.action.settings')}
</Button>
</div>
{/if}
</div>

{#if update && update.status !== 'ready'}
<p class="text-body-small text-on-surface-variant" title={update.error}>
{hostT(`plugins.update.${update.status}`)}
</p>
{#if update.status === 'confirmation-required'}
<Button
variant="outlined"
disabled={isBusy}
onclick={() => handleToggleEnabled(record.manifest.id, true)}
>{hostT('plugins.action.enable')}</Button
>
{:else if update.status !== 'downloading'}
<Button variant="outlined" onclick={() => officialPlugins.retryPendingUpdates()}
>{hostT('plugins.action.retry')}</Button
<div class="flex shrink-0 items-center justify-end">
{#if officialPlugins.isPreinstalledPlugin(record.manifest.id)}
<span
class="text-label-large inline-flex h-7 min-h-7 min-w-24 shrink-0 items-center justify-center gap-1 rounded-full border border-transparent bg-surface-variant px-2.5 text-on-surface-variant"
>
{/if}
{/if}
<div class="flex items-center justify-between gap-2">
{#if !officialPlugins.isPreinstalledPlugin(record.manifest.id)}
<CheckCircleFill class="size-3.5" />
{hostT('plugins.preinstall.label')}
</span>
{:else}
<Button
variant="text"
tone="danger"
Expand All @@ -451,33 +458,6 @@
{hostT('common.uninstall')}
</Button>
{/if}

<div class="flex shrink-0 items-center gap-1.5">
{#if record.manifest.configSchema}
<Button
variant="outlined"
class="text-label-small h-7 px-2.5 font-normal"
disabled={isBusy || !record.enabled}
onclick={() =>
handleOpenConfig(record.manifest.id, name, record.manifest.configSchema)}
>
<TuneFill class="mr-0.5 size-3" />
{hostT('plugins.action.settings')}
</Button>
{/if}
{#if !officialPlugins.isPreinstalledPlugin(record.manifest.id)}
<span class="text-label-small text-on-surface-variant">
{hostT('plugins.action.enable')}
</span>
<Switch
size="sm"
checked={record.enabled && update?.status !== 'confirmation-required'}
disabled={isBusy}
onCheckedChange={(checked) =>
handleToggleEnabled(record.manifest.id, checked === true)}
/>
{/if}
</div>
</div>
</div>
{/each}
Expand Down Expand Up @@ -516,15 +496,10 @@
{#each groupedCatalogManifests as group (group.category)}
{@const groupMeta = getPluginCategoryMeta(group.category)}
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between px-1">
<div class="px-1">
<h3 class="text-label-large font-medium text-on-surface">
{groupMeta.label}
</h3>
<span class="text-label-small text-on-surface-variant">
{hostT('plugins.builtin.count', {
count: group.entries.length
})}
</span>
</div>
<div class="ui-section-surface [&>*+*]:border-t [&>*+*]:border-border/40">
{#each group.entries as entry (entry.manifest.id)}
Expand Down Expand Up @@ -559,10 +534,11 @@
>
{/if}
{#if manifest.downloadSizeBytes !== undefined}
<span class="text-caption text-on-surface-variant/70">
{hostT('plugins.size', {
size: formatBytes(manifest.downloadSizeBytes)
})}
<span
class="text-caption inline-flex items-center gap-1 text-on-surface-variant/70"
>
<span aria-hidden="true">|</span>
{formatBytes(manifest.downloadSizeBytes, 0)}
</span>
{/if}
</div>
Expand Down
13 changes: 5 additions & 8 deletions apps/web/src/lib/i18n/host-messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const en = {
// snackbar
'snackbar.install.failed': 'Install failed: {message}',
'snackbar.uninstall.failed': 'Uninstall failed: {message}',
'snackbar.toggle.failed': 'Action failed: {message}',
'snackbar.confirmCompatibility.failed': 'Compatibility confirmation failed: {message}',
'snackbar.manifestRequired': 'Enter a manifest.json URL',
'snackbar.manifestInvalid': 'Enter a valid http or https manifest URL',

Expand Down Expand Up @@ -116,15 +116,14 @@ export const en = {
'plugins.empty.browse': 'Browse the marketplace',
'plugins.badge.installed': 'Installed',
'plugins.action.settings': 'Settings',
'plugins.action.enable': 'Enable',
'plugins.builtin.defaultEnabled': 'Enabled by default',
'plugins.action.confirmCompatibility': 'Confirm compatibility',
'plugins.action.install': 'Install',
'plugins.action.installing': 'Installing…',
'plugins.action.update': 'Update',
'plugins.update.pending': 'Waiting for a matching version',
'plugins.update.downloading': 'Downloading update',
'plugins.update.confirmation-required':
'Paused after host update; enable to confirm compatibility',
'Paused after app update; confirm compatibility to run this plugin',
'plugins.update.ready': 'Ready',
'plugins.update.failed': 'Update failed; retry',
'plugins.action.retry': 'Retry',
Expand All @@ -146,11 +145,9 @@ export const en = {
'plugins.link.installing': 'Installing from link…',
'plugins.link.placeholder': 'https://example.com/plugin.manifest.json',
'plugins.link.source': 'Source: {origin}. Only install if you trust this source',
'plugins.size': 'Size: {size}',
'plugins.notify.installed': 'Plugin "{pluginId}" installed and enabled',
'plugins.notify.installed': 'Plugin "{pluginId}" installed',
'plugins.notify.uninstalled': 'Plugin "{pluginId}" uninstalled',
'plugins.notify.enabled': 'Plugin "{pluginId}" enabled',
'plugins.notify.disabled': 'Plugin "{pluginId}" disabled',
'plugins.notify.confirmed': 'Compatibility confirmed for plugin "{pluginId}"',
'plugins.config.title': 'Plugin settings — {name}',
'plugins.config.empty': 'This plugin has no configurable options',
'plugins.config.saving': 'Saving…',
Expand Down
13 changes: 5 additions & 8 deletions apps/web/src/lib/i18n/host-messages/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const zhCn = {
// snackbar
'snackbar.install.failed': '安装失败: {message}',
'snackbar.uninstall.failed': '卸载失败: {message}',
'snackbar.toggle.failed': '操作失败: {message}',
'snackbar.confirmCompatibility.failed': '兼容性确认失败: {message}',
'snackbar.manifestRequired': '请输入 manifest.json 链接',
'snackbar.manifestInvalid': '请输入有效的 http 或 https manifest 链接',

Expand Down Expand Up @@ -114,14 +114,13 @@ export const zhCn = {
'plugins.empty.browse': '浏览插件市场',
'plugins.badge.installed': '已安装',
'plugins.action.settings': '设置',
'plugins.action.enable': '启用',
'plugins.builtin.defaultEnabled': '默认启用',
'plugins.action.confirmCompatibility': '确认兼容并运行',
'plugins.action.install': '安装',
'plugins.action.installing': '安装中…',
'plugins.action.update': '更新',
'plugins.update.pending': '等待适配当前应用版本',
'plugins.update.downloading': '正在下载更新',
'plugins.update.confirmation-required': '应用已更新,需手动启用以确认兼容性',
'plugins.update.confirmation-required': '应用已更新,确认兼容后才能运行此插件',
'plugins.update.ready': '已就绪',
'plugins.update.failed': '更新失败,可重试',
'plugins.action.retry': '重试',
Expand All @@ -142,11 +141,9 @@ export const zhCn = {
'plugins.link.installing': '正在从链接安装…',
'plugins.link.placeholder': 'https://example.com/plugin.manifest.json',
'plugins.link.source': '来源:{origin},请确认信任该来源后再安装',
'plugins.size': '大小:{size}',
'plugins.notify.installed': '插件「{pluginId}」已安装并启用',
'plugins.notify.installed': '已安装插件「{pluginId}」',
'plugins.notify.uninstalled': '插件「{pluginId}」已卸载',
'plugins.notify.enabled': '已启用插件「{pluginId}」',
'plugins.notify.disabled': '已停用插件「{pluginId}」',
'plugins.notify.confirmed': '已确认插件「{pluginId}」兼容当前应用',
'plugins.config.title': '插件设置 - {name}',
'plugins.config.empty': '该插件无可配置项',
'plugins.config.saving': '保存中…',
Expand Down
17 changes: 3 additions & 14 deletions apps/web/src/lib/profile-codegen/profile-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ const DEFAULT_PROFILE: ChronosProfile = {
description: '包含分享口令与标准备份能力',
defaultTheme: { pluginId: 'theme-m3', themeId: 'm3-default' },
defaultImportSlot: 'share-link',
preinstall: [
{ id: 'theme-m3', enabled: true },
{ id: 'codec-share', enabled: true }
]
preinstall: [{ id: 'theme-m3' }, { id: 'codec-share' }]
};

const CQUT_PROFILE: ChronosProfile = {
Expand All @@ -21,11 +18,7 @@ const CQUT_PROFILE: ChronosProfile = {
description: '专为重庆理工大学定制,内置知行理工教务直连与校区专属节次',
defaultTheme: { pluginId: 'theme-m3', themeId: 'm3-default' },
defaultImportSlot: 'cqut-online',
preinstall: [
{ id: 'theme-m3', enabled: true },
{ id: 'source-cqut', enabled: true },
{ id: 'codec-share', enabled: true }
]
preinstall: [{ id: 'theme-m3' }, { id: 'source-cqut' }, { id: 'codec-share' }]
};

const CQUT_OFFLINE_PROFILE: ChronosProfile = {
Expand All @@ -35,11 +28,7 @@ const CQUT_OFFLINE_PROFILE: ChronosProfile = {
defaultTheme: { pluginId: 'theme-m3', themeId: 'm3-default' },
defaultImportSlot: 'edu-html',
deniedPluginServerActions: [{ pluginId: 'source-cqut', action: 'preview' }],
preinstall: [
{ id: 'theme-m3', enabled: true },
{ id: 'source-cqut', enabled: true },
{ id: 'codec-share', enabled: true }
]
preinstall: [{ id: 'theme-m3' }, { id: 'source-cqut' }, { id: 'codec-share' }]
};

export const CHRONOS_PROFILES: Record<string, ChronosProfile> = {
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/lib/services/app-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ describe('app-engine bootstrap', () => {
acceptedHostVersion: APP_VERSION,
colorsJson: themeColorsJson,
manifestUrl: 'https://example.com/theme-yumemita.manifest.json',
enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
};
Expand Down Expand Up @@ -354,7 +353,6 @@ describe('theme preferences during deferred boot', () => {
colorsUrl: '/broken.json'
},
colorsJson: 'invalid json',
enabled: true,
origin: { kind: 'user' as const },
installedAt: 1
};
Expand Down
7 changes: 3 additions & 4 deletions apps/web/src/lib/services/app-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,8 @@ async function bootstrapEnginePhase2(engine: ChronosEngine): Promise<void> {
service.listFailures().size > 0 ||
records.some(
(record) =>
record.enabled &&
((!record.manifest.colorsUrl && !service.isPluginActive(record.manifest.id)) ||
record.manifest.themeId === preferred)
(!record.manifest.colorsUrl && !service.isPluginActive(record.manifest.id)) ||
record.manifest.themeId === preferred
);
if (!couldRecover) await engine.revertToDefaultThemes();
}
Expand Down Expand Up @@ -205,7 +204,7 @@ function getMissingPreinstalls(
const installed = new Map(service.listInstalled().map((record) => [record.manifest.id, record]));
return profile.preinstall.flatMap((entry) => {
const record = installed.get(entry.id);
return record && record.enabled === (entry.enabled !== false) ? [] : [entry.id];
return record ? [] : [entry.id];
});
}

Expand Down
Loading
Loading