@dickpy/dsh-imagegen 1.0.2 → 1.0.4
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/README.md +7 -7
- package/docs/images/image-generation-studio.png +0 -0
- package/docs/images/plugin-settings.png +0 -0
- package/lib/client.js +356 -129
- package/lib/client.js.map +1 -1
- package/lib/index.js +3 -1
- package/package.json +1 -1
- package/src/client/ImageGenPanel.tsx +132 -27
- package/src/client/SettingsCard.tsx +5 -0
- package/src/client/locales.ts +22 -0
- package/src/client/panel.module.css +197 -26
- package/src/client/settings-card.module.css +26 -0
- package/src/protocol.ts +3 -0
- package/src/updater.ts +2 -1
package/lib/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import { spawn } from "node:child_process";
|
|
|
13
13
|
*/
|
|
14
14
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
15
15
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
16
|
+
/** Published package version shared by the host updater and the client UI. */
|
|
17
|
+
const PLUGIN_VERSION = "1.0.4";
|
|
16
18
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
17
19
|
const SETTINGS_API = {
|
|
18
20
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -438,7 +440,7 @@ async function readHistoryImage(file) {
|
|
|
438
440
|
//#region src/updater.ts
|
|
439
441
|
/** GitHub Release discovery and explicit, user-triggered plugin updates. */
|
|
440
442
|
/** Keep this in sync with package.json for each published release. */
|
|
441
|
-
const CURRENT_VERSION =
|
|
443
|
+
const CURRENT_VERSION = PLUGIN_VERSION;
|
|
442
444
|
const PACKAGE_NAME = "@dickpy/dsh-imagegen";
|
|
443
445
|
const RELEASES_URL = "https://api.github.com/repos/dickpy/dsh-imagegen/releases/latest";
|
|
444
446
|
const CHECK_TIMEOUT_MS = 1e4;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
3
|
"description": "AI 生图 (image generation) plugin for the dsh web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / gpt-image-1 / dall-e-3), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -42,6 +42,13 @@ const DETAILS = ['', 'standard', 'high'] as const
|
|
|
42
42
|
|
|
43
43
|
const PROMPT_MAX = 2000
|
|
44
44
|
const REF_IMAGE_MAX_BYTES = 10 * 1024 * 1024
|
|
45
|
+
const PREVIEW_SCALE_MIN = 0.5
|
|
46
|
+
const PREVIEW_SCALE_MAX = 3
|
|
47
|
+
const PREVIEW_SCALE_STEP = 0.25
|
|
48
|
+
|
|
49
|
+
function clampPreviewScale(scale: number): number {
|
|
50
|
+
return Math.min(PREVIEW_SCALE_MAX, Math.max(PREVIEW_SCALE_MIN, scale))
|
|
51
|
+
}
|
|
45
52
|
|
|
46
53
|
/** Read the current config from the settings scope snapshot. */
|
|
47
54
|
function useConfig(scope: ImageGenScope): ImageGenConfig | undefined {
|
|
@@ -50,6 +57,13 @@ function useConfig(scope: ImageGenScope): ImageGenConfig | undefined {
|
|
|
50
57
|
return value
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
/** Track the redacted api-key presence bit exposed by the settings bridge. */
|
|
61
|
+
function useKeySet(scope: ImageGenScope): boolean {
|
|
62
|
+
const [keySet, setKeySet] = useState(scope.getKeySetSnapshot())
|
|
63
|
+
useEffect(() => scope.subscribeKeySet(() => { setKeySet(scope.getKeySetSnapshot()) }), [scope])
|
|
64
|
+
return keySet
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
/** Tick a seconds counter while `running`. */
|
|
54
68
|
function useElapsed(running: boolean, startedAt: number | null): number {
|
|
55
69
|
const [elapsed, setElapsed] = useState(0)
|
|
@@ -108,6 +122,8 @@ export function ImageGenPanel(props: {
|
|
|
108
122
|
const enabled = config?.enabled ?? true
|
|
109
123
|
const apiUrl = config?.apiUrl ?? ''
|
|
110
124
|
const configured = apiUrl.trim() !== ''
|
|
125
|
+
const keySet = useKeySet(scope)
|
|
126
|
+
const connected = enabled && configured && keySet
|
|
111
127
|
|
|
112
128
|
const [mode, setMode] = useState<GenerateMode>('text')
|
|
113
129
|
const [prompt, setPrompt] = useState('')
|
|
@@ -124,6 +140,8 @@ export function ImageGenPanel(props: {
|
|
|
124
140
|
const [history, setHistory] = useState<HistoryEntry[]>([])
|
|
125
141
|
const [viewingHistoryId, setViewingHistoryId] = useState<string | null>(null)
|
|
126
142
|
const [preview, setPreview] = useState<{ images: GeneratedImage[]; index: number } | null>(null)
|
|
143
|
+
const [previewScale, setPreviewScale] = useState(1)
|
|
144
|
+
const [promptCopied, setPromptCopied] = useState(false)
|
|
127
145
|
const [update, setUpdate] = useState<UpdateInfo | null>(null)
|
|
128
146
|
const [updating, setUpdating] = useState(false)
|
|
129
147
|
const [updateMessage, setUpdateMessage] = useState<string | null>(null)
|
|
@@ -233,10 +251,20 @@ export function ImageGenPanel(props: {
|
|
|
233
251
|
/** Open the full-screen image preview at a given index. */
|
|
234
252
|
const openPreview = (previewImages: GeneratedImage[], index: number): void => {
|
|
235
253
|
setPreview({ images: previewImages, index })
|
|
254
|
+
setPreviewScale(1)
|
|
255
|
+
setPromptCopied(false)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const closePreview = (): void => {
|
|
259
|
+
setPreview(null)
|
|
260
|
+
setPreviewScale(1)
|
|
261
|
+
setPromptCopied(false)
|
|
236
262
|
}
|
|
237
263
|
|
|
238
264
|
/** Step the preview by ±1, wrapping around. */
|
|
239
265
|
const stepPreview = (delta: number): void => {
|
|
266
|
+
setPreviewScale(1)
|
|
267
|
+
setPromptCopied(false)
|
|
240
268
|
setPreview(current => {
|
|
241
269
|
if (current === null) return null
|
|
242
270
|
const total = current.images.length
|
|
@@ -248,9 +276,12 @@ export function ImageGenPanel(props: {
|
|
|
248
276
|
useEffect(() => {
|
|
249
277
|
if (preview === null) return
|
|
250
278
|
const onKey = (event: KeyboardEvent): void => {
|
|
251
|
-
if (event.key === 'Escape')
|
|
279
|
+
if (event.key === 'Escape') closePreview()
|
|
252
280
|
else if (event.key === 'ArrowLeft') stepPreview(-1)
|
|
253
281
|
else if (event.key === 'ArrowRight') stepPreview(1)
|
|
282
|
+
else if (event.key === '+' || event.key === '=') setPreviewScale(current => clampPreviewScale(current + PREVIEW_SCALE_STEP))
|
|
283
|
+
else if (event.key === '-') setPreviewScale(current => clampPreviewScale(current - PREVIEW_SCALE_STEP))
|
|
284
|
+
else if (event.key === '0') setPreviewScale(1)
|
|
254
285
|
}
|
|
255
286
|
window.addEventListener('keydown', onKey)
|
|
256
287
|
return () => window.removeEventListener('keydown', onKey)
|
|
@@ -313,19 +344,58 @@ export function ImageGenPanel(props: {
|
|
|
313
344
|
const viewingEntry = viewingHistoryId === null ? null : history.find(entry => entry.id === viewingHistoryId) ?? null
|
|
314
345
|
const previewImage = preview === null ? null : preview.images[preview.index] ?? null
|
|
315
346
|
|
|
347
|
+
const copyPreviewPrompt = async (text: string): Promise<void> => {
|
|
348
|
+
try {
|
|
349
|
+
if (navigator.clipboard?.writeText !== undefined) {
|
|
350
|
+
await navigator.clipboard.writeText(text)
|
|
351
|
+
} else {
|
|
352
|
+
const textarea = document.createElement('textarea')
|
|
353
|
+
textarea.value = text
|
|
354
|
+
textarea.style.position = 'fixed'
|
|
355
|
+
textarea.style.opacity = '0'
|
|
356
|
+
document.body.appendChild(textarea)
|
|
357
|
+
textarea.select()
|
|
358
|
+
const copied = document.execCommand('copy')
|
|
359
|
+
textarea.remove()
|
|
360
|
+
if (!copied) throw new Error('copy failed')
|
|
361
|
+
}
|
|
362
|
+
setPromptCopied(true)
|
|
363
|
+
window.setTimeout(() => { setPromptCopied(false) }, 1800)
|
|
364
|
+
} catch {
|
|
365
|
+
setPromptCopied(false)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const addPreviewToEdit = (): void => {
|
|
370
|
+
if (previewImage === null || preview === null) return
|
|
371
|
+
setMode('edit')
|
|
372
|
+
setRefImage({
|
|
373
|
+
dataUrl: srcOf(previewImage),
|
|
374
|
+
name: `dsh-image-${preview.index + 1}.${extensionOf(previewImage.mime)}`,
|
|
375
|
+
})
|
|
376
|
+
if (prompt.trim() === '' && previewImage.revisedPrompt !== undefined) setPrompt(previewImage.revisedPrompt)
|
|
377
|
+
setError(null)
|
|
378
|
+
closePreview()
|
|
379
|
+
}
|
|
380
|
+
|
|
316
381
|
return (
|
|
317
382
|
<div className={css.panel}>
|
|
318
383
|
<header className={css.panelHeader}>
|
|
319
|
-
<
|
|
320
|
-
|
|
384
|
+
<span className={css.panelHeading}>
|
|
385
|
+
<h2 className={css.panelTitle}>{tt('panel.title')}</h2>
|
|
386
|
+
<span className={css.panelSubtitle}>{tt('panel.subtitle')}</span>
|
|
387
|
+
</span>
|
|
388
|
+
<button
|
|
389
|
+
type="button"
|
|
390
|
+
className={css.connectionStatus}
|
|
391
|
+
data-connected={connected ? 'true' : 'false'}
|
|
392
|
+
aria-label={tt(connected ? 'connection.connected' : 'connection.disconnected')}
|
|
393
|
+
>
|
|
394
|
+
<span className={css.connectionDot} aria-hidden="true" />
|
|
395
|
+
{tt(connected ? 'connection.connected' : 'connection.disconnected')}
|
|
396
|
+
</button>
|
|
321
397
|
</header>
|
|
322
398
|
|
|
323
|
-
{!enabled
|
|
324
|
-
? <div className={css.banner} data-kind="warn">{tt('config.disabled')}</div>
|
|
325
|
-
: !configured
|
|
326
|
-
? <div className={css.banner} data-kind="warn">{tt('config.missing')}</div>
|
|
327
|
-
: <div className={css.banner} data-kind="ok">{tt('config.configured', { url: apiUrl })}</div>}
|
|
328
|
-
|
|
329
399
|
{update !== null ? (
|
|
330
400
|
<div className={css.updateBanner} data-kind={updateResult === 'success' ? 'ok' : 'warn'}>
|
|
331
401
|
<span className={css.updateText}>
|
|
@@ -555,7 +625,7 @@ export function ImageGenPanel(props: {
|
|
|
555
625
|
<span className={css.canvasHistoryTag}>{tt('history.viewing', { time: formatTime(viewingEntry.createdAt) })}</span>
|
|
556
626
|
) : null}
|
|
557
627
|
</div>
|
|
558
|
-
<div className={css.grid}>
|
|
628
|
+
<div className={css.grid} data-count={images.length}>
|
|
559
629
|
{images.map((image, index) => (
|
|
560
630
|
<figure
|
|
561
631
|
key={index}
|
|
@@ -663,9 +733,9 @@ export function ImageGenPanel(props: {
|
|
|
663
733
|
role="dialog"
|
|
664
734
|
aria-modal="true"
|
|
665
735
|
aria-label={tt('preview.title')}
|
|
666
|
-
onClick={
|
|
736
|
+
onClick={closePreview}
|
|
667
737
|
>
|
|
668
|
-
<button type="button" className={css.lightboxClose} aria-label={tt('preview.close')}
|
|
738
|
+
<button type="button" className={css.lightboxClose} aria-label={tt('preview.close')} title={tt('preview.close')} onClick={closePreview}>
|
|
669
739
|
<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
|
670
740
|
</button>
|
|
671
741
|
{preview.images.length > 1 ? (
|
|
@@ -679,25 +749,60 @@ export function ImageGenPanel(props: {
|
|
|
679
749
|
</>
|
|
680
750
|
) : null}
|
|
681
751
|
<figure className={css.lightboxFigure} onClick={(event) => { event.stopPropagation() }}>
|
|
682
|
-
<
|
|
683
|
-
className={css.
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
752
|
+
<div
|
|
753
|
+
className={css.lightboxStage}
|
|
754
|
+
onWheel={(event) => {
|
|
755
|
+
event.preventDefault()
|
|
756
|
+
setPreviewScale(current => clampPreviewScale(current + (event.deltaY < 0 ? PREVIEW_SCALE_STEP : -PREVIEW_SCALE_STEP)))
|
|
757
|
+
}}
|
|
758
|
+
>
|
|
759
|
+
<img
|
|
760
|
+
className={css.lightboxImage}
|
|
761
|
+
style={{ transform: `scale(${previewScale})` }}
|
|
762
|
+
src={srcOf(previewImage)}
|
|
763
|
+
alt={previewImage.revisedPrompt ?? tt('preview.title')}
|
|
764
|
+
/>
|
|
765
|
+
</div>
|
|
766
|
+
<div className={css.lightboxTools} role="group" aria-label={tt('preview.zoomControls')}>
|
|
767
|
+
<button type="button" className={css.lightboxTool} aria-label={tt('preview.zoomOut')} title={tt('preview.zoomOut')} onClick={() => { setPreviewScale(current => clampPreviewScale(current - PREVIEW_SCALE_STEP)) }}>
|
|
768
|
+
<svg viewBox="0 0 16 16" width="17" height="17" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.2"/><path d="M4.8 7h4.4M13 13l-2.8-2.8"/></svg>
|
|
769
|
+
</button>
|
|
770
|
+
<button type="button" className={css.lightboxZoomLevel} aria-label={tt('preview.zoomReset')} title={tt('preview.zoomReset')} onClick={() => { setPreviewScale(1) }}>
|
|
771
|
+
{tt('preview.zoomLevel', { percent: Math.round(previewScale * 100) })}
|
|
772
|
+
</button>
|
|
773
|
+
<button type="button" className={css.lightboxTool} aria-label={tt('preview.zoomIn')} title={tt('preview.zoomIn')} onClick={() => { setPreviewScale(current => clampPreviewScale(current + PREVIEW_SCALE_STEP)) }}>
|
|
774
|
+
<svg viewBox="0 0 16 16" width="17" height="17" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.2"/><path d="M7 4.8v4.4M4.8 7h4.4M13 13l-2.8-2.8"/></svg>
|
|
775
|
+
</button>
|
|
776
|
+
</div>
|
|
687
777
|
{previewImage.revisedPrompt !== undefined ? (
|
|
688
|
-
<
|
|
689
|
-
{
|
|
690
|
-
|
|
778
|
+
<div className={css.lightboxCaptionRow}>
|
|
779
|
+
<figcaption className={css.lightboxCaption} title={previewImage.revisedPrompt}>
|
|
780
|
+
{tt('revisedPrompt', { prompt: previewImage.revisedPrompt })}
|
|
781
|
+
</figcaption>
|
|
782
|
+
<button type="button" className={css.lightboxCopy} aria-label={tt(promptCopied ? 'preview.copied' : 'preview.copyPrompt')} title={tt(promptCopied ? 'preview.copied' : 'preview.copyPrompt')} onClick={() => { void copyPreviewPrompt(previewImage.revisedPrompt!) }}>
|
|
783
|
+
{promptCopied ? (
|
|
784
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 8l3 3 7-7"/></svg>
|
|
785
|
+
) : (
|
|
786
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="5" y="5" width="7" height="8" rx="1"/><path d="M3 10V3.8c0-.44.36-.8.8-.8H9"/></svg>
|
|
787
|
+
)}
|
|
788
|
+
<span>{tt(promptCopied ? 'preview.copied' : 'preview.copyPrompt')}</span>
|
|
789
|
+
</button>
|
|
790
|
+
</div>
|
|
691
791
|
) : null}
|
|
692
792
|
<div className={css.lightboxMeta}>
|
|
693
793
|
<span className={css.lightboxIndex}>{tt('preview.index', { index: preview.index + 1, total: preview.images.length })}</span>
|
|
694
|
-
<
|
|
695
|
-
className={css.
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
794
|
+
<span className={css.lightboxActions}>
|
|
795
|
+
<button type="button" className={css.lightboxEdit} onClick={addPreviewToEdit}>
|
|
796
|
+
{tt('preview.addToEdit')}
|
|
797
|
+
</button>
|
|
798
|
+
<a
|
|
799
|
+
className={css.lightboxDownload}
|
|
800
|
+
href={srcOf(previewImage)}
|
|
801
|
+
download={`dsh-image-${preview.index + 1}.${extensionOf(previewImage.mime)}`}
|
|
802
|
+
>
|
|
803
|
+
{tt('download')}
|
|
804
|
+
</a>
|
|
805
|
+
</span>
|
|
701
806
|
</div>
|
|
702
807
|
</figure>
|
|
703
808
|
</div>,
|
|
@@ -11,6 +11,7 @@ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-cli
|
|
|
11
11
|
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
12
12
|
import { CardForm, booleanField, secretField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
13
13
|
import type { ImageGenScope } from './settings-scope.ts'
|
|
14
|
+
import { PLUGIN_VERSION } from '../protocol.ts'
|
|
14
15
|
import css from './settings-card.module.css'
|
|
15
16
|
|
|
16
17
|
/** The fields this card edits (the namespace's full schema). */
|
|
@@ -161,6 +162,10 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
161
162
|
? (
|
|
162
163
|
<div className={css.body}>
|
|
163
164
|
{!state.writable ? <p className={css.readOnly} role="status">{t('settings.readOnly')}</p> : null}
|
|
165
|
+
<div className={css.versionRow}>
|
|
166
|
+
<span className={css.versionLabel}>{t('settings.currentVersion')}</span>
|
|
167
|
+
<code className={css.versionValue}>v{PLUGIN_VERSION}</code>
|
|
168
|
+
</div>
|
|
164
169
|
<ValueField
|
|
165
170
|
id="dsh-imagegen-settings-apikey"
|
|
166
171
|
label={t('settings.apiKey')}
|
package/src/client/locales.ts
CHANGED
|
@@ -72,10 +72,20 @@ export const zh = {
|
|
|
72
72
|
'preview.prev': '上一张',
|
|
73
73
|
'preview.next': '下一张',
|
|
74
74
|
'preview.index': '{index} / {total}',
|
|
75
|
+
'preview.zoomControls': '图片缩放控制',
|
|
76
|
+
'preview.zoomIn': '放大',
|
|
77
|
+
'preview.zoomOut': '缩小',
|
|
78
|
+
'preview.zoomReset': '重置缩放',
|
|
79
|
+
'preview.zoomLevel': '{percent}%',
|
|
80
|
+
'preview.copyPrompt': '复制提示词',
|
|
81
|
+
'preview.copied': '已复制',
|
|
82
|
+
'preview.addToEdit': '添加到图生图',
|
|
75
83
|
// config banner
|
|
76
84
|
'config.missing': '尚未配置 API:请前往「设置 → 插件 → 可配置」为 AI 生图填写 api_url 与 api_key。',
|
|
77
85
|
'config.configured': '已连接 {url}',
|
|
78
86
|
'config.disabled': '插件已停用,请在设置中重新启用。',
|
|
87
|
+
'connection.connected': '已连接',
|
|
88
|
+
'connection.disconnected': '未连接',
|
|
79
89
|
// plugin update
|
|
80
90
|
'update.available': '检测到新版本:{version}',
|
|
81
91
|
'update.install': '在线更新',
|
|
@@ -86,6 +96,7 @@ export const zh = {
|
|
|
86
96
|
// settings card
|
|
87
97
|
'settings.title': 'AI 生图(dsh-imagegen)',
|
|
88
98
|
'settings.description': '配置图像生成 API 地址与密钥',
|
|
99
|
+
'settings.currentVersion': '当前版本',
|
|
89
100
|
'settings.apiUrl': 'API 地址(api_url)',
|
|
90
101
|
'settings.apiUrlHint': 'OpenAI 兼容接口基址,如 https://api.openai.com/v1;将自动拼接 /images/generations 与 /images/edits',
|
|
91
102
|
'settings.apiKey': 'API 密钥(api_key)',
|
|
@@ -175,9 +186,19 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
175
186
|
'preview.prev': 'Previous',
|
|
176
187
|
'preview.next': 'Next',
|
|
177
188
|
'preview.index': '{index} / {total}',
|
|
189
|
+
'preview.zoomControls': 'Image zoom controls',
|
|
190
|
+
'preview.zoomIn': 'Zoom in',
|
|
191
|
+
'preview.zoomOut': 'Zoom out',
|
|
192
|
+
'preview.zoomReset': 'Reset zoom',
|
|
193
|
+
'preview.zoomLevel': '{percent}%',
|
|
194
|
+
'preview.copyPrompt': 'Copy prompt',
|
|
195
|
+
'preview.copied': 'Copied',
|
|
196
|
+
'preview.addToEdit': 'Add to image to image',
|
|
178
197
|
'config.missing': 'API not configured: open "Settings → Plugins → Configurable" and fill in api_url and api_key for AI Image.',
|
|
179
198
|
'config.configured': 'Connected to {url}',
|
|
180
199
|
'config.disabled': 'The plugin is disabled — re-enable it in Settings.',
|
|
200
|
+
'connection.connected': 'Connected',
|
|
201
|
+
'connection.disconnected': 'Disconnected',
|
|
181
202
|
'update.available': 'A new version is available: {version}',
|
|
182
203
|
'update.install': 'Update online',
|
|
183
204
|
'update.installing': 'Updating…',
|
|
@@ -186,6 +207,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
186
207
|
'update.release': 'View Release',
|
|
187
208
|
'settings.title': 'AI Image (dsh-imagegen)',
|
|
188
209
|
'settings.description': 'Configure the image generation API endpoint and key',
|
|
210
|
+
'settings.currentVersion': 'Current version',
|
|
189
211
|
'settings.apiUrl': 'API URL (api_url)',
|
|
190
212
|
'settings.apiUrlHint': 'OpenAI-compatible base URL, e.g. https://api.openai.com/v1; /images/generations and /images/edits are appended',
|
|
191
213
|
'settings.apiKey': 'API Key (api_key)',
|
|
@@ -121,10 +121,18 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
.panelHeader {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: space-between;
|
|
127
|
+
gap: 12px;
|
|
128
|
+
flex: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.panelHeading {
|
|
124
132
|
display: flex;
|
|
125
133
|
align-items: baseline;
|
|
126
134
|
gap: 10px;
|
|
127
|
-
|
|
135
|
+
min-width: 0;
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
.panelTitle {
|
|
@@ -143,27 +151,35 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
143
151
|
text-overflow: ellipsis;
|
|
144
152
|
}
|
|
145
153
|
|
|
146
|
-
/* ---
|
|
154
|
+
/* --- connection status and update notice -------------------------------------- */
|
|
147
155
|
|
|
148
|
-
.
|
|
156
|
+
.connectionStatus {
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 6px;
|
|
149
160
|
flex: none;
|
|
150
|
-
|
|
161
|
+
height: 28px;
|
|
162
|
+
padding: 0 10px;
|
|
163
|
+
border: 1px solid var(--dsw-alias-label-error);
|
|
164
|
+
border-radius: 8px;
|
|
165
|
+
background: transparent;
|
|
166
|
+
color: var(--dsw-alias-label-error);
|
|
167
|
+
font: inherit;
|
|
151
168
|
font-size: 12px;
|
|
152
|
-
line-height: 1
|
|
153
|
-
|
|
154
|
-
border: 1px solid var(--dsw-alias-border-l2);
|
|
155
|
-
color: var(--dsw-alias-label-secondary);
|
|
156
|
-
overflow-wrap: anywhere;
|
|
169
|
+
line-height: 1;
|
|
170
|
+
white-space: nowrap;
|
|
157
171
|
}
|
|
158
172
|
|
|
159
|
-
.
|
|
160
|
-
color: var(--dsw-alias-state-success-primary);
|
|
173
|
+
.connectionStatus[data-connected='true'] {
|
|
161
174
|
border-color: var(--dsw-alias-state-success-primary);
|
|
175
|
+
color: var(--dsw-alias-state-success-primary);
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
.connectionDot {
|
|
179
|
+
width: 6px;
|
|
180
|
+
height: 6px;
|
|
181
|
+
border-radius: 50%;
|
|
182
|
+
background: currentColor;
|
|
167
183
|
}
|
|
168
184
|
|
|
169
185
|
.updateBanner {
|
|
@@ -205,6 +221,16 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
205
221
|
}
|
|
206
222
|
|
|
207
223
|
@media (max-width: 700px) {
|
|
224
|
+
.panelHeader {
|
|
225
|
+
align-items: flex-start;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.panelHeading {
|
|
229
|
+
align-items: flex-start;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
gap: 2px;
|
|
232
|
+
}
|
|
233
|
+
|
|
208
234
|
.updateBanner {
|
|
209
235
|
align-items: flex-start;
|
|
210
236
|
flex-direction: column;
|
|
@@ -772,9 +798,29 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
772
798
|
|
|
773
799
|
.grid {
|
|
774
800
|
display: grid;
|
|
775
|
-
|
|
801
|
+
flex: 1;
|
|
802
|
+
min-height: 0;
|
|
776
803
|
gap: 14px;
|
|
777
|
-
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.grid[data-count='1'] {
|
|
807
|
+
grid-template-columns: minmax(0, 1fr);
|
|
808
|
+
grid-template-rows: minmax(0, 1fr);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.grid[data-count='2'] {
|
|
812
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
813
|
+
grid-template-rows: minmax(0, 1fr);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.grid[data-count='3'] {
|
|
817
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
818
|
+
grid-template-rows: minmax(0, 1fr);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.grid[data-count='4'] {
|
|
822
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
823
|
+
grid-template-rows: repeat(2, minmax(0, 1fr));
|
|
778
824
|
}
|
|
779
825
|
|
|
780
826
|
.imageCard {
|
|
@@ -787,6 +833,7 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
787
833
|
overflow: hidden;
|
|
788
834
|
background: var(--dsw-alias-bg-layer-2);
|
|
789
835
|
cursor: zoom-in;
|
|
836
|
+
min-height: 0;
|
|
790
837
|
}
|
|
791
838
|
|
|
792
839
|
.imageCard:focus-visible {
|
|
@@ -796,9 +843,10 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
796
843
|
|
|
797
844
|
.image {
|
|
798
845
|
display: block;
|
|
846
|
+
flex: 1;
|
|
847
|
+
min-height: 0;
|
|
799
848
|
width: 100%;
|
|
800
|
-
|
|
801
|
-
object-fit: cover;
|
|
849
|
+
object-fit: contain;
|
|
802
850
|
background: var(--dsw-alias-bg-base);
|
|
803
851
|
}
|
|
804
852
|
|
|
@@ -945,11 +993,11 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
945
993
|
}
|
|
946
994
|
|
|
947
995
|
.lightboxNav[data-dir='prev'] {
|
|
948
|
-
left:
|
|
996
|
+
left: max(20px, calc(50% - 640px));
|
|
949
997
|
}
|
|
950
998
|
|
|
951
999
|
.lightboxNav[data-dir='next'] {
|
|
952
|
-
right:
|
|
1000
|
+
right: max(20px, calc(50% - 640px));
|
|
953
1001
|
}
|
|
954
1002
|
|
|
955
1003
|
.lightboxFigure {
|
|
@@ -958,19 +1006,84 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
958
1006
|
gap: 10px;
|
|
959
1007
|
margin: 0;
|
|
960
1008
|
max-width: min(1100px, calc(100vw - 160px));
|
|
961
|
-
|
|
1009
|
+
width: min(1100px, calc(100vw - 160px));
|
|
1010
|
+
height: min(820px, calc(100vh - 48px));
|
|
1011
|
+
min-height: 0;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.lightboxStage {
|
|
1015
|
+
flex: 1;
|
|
1016
|
+
min-height: 0;
|
|
1017
|
+
display: grid;
|
|
1018
|
+
place-items: center;
|
|
1019
|
+
overflow: auto;
|
|
1020
|
+
border-radius: 10px;
|
|
1021
|
+
background: rgba(255, 255, 255, 0.04);
|
|
962
1022
|
}
|
|
963
1023
|
|
|
964
1024
|
.lightboxImage {
|
|
965
|
-
max-width: 100
|
|
966
|
-
max-height: calc(
|
|
1025
|
+
max-width: calc(100% - 4px);
|
|
1026
|
+
max-height: calc(100% - 4px);
|
|
967
1027
|
object-fit: contain;
|
|
968
1028
|
border-radius: 10px;
|
|
969
1029
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
|
|
970
|
-
|
|
1030
|
+
transition: transform 120ms ease;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.lightboxTools {
|
|
1034
|
+
display: flex;
|
|
1035
|
+
align-items: center;
|
|
1036
|
+
justify-content: center;
|
|
1037
|
+
gap: 6px;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.lightboxTool,
|
|
1041
|
+
.lightboxZoomLevel,
|
|
1042
|
+
.lightboxCopy {
|
|
1043
|
+
display: inline-flex;
|
|
1044
|
+
align-items: center;
|
|
1045
|
+
justify-content: center;
|
|
1046
|
+
color: #fff;
|
|
1047
|
+
background: rgba(255, 255, 255, 0.14);
|
|
1048
|
+
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
1049
|
+
cursor: pointer;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
.lightboxTool,
|
|
1053
|
+
.lightboxZoomLevel {
|
|
1054
|
+
height: 32px;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.lightboxTool {
|
|
1058
|
+
width: 32px;
|
|
1059
|
+
border-radius: 50%;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.lightboxZoomLevel {
|
|
1063
|
+
min-width: 58px;
|
|
1064
|
+
padding: 0 9px;
|
|
1065
|
+
border-radius: 999px;
|
|
1066
|
+
font: inherit;
|
|
1067
|
+
font-size: 12px;
|
|
1068
|
+
font-variant-numeric: tabular-nums;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.lightboxTool:hover,
|
|
1072
|
+
.lightboxZoomLevel:hover,
|
|
1073
|
+
.lightboxCopy:hover {
|
|
1074
|
+
background: rgba(255, 255, 255, 0.26);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.lightboxCaptionRow {
|
|
1078
|
+
display: flex;
|
|
1079
|
+
align-items: flex-start;
|
|
1080
|
+
gap: 8px;
|
|
1081
|
+
min-width: 0;
|
|
971
1082
|
}
|
|
972
1083
|
|
|
973
1084
|
.lightboxCaption {
|
|
1085
|
+
flex: 1;
|
|
1086
|
+
min-width: 0;
|
|
974
1087
|
font-size: 12px;
|
|
975
1088
|
line-height: 1.6;
|
|
976
1089
|
color: rgba(255, 255, 255, 0.9);
|
|
@@ -980,6 +1093,17 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
980
1093
|
overflow: hidden;
|
|
981
1094
|
}
|
|
982
1095
|
|
|
1096
|
+
.lightboxCopy {
|
|
1097
|
+
flex: none;
|
|
1098
|
+
gap: 5px;
|
|
1099
|
+
min-height: 28px;
|
|
1100
|
+
padding: 4px 9px;
|
|
1101
|
+
border-radius: 999px;
|
|
1102
|
+
font: inherit;
|
|
1103
|
+
font-size: 12px;
|
|
1104
|
+
white-space: nowrap;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
983
1107
|
.lightboxMeta {
|
|
984
1108
|
display: flex;
|
|
985
1109
|
align-items: center;
|
|
@@ -993,8 +1117,16 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
993
1117
|
font-variant-numeric: tabular-nums;
|
|
994
1118
|
}
|
|
995
1119
|
|
|
996
|
-
.
|
|
1120
|
+
.lightboxActions {
|
|
1121
|
+
display: inline-flex;
|
|
1122
|
+
align-items: center;
|
|
1123
|
+
gap: 8px;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.lightboxDownload,
|
|
1127
|
+
.lightboxEdit {
|
|
997
1128
|
padding: 4px 14px;
|
|
1129
|
+
font: inherit;
|
|
998
1130
|
font-size: 12.5px;
|
|
999
1131
|
font-weight: 500;
|
|
1000
1132
|
color: #fff;
|
|
@@ -1002,12 +1134,51 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
1002
1134
|
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
1003
1135
|
border-radius: 999px;
|
|
1004
1136
|
text-decoration: none;
|
|
1137
|
+
cursor: pointer;
|
|
1005
1138
|
}
|
|
1006
1139
|
|
|
1007
|
-
.lightboxDownload:hover
|
|
1140
|
+
.lightboxDownload:hover,
|
|
1141
|
+
.lightboxEdit:hover {
|
|
1008
1142
|
background: rgba(255, 255, 255, 0.26);
|
|
1009
1143
|
}
|
|
1010
1144
|
|
|
1145
|
+
.lightboxEdit {
|
|
1146
|
+
color: #fff;
|
|
1147
|
+
background: rgba(255, 255, 255, 0.14);
|
|
1148
|
+
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
1149
|
+
border-radius: 999px;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
@media (max-width: 720px) {
|
|
1153
|
+
.lightbox {
|
|
1154
|
+
padding: 16px;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.lightboxFigure {
|
|
1158
|
+
width: calc(100vw - 32px);
|
|
1159
|
+
max-width: none;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.lightboxNav[data-dir='prev'] {
|
|
1163
|
+
left: 20px;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.lightboxNav[data-dir='next'] {
|
|
1167
|
+
right: 20px;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.lightboxCaptionRow,
|
|
1171
|
+
.lightboxMeta {
|
|
1172
|
+
align-items: stretch;
|
|
1173
|
+
flex-direction: column;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.lightboxCopy,
|
|
1177
|
+
.lightboxActions {
|
|
1178
|
+
align-self: flex-end;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1011
1182
|
@keyframes dshImageGenSpin {
|
|
1012
1183
|
to { transform: rotate(360deg); }
|
|
1013
1184
|
}
|