@dsh-plus/image-studio 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +3569 -60
- package/lib/index.d.ts +2 -1
- package/lib/index.js +3 -2
- package/package.json +5 -3
- package/src/client/card.tsx +579 -9
- package/src/client/client.ts +69 -36
- package/src/client/i18n.ts +230 -9
- package/src/client/panel/controller.ts +75 -0
- package/src/client/panel/entry.tsx +35 -0
- package/src/client/panel/gallery.tsx +217 -0
- package/src/client/panel/generator.tsx +521 -0
- package/src/client/panel/panel.tsx +220 -0
- package/src/client/panel/param-state.ts +144 -0
- package/src/client/panel/params-form.tsx +136 -0
- package/src/client/panel/picker.tsx +117 -0
- package/src/client/panel/tasks.tsx +72 -0
- package/src/client/styles.ts +554 -17
- package/src/service.ts +7 -5
- package/src/client/section.tsx +0 -14
package/src/client/client.ts
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* -
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
2
|
+
* 浏览器半入口:
|
|
3
|
+
* - sidebar.footer.action:侧栏底部入口按钮(与文件/终端入口 flex 等分行宽);
|
|
4
|
+
* - shell.overlay:图像工作室面板(文生图/图生图/画廊 + 任务条);
|
|
5
|
+
* - settings.plugin.item:配置卡片(三组预设 + 凭据 + 高级项,key = settings 命名空间)。
|
|
6
|
+
* 模式与 web-files/web-terminal 一致:CJS factory 产物、data-plugin-css 样式
|
|
7
|
+
* 约定、slot 失配静默降级;配置读写经 ctx.remote.settings 直连(远程域名可用)。
|
|
6
8
|
* @module @dsh-plus/image-studio/client
|
|
7
9
|
*/
|
|
8
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
11
|
+
import {
|
|
12
|
+
cardCss,
|
|
13
|
+
createNamespaceApi,
|
|
14
|
+
createSettingsScope,
|
|
15
|
+
injectCardStyle,
|
|
16
|
+
type ScopeHostContext,
|
|
17
|
+
} from '@dsh-plus/shared/client'
|
|
18
|
+
|
|
19
|
+
import { SETTINGS_NS } from '../ns.ts'
|
|
9
20
|
import { StudioConfigCard } from './card.tsx'
|
|
10
|
-
import { en, NS, zh } from './i18n.ts'
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
21
|
+
import { en, NS, type Translate, zh } from './i18n.ts'
|
|
22
|
+
import { createPanelController } from './panel/controller.ts'
|
|
23
|
+
import { StudioEntryButton } from './panel/entry.tsx'
|
|
24
|
+
import { StudioPanel } from './panel/panel.tsx'
|
|
25
|
+
import { injectStudioStyles } from './styles.ts'
|
|
13
26
|
|
|
14
27
|
export const name = 'dsh-plus-image-studio'
|
|
15
28
|
|
|
16
|
-
/** 浏览器半需要的 cordis 服务 key(loader
|
|
29
|
+
/** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
|
|
17
30
|
export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
|
|
18
31
|
|
|
32
|
+
const PLUGIN_ID = '@dsh-plus/image-studio'
|
|
33
|
+
|
|
19
34
|
interface SlotsLike {
|
|
20
35
|
inject(key: string, callback: () => unknown): unknown
|
|
21
36
|
register(options: Record<string, unknown>, component: unknown): () => void
|
|
@@ -26,50 +41,68 @@ interface LocaleLike {
|
|
|
26
41
|
bind(ns: string): (key: string) => string
|
|
27
42
|
}
|
|
28
43
|
|
|
29
|
-
|
|
44
|
+
/** createSettingsScope 要求的宿主窄面 + 本插件用到的 slots/locale 服务。 */
|
|
45
|
+
interface ClientContext extends ScopeHostContext {
|
|
30
46
|
slots: SlotsLike
|
|
31
47
|
locale: LocaleLike
|
|
32
|
-
effect(execute: () => () => void, label?: string): unknown
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
export function apply(ctx: Context): void {
|
|
36
51
|
const c = ctx as unknown as ClientContext
|
|
37
|
-
const
|
|
38
|
-
const cardTag =
|
|
39
|
-
|
|
40
|
-
() => () => {
|
|
41
|
-
sectionTag?.remove()
|
|
42
|
-
cardTag?.remove()
|
|
43
|
-
},
|
|
44
|
-
'image-studio: style',
|
|
45
|
-
)
|
|
52
|
+
const panelTag = injectStudioStyles()
|
|
53
|
+
const cardTag = injectCardStyle(PLUGIN_ID, cardCss('imsc'))
|
|
54
|
+
const controller = createPanelController()
|
|
46
55
|
c.effect(() => c.locale.register(NS, { zh, en }), 'image-studio: locale')
|
|
47
|
-
const t = c.locale.bind(NS)
|
|
56
|
+
const t = c.locale.bind(NS) as Translate
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
c.effect(
|
|
59
|
+
() =>
|
|
60
|
+
c.slots.inject('sidebar.footer.action', () =>
|
|
61
|
+
c.slots.register(
|
|
62
|
+
{
|
|
63
|
+
name: 'sidebar.footer.action',
|
|
64
|
+
id: 'image-studio-entry',
|
|
65
|
+
locale: NS,
|
|
66
|
+
inject: () => ({ studio: controller, t }),
|
|
67
|
+
},
|
|
68
|
+
StudioEntryButton,
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
'image-studio: entry slot',
|
|
72
|
+
)
|
|
73
|
+
const scope = createSettingsScope(c, SETTINGS_NS, 'image-studio: settings scope')
|
|
74
|
+
const api = createNamespaceApi(c.get('remote').settings, SETTINGS_NS)
|
|
75
|
+
c.effect(
|
|
76
|
+
() =>
|
|
77
|
+
c.slots.inject('shell.overlay', () =>
|
|
78
|
+
c.slots.register(
|
|
79
|
+
{
|
|
80
|
+
name: 'shell.overlay',
|
|
81
|
+
id: 'image-studio-panel',
|
|
82
|
+
locale: NS,
|
|
83
|
+
inject: () => ({ studio: controller, t, scope, api }),
|
|
84
|
+
},
|
|
85
|
+
StudioPanel,
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
'image-studio: overlay slot',
|
|
61
89
|
)
|
|
62
|
-
|
|
63
|
-
// 配置卡片占位(settings.plugin.item:keyed 槽位,key = settings 命名空间)。
|
|
64
90
|
c.slots.inject('settings.plugin.item', () =>
|
|
65
91
|
c.slots.register(
|
|
66
92
|
{
|
|
67
93
|
name: 'settings.plugin.item',
|
|
68
|
-
key:
|
|
94
|
+
key: SETTINGS_NS,
|
|
69
95
|
locale: NS,
|
|
70
|
-
inject: () => ({ t }),
|
|
96
|
+
inject: () => ({ t, scope, api }),
|
|
71
97
|
},
|
|
72
98
|
StudioConfigCard,
|
|
73
99
|
),
|
|
74
100
|
)
|
|
101
|
+
c.effect(
|
|
102
|
+
() => () => {
|
|
103
|
+
panelTag?.remove()
|
|
104
|
+
cardTag?.remove()
|
|
105
|
+
},
|
|
106
|
+
'image-studio: cleanup',
|
|
107
|
+
)
|
|
75
108
|
}
|
package/src/client/i18n.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* i18n 字典(zh
|
|
3
|
-
* 仅承载前端文案 key
|
|
2
|
+
* i18n 字典(zh 为键域事实源,en 全量对译);NS 与 settings 命名空间分离,
|
|
3
|
+
* 仅承载前端文案 key。面板 / 入口 / 配置卡片共用同一命名空间。
|
|
4
4
|
* @module image-studio/client/i18n
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -8,13 +8,234 @@
|
|
|
8
8
|
export const NS = 'dsh-plus-image-studio'
|
|
9
9
|
|
|
10
10
|
export const zh = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// 入口与面板骨架
|
|
12
|
+
'entry.label': '图像',
|
|
13
|
+
'panel.title': '图像工作室',
|
|
14
|
+
'tab.generation': '文生图',
|
|
15
|
+
'tab.edit': '图生图',
|
|
16
|
+
'tab.gallery': '画廊',
|
|
17
|
+
'common.close': '关闭',
|
|
18
|
+
'common.cancel': '取消',
|
|
19
|
+
'common.save': '保存',
|
|
20
|
+
'common.saving': '保存中…',
|
|
21
|
+
'common.retry': '重试',
|
|
22
|
+
'common.loading': '加载中…',
|
|
23
|
+
'common.loadFailed': '加载失败',
|
|
24
|
+
'common.delete': '删除',
|
|
25
|
+
'common.confirmDelete': '确认删除',
|
|
26
|
+
'common.refresh': '刷新',
|
|
27
|
+
'common.download': '下载',
|
|
28
|
+
'common.unsaved': '未保存',
|
|
29
|
+
// 提供商
|
|
30
|
+
'provider.label': '提供商',
|
|
31
|
+
'provider.placeholder': '选择提供商预设…',
|
|
32
|
+
'provider.empty': '暂无提供商预设:请到 设置 → 插件 → 图像工作室 添加',
|
|
33
|
+
'provider.credential.configured': 'Key 已配置',
|
|
34
|
+
'provider.credential.missing': 'Key 未配置',
|
|
35
|
+
'provider.inline.toggle': '临时覆盖协议/地址/模型',
|
|
36
|
+
'provider.inline.protocol': '协议',
|
|
37
|
+
'provider.inline.baseUrl': 'API 地址',
|
|
38
|
+
'provider.inline.model': '模型',
|
|
39
|
+
'provider.inline.hint': '覆盖时凭据沿用所选预设的 API Key',
|
|
40
|
+
// 提示词
|
|
41
|
+
'prompt.label': '提示词',
|
|
42
|
+
'prompt.placeholder': '描述想要生成的画面…',
|
|
43
|
+
'prompt.preset.insert': '插入提示词预设…',
|
|
44
|
+
'prompt.preset.save': '存为预设',
|
|
45
|
+
'prompt.preset.name': '预设名称',
|
|
46
|
+
'prompt.preset.saved': '已保存提示词预设',
|
|
47
|
+
// 参数
|
|
48
|
+
'params.basic': '常用参数',
|
|
49
|
+
'params.advanced': '高级参数',
|
|
50
|
+
'params.preset.placeholder': '应用参数预设…',
|
|
51
|
+
'params.preset.save': '存为预设',
|
|
52
|
+
'params.preset.saved': '已保存参数预设',
|
|
53
|
+
'params.enabled': '启用',
|
|
54
|
+
// 源图与遮罩
|
|
55
|
+
'sources.label': '源图',
|
|
56
|
+
'sources.pick': '选择源图',
|
|
57
|
+
'sources.count': '已选 {n}/16 张',
|
|
58
|
+
'sources.mask': '遮罩(可选)',
|
|
59
|
+
'sources.maskPick': '选择遮罩',
|
|
60
|
+
'sources.maskClear': '清除',
|
|
61
|
+
'picker.empty': '画廊暂无图片',
|
|
62
|
+
'picker.confirm': '使用所选({n})',
|
|
63
|
+
// 提交
|
|
64
|
+
'generate.submit': '生成',
|
|
65
|
+
'generate.submitted': '已提交任务 {id},进度见下方任务条',
|
|
66
|
+
'generate.failed': '提交失败:',
|
|
67
|
+
'generate.needProvider': '请先选择提供商预设',
|
|
68
|
+
'generate.needPrompt': '请填写提示词',
|
|
69
|
+
'generate.needSources': '请先选择至少一张源图',
|
|
70
|
+
// 任务条
|
|
71
|
+
'tasks.title': '任务',
|
|
72
|
+
'tasks.state.queued': '排队中',
|
|
73
|
+
'tasks.state.running': '生成中',
|
|
74
|
+
'tasks.state.succeeded': '已完成',
|
|
75
|
+
'tasks.state.failed': '失败',
|
|
76
|
+
'tasks.state.cancelled': '已取消',
|
|
77
|
+
'tasks.view': '查看',
|
|
78
|
+
// 画廊
|
|
79
|
+
'gallery.empty': '画廊为空,去文生图生成第一张吧',
|
|
80
|
+
'gallery.total': '共 {n} 条',
|
|
81
|
+
'gallery.detail.prompt': '提示词',
|
|
82
|
+
'gallery.detail.revised': '改写提示词',
|
|
83
|
+
'gallery.detail.params': '请求参数',
|
|
84
|
+
'gallery.detail.model': '模型',
|
|
85
|
+
'gallery.detail.endpoint': '端点',
|
|
86
|
+
'gallery.detail.time': '时间',
|
|
87
|
+
'gallery.detail.sources': '源图',
|
|
88
|
+
'gallery.edit': '二次编辑',
|
|
89
|
+
// 配置卡片
|
|
90
|
+
'card.title': '图像工作室',
|
|
91
|
+
'card.description': '文生图/图生图:提供商与 API Key、参数/提示词预设',
|
|
92
|
+
'card.providers': '提供商预设',
|
|
93
|
+
'card.prompts': '提示词预设',
|
|
94
|
+
'card.params': '参数预设',
|
|
95
|
+
'card.advanced': '高级设置',
|
|
96
|
+
'card.add': '添加',
|
|
97
|
+
'card.field.id': 'ID',
|
|
98
|
+
'card.field.name': '名称',
|
|
99
|
+
'card.field.protocol': '协议',
|
|
100
|
+
'card.field.baseUrl': 'API 地址',
|
|
101
|
+
'card.field.model': '模型',
|
|
102
|
+
'card.field.extraHeaders': '附加请求头(JSON)',
|
|
103
|
+
'card.field.text': '提示词模板',
|
|
104
|
+
'card.field.endpoint': '适用端点',
|
|
105
|
+
'card.field.paramSpecs': '参数表(JSON:{ "size": { "enabled": true, "value": "1024x1024" } })',
|
|
106
|
+
'card.idHint': '小写字母开头,仅小写字母/数字/中划线(凭据引用名派生用)',
|
|
107
|
+
'card.credential.label': 'API Key',
|
|
108
|
+
'card.credential.placeholder': '输入 Key(保存后不回显)',
|
|
109
|
+
'card.credential.set': '保存 Key',
|
|
110
|
+
'card.credential.unset': '删除 Key',
|
|
111
|
+
'card.credential.configured': '已配置',
|
|
112
|
+
'card.credential.unconfigured': '未配置',
|
|
113
|
+
'card.maxConcurrent': '生图并发上限(0 = 不限)',
|
|
114
|
+
'card.requestTimeoutMs': '单请求超时(毫秒)',
|
|
115
|
+
'card.proxy': '上游代理(空 = 直连)',
|
|
116
|
+
'card.galleryMaxItems': '画廊保留上限(0 = 不清理)',
|
|
117
|
+
'card.specsInvalid': '参数表非法:',
|
|
118
|
+
'card.idInvalid': 'ID 非法:',
|
|
119
|
+
'card.save': '保存',
|
|
120
|
+
'card.discard': '放弃修改',
|
|
121
|
+
'card.saved': '已保存',
|
|
122
|
+
'card.saveFailed': '保存失败:',
|
|
123
|
+
'card.readOnly': '当前部署的设置文档为只读',
|
|
124
|
+
'card.badgeOn': '{n} 个提供商',
|
|
125
|
+
'card.badgeOff': '未配置',
|
|
14
126
|
}
|
|
15
127
|
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
128
|
+
export type DictKey = keyof typeof zh
|
|
129
|
+
|
|
130
|
+
/** 面板/卡片共用的翻译函数类型(slot 注入的 bind 结果按此消费)。 */
|
|
131
|
+
export type Translate = (key: DictKey) => string
|
|
132
|
+
|
|
133
|
+
export const en: Record<DictKey, string> = {
|
|
134
|
+
'entry.label': 'Image',
|
|
135
|
+
'panel.title': 'Image Studio',
|
|
136
|
+
'tab.generation': 'Text to Image',
|
|
137
|
+
'tab.edit': 'Image to Image',
|
|
138
|
+
'tab.gallery': 'Gallery',
|
|
139
|
+
'common.close': 'Close',
|
|
140
|
+
'common.cancel': 'Cancel',
|
|
141
|
+
'common.save': 'Save',
|
|
142
|
+
'common.saving': 'Saving…',
|
|
143
|
+
'common.retry': 'Retry',
|
|
144
|
+
'common.loading': 'Loading…',
|
|
145
|
+
'common.loadFailed': 'Load failed',
|
|
146
|
+
'common.delete': 'Delete',
|
|
147
|
+
'common.confirmDelete': 'Confirm delete',
|
|
148
|
+
'common.refresh': 'Refresh',
|
|
149
|
+
'common.download': 'Download',
|
|
150
|
+
'common.unsaved': 'Unsaved',
|
|
151
|
+
'provider.label': 'Provider',
|
|
152
|
+
'provider.placeholder': 'Select a provider preset…',
|
|
153
|
+
'provider.empty': 'No provider presets: add one in Settings → Plugins → Image Studio',
|
|
154
|
+
'provider.credential.configured': 'Key configured',
|
|
155
|
+
'provider.credential.missing': 'Key missing',
|
|
156
|
+
'provider.inline.toggle': 'Override protocol/URL/model',
|
|
157
|
+
'provider.inline.protocol': 'Protocol',
|
|
158
|
+
'provider.inline.baseUrl': 'API base URL',
|
|
159
|
+
'provider.inline.model': 'Model',
|
|
160
|
+
'provider.inline.hint': 'Overrides reuse the API key of the selected preset',
|
|
161
|
+
'prompt.label': 'Prompt',
|
|
162
|
+
'prompt.placeholder': 'Describe the image you want…',
|
|
163
|
+
'prompt.preset.insert': 'Insert prompt preset…',
|
|
164
|
+
'prompt.preset.save': 'Save as preset',
|
|
165
|
+
'prompt.preset.name': 'Preset name',
|
|
166
|
+
'prompt.preset.saved': 'Prompt preset saved',
|
|
167
|
+
'params.basic': 'Common parameters',
|
|
168
|
+
'params.advanced': 'Advanced parameters',
|
|
169
|
+
'params.preset.placeholder': 'Apply a parameter preset…',
|
|
170
|
+
'params.preset.save': 'Save as preset',
|
|
171
|
+
'params.preset.saved': 'Parameter preset saved',
|
|
172
|
+
'params.enabled': 'Enabled',
|
|
173
|
+
'sources.label': 'Source images',
|
|
174
|
+
'sources.pick': 'Pick sources',
|
|
175
|
+
'sources.count': '{n}/16 selected',
|
|
176
|
+
'sources.mask': 'Mask (optional)',
|
|
177
|
+
'sources.maskPick': 'Pick mask',
|
|
178
|
+
'sources.maskClear': 'Clear',
|
|
179
|
+
'picker.empty': 'No images in the gallery yet',
|
|
180
|
+
'picker.confirm': 'Use selected ({n})',
|
|
181
|
+
'generate.submit': 'Generate',
|
|
182
|
+
'generate.submitted': 'Task {id} submitted; progress in the task bar below',
|
|
183
|
+
'generate.failed': 'Submit failed: ',
|
|
184
|
+
'generate.needProvider': 'Select a provider preset first',
|
|
185
|
+
'generate.needPrompt': 'Enter a prompt first',
|
|
186
|
+
'generate.needSources': 'Pick at least one source image',
|
|
187
|
+
'tasks.title': 'Tasks',
|
|
188
|
+
'tasks.state.queued': 'Queued',
|
|
189
|
+
'tasks.state.running': 'Running',
|
|
190
|
+
'tasks.state.succeeded': 'Done',
|
|
191
|
+
'tasks.state.failed': 'Failed',
|
|
192
|
+
'tasks.state.cancelled': 'Cancelled',
|
|
193
|
+
'tasks.view': 'View',
|
|
194
|
+
'gallery.empty': 'Gallery is empty — generate your first image',
|
|
195
|
+
'gallery.total': '{n} items',
|
|
196
|
+
'gallery.detail.prompt': 'Prompt',
|
|
197
|
+
'gallery.detail.revised': 'Revised prompt',
|
|
198
|
+
'gallery.detail.params': 'Request parameters',
|
|
199
|
+
'gallery.detail.model': 'Model',
|
|
200
|
+
'gallery.detail.endpoint': 'Endpoint',
|
|
201
|
+
'gallery.detail.time': 'Time',
|
|
202
|
+
'gallery.detail.sources': 'Sources',
|
|
203
|
+
'gallery.edit': 'Edit again',
|
|
204
|
+
'card.title': 'Image Studio',
|
|
205
|
+
'card.description': 'Text/Image-to-image: providers & API keys, param/prompt presets',
|
|
206
|
+
'card.providers': 'Provider presets',
|
|
207
|
+
'card.prompts': 'Prompt presets',
|
|
208
|
+
'card.params': 'Parameter presets',
|
|
209
|
+
'card.advanced': 'Advanced',
|
|
210
|
+
'card.add': 'Add',
|
|
211
|
+
'card.field.id': 'ID',
|
|
212
|
+
'card.field.name': 'Name',
|
|
213
|
+
'card.field.protocol': 'Protocol',
|
|
214
|
+
'card.field.baseUrl': 'API base URL',
|
|
215
|
+
'card.field.model': 'Model',
|
|
216
|
+
'card.field.extraHeaders': 'Extra headers (JSON)',
|
|
217
|
+
'card.field.text': 'Prompt template',
|
|
218
|
+
'card.field.endpoint': 'Endpoint',
|
|
219
|
+
'card.field.paramSpecs':
|
|
220
|
+
'Param specs (JSON: { "size": { "enabled": true, "value": "1024x1024" } })',
|
|
221
|
+
'card.idHint': 'Lowercase letter first; lowercase/digits/dashes only (derives credential ref)',
|
|
222
|
+
'card.credential.label': 'API Key',
|
|
223
|
+
'card.credential.placeholder': 'Enter key (never echoed back)',
|
|
224
|
+
'card.credential.set': 'Save key',
|
|
225
|
+
'card.credential.unset': 'Remove key',
|
|
226
|
+
'card.credential.configured': 'Configured',
|
|
227
|
+
'card.credential.unconfigured': 'Not configured',
|
|
228
|
+
'card.maxConcurrent': 'Max concurrent tasks (0 = unlimited)',
|
|
229
|
+
'card.requestTimeoutMs': 'Request timeout (ms)',
|
|
230
|
+
'card.proxy': 'Upstream proxy (empty = direct)',
|
|
231
|
+
'card.galleryMaxItems': 'Gallery retention (0 = keep all)',
|
|
232
|
+
'card.specsInvalid': 'Invalid param specs: ',
|
|
233
|
+
'card.idInvalid': 'Invalid ID: ',
|
|
234
|
+
'card.save': 'Save',
|
|
235
|
+
'card.discard': 'Discard',
|
|
236
|
+
'card.saved': 'Saved',
|
|
237
|
+
'card.saveFailed': 'Save failed: ',
|
|
238
|
+
'card.readOnly': 'Settings document is read-only in this deployment',
|
|
239
|
+
'card.badgeOn': '{n} providers',
|
|
240
|
+
'card.badgeOff': 'Not configured',
|
|
20
241
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板开合与跨标签联动控制器:footer 入口 ↔ overlay 面板共享开合态;
|
|
3
|
+
* 画廊「二次编辑」经 editSeed 单向投递给图生图表单(nonce 保证同参可重复触发)。
|
|
4
|
+
* 模式照抄 web-files/web-terminal 的 PanelController。
|
|
5
|
+
* @module image-studio/client/panel/controller
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 面板标签页。 */
|
|
9
|
+
export type StudioTab = 'generation' | 'edit' | 'gallery'
|
|
10
|
+
|
|
11
|
+
/** 二次编辑种子(画廊条目 → 图生图表单回填)。 */
|
|
12
|
+
export interface EditSeed {
|
|
13
|
+
/** 源图 imageId 列表(≤16)。 */
|
|
14
|
+
sourceIds: string[]
|
|
15
|
+
/** 历史提示词回填。 */
|
|
16
|
+
prompt: string
|
|
17
|
+
/** 历史请求参数回填(画廊条目 params 快照)。 */
|
|
18
|
+
params: Record<string, unknown>
|
|
19
|
+
/** 历史提供商预设(存在则预选)。 */
|
|
20
|
+
providerPresetId: string | null
|
|
21
|
+
/** 触发序号:同一参数再次「二次编辑」也需重新生效。 */
|
|
22
|
+
nonce: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 面板状态(footer 按钮与 overlay 面板共享)。 */
|
|
26
|
+
export interface PanelState {
|
|
27
|
+
open: boolean
|
|
28
|
+
tab: StudioTab
|
|
29
|
+
editSeed: EditSeed | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** footer 按钮与 shell.overlay 面板之间共享的控制器。 */
|
|
33
|
+
export interface PanelController {
|
|
34
|
+
subscribe: (listener: () => void) => () => void
|
|
35
|
+
getSnapshot: () => PanelState
|
|
36
|
+
setOpen: (open: boolean) => void
|
|
37
|
+
toggle: () => void
|
|
38
|
+
setTab: (tab: StudioTab) => void
|
|
39
|
+
/** 投递二次编辑种子并切到图生图标签。 */
|
|
40
|
+
seedEdit: (seed: Omit<EditSeed, 'nonce'>) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 创建一个模块级控制器(apply 内调用一次,inject 给两个 slot 入口)。 */
|
|
44
|
+
export function createPanelController(): PanelController {
|
|
45
|
+
let state: PanelState = { open: false, tab: 'generation', editSeed: null }
|
|
46
|
+
let nonce = 0
|
|
47
|
+
const listeners = new Set<() => void>()
|
|
48
|
+
const publish = () => {
|
|
49
|
+
for (const listener of [...listeners]) listener()
|
|
50
|
+
}
|
|
51
|
+
const set = (next: PanelState) => {
|
|
52
|
+
state = next
|
|
53
|
+
publish()
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
subscribe: (listener) => {
|
|
57
|
+
listeners.add(listener)
|
|
58
|
+
return () => listeners.delete(listener)
|
|
59
|
+
},
|
|
60
|
+
getSnapshot: () => state,
|
|
61
|
+
setOpen: (open) => {
|
|
62
|
+
if (state.open === open) return
|
|
63
|
+
set({ ...state, open })
|
|
64
|
+
},
|
|
65
|
+
toggle: () => set({ ...state, open: !state.open }),
|
|
66
|
+
setTab: (tab) => {
|
|
67
|
+
if (state.tab === tab) return
|
|
68
|
+
set({ ...state, tab })
|
|
69
|
+
},
|
|
70
|
+
seedEdit: (seed) => {
|
|
71
|
+
nonce += 1
|
|
72
|
+
set({ ...state, tab: 'edit', editSeed: { ...seed, nonce } })
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 侧边栏 footer 入口按钮:与文件/终端入口同款(整行图标 + 文案;
|
|
3
|
+
* rail 折叠态退化为居中圆形图标)。flex:1 由样式层与邻居等分行宽。
|
|
4
|
+
* @module image-studio/client/panel/entry
|
|
5
|
+
*/
|
|
6
|
+
import { IconImage } from '@dsh-plus/shared/client'
|
|
7
|
+
import type { ReactElement } from 'react'
|
|
8
|
+
import { useSyncExternalStore } from 'react'
|
|
9
|
+
import type { Translate } from '../i18n.ts'
|
|
10
|
+
import type { PanelController } from './controller.ts'
|
|
11
|
+
|
|
12
|
+
interface EntryProps {
|
|
13
|
+
studio: PanelController
|
|
14
|
+
t: Translate
|
|
15
|
+
/** 侧边栏展开态(slot owner 传入);折叠(rail)时仅渲染圆形图标。 */
|
|
16
|
+
wide?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function StudioEntryButton({ studio, t, wide }: EntryProps): ReactElement {
|
|
20
|
+
const { open } = useSyncExternalStore(studio.subscribe, studio.getSnapshot)
|
|
21
|
+
const rail = wide === false
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
className={rail ? 'ims-entry ims-entry-rail' : 'ims-entry'}
|
|
26
|
+
aria-haspopup="dialog"
|
|
27
|
+
aria-expanded={open}
|
|
28
|
+
aria-label={t('entry.label')}
|
|
29
|
+
onClick={() => studio.toggle()}
|
|
30
|
+
>
|
|
31
|
+
<IconImage size={rail ? 18 : 16} />
|
|
32
|
+
{!rail && <span className="ims-entry-label">{t('entry.label')}</span>}
|
|
33
|
+
</button>
|
|
34
|
+
)
|
|
35
|
+
}
|