@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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 画廊视图:条目网格(首图缩略)+ 详情模态(全部图片、提示词/改写、
|
|
3
|
+
* 请求参数回放、衍生链源图、下载/删除/二次编辑)。
|
|
4
|
+
* @module image-studio/client/panel/gallery
|
|
5
|
+
*/
|
|
6
|
+
import { IconDownload, IconRefreshOutline16, IconTrashOutline16 } from '@dsh-plus/shared/client'
|
|
7
|
+
import { type ReactElement, useState } from 'react'
|
|
8
|
+
import type { GalleryItem } from '../../gallery/store.ts'
|
|
9
|
+
import { deleteGalleryItem, imageUrl } from '../api.ts'
|
|
10
|
+
import type { Translate } from '../i18n.ts'
|
|
11
|
+
|
|
12
|
+
interface GalleryViewProps {
|
|
13
|
+
t: Translate
|
|
14
|
+
items: GalleryItem[]
|
|
15
|
+
loading: boolean
|
|
16
|
+
failed: boolean
|
|
17
|
+
onRefresh(): void
|
|
18
|
+
onEdit(item: GalleryItem): void
|
|
19
|
+
/** 外部请求打开的条目(任务条「查看」);打开后由 onDetailOpened 消费。 */
|
|
20
|
+
detailId: string | null
|
|
21
|
+
onDetailChange(id: string | null): void
|
|
22
|
+
hidden: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fmtTime(iso: string): string {
|
|
26
|
+
const date = new Date(iso)
|
|
27
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleString()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 详情模态。 */
|
|
31
|
+
function GalleryDetail(props: {
|
|
32
|
+
t: Translate
|
|
33
|
+
item: GalleryItem
|
|
34
|
+
onClose(): void
|
|
35
|
+
onEdit(item: GalleryItem): void
|
|
36
|
+
onDeleted(id: string): void
|
|
37
|
+
}): ReactElement {
|
|
38
|
+
const { t, item, onClose, onEdit, onDeleted } = props
|
|
39
|
+
const [confirming, setConfirming] = useState(false)
|
|
40
|
+
const [deleting, setDeleting] = useState(false)
|
|
41
|
+
const [error, setError] = useState<string | null>(null)
|
|
42
|
+
|
|
43
|
+
const doDelete = (): void => {
|
|
44
|
+
setDeleting(true)
|
|
45
|
+
deleteGalleryItem(item.id)
|
|
46
|
+
.then(() => onDeleted(item.id))
|
|
47
|
+
.catch((err: unknown) => {
|
|
48
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
49
|
+
setDeleting(false)
|
|
50
|
+
setConfirming(false)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const paramRows = Object.entries(item.params)
|
|
55
|
+
return (
|
|
56
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: 背板点击关闭是指针便利交互,键盘用户走关闭/取消按钮(同 web-terminal 既有约定)
|
|
57
|
+
<div className="ims-detailBackdrop" role="presentation" onClick={onClose}>
|
|
58
|
+
{/* biome-ignore lint/a11y/useKeyWithClickEvents: 阻断冒泡仅防误触背板关闭,无实际点击行为 */}
|
|
59
|
+
<div
|
|
60
|
+
className="ims-detail"
|
|
61
|
+
role="dialog"
|
|
62
|
+
aria-modal="true"
|
|
63
|
+
aria-label={item.prompt.slice(0, 40)}
|
|
64
|
+
onClick={(event) => event.stopPropagation()}
|
|
65
|
+
>
|
|
66
|
+
<div className="ims-detailHead">
|
|
67
|
+
<span className="ims-detailMeta">
|
|
68
|
+
{t('gallery.detail.model')}:{item.model} · {t('gallery.detail.endpoint')}:
|
|
69
|
+
{item.endpoint} · {t('gallery.detail.time')}:{fmtTime(item.createdAt)}
|
|
70
|
+
</span>
|
|
71
|
+
<button type="button" className="ims-btn ims-btnGhost" onClick={onClose}>
|
|
72
|
+
{t('common.close')}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="ims-detailBody">
|
|
76
|
+
<div className="ims-detailImages">
|
|
77
|
+
{item.imageIds.map((imageId, index) => (
|
|
78
|
+
<figure key={imageId} className="ims-detailFigure">
|
|
79
|
+
<img src={imageUrl(imageId)} alt={item.prompt} />
|
|
80
|
+
<figcaption>
|
|
81
|
+
{item.revisedPrompts[index] !== null &&
|
|
82
|
+
item.revisedPrompts[index] !== undefined ? (
|
|
83
|
+
<span className="ims-revised" title={item.revisedPrompts[index] ?? ''}>
|
|
84
|
+
{t('gallery.detail.revised')}:{item.revisedPrompts[index]}
|
|
85
|
+
</span>
|
|
86
|
+
) : null}
|
|
87
|
+
<a
|
|
88
|
+
className="ims-btn ims-btnGhost ims-btnSmall"
|
|
89
|
+
href={imageUrl(imageId)}
|
|
90
|
+
download
|
|
91
|
+
>
|
|
92
|
+
<IconDownload size={14} /> {t('common.download')}
|
|
93
|
+
</a>
|
|
94
|
+
</figcaption>
|
|
95
|
+
</figure>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
<div className="ims-detailInfo">
|
|
99
|
+
<h4 className="ims-detailLabel">{t('gallery.detail.prompt')}</h4>
|
|
100
|
+
<p className="ims-detailPrompt">{item.prompt}</p>
|
|
101
|
+
{item.sourceIds.length > 0 ? (
|
|
102
|
+
<>
|
|
103
|
+
<h4 className="ims-detailLabel">{t('gallery.detail.sources')}</h4>
|
|
104
|
+
<div className="ims-sources">
|
|
105
|
+
{item.sourceIds.map((imageId) => (
|
|
106
|
+
<span key={imageId} className="ims-thumb">
|
|
107
|
+
<img src={imageUrl(imageId)} alt="" />
|
|
108
|
+
</span>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
</>
|
|
112
|
+
) : null}
|
|
113
|
+
<h4 className="ims-detailLabel">{t('gallery.detail.params')}</h4>
|
|
114
|
+
{paramRows.length === 0 ? (
|
|
115
|
+
<p className="ims-hint">—</p>
|
|
116
|
+
) : (
|
|
117
|
+
<table className="ims-paramTable">
|
|
118
|
+
<tbody>
|
|
119
|
+
{paramRows.map(([key, value]) => (
|
|
120
|
+
<tr key={key}>
|
|
121
|
+
<td className="ims-paramTableKey">{key}</td>
|
|
122
|
+
<td>{JSON.stringify(value)}</td>
|
|
123
|
+
</tr>
|
|
124
|
+
))}
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="ims-detailFoot">
|
|
131
|
+
{error !== null ? (
|
|
132
|
+
<p className="ims-status ims-statusError" role="alert">
|
|
133
|
+
{error}
|
|
134
|
+
</p>
|
|
135
|
+
) : null}
|
|
136
|
+
<button type="button" className="ims-btn ims-btnPrimary" onClick={() => onEdit(item)}>
|
|
137
|
+
{t('gallery.edit')}
|
|
138
|
+
</button>
|
|
139
|
+
{confirming ? (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
className="ims-btn ims-btnDanger"
|
|
143
|
+
disabled={deleting}
|
|
144
|
+
onClick={doDelete}
|
|
145
|
+
>
|
|
146
|
+
{t('common.confirmDelete')}
|
|
147
|
+
</button>
|
|
148
|
+
) : (
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
className="ims-btn ims-btnGhost"
|
|
152
|
+
onClick={() => setConfirming(true)}
|
|
153
|
+
>
|
|
154
|
+
<IconTrashOutline16 size={14} /> {t('common.delete')}
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function GalleryView(props: GalleryViewProps): ReactElement {
|
|
164
|
+
const { t, items, loading, failed, onRefresh, onEdit, detailId, onDetailChange } = props
|
|
165
|
+
const detail = items.find((item) => item.id === detailId) ?? null
|
|
166
|
+
return (
|
|
167
|
+
<div className="ims-gallery" hidden={props.hidden}>
|
|
168
|
+
<div className="ims-galleryBar">
|
|
169
|
+
<span className="ims-hint">{t('gallery.total').replace('{n}', String(items.length))}</span>
|
|
170
|
+
<button type="button" className="ims-btn ims-btnGhost ims-btnSmall" onClick={onRefresh}>
|
|
171
|
+
<IconRefreshOutline16 size={14} /> {t('common.refresh')}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
{failed ? (
|
|
175
|
+
<p className="ims-empty">
|
|
176
|
+
{t('common.loadFailed')}
|
|
177
|
+
<button type="button" className="ims-btn ims-btnGhost ims-btnSmall" onClick={onRefresh}>
|
|
178
|
+
{t('common.retry')}
|
|
179
|
+
</button>
|
|
180
|
+
</p>
|
|
181
|
+
) : loading ? (
|
|
182
|
+
<p className="ims-empty">{t('common.loading')}</p>
|
|
183
|
+
) : items.length === 0 ? (
|
|
184
|
+
<p className="ims-empty">{t('gallery.empty')}</p>
|
|
185
|
+
) : (
|
|
186
|
+
<div className="ims-grid">
|
|
187
|
+
{[...items].reverse().map((item) => (
|
|
188
|
+
<button
|
|
189
|
+
key={item.id}
|
|
190
|
+
type="button"
|
|
191
|
+
className="ims-cell"
|
|
192
|
+
onClick={() => onDetailChange(item.id)}
|
|
193
|
+
>
|
|
194
|
+
<img src={imageUrl(item.imageIds[0] ?? '')} alt={item.prompt} loading="lazy" />
|
|
195
|
+
<span className="ims-cellPrompt">{item.prompt}</span>
|
|
196
|
+
</button>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
{detail !== null ? (
|
|
201
|
+
<GalleryDetail
|
|
202
|
+
t={t}
|
|
203
|
+
item={detail}
|
|
204
|
+
onClose={() => onDetailChange(null)}
|
|
205
|
+
onEdit={(item) => {
|
|
206
|
+
onDetailChange(null)
|
|
207
|
+
onEdit(item)
|
|
208
|
+
}}
|
|
209
|
+
onDeleted={() => {
|
|
210
|
+
onDetailChange(null)
|
|
211
|
+
onRefresh()
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
) : null}
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
}
|