includio-cms 0.0.42 → 0.0.44
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/dist/admin/components/media/media-library.svelte +54 -51
- package/dist/admin/components/media/tag-combobox.svelte +130 -0
- package/dist/admin/components/media/tag-combobox.svelte.d.ts +9 -0
- package/dist/admin/components/media/utils.d.ts +3 -0
- package/dist/admin/components/media/utils.js +3 -0
- package/dist/admin/remote/media.remote.d.ts +5 -1
- package/dist/admin/remote/media.remote.js +10 -2
- package/dist/core/server/media/operations/updateFile.d.ts +1 -0
- package/dist/core/server/media/operations/updateFile.js +6 -0
- package/dist/db-postgres/index.js +1 -1
- package/dist/files-local/index.js +5 -0
- package/dist/sveltekit/components/image.svelte +27 -13
- package/dist/sveltekit/components/image.svelte.d.ts +2 -1
- package/package.json +1 -1
- package/dist/admin/components/media/create-folder.svelte +0 -32
- package/dist/admin/components/media/create-folder.svelte.d.ts +0 -6
- package/dist/admin/components/media/folder-tree.svelte +0 -24
- package/dist/admin/components/media/folder-tree.svelte.d.ts +0 -7
- package/dist/admin/components/media/tree-node.svelte +0 -40
- package/dist/admin/components/media/tree-node.svelte.d.ts +0 -12
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { getInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
15
15
|
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
16
16
|
import Skeleton from '../../../components/ui/skeleton/skeleton.svelte';
|
|
17
|
+
import TagCombobox from './tag-combobox.svelte';
|
|
17
18
|
|
|
18
19
|
const lang: Record<
|
|
19
20
|
InterfaceLanguage,
|
|
@@ -21,7 +22,6 @@
|
|
|
21
22
|
fileDeletedToast: string;
|
|
22
23
|
allTabs: string;
|
|
23
24
|
fileDeleteLabel: string;
|
|
24
|
-
fileTagLabel: string;
|
|
25
25
|
fileNameLabel: string;
|
|
26
26
|
fileUrlLabel: string;
|
|
27
27
|
fileAltLabel: string;
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
> = {
|
|
32
32
|
pl: {
|
|
33
33
|
fileDeleteLabel: 'Usuń plik',
|
|
34
|
-
fileTagLabel: 'Tag',
|
|
35
34
|
fileNameLabel: 'Nazwa',
|
|
36
35
|
fileUrlLabel: 'URL',
|
|
37
36
|
fileAltLabel: 'Tekst alternatywny',
|
|
@@ -42,7 +41,6 @@
|
|
|
42
41
|
},
|
|
43
42
|
en: {
|
|
44
43
|
fileDeleteLabel: 'Delete file',
|
|
45
|
-
fileTagLabel: 'Tag',
|
|
46
44
|
fileNameLabel: 'Name',
|
|
47
45
|
fileUrlLabel: 'URL',
|
|
48
46
|
fileAltLabel: 'Alt text',
|
|
@@ -76,7 +74,7 @@
|
|
|
76
74
|
})
|
|
77
75
|
);
|
|
78
76
|
|
|
79
|
-
let
|
|
77
|
+
let foldersQuery = $derived(remotes.getMediaFolders());
|
|
80
78
|
|
|
81
79
|
async function deleteFileCommand() {
|
|
82
80
|
if (currentFile) {
|
|
@@ -87,6 +85,15 @@
|
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
|
|
88
|
+
async function onTagUpdate(folderId: string) {
|
|
89
|
+
if (currentFile) {
|
|
90
|
+
await remotes.setMediaFileFolder({ folderId, fileId: currentFile.id });
|
|
91
|
+
currentFile = { ...currentFile, folderId };
|
|
92
|
+
await foldersQuery.refresh();
|
|
93
|
+
await filesQuery.refresh();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
onMount(() => {
|
|
91
98
|
if (Array.isArray(selected)) {
|
|
92
99
|
selected = selected.filter((id) => !id.startsWith('/uploads'));
|
|
@@ -96,29 +103,45 @@
|
|
|
96
103
|
});
|
|
97
104
|
</script>
|
|
98
105
|
|
|
106
|
+
{#snippet filePreview(file: MediaFile)}
|
|
107
|
+
{#if file.type === 'image'}
|
|
108
|
+
<img class="pointer-events-none h-full w-full object-contain" src={file.url} alt={file.name} />
|
|
109
|
+
{:else if file.type === 'video'}
|
|
110
|
+
<img
|
|
111
|
+
class="pointer-events-none h-full w-full object-contain"
|
|
112
|
+
src={file.thumbnailUrl}
|
|
113
|
+
alt={file.name}
|
|
114
|
+
/>
|
|
115
|
+
{:else}
|
|
116
|
+
<Pdf class="pointer-events-none h-full w-full object-contain" />
|
|
117
|
+
{/if}
|
|
118
|
+
{/snippet}
|
|
119
|
+
|
|
99
120
|
<div class="flex gap-6 p-6">
|
|
100
121
|
<div class="grow">
|
|
101
122
|
<div class="mb-6 flex items-center justify-between">
|
|
102
123
|
<div>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<Tabs.
|
|
115
|
-
|
|
116
|
-
{#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
{#await foldersQuery then folders}
|
|
125
|
+
<Tabs.Root
|
|
126
|
+
value="all"
|
|
127
|
+
onValueChange={(val) => {
|
|
128
|
+
if (val === 'all') {
|
|
129
|
+
selectedFolder = null;
|
|
130
|
+
} else {
|
|
131
|
+
selectedFolder = val;
|
|
132
|
+
}
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<Tabs.List>
|
|
136
|
+
<Tabs.Trigger value="all">{lang[interfaceLanguage.current].allTabs}</Tabs.Trigger>
|
|
137
|
+
{#each folders as folder}
|
|
138
|
+
{#if !folder.parentId}
|
|
139
|
+
<Tabs.Trigger value={folder.id}>{folder.name}</Tabs.Trigger>
|
|
140
|
+
{/if}
|
|
141
|
+
{/each}
|
|
142
|
+
</Tabs.List>
|
|
143
|
+
</Tabs.Root>
|
|
144
|
+
{/await}
|
|
122
145
|
</div>
|
|
123
146
|
<div class="flex items-center gap-2">
|
|
124
147
|
<FileUpload
|
|
@@ -157,21 +180,7 @@
|
|
|
157
180
|
}
|
|
158
181
|
}}
|
|
159
182
|
>
|
|
160
|
-
{
|
|
161
|
-
<img
|
|
162
|
-
class="pointer-events-none h-full w-full object-contain"
|
|
163
|
-
src={file.url}
|
|
164
|
-
alt={file.name}
|
|
165
|
-
/>
|
|
166
|
-
{:else if file.type === 'video'}
|
|
167
|
-
<img
|
|
168
|
-
class="pointer-events-none h-full w-full object-contain"
|
|
169
|
-
src={file.thumbnailUrl}
|
|
170
|
-
alt={file.name}
|
|
171
|
-
/>
|
|
172
|
-
{:else}
|
|
173
|
-
<Pdf class="pointer-events-none h-full w-full object-contain" />
|
|
174
|
-
{/if}
|
|
183
|
+
{@render filePreview(file)}
|
|
175
184
|
</button>
|
|
176
185
|
{/each}
|
|
177
186
|
{/await}
|
|
@@ -179,7 +188,7 @@
|
|
|
179
188
|
</div>
|
|
180
189
|
</div>
|
|
181
190
|
|
|
182
|
-
<div class="w-
|
|
191
|
+
<div class="w-101.5 shrink-0">
|
|
183
192
|
<Item.Root class="sticky top-6 p-0" variant="outline">
|
|
184
193
|
<Item.Content class="h-full">
|
|
185
194
|
{#key currentFile}
|
|
@@ -199,20 +208,14 @@
|
|
|
199
208
|
<div
|
|
200
209
|
class="aspect-square w-[40.2%] shrink-0 overflow-hidden rounded-2xl border p-1"
|
|
201
210
|
>
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
class="size-full rounded-lg object-contain"
|
|
206
|
-
/>
|
|
211
|
+
<div class="size-full overflow-hidden rounded-lg">
|
|
212
|
+
{@render filePreview(currentFile)}
|
|
213
|
+
</div>
|
|
207
214
|
</div>
|
|
208
215
|
<div class="mr-2.5 w-[50.2%] shrink-0 space-y-4">
|
|
209
|
-
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
disabled
|
|
213
|
-
value={folders.find((folder) => folder.id === currentFile?.folderId)?.name}
|
|
214
|
-
/>
|
|
215
|
-
</div>
|
|
216
|
+
{#await foldersQuery then folders}
|
|
217
|
+
<TagCombobox {folders} onSelect={onTagUpdate} value={currentFile.folderId} />
|
|
218
|
+
{/await}
|
|
216
219
|
<div class="grid grid-cols-1 gap-2">
|
|
217
220
|
<Label>{lang[interfaceLanguage.current].fileNameLabel}</Label>
|
|
218
221
|
<Input disabled value={currentFile.name} />
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Popover from '../../../components/ui/popover/index.js';
|
|
3
|
+
import * as Command from '../../../components/ui/command/index.js';
|
|
4
|
+
import Label from '../../../components/ui/label/label.svelte';
|
|
5
|
+
import type { MediaFolder } from '../../../types/media.js';
|
|
6
|
+
import { tick } from 'svelte';
|
|
7
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
8
|
+
import SelectorIcon from '@tabler/icons-svelte/icons/selector';
|
|
9
|
+
import { cn } from '../../../utils.js';
|
|
10
|
+
import CheckIcon from '@tabler/icons-svelte/icons/check';
|
|
11
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
12
|
+
import { getInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
13
|
+
import { getRemotes } from '../../../sveltekit/index.js';
|
|
14
|
+
|
|
15
|
+
const lang: Record<
|
|
16
|
+
InterfaceLanguage,
|
|
17
|
+
{
|
|
18
|
+
label: string;
|
|
19
|
+
placeholder: string;
|
|
20
|
+
empty: string;
|
|
21
|
+
}
|
|
22
|
+
> = {
|
|
23
|
+
pl: {
|
|
24
|
+
label: 'Ustaw tag',
|
|
25
|
+
placeholder: 'Wybierz tag...',
|
|
26
|
+
empty: 'Brak dostępnych tagów o takiej nazwie. Moesz go dodać klikając Enter.'
|
|
27
|
+
},
|
|
28
|
+
en: {
|
|
29
|
+
label: 'Set tag',
|
|
30
|
+
placeholder: 'Select tag...',
|
|
31
|
+
empty: 'No tags found with that name. You can add it by pressing Enter.'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const interfaceLanguage = getInterfaceLanguage();
|
|
36
|
+
const remotes = getRemotes();
|
|
37
|
+
|
|
38
|
+
type Props = {
|
|
39
|
+
folders: MediaFolder[];
|
|
40
|
+
onSelect?: (folderId: string) => void;
|
|
41
|
+
value: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let { onSelect, folders, value }: Props = $props();
|
|
45
|
+
|
|
46
|
+
let open = $state(false);
|
|
47
|
+
let triggerRef = $state<HTMLButtonElement>(null!);
|
|
48
|
+
|
|
49
|
+
const selectedValue = $derived(folders.find((f) => f.id === value)?.name);
|
|
50
|
+
|
|
51
|
+
// We want to refocus the trigger button when the user selects
|
|
52
|
+
// an item from the list so users can continue navigating the
|
|
53
|
+
// rest of the form with the keyboard.
|
|
54
|
+
function closeAndFocusTrigger() {
|
|
55
|
+
open = false;
|
|
56
|
+
tick().then(() => {
|
|
57
|
+
triggerRef.focus();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function handleKeyDown(event: KeyboardEvent) {
|
|
62
|
+
const target = event.target as HTMLInputElement;
|
|
63
|
+
|
|
64
|
+
if (event.key === 'Enter') {
|
|
65
|
+
const existingFolder = folders.find(
|
|
66
|
+
(folder) => folder.name.toLowerCase() === target.value.toLowerCase()
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (existingFolder) {
|
|
70
|
+
value = existingFolder.id;
|
|
71
|
+
} else {
|
|
72
|
+
const newFolder = await remotes.createFolder({
|
|
73
|
+
name: target.value
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
folders = [...folders, newFolder];
|
|
77
|
+
value = newFolder.id;
|
|
78
|
+
|
|
79
|
+
closeAndFocusTrigger();
|
|
80
|
+
onSelect?.(newFolder.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<div class="grid grid-cols-1 gap-2">
|
|
87
|
+
<Label>Tag</Label>
|
|
88
|
+
<Popover.Root bind:open>
|
|
89
|
+
<Popover.Trigger bind:ref={triggerRef}>
|
|
90
|
+
{#snippet child({ props })}
|
|
91
|
+
<Button
|
|
92
|
+
{...props}
|
|
93
|
+
variant="outline"
|
|
94
|
+
class="w-full justify-between"
|
|
95
|
+
role="combobox"
|
|
96
|
+
aria-expanded={open}
|
|
97
|
+
>
|
|
98
|
+
{selectedValue || lang[interfaceLanguage.current].label}
|
|
99
|
+
<SelectorIcon class="opacity-50" />
|
|
100
|
+
</Button>
|
|
101
|
+
{/snippet}
|
|
102
|
+
</Popover.Trigger>
|
|
103
|
+
<Popover.Content class="w-[200px] p-0">
|
|
104
|
+
<Command.Root>
|
|
105
|
+
<Command.Input
|
|
106
|
+
onkeydown={handleKeyDown}
|
|
107
|
+
placeholder={lang[interfaceLanguage.current].placeholder}
|
|
108
|
+
/>
|
|
109
|
+
<Command.List>
|
|
110
|
+
<Command.Empty>{lang[interfaceLanguage.current].empty}</Command.Empty>
|
|
111
|
+
<Command.Group value="folders">
|
|
112
|
+
{#each folders as folder (folder.id)}
|
|
113
|
+
<Command.Item
|
|
114
|
+
value={folder.id}
|
|
115
|
+
onSelect={() => {
|
|
116
|
+
value = folder.id;
|
|
117
|
+
closeAndFocusTrigger();
|
|
118
|
+
onSelect?.(folder.id);
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<CheckIcon class={cn(value !== folder.id && 'text-transparent')} />
|
|
122
|
+
{folder.name}
|
|
123
|
+
</Command.Item>
|
|
124
|
+
{/each}
|
|
125
|
+
</Command.Group>
|
|
126
|
+
</Command.List>
|
|
127
|
+
</Command.Root>
|
|
128
|
+
</Popover.Content>
|
|
129
|
+
</Popover.Root>
|
|
130
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MediaFolder } from '../../../types/media.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
folders: MediaFolder[];
|
|
4
|
+
onSelect?: (folderId: string) => void;
|
|
5
|
+
value: string | null;
|
|
6
|
+
};
|
|
7
|
+
declare const TagCombobox: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type TagCombobox = ReturnType<typeof TagCombobox>;
|
|
9
|
+
export default TagCombobox;
|
|
@@ -7,10 +7,14 @@ export declare const setMediaFileAlt: import("@sveltejs/kit").RemoteCommand<{
|
|
|
7
7
|
alt: string | null;
|
|
8
8
|
fileId: string;
|
|
9
9
|
}, Promise<void>>;
|
|
10
|
+
export declare const setMediaFileFolder: import("@sveltejs/kit").RemoteCommand<{
|
|
11
|
+
folderId: string | null;
|
|
12
|
+
fileId: string;
|
|
13
|
+
}, Promise<import("../../types/media.js").MediaFile>>;
|
|
10
14
|
export declare const createFolder: import("@sveltejs/kit").RemoteCommand<{
|
|
11
15
|
name: string;
|
|
12
16
|
parentId?: string | null | undefined;
|
|
13
|
-
}, Promise<
|
|
17
|
+
}, Promise<import("../../types/media.js").MediaFolder>>;
|
|
14
18
|
export declare const getMedia: import("@sveltejs/kit").RemoteQueryFunction<void, import("../../types/media.js").MediaFile[]>;
|
|
15
19
|
export declare const getMediaFiles: import("@sveltejs/kit").RemoteQueryFunction<{
|
|
16
20
|
data: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { command, query } from '$app/server';
|
|
2
|
-
import { setAlt } from '../../core/server/media/operations/updateFile.js';
|
|
2
|
+
import { setAlt, updateMediaFileFolder } from '../../core/server/media/operations/updateFile.js';
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
import { createFolderSchema } from '../components/media/schema.js';
|
|
5
5
|
import { createMediaFolder } from '../../core/server/media/operations/createMediaFolder.js';
|
|
@@ -26,9 +26,17 @@ export const setMediaFileAlt = command(setMediaFileAltSchema, async ({ fileId, a
|
|
|
26
26
|
requireAuth();
|
|
27
27
|
await setAlt(fileId, alt);
|
|
28
28
|
});
|
|
29
|
+
const setMediaFolderSchema = z.object({
|
|
30
|
+
fileId: z.string().uuid(),
|
|
31
|
+
folderId: z.string().uuid().nullable()
|
|
32
|
+
});
|
|
33
|
+
export const setMediaFileFolder = command(setMediaFolderSchema, async ({ fileId, folderId }) => {
|
|
34
|
+
requireAuth();
|
|
35
|
+
return updateMediaFileFolder(fileId, folderId);
|
|
36
|
+
});
|
|
29
37
|
export const createFolder = command(createFolderSchema, async (data) => {
|
|
30
38
|
requireAuth();
|
|
31
|
-
|
|
39
|
+
return createMediaFolder(data.name, data.parentId);
|
|
32
40
|
});
|
|
33
41
|
export const getMedia = query(async () => {
|
|
34
42
|
return getFiles({
|
|
@@ -240,7 +240,7 @@ export function pg(config) {
|
|
|
240
240
|
? inArray(schema.mediaFilesTable.mimeType, options.data.mimeTypes)
|
|
241
241
|
: undefined));
|
|
242
242
|
}
|
|
243
|
-
return await query;
|
|
243
|
+
return await query.orderBy(desc(schema.mediaFilesTable.createdAt));
|
|
244
244
|
},
|
|
245
245
|
getMediaFile: async (options) => {
|
|
246
246
|
const query = db.select().from(schema.mediaFilesTable);
|
|
@@ -4,6 +4,11 @@ import { readFile, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import ffmpeg from 'fluent-ffmpeg';
|
|
6
6
|
const fullDir = process.env.NODE_ENV === 'production' ? `/data/uploads` : `./static/uploads`;
|
|
7
|
+
// Ustawienie ścieżek do ffmpeg
|
|
8
|
+
const ffmpegPath = process.env.FFMPEG_PATH || '/usr/bin/ffmpeg';
|
|
9
|
+
const ffprobePath = process.env.FFPROBE_PATH || '/usr/bin/ffprobe';
|
|
10
|
+
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
11
|
+
ffmpeg.setFfprobePath(ffprobePath);
|
|
7
12
|
// Funkcja pomocnicza do przetwarzania wideo
|
|
8
13
|
async function processVideo(filepath, filename) {
|
|
9
14
|
return new Promise((resolve) => {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { isProperImageObject } from '../../admin/components/media/utils.js';
|
|
2
3
|
import type { ImageFieldData } from '../../types/fields.js';
|
|
4
|
+
import type { MediaFile } from '../../types/media.js';
|
|
3
5
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
4
6
|
|
|
5
7
|
type Props = HTMLImgAttributes & {
|
|
6
|
-
data: ImageFieldData;
|
|
8
|
+
data: ImageFieldData | MediaFile;
|
|
7
9
|
pictureClass?: string;
|
|
8
10
|
};
|
|
9
11
|
|
|
@@ -17,17 +19,29 @@
|
|
|
17
19
|
</script>
|
|
18
20
|
|
|
19
21
|
<picture class={pictureClass}>
|
|
20
|
-
{#
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
{#if isProperImageObject(data)}
|
|
23
|
+
{#each Object.values(data.styles) as style}
|
|
24
|
+
<source srcset={style.url} type={style.mimeType} media={style.media} />
|
|
25
|
+
{/each}
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
<img
|
|
28
|
+
src={data.data.url}
|
|
29
|
+
alt={data.data.alt}
|
|
30
|
+
width={data.data.width}
|
|
31
|
+
height={data.data.height}
|
|
32
|
+
class={className}
|
|
33
|
+
{loading}
|
|
34
|
+
{...restProps}
|
|
35
|
+
/>
|
|
36
|
+
{:else}
|
|
37
|
+
<img
|
|
38
|
+
src={data.url}
|
|
39
|
+
alt={data.alt}
|
|
40
|
+
width={data.width}
|
|
41
|
+
height={data.height}
|
|
42
|
+
class={className}
|
|
43
|
+
{loading}
|
|
44
|
+
{...restProps}
|
|
45
|
+
/>
|
|
46
|
+
{/if}
|
|
33
47
|
</picture>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ImageFieldData } from '../../types/fields.js';
|
|
2
|
+
import type { MediaFile } from '../../types/media.js';
|
|
2
3
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
3
4
|
type Props = HTMLImgAttributes & {
|
|
4
|
-
data: ImageFieldData;
|
|
5
|
+
data: ImageFieldData | MediaFile;
|
|
5
6
|
pictureClass?: string;
|
|
6
7
|
};
|
|
7
8
|
declare const Image: import("svelte").Component<Props, {}, "">;
|
package/package.json
CHANGED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { defaults, superForm } from 'sveltekit-superforms';
|
|
3
|
-
import { zod, zodClient } from 'sveltekit-superforms/adapters';
|
|
4
|
-
import { createFolderSchema } from './schema.js';
|
|
5
|
-
import { getRemotes } from '../../context/remotes.js';
|
|
6
|
-
|
|
7
|
-
const remotes = getRemotes();
|
|
8
|
-
|
|
9
|
-
type Props = {
|
|
10
|
-
parentId?: string | null;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
let { parentId = $bindable(null) }: Props = $props();
|
|
14
|
-
|
|
15
|
-
const form = superForm(defaults(zod(createFolderSchema)), {
|
|
16
|
-
validators: zodClient(createFolderSchema),
|
|
17
|
-
SPA: true,
|
|
18
|
-
onUpdate: async ({ form }) => {
|
|
19
|
-
if (form.valid) {
|
|
20
|
-
await remotes.createFolder(form.data);
|
|
21
|
-
remotes.getMedia().refresh();
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const { form: formData, enhance } = form;
|
|
27
|
-
</script>
|
|
28
|
-
|
|
29
|
-
<form use:enhance method="POST">
|
|
30
|
-
<input bind:value={$formData.name} placeholder="Nazwa folderu" required />
|
|
31
|
-
<button type="submit">Utwórz folder</button>
|
|
32
|
-
</form>
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getRemotes } from '../../context/remotes.js';
|
|
3
|
-
import TreeNode from './tree-node.svelte';
|
|
4
|
-
|
|
5
|
-
const remotes = getRemotes();
|
|
6
|
-
|
|
7
|
-
type Props = {
|
|
8
|
-
selected: string | null;
|
|
9
|
-
onSelect: (id: string | null) => void;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
let { selected = $bindable(), onSelect }: Props = $props();
|
|
13
|
-
</script>
|
|
14
|
-
|
|
15
|
-
{#await remotes.getMediaFolders() then folders}
|
|
16
|
-
<ul>
|
|
17
|
-
<li on:click={() => onSelect(null)} class:selected={selected === null}>📁 Wszystkie</li>
|
|
18
|
-
{#each folders as folder}
|
|
19
|
-
{#if !folder.parentId}
|
|
20
|
-
<TreeNode {folder} bind:selected {onSelect} {folders} />
|
|
21
|
-
{/if}
|
|
22
|
-
{/each}
|
|
23
|
-
</ul>
|
|
24
|
-
{/await}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { MediaFolder } from '../../../types/media.js';
|
|
3
|
-
import TreeNode from './tree-node.svelte';
|
|
4
|
-
|
|
5
|
-
type Props = {
|
|
6
|
-
folder: MediaFolder;
|
|
7
|
-
selected: string | null;
|
|
8
|
-
onSelect: (id: string | null) => void;
|
|
9
|
-
folders: MediaFolder[];
|
|
10
|
-
level?: number;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
let { folder, selected = $bindable(), onSelect, folders, level = 0 }: Props = $props();
|
|
14
|
-
|
|
15
|
-
let children = getChildren(folder.id, folders);
|
|
16
|
-
|
|
17
|
-
function getChildren(parentId: string | null, all: MediaFolder[]) {
|
|
18
|
-
return all.filter((f) => f.parentId === parentId);
|
|
19
|
-
}
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<li>
|
|
23
|
-
<div onclick={() => onSelect(folder.id)} class:selected={selected === folder.id}>
|
|
24
|
-
📁 {folder.name}
|
|
25
|
-
</div>
|
|
26
|
-
{#if children.length}
|
|
27
|
-
<ul style:margin-left={`${level * 12}px`}>
|
|
28
|
-
{#each children as child}
|
|
29
|
-
<TreeNode folder={child} {selected} {onSelect} {folders} level={level + 1} />
|
|
30
|
-
{/each}
|
|
31
|
-
</ul>
|
|
32
|
-
{/if}
|
|
33
|
-
</li>
|
|
34
|
-
|
|
35
|
-
<style>
|
|
36
|
-
.selected {
|
|
37
|
-
font-weight: bold;
|
|
38
|
-
color: blue;
|
|
39
|
-
}
|
|
40
|
-
</style>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { MediaFolder } from '../../../types/media.js';
|
|
2
|
-
import TreeNode from './tree-node.svelte';
|
|
3
|
-
type Props = {
|
|
4
|
-
folder: MediaFolder;
|
|
5
|
-
selected: string | null;
|
|
6
|
-
onSelect: (id: string | null) => void;
|
|
7
|
-
folders: MediaFolder[];
|
|
8
|
-
level?: number;
|
|
9
|
-
};
|
|
10
|
-
declare const TreeNode: import("svelte").Component<Props, {}, "selected">;
|
|
11
|
-
type TreeNode = ReturnType<typeof TreeNode>;
|
|
12
|
-
export default TreeNode;
|