includio-cms 0.7.2 → 0.13.0
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/CHANGELOG.md +110 -0
- package/ROADMAP.md +40 -2
- package/dist/admin/api/generate-styles.d.ts +2 -0
- package/dist/admin/api/generate-styles.js +32 -0
- package/dist/admin/api/handler.js +33 -0
- package/dist/admin/api/media-gc.js +10 -4
- package/dist/admin/api/rest/handler.js +17 -0
- package/dist/admin/api/rest/routes/collections.js +25 -13
- package/dist/admin/api/rest/routes/entries.d.ts +1 -1
- package/dist/admin/api/rest/routes/entries.js +10 -10
- package/dist/admin/api/rest/routes/media.d.ts +2 -0
- package/dist/admin/api/rest/routes/media.js +9 -0
- package/dist/admin/api/rest/routes/schema.d.ts +5 -0
- package/dist/admin/api/rest/routes/schema.js +152 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
- package/dist/admin/api/rest/routes/singletons.js +8 -7
- package/dist/admin/api/rest/routes/upload.d.ts +2 -0
- package/dist/admin/api/rest/routes/upload.js +28 -0
- package/dist/admin/api/upload.js +13 -0
- package/dist/admin/client/collection/collection-entries.svelte +19 -6
- package/dist/admin/client/entry/entry.svelte +21 -23
- package/dist/admin/client/entry/header/a11y-validator.js +2 -2
- package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
- package/dist/admin/client/entry/header/status-badge.svelte +2 -2
- package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
- package/dist/admin/client/entry/header/visibility.svelte +16 -10
- package/dist/admin/client/entry/utils.d.ts +3 -0
- package/dist/admin/client/entry/utils.js +22 -4
- package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
- package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
- package/dist/admin/components/fields/blocks-field.svelte +9 -10
- package/dist/admin/components/fields/field-renderer.svelte +4 -8
- package/dist/admin/components/fields/object-field.svelte +7 -12
- package/dist/admin/components/fields/select-field.svelte +8 -2
- package/dist/admin/components/fields/seo-field.svelte +40 -93
- package/dist/admin/components/fields/simple-array-field.svelte +5 -5
- package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
- package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
- package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
- package/dist/admin/components/fields/url-field.svelte +61 -72
- package/dist/admin/components/media/file-upload.svelte +5 -1
- package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
- package/dist/admin/components/media/media-library.svelte +109 -37
- package/dist/admin/components/media/media-selector.svelte +79 -11
- package/dist/admin/components/media/tag-sidebar.svelte +10 -6
- package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +21 -93
- package/dist/admin/components/tiptap/inline-block-node.js +6 -5
- package/dist/admin/components/tiptap/link-dialog.svelte +10 -11
- package/dist/admin/components/tiptap/slash-command.js +1 -1
- package/dist/admin/remote/entry.remote.d.ts +2 -5
- package/dist/admin/remote/entry.remote.js +22 -27
- package/dist/admin/remote/media.remote.d.ts +15 -0
- package/dist/admin/remote/media.remote.js +18 -2
- package/dist/admin/remote/preview.remote.js +3 -1
- package/dist/admin/utils/entryLabel.js +9 -6
- package/dist/admin/utils/translationStatus.js +1 -2
- package/dist/cli/scaffold/admin.js +34 -2
- package/dist/cms/runtime/api.d.ts +16 -12
- package/dist/cms/runtime/api.js +7 -6
- package/dist/cms/runtime/remote.js +2 -2
- package/dist/cms/runtime/schemas.d.ts +1 -1
- package/dist/cms/runtime/schemas.js +1 -1
- package/dist/cms/runtime/types.d.ts +118 -112
- package/dist/cms/runtime/types.js +0 -12
- package/dist/core/cms.d.ts +3 -1
- package/dist/core/cms.js +30 -0
- package/dist/core/fields/fieldSchemaToTs.js +9 -15
- package/dist/core/fields/formFieldSchemaToTs.js +7 -0
- package/dist/core/server/entries/operations/create.js +10 -4
- package/dist/core/server/entries/operations/get.d.ts +1 -0
- package/dist/core/server/entries/operations/get.js +186 -191
- package/dist/core/server/entries/operations/update.d.ts +6 -7
- package/dist/core/server/entries/operations/update.js +20 -38
- package/dist/core/server/fields/populateEntry.js +16 -52
- package/dist/core/server/fields/resolveImageFields.js +69 -120
- package/dist/core/server/fields/resolveRelationFields.js +30 -51
- package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
- package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
- package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
- package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
- package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
- package/dist/core/server/fields/resolveUrlFields.js +47 -56
- package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
- package/dist/core/server/fields/utils/fixOrphans.js +12 -0
- package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
- package/dist/core/server/fields/utils/imageStyles.js +41 -25
- package/dist/core/server/fields/utils/resolveMedia.js +1 -6
- package/dist/core/server/forms/submissions/operations/delete.js +26 -2
- package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
- package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
- package/dist/core/server/generator/fields.d.ts +6 -0
- package/dist/core/server/generator/fields.js +43 -5
- package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
- package/dist/core/server/generator/formFields.js +1 -0
- package/dist/core/server/generator/generator.js +98 -30
- package/dist/core/server/media/operations/getFiles.d.ts +5 -0
- package/dist/core/server/media/operations/getFiles.js +6 -0
- package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
- package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
- package/dist/db-postgres/index.js +303 -37
- package/dist/db-postgres/schema/entry.d.ts +0 -94
- package/dist/db-postgres/schema/entry.js +0 -6
- package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
- package/dist/db-postgres/schema/entryVersion.js +1 -0
- package/dist/entity/index.d.ts +9 -4
- package/dist/entity/index.js +24 -24
- package/dist/files-local/index.js +43 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/sveltekit/components/preview.svelte +2 -326
- package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
- package/dist/sveltekit/server/index.d.ts +2 -1
- package/dist/sveltekit/server/index.js +2 -1
- package/dist/sveltekit/server/preview.js +4 -7
- package/dist/types/adapters/db.d.ts +15 -1
- package/dist/types/adapters/files.d.ts +6 -0
- package/dist/types/cms.d.ts +5 -0
- package/dist/types/entries.d.ts +54 -18
- package/dist/types/fields.d.ts +14 -24
- package/dist/types/formFields.d.ts +7 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/structured-content.d.ts +5 -0
- package/dist/updates/0.10.0/index.d.ts +2 -0
- package/dist/updates/0.10.0/index.js +15 -0
- package/dist/updates/0.11.0/index.d.ts +2 -0
- package/dist/updates/0.11.0/index.js +12 -0
- package/dist/updates/0.12.0/index.d.ts +2 -0
- package/dist/updates/0.12.0/index.js +12 -0
- package/dist/updates/0.13.0/index.d.ts +2 -0
- package/dist/updates/0.13.0/index.js +10 -0
- package/dist/updates/0.7.3/index.d.ts +2 -0
- package/dist/updates/0.7.3/index.js +10 -0
- package/dist/updates/0.8.0/index.d.ts +2 -0
- package/dist/updates/0.8.0/index.js +18 -0
- package/dist/updates/0.8.0/migrate.d.ts +2 -0
- package/dist/updates/0.8.0/migrate.js +101 -0
- package/dist/updates/0.9.0/index.d.ts +2 -0
- package/dist/updates/0.9.0/index.js +38 -0
- package/dist/updates/index.js +8 -1
- package/package.json +7 -6
- package/dist/admin/components/fields/image-field.svelte +0 -198
- package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
- package/dist/admin/components/fields/richtext-field.svelte +0 -13
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
- package/dist/admin/components/tiptap.svelte +0 -11
- package/dist/admin/components/tiptap.svelte.d.ts +0 -6
- package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
- package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import FileUpload from './file-upload.svelte';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
3
|
+
import { onMount, untrack } from 'svelte';
|
|
4
4
|
import type { MediaFile, MediaTag } from '../../../types/media.js';
|
|
5
5
|
import { getRemotes } from '../../context/remotes.js';
|
|
6
6
|
import { toast } from 'svelte-sonner';
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
selectModeActive: string;
|
|
29
29
|
selectModeAnnounce: string;
|
|
30
30
|
selectModeOffAnnounce: string;
|
|
31
|
+
loadMore: string;
|
|
31
32
|
}
|
|
32
33
|
> = {
|
|
33
34
|
pl: {
|
|
@@ -38,7 +39,8 @@
|
|
|
38
39
|
selectMode: 'Wybierz',
|
|
39
40
|
selectModeActive: 'Zaznaczanie',
|
|
40
41
|
selectModeAnnounce: 'Tryb zaznaczania włączony. Klikaj pliki, aby je zaznaczyć.',
|
|
41
|
-
selectModeOffAnnounce: 'Tryb zaznaczania wyłączony.'
|
|
42
|
+
selectModeOffAnnounce: 'Tryb zaznaczania wyłączony.',
|
|
43
|
+
loadMore: 'Wczytaj więcej'
|
|
42
44
|
},
|
|
43
45
|
en: {
|
|
44
46
|
fileDeletedToast: 'File has been deleted',
|
|
@@ -48,7 +50,8 @@
|
|
|
48
50
|
selectMode: 'Select',
|
|
49
51
|
selectModeActive: 'Selecting',
|
|
50
52
|
selectModeAnnounce: 'Selection mode enabled. Click files to select them.',
|
|
51
|
-
selectModeOffAnnounce: 'Selection mode disabled.'
|
|
53
|
+
selectModeOffAnnounce: 'Selection mode disabled.',
|
|
54
|
+
loadMore: 'Load more'
|
|
52
55
|
}
|
|
53
56
|
};
|
|
54
57
|
|
|
@@ -63,6 +66,8 @@
|
|
|
63
66
|
|
|
64
67
|
let { selected = $bindable([]), multiple = false, accept }: Props = $props();
|
|
65
68
|
|
|
69
|
+
const PAGE_SIZE = 48;
|
|
70
|
+
|
|
66
71
|
let currentFile: MediaFile | null = $state(null);
|
|
67
72
|
let activeTagFilter = $state<string | null>(null);
|
|
68
73
|
let searchQuery = $state('');
|
|
@@ -70,6 +75,8 @@
|
|
|
70
75
|
let dropZoneRef = $state<HTMLElement | null>(null);
|
|
71
76
|
let selectionMode = $state(false);
|
|
72
77
|
let selectionAnnouncement = $state('');
|
|
78
|
+
let page = $state(0);
|
|
79
|
+
let loadedFiles = $state<MediaFile[]>([]);
|
|
73
80
|
|
|
74
81
|
function exitSelectionMode() {
|
|
75
82
|
selectionMode = false;
|
|
@@ -83,21 +90,68 @@
|
|
|
83
90
|
selectionAnnouncement = lang[interfaceLanguage.current].selectModeAnnounce;
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
const filterData = $derived({
|
|
94
|
+
tagIds: activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined,
|
|
95
|
+
untagged: activeTagFilter === 'untagged' ? true : undefined,
|
|
96
|
+
mimeTypes: accept?.split(','),
|
|
97
|
+
search: searchQuery || undefined
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Reset page on filter/search change — track primitive sources only
|
|
101
|
+
$effect(() => {
|
|
102
|
+
void activeTagFilter;
|
|
103
|
+
void searchQuery;
|
|
104
|
+
void accept;
|
|
105
|
+
untrack(() => {
|
|
106
|
+
page = 0;
|
|
107
|
+
loadedFiles = [];
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
86
111
|
const filesQuery = $derived(
|
|
87
112
|
remotes.getMediaFiles({
|
|
88
113
|
data: {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
search: searchQuery || undefined
|
|
114
|
+
...filterData,
|
|
115
|
+
limit: PAGE_SIZE,
|
|
116
|
+
offset: page * PAGE_SIZE
|
|
93
117
|
}
|
|
94
118
|
})
|
|
95
119
|
);
|
|
96
120
|
|
|
97
|
-
const
|
|
98
|
-
remotes.
|
|
121
|
+
const countQuery = $derived(
|
|
122
|
+
remotes.countMediaFiles({
|
|
123
|
+
data: {
|
|
124
|
+
tagIds: filterData.tagIds,
|
|
125
|
+
untagged: filterData.untagged,
|
|
126
|
+
mimeTypes: filterData.mimeTypes,
|
|
127
|
+
search: filterData.search
|
|
128
|
+
}
|
|
129
|
+
})
|
|
99
130
|
);
|
|
100
131
|
|
|
132
|
+
const tagCountsQuery = $derived(remotes.getMediaTagsWithCounts());
|
|
133
|
+
const totalCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(',') } }));
|
|
134
|
+
const untaggedCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(','), untagged: true } }));
|
|
135
|
+
|
|
136
|
+
// Append new page results — only react to filesQuery changes
|
|
137
|
+
$effect(() => {
|
|
138
|
+
const ready = filesQuery.ready;
|
|
139
|
+
const current = filesQuery.current;
|
|
140
|
+
if (ready && current) {
|
|
141
|
+
untrack(() => {
|
|
142
|
+
if (page === 0) {
|
|
143
|
+
loadedFiles = current;
|
|
144
|
+
} else {
|
|
145
|
+
const existingIds = new Set(loadedFiles.map((f) => f.id));
|
|
146
|
+
loadedFiles = [...loadedFiles, ...current.filter((f) => !existingIds.has(f.id))];
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const totalCount = $derived(countQuery.ready ? (countQuery.current as number) : 0);
|
|
153
|
+
const hasMore = $derived(loadedFiles.length < totalCount);
|
|
154
|
+
|
|
101
155
|
let tagsQuery = $derived(remotes.getMediaTags());
|
|
102
156
|
|
|
103
157
|
function handleFileSelect(file: MediaFile, event?: MouseEvent) {
|
|
@@ -136,12 +190,21 @@
|
|
|
136
190
|
selectedFileIds = [...merged];
|
|
137
191
|
}
|
|
138
192
|
|
|
193
|
+
function refreshAll() {
|
|
194
|
+
page = 0;
|
|
195
|
+
loadedFiles = [];
|
|
196
|
+
filesQuery.refresh();
|
|
197
|
+
countQuery.refresh();
|
|
198
|
+
tagCountsQuery.refresh();
|
|
199
|
+
totalCountQuery.refresh();
|
|
200
|
+
untaggedCountQuery.refresh();
|
|
201
|
+
}
|
|
202
|
+
|
|
139
203
|
async function deleteFileCommand() {
|
|
140
204
|
if (currentFile) {
|
|
141
205
|
await remotes.deleteMediaFile(currentFile.id);
|
|
142
206
|
toast.success(lang[interfaceLanguage.current].fileDeletedToast);
|
|
143
|
-
|
|
144
|
-
allFilesQuery.refresh();
|
|
207
|
+
refreshAll();
|
|
145
208
|
currentFile = null;
|
|
146
209
|
}
|
|
147
210
|
}
|
|
@@ -153,8 +216,7 @@
|
|
|
153
216
|
if (tags) {
|
|
154
217
|
currentFile = { ...currentFile, tags: tags.filter((t) => tagIds.includes(t.id)) };
|
|
155
218
|
}
|
|
156
|
-
|
|
157
|
-
await allFilesQuery.refresh();
|
|
219
|
+
refreshAll();
|
|
158
220
|
}
|
|
159
221
|
}
|
|
160
222
|
|
|
@@ -163,8 +225,7 @@
|
|
|
163
225
|
const result = await remotes.renameMediaFile({ fileId: currentFile.id, newName });
|
|
164
226
|
if (result.success === true) {
|
|
165
227
|
currentFile = { ...currentFile, name: result.name, url: result.url };
|
|
166
|
-
|
|
167
|
-
await allFilesQuery.refresh();
|
|
228
|
+
refreshAll();
|
|
168
229
|
}
|
|
169
230
|
return result;
|
|
170
231
|
}
|
|
@@ -174,27 +235,25 @@
|
|
|
174
235
|
async function handleCreateTag(name: string, color: string) {
|
|
175
236
|
await remotes.createMediaTag({ name, color });
|
|
176
237
|
await tagsQuery.refresh();
|
|
238
|
+
await tagCountsQuery.refresh();
|
|
177
239
|
}
|
|
178
240
|
|
|
179
241
|
async function handleUpdateTag(id: string, name: string, color: string) {
|
|
180
242
|
await remotes.updateMediaTag({ id, name, color });
|
|
181
243
|
await tagsQuery.refresh();
|
|
182
|
-
|
|
183
|
-
await allFilesQuery.refresh();
|
|
244
|
+
refreshAll();
|
|
184
245
|
}
|
|
185
246
|
|
|
186
247
|
async function handleDeleteTag(id: string) {
|
|
187
248
|
await remotes.deleteMediaTag(id);
|
|
188
249
|
await tagsQuery.refresh();
|
|
189
|
-
|
|
190
|
-
await allFilesQuery.refresh();
|
|
250
|
+
refreshAll();
|
|
191
251
|
}
|
|
192
252
|
|
|
193
253
|
async function handleBulkTag(tagIds: string[]) {
|
|
194
254
|
const fileIds = selectedFileIds;
|
|
195
255
|
await remotes.bulkSetMediaFileTags({ fileIds, tagIds });
|
|
196
|
-
|
|
197
|
-
await allFilesQuery.refresh();
|
|
256
|
+
refreshAll();
|
|
198
257
|
}
|
|
199
258
|
|
|
200
259
|
async function handleBulkDelete() {
|
|
@@ -202,8 +261,7 @@
|
|
|
202
261
|
toast.success(lang[interfaceLanguage.current].bulkDeletedToast);
|
|
203
262
|
selectedFileIds = [];
|
|
204
263
|
currentFile = null;
|
|
205
|
-
|
|
206
|
-
await allFilesQuery.refresh();
|
|
264
|
+
refreshAll();
|
|
207
265
|
}
|
|
208
266
|
|
|
209
267
|
onMount(() => {
|
|
@@ -218,10 +276,12 @@
|
|
|
218
276
|
<div class="flex h-full overflow-hidden" bind:this={dropZoneRef}>
|
|
219
277
|
<!-- Tag sidebar (192px) -->
|
|
220
278
|
<aside class="w-48 min-w-48 shrink-0 border-r bg-card flex flex-col overflow-hidden" aria-label="Filtry tagów">
|
|
221
|
-
{#if tagsQuery.ready &&
|
|
279
|
+
{#if tagsQuery.ready && tagsQuery.current}
|
|
222
280
|
<TagSidebar
|
|
223
281
|
tags={tagsQuery.current}
|
|
224
|
-
|
|
282
|
+
tagCounts={tagCountsQuery.ready ? tagCountsQuery.current : []}
|
|
283
|
+
totalCount={totalCountQuery.ready ? totalCountQuery.current : 0}
|
|
284
|
+
untaggedCount={untaggedCountQuery.ready ? untaggedCountQuery.current : 0}
|
|
225
285
|
activeFilter={activeTagFilter}
|
|
226
286
|
onFilterChange={(f) => (activeTagFilter = f)}
|
|
227
287
|
onCreateTag={handleCreateTag}
|
|
@@ -250,9 +310,10 @@
|
|
|
250
310
|
</Toggle>
|
|
251
311
|
<div class="flex-1"></div>
|
|
252
312
|
<FileUpload
|
|
253
|
-
onUpload={() =>
|
|
313
|
+
onUpload={() => refreshAll()}
|
|
254
314
|
{accept}
|
|
255
315
|
bind:dropZoneRef
|
|
316
|
+
tagIds={activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined}
|
|
256
317
|
/>
|
|
257
318
|
</div>
|
|
258
319
|
|
|
@@ -261,14 +322,14 @@
|
|
|
261
322
|
|
|
262
323
|
<!-- File grid -->
|
|
263
324
|
<div class="flex-1 overflow-y-auto px-5 pt-4 pb-24 scrollbar-thin" role="listbox" aria-multiselectable="true" aria-label="Siatka plików">
|
|
264
|
-
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3" class:opacity-60={filesQuery.loading} style="transition: opacity 200ms">
|
|
265
|
-
{#if !filesQuery.ready}
|
|
325
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3" class:opacity-60={filesQuery.loading && page === 0} style="transition: opacity 200ms">
|
|
326
|
+
{#if !filesQuery.ready && page === 0}
|
|
266
327
|
{#each Array(8) as _}
|
|
267
328
|
<Skeleton class="block h-[168px] rounded-xl" />
|
|
268
329
|
{/each}
|
|
269
|
-
{:else
|
|
330
|
+
{:else}
|
|
270
331
|
<FilesList
|
|
271
|
-
files={
|
|
332
|
+
files={loadedFiles}
|
|
272
333
|
selected={selectedFileIds.length > 0 ? selectedFileIds : (selected ?? '')}
|
|
273
334
|
onSelect={handleFileSelect}
|
|
274
335
|
onRangeSelect={handleRangeSelect}
|
|
@@ -276,15 +337,27 @@
|
|
|
276
337
|
/>
|
|
277
338
|
{/if}
|
|
278
339
|
</div>
|
|
340
|
+
{#if hasMore}
|
|
341
|
+
<div class="flex justify-center py-6">
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
class="inline-flex items-center gap-2 rounded-lg border border-border px-5 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted disabled:opacity-50"
|
|
345
|
+
disabled={filesQuery.loading}
|
|
346
|
+
onclick={() => page++}
|
|
347
|
+
>
|
|
348
|
+
{lang[interfaceLanguage.current].loadMore} ({loadedFiles.length} / {totalCount})
|
|
349
|
+
</button>
|
|
350
|
+
</div>
|
|
351
|
+
{/if}
|
|
279
352
|
</div>
|
|
280
353
|
</section>
|
|
281
354
|
|
|
282
355
|
<!-- Detail panel (384px) -->
|
|
283
356
|
<aside class="w-96 min-w-96 shrink-0 border-l bg-card flex flex-col overflow-hidden" aria-label="Szczegóły pliku">
|
|
284
357
|
{#if selectionMode}
|
|
285
|
-
{#if
|
|
358
|
+
{#if tagsQuery.ready && tagsQuery.current}
|
|
286
359
|
<MultiFileSummary
|
|
287
|
-
files={
|
|
360
|
+
files={loadedFiles.filter((f) => selectedFileIds.includes(f.id))}
|
|
288
361
|
allTags={tagsQuery.current}
|
|
289
362
|
onBulkTag={handleBulkTag}
|
|
290
363
|
onBulkDelete={handleBulkDelete}
|
|
@@ -299,8 +372,7 @@
|
|
|
299
372
|
onDelete={deleteFileCommand}
|
|
300
373
|
onReplace={(updated) => {
|
|
301
374
|
currentFile = updated;
|
|
302
|
-
|
|
303
|
-
allFilesQuery.refresh();
|
|
375
|
+
refreshAll();
|
|
304
376
|
}}
|
|
305
377
|
{onTagUpdate}
|
|
306
378
|
{onNameUpdate}
|
|
@@ -323,15 +395,15 @@
|
|
|
323
395
|
</div>
|
|
324
396
|
|
|
325
397
|
<!-- Bulk action bar -->
|
|
326
|
-
{#if selectedFileIds.length > 0 && tagsQuery.ready && tagsQuery.current
|
|
398
|
+
{#if selectedFileIds.length > 0 && tagsQuery.ready && tagsQuery.current}
|
|
327
399
|
<BulkActionBar
|
|
328
400
|
selectedCount={selectedFileIds.length}
|
|
329
|
-
totalCount={
|
|
401
|
+
totalCount={totalCount}
|
|
330
402
|
tags={tagsQuery.current}
|
|
331
403
|
onBulkTag={handleBulkTag}
|
|
332
404
|
onBulkDelete={handleBulkDelete}
|
|
333
405
|
onClear={exitSelectionMode}
|
|
334
|
-
onSelectAll={() => { selectedFileIds =
|
|
406
|
+
onSelectAll={() => { selectedFileIds = loadedFiles.map((f) => f.id); }}
|
|
335
407
|
onCreateTag={handleCreateTag}
|
|
336
408
|
onUpdateTagColor={async (id, color) => {
|
|
337
409
|
const tag = tagsQuery.current?.find((t) => t.id === id);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import FileUpload from './file-upload.svelte';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
3
|
+
import { onMount, untrack } from 'svelte';
|
|
4
4
|
import type { MediaFile, MediaTag } from '../../../types/media.js';
|
|
5
5
|
import { getRemotes } from '../../context/remotes.js';
|
|
6
6
|
import Button from '../../../components/ui/button/button.svelte';
|
|
@@ -66,10 +66,14 @@
|
|
|
66
66
|
|
|
67
67
|
let { selected = $bindable([]), multiple = false, accept, onConfirm, onCancel }: Props = $props();
|
|
68
68
|
|
|
69
|
+
const PAGE_SIZE = 48;
|
|
70
|
+
|
|
69
71
|
let currentFile: MediaFile | null = $state(null);
|
|
70
72
|
let activeTagFilter = $state<string | null>(null);
|
|
71
73
|
let searchQuery = $state('');
|
|
72
74
|
let dropZoneRef = $state<HTMLElement | null>(null);
|
|
75
|
+
let page = $state(0);
|
|
76
|
+
let loadedFiles = $state<MediaFile[]>([]);
|
|
73
77
|
|
|
74
78
|
// Staged selection - changes are only committed on confirm
|
|
75
79
|
let stagedSelection = $state<string[] | string>(
|
|
@@ -86,17 +90,66 @@
|
|
|
86
90
|
onCancel?.();
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
const filterData = $derived({
|
|
94
|
+
tagIds: activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined,
|
|
95
|
+
untagged: activeTagFilter === 'untagged' ? true : undefined,
|
|
96
|
+
mimeTypes: accept?.split(','),
|
|
97
|
+
search: searchQuery || undefined
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
$effect(() => {
|
|
101
|
+
void activeTagFilter;
|
|
102
|
+
void searchQuery;
|
|
103
|
+
void accept;
|
|
104
|
+
untrack(() => {
|
|
105
|
+
page = 0;
|
|
106
|
+
loadedFiles = [];
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
89
110
|
const filesQuery = $derived(
|
|
90
111
|
remotes.getMediaFiles({
|
|
91
112
|
data: {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
...filterData,
|
|
114
|
+
limit: PAGE_SIZE,
|
|
115
|
+
offset: page * PAGE_SIZE
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const countQuery = $derived(
|
|
121
|
+
remotes.countMediaFiles({
|
|
122
|
+
data: {
|
|
123
|
+
tagIds: filterData.tagIds,
|
|
124
|
+
untagged: filterData.untagged,
|
|
125
|
+
mimeTypes: filterData.mimeTypes,
|
|
126
|
+
search: filterData.search
|
|
96
127
|
}
|
|
97
128
|
})
|
|
98
129
|
);
|
|
99
130
|
|
|
131
|
+
const tagCountsQuery = $derived(remotes.getMediaTagsWithCounts());
|
|
132
|
+
const totalCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(',') } }));
|
|
133
|
+
const untaggedCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(','), untagged: true } }));
|
|
134
|
+
|
|
135
|
+
$effect(() => {
|
|
136
|
+
const ready = filesQuery.ready;
|
|
137
|
+
const current = filesQuery.current;
|
|
138
|
+
if (ready && current) {
|
|
139
|
+
untrack(() => {
|
|
140
|
+
if (page === 0) {
|
|
141
|
+
loadedFiles = current;
|
|
142
|
+
} else {
|
|
143
|
+
const existingIds = new Set(loadedFiles.map((f) => f.id));
|
|
144
|
+
loadedFiles = [...loadedFiles, ...current.filter((f) => !existingIds.has(f.id))];
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const totalFilteredCount = $derived(countQuery.ready ? (countQuery.current as number) : 0);
|
|
151
|
+
const hasMore = $derived(loadedFiles.length < totalFilteredCount);
|
|
152
|
+
|
|
100
153
|
let tagsQuery = $derived(remotes.getMediaTags());
|
|
101
154
|
|
|
102
155
|
onMount(() => {
|
|
@@ -115,7 +168,9 @@
|
|
|
115
168
|
{#if tagsQuery.ready && tagsQuery.current}
|
|
116
169
|
<TagSidebar
|
|
117
170
|
tags={tagsQuery.current}
|
|
118
|
-
|
|
171
|
+
tagCounts={tagCountsQuery.ready ? tagCountsQuery.current : []}
|
|
172
|
+
totalCount={totalCountQuery.ready ? totalCountQuery.current : 0}
|
|
173
|
+
untaggedCount={untaggedCountQuery.ready ? untaggedCountQuery.current : 0}
|
|
119
174
|
activeFilter={activeTagFilter}
|
|
120
175
|
onFilterChange={(f) => (activeTagFilter = f)}
|
|
121
176
|
onCreateTag={async (name, color) => {
|
|
@@ -141,22 +196,23 @@
|
|
|
141
196
|
<MediaSearch bind:value={searchQuery} />
|
|
142
197
|
<MediaSort />
|
|
143
198
|
<FileUpload
|
|
144
|
-
onUpload={() => filesQuery.refresh()}
|
|
199
|
+
onUpload={() => { page = 0; loadedFiles = []; filesQuery.refresh(); countQuery.refresh(); tagCountsQuery.refresh(); totalCountQuery.refresh(); untaggedCountQuery.refresh(); }}
|
|
145
200
|
{accept}
|
|
146
201
|
bind:dropZoneRef
|
|
202
|
+
tagIds={activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined}
|
|
147
203
|
/>
|
|
148
204
|
</div>
|
|
149
205
|
|
|
150
206
|
<!-- Grid z plikami -->
|
|
151
207
|
<div class="flex-1 overflow-y-auto pr-2">
|
|
152
|
-
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3 p-4" class:opacity-60={filesQuery.loading} style="transition: opacity 200ms">
|
|
153
|
-
{#if !filesQuery.ready}
|
|
208
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3 p-4" class:opacity-60={filesQuery.loading && page === 0} style="transition: opacity 200ms">
|
|
209
|
+
{#if !filesQuery.ready && page === 0}
|
|
154
210
|
{#each Array(8) as _}
|
|
155
211
|
<Skeleton class="block aspect-square rounded-2xl" />
|
|
156
212
|
{/each}
|
|
157
|
-
{:else
|
|
213
|
+
{:else}
|
|
158
214
|
<FilesList
|
|
159
|
-
files={
|
|
215
|
+
files={loadedFiles}
|
|
160
216
|
selected={stagedSelection}
|
|
161
217
|
onSelect={(file) => {
|
|
162
218
|
currentFile = file;
|
|
@@ -171,6 +227,18 @@
|
|
|
171
227
|
/>
|
|
172
228
|
{/if}
|
|
173
229
|
</div>
|
|
230
|
+
{#if hasMore}
|
|
231
|
+
<div class="flex justify-center py-4">
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
class="inline-flex items-center gap-2 rounded-lg border border-border px-5 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted disabled:opacity-50"
|
|
235
|
+
disabled={filesQuery.loading}
|
|
236
|
+
onclick={() => page++}
|
|
237
|
+
>
|
|
238
|
+
{loadedFiles.length} / {totalFilteredCount}
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
{/if}
|
|
174
242
|
</div>
|
|
175
243
|
</div>
|
|
176
244
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
3
3
|
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { MediaTag } from '../../../types/media.js';
|
|
5
5
|
import Input from '../../../components/ui/input/input.svelte';
|
|
6
6
|
import * as Popover from '../../../components/ui/popover/index.js';
|
|
7
7
|
import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
|
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
|
|
66
66
|
type Props = {
|
|
67
67
|
tags: MediaTag[];
|
|
68
|
-
|
|
68
|
+
tagCounts?: { tag: MediaTag; count: number }[];
|
|
69
|
+
totalCount?: number;
|
|
70
|
+
untaggedCount?: number;
|
|
69
71
|
activeFilter: string | null;
|
|
70
72
|
onFilterChange: (filter: string | null) => void;
|
|
71
73
|
onCreateTag: (name: string, color: string) => Promise<void>;
|
|
@@ -75,7 +77,9 @@
|
|
|
75
77
|
|
|
76
78
|
let {
|
|
77
79
|
tags,
|
|
78
|
-
|
|
80
|
+
tagCounts = [],
|
|
81
|
+
totalCount = 0,
|
|
82
|
+
untaggedCount: untaggedCountProp = 0,
|
|
79
83
|
activeFilter,
|
|
80
84
|
onFilterChange,
|
|
81
85
|
onCreateTag,
|
|
@@ -92,10 +96,10 @@
|
|
|
92
96
|
let menuOpenTagId = $state<string | null>(null);
|
|
93
97
|
|
|
94
98
|
function getTagFileCount(tagId: string): number {
|
|
95
|
-
return
|
|
99
|
+
return tagCounts.find((tc) => tc.tag.id === tagId)?.count ?? 0;
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
const untaggedCount = $derived(
|
|
102
|
+
const untaggedCount = $derived(untaggedCountProp);
|
|
99
103
|
|
|
100
104
|
function getNextColor(): string {
|
|
101
105
|
const usedColors = tags.map((t) => t.color);
|
|
@@ -152,7 +156,7 @@
|
|
|
152
156
|
<Tag class="h-4 w-4" />
|
|
153
157
|
</span>
|
|
154
158
|
<span class="flex-1 truncate">{lang[interfaceLanguage.current].all}</span>
|
|
155
|
-
<span class="text-[11px] font-semibold tabular-nums {activeFilter === null ? 'text-primary' : 'text-text-light'}">{
|
|
159
|
+
<span class="text-[11px] font-semibold tabular-nums {activeFilter === null ? 'text-primary' : 'text-text-light'}">{totalCount}</span>
|
|
156
160
|
</button>
|
|
157
161
|
</li>
|
|
158
162
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MediaTag } from '../../../types/media.js';
|
|
2
2
|
type Props = {
|
|
3
3
|
tags: MediaTag[];
|
|
4
|
-
|
|
4
|
+
tagCounts?: {
|
|
5
|
+
tag: MediaTag;
|
|
6
|
+
count: number;
|
|
7
|
+
}[];
|
|
8
|
+
totalCount?: number;
|
|
9
|
+
untaggedCount?: number;
|
|
5
10
|
activeFilter: string | null;
|
|
6
11
|
onFilterChange: (filter: string | null) => void;
|
|
7
12
|
onCreateTag: (name: string, color: string) => Promise<void>;
|