@btst/stack 2.8.1 → 2.9.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/README.md +3 -2
- package/dist/components/markdown/index.d.cts +15 -2
- package/dist/components/markdown/index.d.mts +15 -2
- package/dist/components/markdown/index.d.ts +15 -2
- package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.cjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.mjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.cjs +49 -9
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.mjs +50 -10
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.cjs +77 -9
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.mjs +77 -9
- package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -5
- package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.mjs +24 -5
- package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.cjs +47 -13
- package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.mjs +47 -13
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.cjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.mjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +6 -2
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +6 -2
- package/dist/packages/stack/src/plugins/media/api/adapters/local.cjs +55 -0
- package/dist/packages/stack/src/plugins/media/api/adapters/local.mjs +37 -0
- package/dist/packages/stack/src/plugins/media/api/getters.cjs +83 -0
- package/dist/packages/stack/src/plugins/media/api/getters.mjs +78 -0
- package/dist/packages/stack/src/plugins/media/api/mutations.cjs +88 -0
- package/dist/packages/stack/src/plugins/media/api/mutations.mjs +82 -0
- package/dist/packages/stack/src/plugins/media/api/plugin.cjs +525 -0
- package/dist/packages/stack/src/plugins/media/api/plugin.mjs +523 -0
- package/dist/packages/stack/src/plugins/media/api/query-key-defs.cjs +19 -0
- package/dist/packages/stack/src/plugins/media/api/query-key-defs.mjs +16 -0
- package/dist/packages/stack/src/plugins/media/api/serializers.cjs +17 -0
- package/dist/packages/stack/src/plugins/media/api/serializers.mjs +14 -0
- package/dist/packages/stack/src/plugins/media/api/storage-adapter.cjs +15 -0
- package/dist/packages/stack/src/plugins/media/api/storage-adapter.mjs +11 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.cjs +129 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.mjs +127 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.cjs +58 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.mjs +56 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.cjs +94 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.mjs +92 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.cjs +171 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.mjs +168 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.cjs +308 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.mjs +305 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.cjs +104 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.mjs +102 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.cjs +70 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.mjs +68 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.cjs +21 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.mjs +17 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.cjs +35 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.cjs +125 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.mjs +123 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.mjs +33 -0
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +222 -0
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +214 -0
- package/dist/packages/stack/src/plugins/media/client/plugin.cjs +94 -0
- package/dist/packages/stack/src/plugins/media/client/plugin.mjs +92 -0
- package/dist/packages/stack/src/plugins/media/client/upload.cjs +121 -0
- package/dist/packages/stack/src/plugins/media/client/upload.mjs +119 -0
- package/dist/packages/stack/src/plugins/media/client/utils/image-compression.cjs +67 -0
- package/dist/packages/stack/src/plugins/media/client/utils/image-compression.mjs +65 -0
- package/dist/packages/stack/src/plugins/media/db.cjs +62 -0
- package/dist/packages/stack/src/plugins/media/db.mjs +60 -0
- package/dist/packages/stack/src/plugins/media/schemas.cjs +41 -0
- package/dist/packages/stack/src/plugins/media/schemas.mjs +35 -0
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.cjs +18 -1
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.mjs +19 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.cjs +2 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.mjs +2 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.cjs +3 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.mjs +3 -2
- package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.cjs +12 -5
- package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.mjs +12 -5
- package/dist/plugins/blog/client/index.d.cts +58 -1
- package/dist/plugins/blog/client/index.d.mts +58 -1
- package/dist/plugins/blog/client/index.d.ts +58 -1
- package/dist/plugins/cms/client/index.d.cts +73 -3
- package/dist/plugins/cms/client/index.d.mts +73 -3
- package/dist/plugins/cms/client/index.d.ts +73 -3
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/plugins/media/api/adapters/s3.cjs +106 -0
- package/dist/plugins/media/api/adapters/s3.d.cts +60 -0
- package/dist/plugins/media/api/adapters/s3.d.mts +60 -0
- package/dist/plugins/media/api/adapters/s3.d.ts +60 -0
- package/dist/plugins/media/api/adapters/s3.mjs +104 -0
- package/dist/plugins/media/api/adapters/vercel-blob.cjs +53 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.cts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.mts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.ts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.mjs +51 -0
- package/dist/plugins/media/api/index.cjs +26 -0
- package/dist/plugins/media/api/index.d.cts +116 -0
- package/dist/plugins/media/api/index.d.mts +116 -0
- package/dist/plugins/media/api/index.d.ts +116 -0
- package/dist/plugins/media/api/index.mjs +6 -0
- package/dist/plugins/media/client/components/index.cjs +10 -0
- package/dist/plugins/media/client/components/index.d.cts +55 -0
- package/dist/plugins/media/client/components/index.d.mts +55 -0
- package/dist/plugins/media/client/components/index.d.ts +55 -0
- package/dist/plugins/media/client/components/index.mjs +2 -0
- package/dist/plugins/media/client/hooks/index.cjs +13 -0
- package/dist/plugins/media/client/hooks/index.d.cts +53 -0
- package/dist/plugins/media/client/hooks/index.d.mts +53 -0
- package/dist/plugins/media/client/hooks/index.d.ts +53 -0
- package/dist/plugins/media/client/hooks/index.mjs +1 -0
- package/dist/plugins/media/client/index.cjs +9 -0
- package/dist/plugins/media/client/index.d.cts +242 -0
- package/dist/plugins/media/client/index.d.mts +242 -0
- package/dist/plugins/media/client/index.d.ts +242 -0
- package/dist/plugins/media/client/index.mjs +2 -0
- package/dist/plugins/media/client.css +1 -0
- package/dist/plugins/media/query-keys.cjs +72 -0
- package/dist/plugins/media/query-keys.d.cts +49 -0
- package/dist/plugins/media/query-keys.d.mts +49 -0
- package/dist/plugins/media/query-keys.d.ts +49 -0
- package/dist/plugins/media/query-keys.mjs +70 -0
- package/dist/plugins/media/style.css +1 -0
- package/dist/shared/{stack.DRpeDS6X.d.ts → stack.BMx2QYOK.d.ts} +25 -0
- package/dist/shared/stack.BttDsJJn.d.cts +109 -0
- package/dist/shared/stack.BttDsJJn.d.mts +109 -0
- package/dist/shared/stack.BttDsJJn.d.ts +109 -0
- package/dist/shared/stack.C7vfOBmO.d.mts +63 -0
- package/dist/shared/stack.CAni8dnD.d.cts +63 -0
- package/dist/shared/stack.CI8iRKKi.d.cts +286 -0
- package/dist/shared/stack.CLcnSF_b.d.cts +25 -0
- package/dist/shared/stack.CLcnSF_b.d.mts +25 -0
- package/dist/shared/stack.CLcnSF_b.d.ts +25 -0
- package/dist/shared/stack.CYSwntXC.d.ts +63 -0
- package/dist/shared/{stack.Jb0kQDJC.d.mts → stack.Cd6McBu1.d.mts} +25 -0
- package/dist/shared/stack.DJDjdG64.d.ts +286 -0
- package/dist/shared/{stack.BxFl46lB.d.cts → stack.DxQl8Wa1.d.cts} +25 -0
- package/dist/shared/stack.FgBVDSPi.d.mts +286 -0
- package/package.json +113 -4
- package/src/plugins/blog/client/components/forms/image-field.tsx +35 -4
- package/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +67 -12
- package/src/plugins/blog/client/components/forms/markdown-editor.tsx +106 -10
- package/src/plugins/blog/client/overrides.ts +58 -1
- package/src/plugins/cms/client/components/forms/content-form.tsx +26 -7
- package/src/plugins/cms/client/components/forms/file-upload.tsx +73 -15
- package/src/plugins/cms/client/overrides.ts +57 -2
- package/src/plugins/kanban/client/components/forms/board-form.tsx +1 -1
- package/src/plugins/kanban/client/components/forms/task-form.tsx +7 -1
- package/src/plugins/kanban/client/overrides.ts +25 -0
- package/src/plugins/media/__tests__/__stubs__/vercel-blob-server.ts +9 -0
- package/src/plugins/media/__tests__/getters.test.ts +274 -0
- package/src/plugins/media/__tests__/mutations.test.ts +299 -0
- package/src/plugins/media/__tests__/plugin.test.ts +752 -0
- package/src/plugins/media/__tests__/query-key-defs.test.ts +54 -0
- package/src/plugins/media/__tests__/storage-adapters.test.ts +351 -0
- package/src/plugins/media/api/adapters/local.ts +79 -0
- package/src/plugins/media/api/adapters/s3.ts +198 -0
- package/src/plugins/media/api/adapters/vercel-blob.ts +131 -0
- package/src/plugins/media/api/getters.ts +174 -0
- package/src/plugins/media/api/index.ts +41 -0
- package/src/plugins/media/api/mutations.ts +179 -0
- package/src/plugins/media/api/plugin.ts +855 -0
- package/src/plugins/media/api/query-key-defs.ts +41 -0
- package/src/plugins/media/api/serializers.ts +28 -0
- package/src/plugins/media/api/storage-adapter.ts +139 -0
- package/src/plugins/media/client/components/index.tsx +6 -0
- package/src/plugins/media/client/components/media-picker/asset-card.tsx +150 -0
- package/src/plugins/media/client/components/media-picker/asset-preview-button.tsx +67 -0
- package/src/plugins/media/client/components/media-picker/browse-tab.tsx +116 -0
- package/src/plugins/media/client/components/media-picker/folder-tree.tsx +188 -0
- package/src/plugins/media/client/components/media-picker/index.tsx +347 -0
- package/src/plugins/media/client/components/media-picker/upload-tab.tsx +108 -0
- package/src/plugins/media/client/components/media-picker/url-tab.tsx +72 -0
- package/src/plugins/media/client/components/media-picker/utils.ts +17 -0
- package/src/plugins/media/client/components/pages/library-page.internal.tsx +134 -0
- package/src/plugins/media/client/components/pages/library-page.tsx +42 -0
- package/src/plugins/media/client/hooks/index.tsx +9 -0
- package/src/plugins/media/client/hooks/use-media.tsx +289 -0
- package/src/plugins/media/client/index.ts +4 -0
- package/src/plugins/media/client/overrides.ts +127 -0
- package/src/plugins/media/client/plugin.tsx +184 -0
- package/src/plugins/media/client/upload.ts +171 -0
- package/src/plugins/media/client/utils/image-compression.ts +131 -0
- package/src/plugins/media/client.css +1 -0
- package/src/plugins/media/db.ts +62 -0
- package/src/plugins/media/query-keys.ts +96 -0
- package/src/plugins/media/schemas.ts +37 -0
- package/src/plugins/media/style.css +1 -0
- package/src/plugins/media/types.ts +26 -0
- package/dist/shared/{stack.BOokfhZD.d.cts → stack.B6S3cgwN.d.cts} +16 -16
- package/dist/shared/{stack.CWxAl9K3.d.mts → stack.Bzfx-_lq.d.mts} +16 -16
- package/dist/shared/{stack.BvCR4-9H.d.ts → stack.j5SFLC1d.d.ts} +16 -16
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useInfiniteQuery, useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
|
|
3
|
+
import { usePluginOverrides } from '@btst/stack/context';
|
|
4
|
+
import { createApiClient } from '@btst/stack/plugins/client';
|
|
5
|
+
import { createMediaQueryKeys } from '../../../../../../../plugins/media/query-keys.mjs';
|
|
6
|
+
import { uploadAsset } from '../upload.mjs';
|
|
7
|
+
|
|
8
|
+
function useMediaConfig() {
|
|
9
|
+
return usePluginOverrides("media");
|
|
10
|
+
}
|
|
11
|
+
function useMediaApiClient() {
|
|
12
|
+
const { apiBaseURL, apiBasePath, headers } = useMediaConfig();
|
|
13
|
+
const client = createApiClient({
|
|
14
|
+
baseURL: apiBaseURL,
|
|
15
|
+
basePath: apiBasePath
|
|
16
|
+
});
|
|
17
|
+
return { client, headers };
|
|
18
|
+
}
|
|
19
|
+
function useAssets(params) {
|
|
20
|
+
const { client, headers } = useMediaApiClient();
|
|
21
|
+
const queries = createMediaQueryKeys(client, headers);
|
|
22
|
+
const { queryClient } = useMediaConfig();
|
|
23
|
+
params?.limit ?? 20;
|
|
24
|
+
return useInfiniteQuery(
|
|
25
|
+
{
|
|
26
|
+
...queries.mediaAssets.list(params),
|
|
27
|
+
initialPageParam: 0,
|
|
28
|
+
refetchOnMount: "always",
|
|
29
|
+
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
|
|
30
|
+
const offset = (lastPage.offset ?? 0) + lastPage.items.length;
|
|
31
|
+
return offset < lastPage.total ? offset : void 0;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
queryClient
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function useFolders(parentId) {
|
|
38
|
+
const { client, headers } = useMediaApiClient();
|
|
39
|
+
const queries = createMediaQueryKeys(client, headers);
|
|
40
|
+
const { queryClient } = useMediaConfig();
|
|
41
|
+
return useQuery(
|
|
42
|
+
{
|
|
43
|
+
...queries.mediaFolders.list(parentId)
|
|
44
|
+
},
|
|
45
|
+
queryClient
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function useUploadAsset() {
|
|
49
|
+
const {
|
|
50
|
+
apiBaseURL,
|
|
51
|
+
apiBasePath,
|
|
52
|
+
headers,
|
|
53
|
+
uploadMode = "direct",
|
|
54
|
+
imageCompression,
|
|
55
|
+
queryClient: qc
|
|
56
|
+
} = useMediaConfig();
|
|
57
|
+
const reactQueryClient = useQueryClient(qc);
|
|
58
|
+
return useMutation(
|
|
59
|
+
{
|
|
60
|
+
mutationFn: async ({
|
|
61
|
+
file,
|
|
62
|
+
folderId
|
|
63
|
+
}) => uploadAsset(
|
|
64
|
+
{
|
|
65
|
+
apiBaseURL,
|
|
66
|
+
apiBasePath,
|
|
67
|
+
headers,
|
|
68
|
+
uploadMode,
|
|
69
|
+
imageCompression
|
|
70
|
+
},
|
|
71
|
+
{ file, folderId }
|
|
72
|
+
),
|
|
73
|
+
onSuccess: () => {
|
|
74
|
+
reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] });
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
qc
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
function useRegisterAsset() {
|
|
81
|
+
const {
|
|
82
|
+
apiBaseURL,
|
|
83
|
+
apiBasePath,
|
|
84
|
+
headers,
|
|
85
|
+
queryClient: qc
|
|
86
|
+
} = useMediaConfig();
|
|
87
|
+
const reactQueryClient = useQueryClient(qc);
|
|
88
|
+
return useMutation(
|
|
89
|
+
{
|
|
90
|
+
mutationFn: async (input) => {
|
|
91
|
+
const base = `${apiBaseURL}${apiBasePath}`;
|
|
92
|
+
const headersObj = new Headers(headers);
|
|
93
|
+
const res = await fetch(`${base}/media/assets`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
...Object.fromEntries(headersObj.entries()),
|
|
97
|
+
"Content-Type": "application/json"
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
filename: input.filename,
|
|
101
|
+
originalName: input.filename,
|
|
102
|
+
mimeType: input.mimeType ?? "application/octet-stream",
|
|
103
|
+
size: input.size ?? 0,
|
|
104
|
+
url: input.url,
|
|
105
|
+
folderId: input.folderId
|
|
106
|
+
})
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
110
|
+
throw new Error(err.message ?? "Failed to register asset");
|
|
111
|
+
}
|
|
112
|
+
return res.json();
|
|
113
|
+
},
|
|
114
|
+
onSuccess: () => {
|
|
115
|
+
reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
qc
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
function useDeleteAsset() {
|
|
122
|
+
const {
|
|
123
|
+
apiBaseURL,
|
|
124
|
+
apiBasePath,
|
|
125
|
+
headers,
|
|
126
|
+
queryClient: qc
|
|
127
|
+
} = useMediaConfig();
|
|
128
|
+
const reactQueryClient = useQueryClient(qc);
|
|
129
|
+
return useMutation(
|
|
130
|
+
{
|
|
131
|
+
mutationFn: async (id) => {
|
|
132
|
+
const base = `${apiBaseURL}${apiBasePath}`;
|
|
133
|
+
const headersObj = new Headers(headers);
|
|
134
|
+
const res = await fetch(`${base}/media/assets/${id}`, {
|
|
135
|
+
method: "DELETE",
|
|
136
|
+
headers: headersObj
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
140
|
+
throw new Error(err.message ?? "Delete failed");
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
onSuccess: () => {
|
|
144
|
+
reactQueryClient.invalidateQueries({ queryKey: ["mediaAssets"] });
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
qc
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
function useCreateFolder() {
|
|
151
|
+
const {
|
|
152
|
+
apiBaseURL,
|
|
153
|
+
apiBasePath,
|
|
154
|
+
headers,
|
|
155
|
+
queryClient: qc
|
|
156
|
+
} = useMediaConfig();
|
|
157
|
+
const reactQueryClient = useQueryClient(qc);
|
|
158
|
+
return useMutation(
|
|
159
|
+
{
|
|
160
|
+
mutationFn: async (input) => {
|
|
161
|
+
const base = `${apiBaseURL}${apiBasePath}`;
|
|
162
|
+
const headersObj = new Headers(headers);
|
|
163
|
+
const res = await fetch(`${base}/media/folders`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: {
|
|
166
|
+
...Object.fromEntries(headersObj.entries()),
|
|
167
|
+
"Content-Type": "application/json"
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify(input)
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
173
|
+
throw new Error(err.message ?? "Failed to create folder");
|
|
174
|
+
}
|
|
175
|
+
return res.json();
|
|
176
|
+
},
|
|
177
|
+
onSuccess: () => {
|
|
178
|
+
reactQueryClient.invalidateQueries({ queryKey: ["mediaFolders"] });
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
qc
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
function useDeleteFolder() {
|
|
185
|
+
const {
|
|
186
|
+
apiBaseURL,
|
|
187
|
+
apiBasePath,
|
|
188
|
+
headers,
|
|
189
|
+
queryClient: qc
|
|
190
|
+
} = useMediaConfig();
|
|
191
|
+
const reactQueryClient = useQueryClient(qc);
|
|
192
|
+
return useMutation(
|
|
193
|
+
{
|
|
194
|
+
mutationFn: async (id) => {
|
|
195
|
+
const base = `${apiBaseURL}${apiBasePath}`;
|
|
196
|
+
const headersObj = new Headers(headers);
|
|
197
|
+
const res = await fetch(`${base}/media/folders/${id}`, {
|
|
198
|
+
method: "DELETE",
|
|
199
|
+
headers: headersObj
|
|
200
|
+
});
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
203
|
+
throw new Error(err.message ?? "Failed to delete folder");
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
onSuccess: () => {
|
|
207
|
+
reactQueryClient.invalidateQueries({ queryKey: ["mediaFolders"] });
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
qc
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { useAssets, useCreateFolder, useDeleteAsset, useDeleteFolder, useFolders, useRegisterAsset, useUploadAsset };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const client = require('@btst/stack/plugins/client');
|
|
4
|
+
const yar = require('@btst/yar');
|
|
5
|
+
const libraryPage = require('./components/pages/library-page.cjs');
|
|
6
|
+
const plugins_media_queryKeys = require('../../../../../../plugins/media/query-keys.cjs');
|
|
7
|
+
|
|
8
|
+
const mediaClientPlugin = (config) => client.defineClientPlugin({
|
|
9
|
+
name: "media",
|
|
10
|
+
routes: () => ({
|
|
11
|
+
library: yar.createRoute("/media", () => {
|
|
12
|
+
const CustomLibrary = config.pageComponents?.library;
|
|
13
|
+
return {
|
|
14
|
+
PageComponent: CustomLibrary ?? libraryPage.LibraryPageComponent,
|
|
15
|
+
loader: createMediaLibraryLoader(config),
|
|
16
|
+
meta: createMediaLibraryMeta(config)
|
|
17
|
+
};
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
function createMediaLibraryLoader(config) {
|
|
22
|
+
return async () => {
|
|
23
|
+
if (typeof window === "undefined") {
|
|
24
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
|
|
25
|
+
const context = {
|
|
26
|
+
path: "/media",
|
|
27
|
+
isSSR: true,
|
|
28
|
+
apiBaseURL,
|
|
29
|
+
apiBasePath,
|
|
30
|
+
headers
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
if (hooks?.beforeLoadLibrary) {
|
|
34
|
+
await hooks.beforeLoadLibrary(context);
|
|
35
|
+
}
|
|
36
|
+
const client$1 = client.createApiClient({
|
|
37
|
+
baseURL: apiBaseURL,
|
|
38
|
+
basePath: apiBasePath
|
|
39
|
+
});
|
|
40
|
+
const queries = plugins_media_queryKeys.createMediaQueryKeys(client$1, headers);
|
|
41
|
+
await queryClient.prefetchInfiniteQuery({
|
|
42
|
+
...queries.mediaAssets.list({ limit: 40 }),
|
|
43
|
+
initialPageParam: 0
|
|
44
|
+
});
|
|
45
|
+
await queryClient.prefetchQuery(queries.mediaFolders.list(null));
|
|
46
|
+
if (hooks?.afterLoadLibrary) {
|
|
47
|
+
await hooks.afterLoadLibrary(context);
|
|
48
|
+
}
|
|
49
|
+
const queryState = queryClient.getQueryState(
|
|
50
|
+
queries.mediaAssets.list({ limit: 40 }).queryKey
|
|
51
|
+
);
|
|
52
|
+
if (queryState?.error && hooks?.onLoadError) {
|
|
53
|
+
const error = queryState.error instanceof Error ? queryState.error : new Error(String(queryState.error));
|
|
54
|
+
await hooks.onLoadError(error, context);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (client.isConnectionError(error)) {
|
|
58
|
+
console.warn(
|
|
59
|
+
"[btst/media] route.loader() failed \u2014 no server running at build time. The media library does not support SSG."
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (hooks?.onLoadError) {
|
|
63
|
+
await hooks.onLoadError(error, context);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function createMediaLibraryMeta(config) {
|
|
70
|
+
return () => {
|
|
71
|
+
const { siteBaseURL, siteBasePath } = config;
|
|
72
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/media`;
|
|
73
|
+
const title = "Media Library";
|
|
74
|
+
return [
|
|
75
|
+
{ title },
|
|
76
|
+
{ name: "title", content: title },
|
|
77
|
+
{ name: "description", content: "Manage your media assets" },
|
|
78
|
+
{ name: "robots", content: "noindex, nofollow" },
|
|
79
|
+
// Open Graph
|
|
80
|
+
{ property: "og:type", content: "website" },
|
|
81
|
+
{ property: "og:title", content: title },
|
|
82
|
+
{
|
|
83
|
+
property: "og:description",
|
|
84
|
+
content: "Manage your media assets"
|
|
85
|
+
},
|
|
86
|
+
{ property: "og:url", content: fullUrl },
|
|
87
|
+
// Twitter
|
|
88
|
+
{ name: "twitter:card", content: "summary" },
|
|
89
|
+
{ name: "twitter:title", content: title }
|
|
90
|
+
];
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exports.mediaClientPlugin = mediaClientPlugin;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineClientPlugin, createApiClient, isConnectionError } from '@btst/stack/plugins/client';
|
|
2
|
+
import { createRoute } from '@btst/yar';
|
|
3
|
+
import { LibraryPageComponent } from './components/pages/library-page.mjs';
|
|
4
|
+
import { createMediaQueryKeys } from '../../../../../../plugins/media/query-keys.mjs';
|
|
5
|
+
|
|
6
|
+
const mediaClientPlugin = (config) => defineClientPlugin({
|
|
7
|
+
name: "media",
|
|
8
|
+
routes: () => ({
|
|
9
|
+
library: createRoute("/media", () => {
|
|
10
|
+
const CustomLibrary = config.pageComponents?.library;
|
|
11
|
+
return {
|
|
12
|
+
PageComponent: CustomLibrary ?? LibraryPageComponent,
|
|
13
|
+
loader: createMediaLibraryLoader(config),
|
|
14
|
+
meta: createMediaLibraryMeta(config)
|
|
15
|
+
};
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
function createMediaLibraryLoader(config) {
|
|
20
|
+
return async () => {
|
|
21
|
+
if (typeof window === "undefined") {
|
|
22
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
|
|
23
|
+
const context = {
|
|
24
|
+
path: "/media",
|
|
25
|
+
isSSR: true,
|
|
26
|
+
apiBaseURL,
|
|
27
|
+
apiBasePath,
|
|
28
|
+
headers
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
if (hooks?.beforeLoadLibrary) {
|
|
32
|
+
await hooks.beforeLoadLibrary(context);
|
|
33
|
+
}
|
|
34
|
+
const client = createApiClient({
|
|
35
|
+
baseURL: apiBaseURL,
|
|
36
|
+
basePath: apiBasePath
|
|
37
|
+
});
|
|
38
|
+
const queries = createMediaQueryKeys(client, headers);
|
|
39
|
+
await queryClient.prefetchInfiniteQuery({
|
|
40
|
+
...queries.mediaAssets.list({ limit: 40 }),
|
|
41
|
+
initialPageParam: 0
|
|
42
|
+
});
|
|
43
|
+
await queryClient.prefetchQuery(queries.mediaFolders.list(null));
|
|
44
|
+
if (hooks?.afterLoadLibrary) {
|
|
45
|
+
await hooks.afterLoadLibrary(context);
|
|
46
|
+
}
|
|
47
|
+
const queryState = queryClient.getQueryState(
|
|
48
|
+
queries.mediaAssets.list({ limit: 40 }).queryKey
|
|
49
|
+
);
|
|
50
|
+
if (queryState?.error && hooks?.onLoadError) {
|
|
51
|
+
const error = queryState.error instanceof Error ? queryState.error : new Error(String(queryState.error));
|
|
52
|
+
await hooks.onLoadError(error, context);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (isConnectionError(error)) {
|
|
56
|
+
console.warn(
|
|
57
|
+
"[btst/media] route.loader() failed \u2014 no server running at build time. The media library does not support SSG."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (hooks?.onLoadError) {
|
|
61
|
+
await hooks.onLoadError(error, context);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function createMediaLibraryMeta(config) {
|
|
68
|
+
return () => {
|
|
69
|
+
const { siteBaseURL, siteBasePath } = config;
|
|
70
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/media`;
|
|
71
|
+
const title = "Media Library";
|
|
72
|
+
return [
|
|
73
|
+
{ title },
|
|
74
|
+
{ name: "title", content: title },
|
|
75
|
+
{ name: "description", content: "Manage your media assets" },
|
|
76
|
+
{ name: "robots", content: "noindex, nofollow" },
|
|
77
|
+
// Open Graph
|
|
78
|
+
{ property: "og:type", content: "website" },
|
|
79
|
+
{ property: "og:title", content: title },
|
|
80
|
+
{
|
|
81
|
+
property: "og:description",
|
|
82
|
+
content: "Manage your media assets"
|
|
83
|
+
},
|
|
84
|
+
{ property: "og:url", content: fullUrl },
|
|
85
|
+
// Twitter
|
|
86
|
+
{ name: "twitter:card", content: "summary" },
|
|
87
|
+
{ name: "twitter:title", content: title }
|
|
88
|
+
];
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { mediaClientPlugin };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const imageCompression = require('./utils/image-compression.cjs');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_IMAGE_COMPRESSION = {
|
|
7
|
+
maxWidth: 2048,
|
|
8
|
+
maxHeight: 2048,
|
|
9
|
+
quality: 0.85
|
|
10
|
+
};
|
|
11
|
+
async function uploadAsset(config, input) {
|
|
12
|
+
const {
|
|
13
|
+
apiBaseURL,
|
|
14
|
+
apiBasePath,
|
|
15
|
+
headers,
|
|
16
|
+
uploadMode = "direct",
|
|
17
|
+
imageCompression: imageCompression$1
|
|
18
|
+
} = config;
|
|
19
|
+
const { file, folderId } = input;
|
|
20
|
+
const processedFile = imageCompression$1 === false ? file : await imageCompression.compressImage(
|
|
21
|
+
file,
|
|
22
|
+
imageCompression$1 ?? DEFAULT_IMAGE_COMPRESSION
|
|
23
|
+
);
|
|
24
|
+
const base = `${apiBaseURL}${apiBasePath}`;
|
|
25
|
+
const headersObj = new Headers(headers);
|
|
26
|
+
if (uploadMode === "direct") {
|
|
27
|
+
const formData = new FormData();
|
|
28
|
+
formData.append("file", processedFile);
|
|
29
|
+
if (folderId) formData.append("folderId", folderId);
|
|
30
|
+
const res = await fetch(`${base}/media/upload`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: headersObj,
|
|
33
|
+
body: formData
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
37
|
+
throw new Error(err.message ?? "Upload failed");
|
|
38
|
+
}
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
if (uploadMode === "s3") {
|
|
42
|
+
const tokenRes = await fetch(`${base}/media/upload/token`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
...Object.fromEntries(headersObj.entries()),
|
|
46
|
+
"Content-Type": "application/json"
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
filename: processedFile.name,
|
|
50
|
+
mimeType: processedFile.type,
|
|
51
|
+
size: processedFile.size,
|
|
52
|
+
folderId
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
if (!tokenRes.ok) {
|
|
56
|
+
const err = await tokenRes.json().catch(() => ({ message: tokenRes.statusText }));
|
|
57
|
+
throw new Error(err.message ?? "Failed to get upload token");
|
|
58
|
+
}
|
|
59
|
+
const token = await tokenRes.json();
|
|
60
|
+
const putRes = await fetch(token.payload.uploadUrl, {
|
|
61
|
+
method: "PUT",
|
|
62
|
+
headers: token.payload.headers,
|
|
63
|
+
body: processedFile
|
|
64
|
+
});
|
|
65
|
+
if (!putRes.ok) throw new Error("Failed to upload to S3");
|
|
66
|
+
const assetRes = await fetch(`${base}/media/assets`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
...Object.fromEntries(headersObj.entries()),
|
|
70
|
+
"Content-Type": "application/json"
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
filename: processedFile.name,
|
|
74
|
+
originalName: file.name,
|
|
75
|
+
mimeType: processedFile.type,
|
|
76
|
+
size: processedFile.size,
|
|
77
|
+
url: token.payload.publicUrl,
|
|
78
|
+
folderId
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
if (!assetRes.ok) {
|
|
82
|
+
const err = await assetRes.json().catch(() => ({ message: assetRes.statusText }));
|
|
83
|
+
throw new Error(err.message ?? "Failed to register asset");
|
|
84
|
+
}
|
|
85
|
+
return assetRes.json();
|
|
86
|
+
}
|
|
87
|
+
if (uploadMode === "vercel-blob") {
|
|
88
|
+
const { upload } = await import('@vercel/blob/client');
|
|
89
|
+
const blob = await upload(processedFile.name, processedFile, {
|
|
90
|
+
access: "public",
|
|
91
|
+
handleUploadUrl: `${base}/media/upload/vercel-blob`,
|
|
92
|
+
clientPayload: JSON.stringify({
|
|
93
|
+
mimeType: processedFile.type,
|
|
94
|
+
size: processedFile.size
|
|
95
|
+
})
|
|
96
|
+
});
|
|
97
|
+
const assetRes = await fetch(`${base}/media/assets`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
...Object.fromEntries(headersObj.entries()),
|
|
101
|
+
"Content-Type": "application/json"
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
filename: processedFile.name,
|
|
105
|
+
originalName: file.name,
|
|
106
|
+
mimeType: processedFile.type,
|
|
107
|
+
size: processedFile.size,
|
|
108
|
+
url: blob.url,
|
|
109
|
+
folderId
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
if (!assetRes.ok) {
|
|
113
|
+
const err = await assetRes.json().catch(() => ({ message: assetRes.statusText }));
|
|
114
|
+
throw new Error(err.message ?? "Failed to register asset");
|
|
115
|
+
}
|
|
116
|
+
return assetRes.json();
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Unknown uploadMode: ${uploadMode}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
exports.uploadAsset = uploadAsset;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { compressImage } from './utils/image-compression.mjs';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_IMAGE_COMPRESSION = {
|
|
5
|
+
maxWidth: 2048,
|
|
6
|
+
maxHeight: 2048,
|
|
7
|
+
quality: 0.85
|
|
8
|
+
};
|
|
9
|
+
async function uploadAsset(config, input) {
|
|
10
|
+
const {
|
|
11
|
+
apiBaseURL,
|
|
12
|
+
apiBasePath,
|
|
13
|
+
headers,
|
|
14
|
+
uploadMode = "direct",
|
|
15
|
+
imageCompression
|
|
16
|
+
} = config;
|
|
17
|
+
const { file, folderId } = input;
|
|
18
|
+
const processedFile = imageCompression === false ? file : await compressImage(
|
|
19
|
+
file,
|
|
20
|
+
imageCompression ?? DEFAULT_IMAGE_COMPRESSION
|
|
21
|
+
);
|
|
22
|
+
const base = `${apiBaseURL}${apiBasePath}`;
|
|
23
|
+
const headersObj = new Headers(headers);
|
|
24
|
+
if (uploadMode === "direct") {
|
|
25
|
+
const formData = new FormData();
|
|
26
|
+
formData.append("file", processedFile);
|
|
27
|
+
if (folderId) formData.append("folderId", folderId);
|
|
28
|
+
const res = await fetch(`${base}/media/upload`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: headersObj,
|
|
31
|
+
body: formData
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
35
|
+
throw new Error(err.message ?? "Upload failed");
|
|
36
|
+
}
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
39
|
+
if (uploadMode === "s3") {
|
|
40
|
+
const tokenRes = await fetch(`${base}/media/upload/token`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
...Object.fromEntries(headersObj.entries()),
|
|
44
|
+
"Content-Type": "application/json"
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
filename: processedFile.name,
|
|
48
|
+
mimeType: processedFile.type,
|
|
49
|
+
size: processedFile.size,
|
|
50
|
+
folderId
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
if (!tokenRes.ok) {
|
|
54
|
+
const err = await tokenRes.json().catch(() => ({ message: tokenRes.statusText }));
|
|
55
|
+
throw new Error(err.message ?? "Failed to get upload token");
|
|
56
|
+
}
|
|
57
|
+
const token = await tokenRes.json();
|
|
58
|
+
const putRes = await fetch(token.payload.uploadUrl, {
|
|
59
|
+
method: "PUT",
|
|
60
|
+
headers: token.payload.headers,
|
|
61
|
+
body: processedFile
|
|
62
|
+
});
|
|
63
|
+
if (!putRes.ok) throw new Error("Failed to upload to S3");
|
|
64
|
+
const assetRes = await fetch(`${base}/media/assets`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
...Object.fromEntries(headersObj.entries()),
|
|
68
|
+
"Content-Type": "application/json"
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
filename: processedFile.name,
|
|
72
|
+
originalName: file.name,
|
|
73
|
+
mimeType: processedFile.type,
|
|
74
|
+
size: processedFile.size,
|
|
75
|
+
url: token.payload.publicUrl,
|
|
76
|
+
folderId
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
if (!assetRes.ok) {
|
|
80
|
+
const err = await assetRes.json().catch(() => ({ message: assetRes.statusText }));
|
|
81
|
+
throw new Error(err.message ?? "Failed to register asset");
|
|
82
|
+
}
|
|
83
|
+
return assetRes.json();
|
|
84
|
+
}
|
|
85
|
+
if (uploadMode === "vercel-blob") {
|
|
86
|
+
const { upload } = await import('@vercel/blob/client');
|
|
87
|
+
const blob = await upload(processedFile.name, processedFile, {
|
|
88
|
+
access: "public",
|
|
89
|
+
handleUploadUrl: `${base}/media/upload/vercel-blob`,
|
|
90
|
+
clientPayload: JSON.stringify({
|
|
91
|
+
mimeType: processedFile.type,
|
|
92
|
+
size: processedFile.size
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
const assetRes = await fetch(`${base}/media/assets`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
...Object.fromEntries(headersObj.entries()),
|
|
99
|
+
"Content-Type": "application/json"
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
filename: processedFile.name,
|
|
103
|
+
originalName: file.name,
|
|
104
|
+
mimeType: processedFile.type,
|
|
105
|
+
size: processedFile.size,
|
|
106
|
+
url: blob.url,
|
|
107
|
+
folderId
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
if (!assetRes.ok) {
|
|
111
|
+
const err = await assetRes.json().catch(() => ({ message: assetRes.statusText }));
|
|
112
|
+
throw new Error(err.message ?? "Failed to register asset");
|
|
113
|
+
}
|
|
114
|
+
return assetRes.json();
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Unknown uploadMode: ${uploadMode}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { uploadAsset };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadImage(file) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const url = URL.createObjectURL(file);
|
|
6
|
+
const img = new Image();
|
|
7
|
+
img.onload = () => {
|
|
8
|
+
URL.revokeObjectURL(url);
|
|
9
|
+
resolve(img);
|
|
10
|
+
};
|
|
11
|
+
img.onerror = () => {
|
|
12
|
+
URL.revokeObjectURL(url);
|
|
13
|
+
reject(new Error(`Failed to load image: ${file.name}`));
|
|
14
|
+
};
|
|
15
|
+
img.src = url;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const SKIP_TYPES = /* @__PURE__ */ new Set(["image/svg+xml", "image/gif"]);
|
|
19
|
+
async function compressImage(file, options = {}) {
|
|
20
|
+
if (!file.type.startsWith("image/") || SKIP_TYPES.has(file.type)) {
|
|
21
|
+
return file;
|
|
22
|
+
}
|
|
23
|
+
if (typeof document === "undefined") return file;
|
|
24
|
+
const {
|
|
25
|
+
maxWidth = 2048,
|
|
26
|
+
maxHeight = 2048,
|
|
27
|
+
quality = 0.85,
|
|
28
|
+
outputFormat
|
|
29
|
+
} = options;
|
|
30
|
+
const img = await loadImage(file);
|
|
31
|
+
let { width, height } = img;
|
|
32
|
+
const needsResize = width > maxWidth || height > maxHeight;
|
|
33
|
+
const needsFormatChange = outputFormat !== void 0 && outputFormat !== file.type;
|
|
34
|
+
if (!needsResize && !needsFormatChange) return file;
|
|
35
|
+
if (needsResize) {
|
|
36
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
37
|
+
width = Math.round(width * ratio);
|
|
38
|
+
height = Math.round(height * ratio);
|
|
39
|
+
}
|
|
40
|
+
const canvas = document.createElement("canvas");
|
|
41
|
+
canvas.width = width;
|
|
42
|
+
canvas.height = height;
|
|
43
|
+
const ctx = canvas.getContext("2d");
|
|
44
|
+
if (!ctx) return file;
|
|
45
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
46
|
+
const mimeType = outputFormat ?? file.type;
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
canvas.toBlob(
|
|
49
|
+
(blob) => {
|
|
50
|
+
if (!blob) {
|
|
51
|
+
reject(new Error("canvas.toBlob returned null"));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let name = file.name;
|
|
55
|
+
if (outputFormat && outputFormat !== file.type) {
|
|
56
|
+
const ext = outputFormat.split("/")[1] ?? "jpg";
|
|
57
|
+
name = name.replace(/\.[^.]+$/, `.${ext}`);
|
|
58
|
+
}
|
|
59
|
+
resolve(new File([blob], name, { type: mimeType }));
|
|
60
|
+
},
|
|
61
|
+
mimeType,
|
|
62
|
+
quality
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
exports.compressImage = compressImage;
|