inicontent 1.0.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/.github/pull_request.yml +18 -0
- package/CONTRIBUTING.md +67 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/app/app.vue +106 -0
- package/app/assets/main.css +974 -0
- package/app/components/Api/EndpointCard.vue +169 -0
- package/app/components/Api/SchemaDocsTable.vue +208 -0
- package/app/components/Asset/Card.vue +1363 -0
- package/app/components/Asset/Grid.vue +295 -0
- package/app/components/Asset/Icon.vue +38 -0
- package/app/components/Asset/Preview.vue +486 -0
- package/app/components/Asset/Scanner.vue +700 -0
- package/app/components/Asset/Thumb.vue +708 -0
- package/app/components/Auth/Reset.vue +99 -0
- package/app/components/Auth/index.vue +346 -0
- package/app/components/ChatInterface.vue +2326 -0
- package/app/components/ChatPagePreview.vue +34 -0
- package/app/components/Column/Array.vue +29 -0
- package/app/components/Column/Asset.vue +21 -0
- package/app/components/Column/Boolean.vue +12 -0
- package/app/components/Column/Color.vue +9 -0
- package/app/components/Column/Date.vue +57 -0
- package/app/components/Column/Edit.vue +215 -0
- package/app/components/Column/Email.vue +9 -0
- package/app/components/Column/Html.vue +27 -0
- package/app/components/Column/Id.vue +20 -0
- package/app/components/Column/Locale.vue +32 -0
- package/app/components/Column/Object.vue +18 -0
- package/app/components/Column/Password.vue +7 -0
- package/app/components/Column/Role.vue +15 -0
- package/app/components/Column/S.vue +14 -0
- package/app/components/Column/Table/Single.vue +49 -0
- package/app/components/Column/Table/index.vue +42 -0
- package/app/components/Column/Tags.vue +31 -0
- package/app/components/Column/Text.vue +9 -0
- package/app/components/Column/Url.vue +9 -0
- package/app/components/Column/index.vue +41 -0
- package/app/components/Dashboard/Editor.vue +221 -0
- package/app/components/Dashboard/Grid.vue +270 -0
- package/app/components/Dashboard/View.vue +131 -0
- package/app/components/Dashboard/Widget/BarChart.vue +73 -0
- package/app/components/Dashboard/Widget/Counter.vue +51 -0
- package/app/components/Dashboard/Widget/LineChart.vue +76 -0
- package/app/components/Dashboard/Widget/PieChart.vue +75 -0
- package/app/components/Dashboard/Widget/Table.vue +94 -0
- package/app/components/Dashboard/WidgetEditor.vue +274 -0
- package/app/components/Data/Array.vue +102 -0
- package/app/components/Data/Html.vue +9 -0
- package/app/components/Data/Icon.vue +9 -0
- package/app/components/Data/Object.vue +10 -0
- package/app/components/Data/S.vue +16 -0
- package/app/components/Data/Table.vue +53 -0
- package/app/components/Data/Value.vue +34 -0
- package/app/components/Data/index.vue +14 -0
- package/app/components/Field/Array.vue +324 -0
- package/app/components/Field/Asset/Actions.vue +97 -0
- package/app/components/Field/Asset/index.vue +639 -0
- package/app/components/Field/Boolean.vue +26 -0
- package/app/components/Field/Checkbox.vue +48 -0
- package/app/components/Field/Color.vue +27 -0
- package/app/components/Field/Date.vue +30 -0
- package/app/components/Field/Email.vue +51 -0
- package/app/components/Field/EmailTemplate.vue +420 -0
- package/app/components/Field/Html/ColorPicker.vue +127 -0
- package/app/components/Field/Html/Editor.vue +544 -0
- package/app/components/Field/Html/index.vue +25 -0
- package/app/components/Field/Icon.vue +114 -0
- package/app/components/Field/JSON.vue +113 -0
- package/app/components/Field/Mention.vue +36 -0
- package/app/components/Field/Number.vue +27 -0
- package/app/components/Field/Object.vue +92 -0
- package/app/components/Field/Password.vue +55 -0
- package/app/components/Field/Radio.vue +47 -0
- package/app/components/Field/S.vue +44 -0
- package/app/components/Field/Select.vue +49 -0
- package/app/components/Field/Table.vue +348 -0
- package/app/components/Field/Tags.vue +113 -0
- package/app/components/Field/Text.vue +31 -0
- package/app/components/Field/Textarea.vue +33 -0
- package/app/components/Field/Url.vue +40 -0
- package/app/components/Field/Wrapper.vue +128 -0
- package/app/components/Field/index.vue +68 -0
- package/app/components/FloatingChatbot.vue +73 -0
- package/app/components/Form/Card.vue +151 -0
- package/app/components/Form/Drawer.vue +195 -0
- package/app/components/Form/index.vue +621 -0
- package/app/components/Header.vue +328 -0
- package/app/components/Offline/ConflictModal.vue +200 -0
- package/app/components/Offline/SyncStatus.vue +249 -0
- package/app/components/PlatformFeatureLanding.vue +65 -0
- package/app/components/Table/Backups.vue +638 -0
- package/app/components/Table/Flows.vue +814 -0
- package/app/components/Table/Grid.vue +296 -0
- package/app/components/Table/Icon.vue +36 -0
- package/app/components/Table/Logs.vue +181 -0
- package/app/components/Table/Schedules.vue +722 -0
- package/app/components/Table/Search/Button.vue +256 -0
- package/app/components/Table/Search/Items.vue +286 -0
- package/app/components/Table/Search/index.vue +87 -0
- package/app/components/Table/Settings/Schema.vue +801 -0
- package/app/components/Table/Settings/index.vue +631 -0
- package/app/components/Table/TranslateDrawer.vue +526 -0
- package/app/components/Table/Views/Kanban.vue +289 -0
- package/app/components/Table/Views/Table.vue +1273 -0
- package/app/components/Table/index.vue +890 -0
- package/app/composables/Translation/define.ts +41 -0
- package/app/composables/Translation/fetch.ts +107 -0
- package/app/composables/Translation/languages.ts +1 -0
- package/app/composables/Translation/loadCoreTranslations.ts +38 -0
- package/app/composables/Translation/translate.ts +250 -0
- package/app/composables/databaseCookies.ts +79 -0
- package/app/composables/fieldValidator.ts +72 -0
- package/app/composables/fieldsList.ts +301 -0
- package/app/composables/formatDatabase.ts +101 -0
- package/app/composables/index.ts +444 -0
- package/app/composables/openDrawer.ts +118 -0
- package/app/composables/optimizeFile.ts +287 -0
- package/app/composables/renderLabel.ts +88 -0
- package/app/composables/schemaClipboard.ts +95 -0
- package/app/composables/search.ts +353 -0
- package/app/composables/setThemeConfig.ts +48 -0
- package/app/composables/translationValue.ts +169 -0
- package/app/composables/useAssetPreview.ts +84 -0
- package/app/composables/useAssetUploader.ts +56 -0
- package/app/composables/useDashboardData.ts +310 -0
- package/app/composables/useDocumentThumbnail.ts +351 -0
- package/app/composables/useEcharts.ts +33 -0
- package/app/composables/useLocalDatabaseConfig.ts +51 -0
- package/app/composables/useOcr.ts +300 -0
- package/app/composables/useOfflineFetch.ts +194 -0
- package/app/composables/useOfflineItem.ts +107 -0
- package/app/composables/useOfflineQueue.ts +302 -0
- package/app/composables/useOfflineSync.ts +235 -0
- package/app/composables/usePasskeyAuth.ts +156 -0
- package/app/composables/usePdfCompressor.ts +180 -0
- package/app/composables/usePlatformRedirect.ts +72 -0
- package/app/composables/useRealtimeSync.ts +233 -0
- package/app/composables/useReferencedItems.ts +180 -0
- package/app/composables/useScanner.ts +664 -0
- package/app/composables/useSubscription.ts +650 -0
- package/app/composables/useThumbnailCache.ts +136 -0
- package/app/composables/useVideoCompressor.ts +194 -0
- package/app/error.vue +112 -0
- package/app/layouts/api.vue +188 -0
- package/app/layouts/dashboard.vue +91 -0
- package/app/layouts/default.vue +228 -0
- package/app/layouts/table.vue +471 -0
- package/app/locales/ar.ts +996 -0
- package/app/locales/en.ts +655 -0
- package/app/locales/es.ts +996 -0
- package/app/locales/fr.ts +997 -0
- package/app/middleware/dashboard.ts +33 -0
- package/app/middleware/database.ts +83 -0
- package/app/middleware/global.ts +1 -0
- package/app/middleware/sid-ingest.global.ts +29 -0
- package/app/middleware/table.ts +54 -0
- package/app/middleware/user.ts +26 -0
- package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
- package/app/pages/[[database]]/admin/api/index.vue +18 -0
- package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
- package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
- package/app/pages/[[database]]/admin/billing/index.vue +16 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
- package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
- package/app/pages/[[database]]/admin/index.vue +28 -0
- package/app/pages/[[database]]/admin/settings.vue +526 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
- package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
- package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
- package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
- package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/index.vue +20 -0
- package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
- package/app/pages/[[database]]/api/index.vue +120 -0
- package/app/pages/[[database]]/auth/index.vue +12 -0
- package/app/pages/[[database]]/auth/reset.vue +10 -0
- package/app/pages/[[database]]/index.vue +10 -0
- package/app/plugins/offline-warmup.client.ts +53 -0
- package/app/plugins/template-globals.ts +9 -0
- package/app/spa-loading-template.html +93 -0
- package/biome.json +53 -0
- package/index.d.ts +380 -0
- package/modules/naiveui.ts +41 -0
- package/nuxt.config.ts +269 -0
- package/package.json +56 -0
- package/pnpm-workspace.yaml +5 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/pwa-192x192.png +0 -0
- package/public/pwa-512x512.png +0 -0
- package/public/pwa-maskable-192x192.png +0 -0
- package/public/pwa-maskable-512x512.png +0 -0
- package/public/vendor/opencv.js +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper :field :rule v-model="modelValue">
|
|
3
|
+
<template #label>
|
|
4
|
+
<NFlex inline align="center" size="small" :style="`margin-${(Language === 'ar' ? 'right' : 'left')}: 5px`">
|
|
5
|
+
<LazyFieldAssetActions v-model:showAssetsModal="showAssetsModal" :field
|
|
6
|
+
:callback="importAssetCallback" />
|
|
7
|
+
<NButton v-if="acceptsImages" circle secondary size="tiny" :title="t('scanDocument')"
|
|
8
|
+
@click.prevent.stop="showScanner = true">
|
|
9
|
+
<template #icon>
|
|
10
|
+
<NIcon>
|
|
11
|
+
<Icon name="tabler:scan" />
|
|
12
|
+
</NIcon>
|
|
13
|
+
</template>
|
|
14
|
+
</NButton>
|
|
15
|
+
</NFlex>
|
|
16
|
+
</template>
|
|
17
|
+
<NUpload ref="uploadRef" directory-dnd :max="!field.isArray ? 1 : undefined" :multiple="!!field.isArray"
|
|
18
|
+
:accept="acceptedFileType"
|
|
19
|
+
:action="`${config.public.apiBase}${database.slug ?? 'inicontent'}/assets${field.suffix ? renderLabel({ ...table, label: field.suffix }, currentItem) : ''}`"
|
|
20
|
+
:fileList @update:file-list="setModelValue" :custom-request @remove="handleRemoveUpload"
|
|
21
|
+
:list-type="!field.isTable ? 'image' : 'image-card'" :renderIcon :shouldUseThumbnailUrl="() => false">
|
|
22
|
+
<NUploadDragger v-if="compressionIndicator">
|
|
23
|
+
<NProgress type="circle" status="warning" :percentage="compressionIndicator" :stroke-width="10"
|
|
24
|
+
style="width:54px" indicator-placement="inside">
|
|
25
|
+
<NTooltip v-model:show="showSkipCompressionTooltip" placement="top">
|
|
26
|
+
<template #trigger>
|
|
27
|
+
<Icon @click.stop="skipCompression" name="tabler:player-track-next-filled" />
|
|
28
|
+
</template>
|
|
29
|
+
{{ t("skipCompression") }}
|
|
30
|
+
</NTooltip>
|
|
31
|
+
</NProgress>
|
|
32
|
+
</NUploadDragger>
|
|
33
|
+
<NUploadDragger v-else-if="!field.isTable">
|
|
34
|
+
<NFlex vertical align="center">
|
|
35
|
+
<NIcon size="48" depth="3">
|
|
36
|
+
<Icon name="tabler:upload" />
|
|
37
|
+
</NIcon>
|
|
38
|
+
<NFlex v-if="field.labelProps?.showLabel === false">
|
|
39
|
+
<LazyFieldAssetActions v-model:showAssetsModal="showAssetsModal" :field
|
|
40
|
+
:callback="importAssetCallback" />
|
|
41
|
+
<NButton v-if="acceptsImages" circle secondary size="tiny" :title="t('scanDocument')"
|
|
42
|
+
@click.prevent.stop="showScanner = true">
|
|
43
|
+
<template #icon>
|
|
44
|
+
<NIcon>
|
|
45
|
+
<Icon name="tabler:scan" />
|
|
46
|
+
</NIcon>
|
|
47
|
+
</template>
|
|
48
|
+
</NButton>
|
|
49
|
+
</NFlex>
|
|
50
|
+
</NFlex>
|
|
51
|
+
</NUploadDragger>
|
|
52
|
+
<NFlex v-else align="center" size="small">
|
|
53
|
+
<LazyFieldAssetActions v-model:showAssetsModal="showAssetsModal" :field
|
|
54
|
+
:callback="importAssetCallback" />
|
|
55
|
+
<NButton v-if="acceptsImages" circle secondary size="tiny" :title="t('scanDocument')"
|
|
56
|
+
@click.prevent.stop="showScanner = true">
|
|
57
|
+
<template #icon>
|
|
58
|
+
<NIcon>
|
|
59
|
+
<Icon name="tabler:scan" />
|
|
60
|
+
</NIcon>
|
|
61
|
+
</template>
|
|
62
|
+
</NButton>
|
|
63
|
+
</NFlex>
|
|
64
|
+
</NUpload>
|
|
65
|
+
<NDrawer v-model:show="showAssetsModal" defaultHeight="50%" placement="bottom" resizable>
|
|
66
|
+
<NDrawerContent id="assetsModal" :nativeScrollbar="false" :bodyContentStyle="{ paddingTop: 0, paddingBottom: 0 }">
|
|
67
|
+
<AssetCard targetID="assetsModal" :where="assetWhere" :suffix="field.suffix">
|
|
68
|
+
<template v-slot="{ asset }">
|
|
69
|
+
<template v-if="asset.type !== 'dir'">
|
|
70
|
+
<NCheckbox v-if="field.isArray" :checked="getChecked(asset)"
|
|
71
|
+
@update:checked="handleSelectAsset(asset)" />
|
|
72
|
+
<NRadio v-else :checked="getChecked(asset)" @update:checked="handleSelectAsset(asset)" />
|
|
73
|
+
</template>
|
|
74
|
+
</template>
|
|
75
|
+
</AssetCard>
|
|
76
|
+
</NDrawerContent>
|
|
77
|
+
</NDrawer>
|
|
78
|
+
|
|
79
|
+
<LazyAssetScanner v-model:show="showScanner" :allowOCR="false"
|
|
80
|
+
@scanned="(blob) => uploadScannedDocument(blob)" />
|
|
81
|
+
</FieldWrapper>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script lang="ts" setup>
|
|
85
|
+
import { isArrayOfObjects, isObject } from "inibase/utils";
|
|
86
|
+
import type {
|
|
87
|
+
FormItemRule,
|
|
88
|
+
UploadCustomRequestOptions,
|
|
89
|
+
UploadFileInfo,
|
|
90
|
+
} from "naive-ui";
|
|
91
|
+
import { Icon, LazyAssetThumb } from "#components";
|
|
92
|
+
import { getFileNameAndExtension } from "~/composables";
|
|
93
|
+
import { useOptimizeFile } from "~/composables/optimizeFile";
|
|
94
|
+
import renderLabel from "~/composables/renderLabel";
|
|
95
|
+
import { useAssetUploader } from "~/composables/useAssetUploader";
|
|
96
|
+
import { usePdfCompressor } from "~/composables/usePdfCompressor";
|
|
97
|
+
import { useVideoCompressor } from "~/composables/useVideoCompressor";
|
|
98
|
+
|
|
99
|
+
const { field } = defineProps<{ field: Field }>();
|
|
100
|
+
|
|
101
|
+
const modelValue = defineModel<string | Asset | (string | Asset)[]>();
|
|
102
|
+
|
|
103
|
+
const database = useState<Database>("database");
|
|
104
|
+
|
|
105
|
+
const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
|
|
106
|
+
|
|
107
|
+
const config = useRuntimeConfig();
|
|
108
|
+
const LARGE_VIDEO_BYTES = 512 * 1024 * 1024;
|
|
109
|
+
const HUGE_VIDEO_BYTES = 2 * 1024 * 1024 * 1024;
|
|
110
|
+
const LARGE_PDF_BYTES = 50 * 1024 * 1024;
|
|
111
|
+
const HUGE_PDF_BYTES = 200 * 1024 * 1024;
|
|
112
|
+
|
|
113
|
+
const {
|
|
114
|
+
compressVideo,
|
|
115
|
+
loading: videoLoading,
|
|
116
|
+
progress: videoProgress,
|
|
117
|
+
abort: abortVideo,
|
|
118
|
+
resetSkip: resetVideoSkip,
|
|
119
|
+
} = useVideoCompressor();
|
|
120
|
+
const {
|
|
121
|
+
compressPdf,
|
|
122
|
+
loading: pdfLoading,
|
|
123
|
+
progress: pdfProgress,
|
|
124
|
+
abort: abortPdf,
|
|
125
|
+
resetSkip: resetPdfSkip,
|
|
126
|
+
} = usePdfCompressor();
|
|
127
|
+
|
|
128
|
+
const compressionIndicator = ref<number | null>(null);
|
|
129
|
+
const skipCompressionRequested = ref(false);
|
|
130
|
+
const processingFileId = ref<string | null>(null); // Track which file is being processed
|
|
131
|
+
|
|
132
|
+
const skipCompression = () => {
|
|
133
|
+
if (compressionIndicator.value === null) return;
|
|
134
|
+
skipCompressionRequested.value = true;
|
|
135
|
+
abortVideo();
|
|
136
|
+
abortPdf();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
async function handleRemoveUpload({
|
|
140
|
+
file,
|
|
141
|
+
}: {
|
|
142
|
+
file: Required<UploadFileInfo>;
|
|
143
|
+
}) {
|
|
144
|
+
if (file.status !== "finished") {
|
|
145
|
+
// If this is the file currently being processed, abort and reset
|
|
146
|
+
if (processingFileId.value === file.id) {
|
|
147
|
+
skipCompressionRequested.value = true;
|
|
148
|
+
abortVideo();
|
|
149
|
+
abortPdf();
|
|
150
|
+
processingFileId.value = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fileList.value = (fileList.value || []).filter(
|
|
154
|
+
(item) => item.id !== file.id,
|
|
155
|
+
);
|
|
156
|
+
await nextTick();
|
|
157
|
+
setModelValue(fileList.value);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Only assets uploaded in this session (tracked in fileIdObject) should be hard-deleted;
|
|
162
|
+
// pre-existing assets (loaded on mount or picked from the library) are just detached.
|
|
163
|
+
const uploadedAssetId = fileIdObject.value[file.id];
|
|
164
|
+
if (uploadedAssetId) {
|
|
165
|
+
try {
|
|
166
|
+
const path = field.suffix
|
|
167
|
+
? renderLabel(
|
|
168
|
+
{ ...table.value, label: field.suffix },
|
|
169
|
+
currentItem.value,
|
|
170
|
+
)
|
|
171
|
+
: "";
|
|
172
|
+
await $fetch(
|
|
173
|
+
`${config.public.apiBase}${database.value.slug}/assets${path}/${uploadedAssetId}`,
|
|
174
|
+
{
|
|
175
|
+
method: "DELETE",
|
|
176
|
+
params: {
|
|
177
|
+
locale: Language.value,
|
|
178
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
179
|
+
},
|
|
180
|
+
credentials: "include",
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const asset = Array.isArray(modelValue.value)
|
|
185
|
+
? (modelValue.value as Asset[]).find((a) => a.id === uploadedAssetId)
|
|
186
|
+
: (modelValue.value as Asset | undefined);
|
|
187
|
+
|
|
188
|
+
if (asset && asset.id === uploadedAssetId && database.value.size) {
|
|
189
|
+
database.value.size -= asset.size ?? 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
delete fileIdObject.value[file.id];
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.error("Failed to delete asset", e);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const getIndicatorPercent = (progress: number) => {
|
|
202
|
+
const raw = Math.round(progress * 100);
|
|
203
|
+
if (!Number.isFinite(raw)) return 1;
|
|
204
|
+
return Math.min(99, Math.max(1, raw));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
watchEffect(() => {
|
|
208
|
+
if (videoLoading.value) {
|
|
209
|
+
compressionIndicator.value = getIndicatorPercent(videoProgress.value);
|
|
210
|
+
} else if (pdfLoading.value) {
|
|
211
|
+
compressionIndicator.value = getIndicatorPercent(pdfProgress.value);
|
|
212
|
+
} else {
|
|
213
|
+
compressionIndicator.value = null;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const notifyVideoSize = (size: number) => {
|
|
218
|
+
if (!import.meta.client) return;
|
|
219
|
+
const messageApi = window?.$message;
|
|
220
|
+
if (!messageApi) return;
|
|
221
|
+
const formattedSize = humanFileSize(size);
|
|
222
|
+
if (size >= HUGE_VIDEO_BYTES)
|
|
223
|
+
messageApi.warning(t("compression.videoHuge", { size: formattedSize }));
|
|
224
|
+
else if (size >= LARGE_VIDEO_BYTES)
|
|
225
|
+
messageApi.info(t("compression.videoLarge", { size: formattedSize }));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const notifyPdfSize = (size: number) => {
|
|
229
|
+
if (!import.meta.client) return;
|
|
230
|
+
const messageApi = window?.$message;
|
|
231
|
+
if (!messageApi) return;
|
|
232
|
+
const formattedSize = humanFileSize(size);
|
|
233
|
+
if (size >= HUGE_PDF_BYTES)
|
|
234
|
+
messageApi.warning(t("compression.pdfHuge", { size: formattedSize }));
|
|
235
|
+
else if (size >= LARGE_PDF_BYTES)
|
|
236
|
+
messageApi.info(t("compression.pdfLarge", { size: formattedSize }));
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const rule: FormItemRule = {
|
|
240
|
+
type: field.isArray ? "array" : "object",
|
|
241
|
+
required: field.required,
|
|
242
|
+
min: field.isArray ? field.min : undefined,
|
|
243
|
+
max: field.isArray ? field.max : undefined,
|
|
244
|
+
validator: async () => {
|
|
245
|
+
await nextTick();
|
|
246
|
+
return fieldValidator(field, modelValue.value);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const showAssetsModal = ref(false);
|
|
251
|
+
const showScanner = ref(false);
|
|
252
|
+
const uploadRef = ref<{ submit?: (options?: { fileId?: string }) => void }>();
|
|
253
|
+
|
|
254
|
+
const acceptsImages = computed(
|
|
255
|
+
() => !field.accept || field.accept.includes("image"),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
function uploadScannedDocument(blob: Blob) {
|
|
259
|
+
const fileName = `scan-${new Date().toISOString().replace(/[:.]/g, "-")}.png`;
|
|
260
|
+
const file = new File([blob], fileName, { type: "image/png" });
|
|
261
|
+
|
|
262
|
+
const newFile: UploadFileInfo = {
|
|
263
|
+
id: `scan-${Date.now()}`,
|
|
264
|
+
name: fileName,
|
|
265
|
+
file,
|
|
266
|
+
status: "pending",
|
|
267
|
+
type: "image/png",
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Prepend to the controlled list so NUpload renders it, then submit it
|
|
271
|
+
fileList.value = [newFile, ...(fileList.value ?? [])];
|
|
272
|
+
showScanner.value = false;
|
|
273
|
+
|
|
274
|
+
nextTick(() => {
|
|
275
|
+
uploadRef.value?.submit?.();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const acceptedFileType = computed(() => {
|
|
280
|
+
if (!field.accept) return undefined;
|
|
281
|
+
const RETURN = [];
|
|
282
|
+
for (const type of field.accept) {
|
|
283
|
+
switch (type) {
|
|
284
|
+
case "image":
|
|
285
|
+
RETURN.push("image/*");
|
|
286
|
+
break;
|
|
287
|
+
case "video":
|
|
288
|
+
RETURN.push("video/*");
|
|
289
|
+
break;
|
|
290
|
+
case "audio":
|
|
291
|
+
RETURN.push("audio/*");
|
|
292
|
+
break;
|
|
293
|
+
case "document":
|
|
294
|
+
RETURN.push(".doc, .docx, .pdf, .txt, .rtf, .odt");
|
|
295
|
+
break;
|
|
296
|
+
case "archive":
|
|
297
|
+
RETURN.push(".zip, .rar, .7z, .tar, .gz");
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return RETURN.join(",");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const assetWhere = computed(() => {
|
|
305
|
+
if (!field.accept) return undefined;
|
|
306
|
+
let whereType = "[]";
|
|
307
|
+
for (const type of field.accept) {
|
|
308
|
+
switch (type) {
|
|
309
|
+
case "image":
|
|
310
|
+
whereType += "image/png,image/jpeg,image/webp,image/avif,";
|
|
311
|
+
break;
|
|
312
|
+
case "video":
|
|
313
|
+
whereType +=
|
|
314
|
+
"video/mp4, video/mpeg, video/quicktime, video/x-msvideo, video/x-flv, video/webm, video/ogg, video/3gpp, video/3gpp2, video/x-matroska, video/vnd.mpegurl, video/h264, video/x-ms-wmv, video/x-ms-asf,";
|
|
315
|
+
break;
|
|
316
|
+
case "audio":
|
|
317
|
+
whereType +=
|
|
318
|
+
"audio/mpeg, audio/wav, audio/ogg, audio/aac, audio/flac, audio/mp4, audio/x-wav, audio/midi, audio/x-midi, audio/x-pn-wav, audio/x-aiff, audio/x-flac, audio/webm, audio/3gpp, audio/3gpp2, audio/amr, audio/x-ms-wma, audio/vnd.rn-realaudio, audio/vnd.wave,";
|
|
319
|
+
break;
|
|
320
|
+
case "document":
|
|
321
|
+
whereType +=
|
|
322
|
+
"application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/pdf, text/plain, application/rtf, application/vnd.oasis.opendocument.text,";
|
|
323
|
+
break;
|
|
324
|
+
case "archive":
|
|
325
|
+
whereType +=
|
|
326
|
+
"application/zip, application/x-rar-compressed, application/x-7z-compressed, application/x-tar, application/gzip,";
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { type: whereType.slice(0, -1) };
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
function importAssetCallback(assets: Asset | Asset[]) {
|
|
334
|
+
const isModelValueTable = field.type !== "url" && field.children !== "url";
|
|
335
|
+
const value = ([] as Asset[])
|
|
336
|
+
.concat(assets)
|
|
337
|
+
.map((asset) => (isModelValueTable ? asset : asset.publicURL));
|
|
338
|
+
|
|
339
|
+
if (field.isArray)
|
|
340
|
+
modelValue.value = [
|
|
341
|
+
...(modelValue.value
|
|
342
|
+
? Array.isArray(modelValue.value)
|
|
343
|
+
? modelValue.value
|
|
344
|
+
: [modelValue.value]
|
|
345
|
+
: []),
|
|
346
|
+
...value,
|
|
347
|
+
];
|
|
348
|
+
else modelValue.value = value[0];
|
|
349
|
+
|
|
350
|
+
nextTick(() => {
|
|
351
|
+
fileList.value = getFileList();
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function handleSelectAsset(asset?: Asset) {
|
|
356
|
+
if (!asset) return;
|
|
357
|
+
|
|
358
|
+
const value =
|
|
359
|
+
field.type === "url" || field.children === "url" ? asset.publicURL : asset;
|
|
360
|
+
|
|
361
|
+
if (!field.isArray) {
|
|
362
|
+
if (
|
|
363
|
+
modelValue.value &&
|
|
364
|
+
!Array.isArray(modelValue.value) &&
|
|
365
|
+
((typeof modelValue.value === "string" &&
|
|
366
|
+
modelValue.value === asset.publicURL) ||
|
|
367
|
+
(isObject(modelValue.value) && modelValue.value.id === asset.id))
|
|
368
|
+
)
|
|
369
|
+
modelValue.value = undefined;
|
|
370
|
+
else modelValue.value = value;
|
|
371
|
+
} else {
|
|
372
|
+
if (modelValue.value && Array.isArray(modelValue.value)) {
|
|
373
|
+
const index = isArrayOfObjects(modelValue.value)
|
|
374
|
+
? (modelValue.value as Asset[]).findIndex(
|
|
375
|
+
(value) => value.id === asset.id,
|
|
376
|
+
)
|
|
377
|
+
: (modelValue.value as string[]).indexOf(asset.publicURL);
|
|
378
|
+
if (index > -1) modelValue.value.splice(index, 1);
|
|
379
|
+
else modelValue.value.push(value);
|
|
380
|
+
} else modelValue.value = [value];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
nextTick(() => {
|
|
384
|
+
fileList.value = getFileList();
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function getFileList() {
|
|
389
|
+
if (!modelValue.value) return;
|
|
390
|
+
|
|
391
|
+
return ([] as (Asset | string)[]).concat(modelValue.value).map((asset) =>
|
|
392
|
+
typeof asset === "string"
|
|
393
|
+
? {
|
|
394
|
+
id: asset,
|
|
395
|
+
name: asset.split("/").pop(),
|
|
396
|
+
status: "finished",
|
|
397
|
+
url: asset,
|
|
398
|
+
type: field.accept?.includes("image") ? "image/jpeg" : undefined,
|
|
399
|
+
}
|
|
400
|
+
: {
|
|
401
|
+
id: asset.id,
|
|
402
|
+
name: asset.name || asset.id,
|
|
403
|
+
status: "finished",
|
|
404
|
+
url: (asset as Asset).publicURL,
|
|
405
|
+
type: asset.type,
|
|
406
|
+
},
|
|
407
|
+
) as UploadFileInfo[];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const fileList = ref<undefined | UploadFileInfo[]>(getFileList());
|
|
411
|
+
async function setModelValue(value?: UploadFileInfo[]) {
|
|
412
|
+
fileList.value = value;
|
|
413
|
+
|
|
414
|
+
if (value) {
|
|
415
|
+
if (value.length) {
|
|
416
|
+
if (
|
|
417
|
+
value.length ===
|
|
418
|
+
value.filter(({ status }) => status === "finished").length
|
|
419
|
+
) {
|
|
420
|
+
const finalFileList = value
|
|
421
|
+
.filter(({ status }) => status === "finished")
|
|
422
|
+
.map((asset) =>
|
|
423
|
+
!asset.file
|
|
424
|
+
? field.isArray
|
|
425
|
+
? (modelValue.value as Asset[]).find(
|
|
426
|
+
(item) => item.id === asset.id,
|
|
427
|
+
)
|
|
428
|
+
: modelValue.value
|
|
429
|
+
: {
|
|
430
|
+
id: fileIdObject.value[asset.id],
|
|
431
|
+
name: asset.name,
|
|
432
|
+
type: asset.type,
|
|
433
|
+
publicURL: asset.url,
|
|
434
|
+
size: asset.file?.size ?? 0,
|
|
435
|
+
createdAt: asset.file?.lastModified ?? 0,
|
|
436
|
+
},
|
|
437
|
+
) as Asset[];
|
|
438
|
+
if (finalFileList.length) {
|
|
439
|
+
modelValue.value = field.isArray ? finalFileList : finalFileList[0];
|
|
440
|
+
await nextTick();
|
|
441
|
+
fileList.value = getFileList();
|
|
442
|
+
} else modelValue.value = undefined;
|
|
443
|
+
}
|
|
444
|
+
} else modelValue.value = undefined;
|
|
445
|
+
} else modelValue.value = undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const fileIdObject = ref<Record<string, string>>({});
|
|
449
|
+
|
|
450
|
+
const { optimizeFile } = useOptimizeFile();
|
|
451
|
+
const { uploadAssetWithProgress } = useAssetUploader();
|
|
452
|
+
|
|
453
|
+
// Determine if optimization is enabled (undefined or true = enabled)
|
|
454
|
+
const shouldOptimize = computed(() => field.optimize !== false);
|
|
455
|
+
const showSkipCompressionTooltip = ref(false);
|
|
456
|
+
|
|
457
|
+
async function customRequest({
|
|
458
|
+
file,
|
|
459
|
+
headers,
|
|
460
|
+
action,
|
|
461
|
+
onFinish,
|
|
462
|
+
onError,
|
|
463
|
+
onProgress,
|
|
464
|
+
}: UploadCustomRequestOptions) {
|
|
465
|
+
// Track this file as being processed
|
|
466
|
+
processingFileId.value = file.id;
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
if (shouldOptimize.value) {
|
|
470
|
+
const originalFile = file.file;
|
|
471
|
+
if (!originalFile) throw new Error("Missing file payload");
|
|
472
|
+
|
|
473
|
+
let fileToUpload: File = originalFile;
|
|
474
|
+
let compressionSkipped = false;
|
|
475
|
+
const isVideo = originalFile.type?.startsWith("video/") ?? false;
|
|
476
|
+
const isPdf =
|
|
477
|
+
originalFile.type === "application/pdf" ||
|
|
478
|
+
originalFile.name.toLowerCase().endsWith(".pdf");
|
|
479
|
+
|
|
480
|
+
if (isVideo || isPdf) {
|
|
481
|
+
showSkipCompressionTooltip.value = true;
|
|
482
|
+
setTimeout(() => {
|
|
483
|
+
showSkipCompressionTooltip.value = false;
|
|
484
|
+
}, 800);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (isVideo) {
|
|
488
|
+
notifyVideoSize(originalFile.size);
|
|
489
|
+
try {
|
|
490
|
+
fileToUpload = await compressVideo(
|
|
491
|
+
new File([originalFile], originalFile.name, {
|
|
492
|
+
type: originalFile.type,
|
|
493
|
+
}),
|
|
494
|
+
);
|
|
495
|
+
} catch (videoError) {
|
|
496
|
+
// If compression was aborted/terminated, use original file
|
|
497
|
+
if (String(videoError).includes("terminated")) {
|
|
498
|
+
fileToUpload = originalFile;
|
|
499
|
+
compressionSkipped = true;
|
|
500
|
+
compressionIndicator.value = null;
|
|
501
|
+
} else throw videoError;
|
|
502
|
+
}
|
|
503
|
+
} else if (isPdf && false) {
|
|
504
|
+
notifyPdfSize(originalFile.size);
|
|
505
|
+
try {
|
|
506
|
+
fileToUpload = await compressPdf(fileToUpload);
|
|
507
|
+
} catch (pdfError) {
|
|
508
|
+
// If compression was aborted, use original file
|
|
509
|
+
if (String(pdfError).toLowerCase().includes("abort")) {
|
|
510
|
+
fileToUpload = originalFile;
|
|
511
|
+
compressionSkipped = true;
|
|
512
|
+
compressionIndicator.value = null;
|
|
513
|
+
} else throw pdfError;
|
|
514
|
+
}
|
|
515
|
+
} else if (originalFile.type?.startsWith("image/")) {
|
|
516
|
+
const optimizationResult = await optimizeFile(
|
|
517
|
+
new File([originalFile], originalFile.name, {
|
|
518
|
+
type: originalFile.type,
|
|
519
|
+
}),
|
|
520
|
+
);
|
|
521
|
+
if (optimizationResult.optimized)
|
|
522
|
+
fileToUpload = optimizationResult.file;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// IMPORTANT: Completely replace the file object to prevent uploading both files
|
|
526
|
+
// Create a new File instance to ensure no reference to original
|
|
527
|
+
if (fileToUpload !== originalFile) {
|
|
528
|
+
fileToUpload = new File([fileToUpload], fileToUpload.name, {
|
|
529
|
+
type: fileToUpload.type,
|
|
530
|
+
lastModified: fileToUpload.lastModified,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// Update the file reference in the upload info
|
|
534
|
+
file.file = fileToUpload;
|
|
535
|
+
file.name = fileToUpload.name;
|
|
536
|
+
file.type = fileToUpload.type;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const { name, extension } = getFileNameAndExtension(file.name);
|
|
540
|
+
|
|
541
|
+
onProgress?.({ percent: 10 });
|
|
542
|
+
|
|
543
|
+
const { result } = await $fetch<apiResponse<Asset>>(
|
|
544
|
+
`${action}?${database.value.slug}_sid=${sessionID.value}`,
|
|
545
|
+
{
|
|
546
|
+
method: "POST",
|
|
547
|
+
credentials: "include",
|
|
548
|
+
headers: headers as Record<string, string>,
|
|
549
|
+
body: { name, size: file.file?.size, type: file.type, extension },
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
if (!result.uploadURL) {
|
|
554
|
+
if (result.id)
|
|
555
|
+
await $fetch<apiResponse<Asset>>(
|
|
556
|
+
`${action}/${result.id}?${database.value.slug}_sid=${sessionID.value}`,
|
|
557
|
+
{
|
|
558
|
+
method: "DELETE",
|
|
559
|
+
credentials: "include",
|
|
560
|
+
headers: headers as Record<string, string>,
|
|
561
|
+
},
|
|
562
|
+
);
|
|
563
|
+
throw new Error("Failed to get upload URL");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await uploadAssetWithProgress({
|
|
567
|
+
url: result.uploadURL as string,
|
|
568
|
+
method: result.uploadURL.includes("s3") ? "PUT" : "POST",
|
|
569
|
+
headers: { "Content-Type": file.type as string },
|
|
570
|
+
file: file.file,
|
|
571
|
+
onProgress,
|
|
572
|
+
});
|
|
573
|
+
compressionIndicator.value = null;
|
|
574
|
+
|
|
575
|
+
file.url = result.publicURL;
|
|
576
|
+
file.status = "finished";
|
|
577
|
+
fileIdObject.value[file.id] = result.id as string;
|
|
578
|
+
// fileList.value = [...(fileList.value || []), file] // Do not manually append to fileList, NUpload handles it
|
|
579
|
+
if (!database.value.size) database.value.size = 0;
|
|
580
|
+
database.value.size += result.size ?? 0;
|
|
581
|
+
|
|
582
|
+
setModelValue(fileList.value);
|
|
583
|
+
|
|
584
|
+
onFinish();
|
|
585
|
+
} catch (error) {
|
|
586
|
+
// Handle terminated/aborted compression
|
|
587
|
+
if (
|
|
588
|
+
String(error).includes("terminated") ||
|
|
589
|
+
String(error).includes("abort")
|
|
590
|
+
) {
|
|
591
|
+
// Current file was being compressed when skip was clicked
|
|
592
|
+
// DON'T modify fileList as this might trigger re-uploads of finished files
|
|
593
|
+
// Just mark as error and finish silently
|
|
594
|
+
file.status = "error";
|
|
595
|
+
compressionIndicator.value = null;
|
|
596
|
+
console.log("Compression skipped for:", file.name);
|
|
597
|
+
onFinish(); // Finish without calling onError to prevent error message
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
compressionIndicator.value = null;
|
|
601
|
+
console.error("Error in customRequest:", error);
|
|
602
|
+
onError();
|
|
603
|
+
} finally {
|
|
604
|
+
// Clean up
|
|
605
|
+
if (processingFileId.value === file.id) {
|
|
606
|
+
processingFileId.value = null;
|
|
607
|
+
}
|
|
608
|
+
// Reset skip flag only after current file is done
|
|
609
|
+
if (skipCompressionRequested.value) {
|
|
610
|
+
skipCompressionRequested.value = false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const table = useState<Table>("table");
|
|
616
|
+
const currentItem = useState<Item>("currentItem");
|
|
617
|
+
|
|
618
|
+
function renderIcon(file: UploadFileInfo) {
|
|
619
|
+
const asset = Array.isArray(modelValue.value)
|
|
620
|
+
? (modelValue.value as Asset[]).find((item) => item.id === file.id)
|
|
621
|
+
: (modelValue.value as Asset);
|
|
622
|
+
if (!asset) return;
|
|
623
|
+
return h(LazyAssetThumb, {
|
|
624
|
+
asset,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const getChecked = (asset: Asset) =>
|
|
629
|
+
!!modelValue.value &&
|
|
630
|
+
([] as (Asset | string)[])
|
|
631
|
+
.concat(modelValue.value)
|
|
632
|
+
.findIndex((value) =>
|
|
633
|
+
typeof value === "string"
|
|
634
|
+
? value === asset.publicURL
|
|
635
|
+
: value.id === asset.id,
|
|
636
|
+
) > -1;
|
|
637
|
+
|
|
638
|
+
const sessionID = useScopedCookie<string>("sid", database.value?.slug);
|
|
639
|
+
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper :field :rule v-model="modelValue">
|
|
3
|
+
<NSwitch v-model:value="modelValue" v-bind="field.inputProps
|
|
4
|
+
? typeof field.inputProps === 'function'
|
|
5
|
+
? field.inputProps(modelValue) ?? {}
|
|
6
|
+
: field.inputProps
|
|
7
|
+
: {}" />
|
|
8
|
+
</FieldWrapper>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script lang="ts" setup>
|
|
12
|
+
import type { FormItemRule } from "naive-ui"
|
|
13
|
+
|
|
14
|
+
const { field } = defineProps<{ field: Field }>()
|
|
15
|
+
|
|
16
|
+
const modelValue = defineModel<boolean>()
|
|
17
|
+
if (modelValue.value === null || modelValue.value === undefined) modelValue.value = false;
|
|
18
|
+
|
|
19
|
+
const rule: FormItemRule = {
|
|
20
|
+
required: field.required,
|
|
21
|
+
validator: async () => {
|
|
22
|
+
await nextTick()
|
|
23
|
+
return fieldValidator(field, modelValue.value)
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
</script>
|