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,136 @@
|
|
|
1
|
+
import type { IDBPDatabase } from 'idb'
|
|
2
|
+
import { openDB } from 'idb'
|
|
3
|
+
|
|
4
|
+
const DB_NAME = 'inicontent-thumbnails'
|
|
5
|
+
const STORE_NAME = 'thumbnails'
|
|
6
|
+
const DB_VERSION = 1
|
|
7
|
+
|
|
8
|
+
let db: IDBPDatabase | null = null
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize the thumbnail cache database
|
|
12
|
+
*/
|
|
13
|
+
async function initDB(): Promise<IDBPDatabase> {
|
|
14
|
+
if (db) return db
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
db = await openDB(DB_NAME, DB_VERSION, {
|
|
18
|
+
upgrade(db) {
|
|
19
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
20
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: 'key' })
|
|
21
|
+
store.createIndex('timestamp', 'timestamp', { unique: false })
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
return db
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.warn('Failed to initialize thumbnail cache:', error)
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get cached thumbnail from IndexedDB
|
|
34
|
+
* @param key - Unique key for the asset
|
|
35
|
+
* @returns Cached thumbnail data URL or null if not found
|
|
36
|
+
*/
|
|
37
|
+
export async function getCachedThumbnail(key: string): Promise<string | null> {
|
|
38
|
+
try {
|
|
39
|
+
const database = await initDB()
|
|
40
|
+
const cached = await database.get(STORE_NAME, key)
|
|
41
|
+
return cached?.data || null
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn(`Failed to retrieve cached thumbnail for ${key}:`, error)
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Cache a generated thumbnail in IndexedDB
|
|
50
|
+
* @param key - Unique key for the asset
|
|
51
|
+
* @param dataUrl - Generated thumbnail data URL
|
|
52
|
+
* @param expirationDays - Cache expiration in days (default: 30)
|
|
53
|
+
*/
|
|
54
|
+
export async function cacheThumbnail(
|
|
55
|
+
key: string,
|
|
56
|
+
dataUrl: string,
|
|
57
|
+
expirationDays = 30,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
try {
|
|
60
|
+
const database = await initDB()
|
|
61
|
+
const expirationTime = Date.now() + expirationDays * 24 * 60 * 60 * 1000
|
|
62
|
+
|
|
63
|
+
await database.put(STORE_NAME, {
|
|
64
|
+
key,
|
|
65
|
+
data: dataUrl,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
expiration: expirationTime,
|
|
68
|
+
})
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn(`Failed to cache thumbnail for ${key}:`, error)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clear expired thumbnails from cache
|
|
76
|
+
*/
|
|
77
|
+
export async function clearExpiredThumbnails(): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
const database = await initDB()
|
|
80
|
+
const allThumbnails = await database.getAll(STORE_NAME)
|
|
81
|
+
const now = Date.now()
|
|
82
|
+
|
|
83
|
+
for (const thumbnail of allThumbnails) {
|
|
84
|
+
if (thumbnail.expiration && thumbnail.expiration < now) {
|
|
85
|
+
await database.delete(STORE_NAME, thumbnail.key)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn('Failed to clear expired thumbnails:', error)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clear all the cache
|
|
95
|
+
*/
|
|
96
|
+
export async function clearAllCache(): Promise<void> {
|
|
97
|
+
try {
|
|
98
|
+
const database = await initDB()
|
|
99
|
+
await database.clear(STORE_NAME)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn('Failed to clear all thumbnails:', error)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get cache stats (useful for debugging)
|
|
107
|
+
*/
|
|
108
|
+
export async function getCacheStats(): Promise<{
|
|
109
|
+
total: number
|
|
110
|
+
expired: number
|
|
111
|
+
size: number
|
|
112
|
+
}> {
|
|
113
|
+
try {
|
|
114
|
+
const database = await initDB()
|
|
115
|
+
const allThumbnails = await database.getAll(STORE_NAME)
|
|
116
|
+
const now = Date.now()
|
|
117
|
+
let totalSize = 0
|
|
118
|
+
let expiredCount = 0
|
|
119
|
+
|
|
120
|
+
for (const thumbnail of allThumbnails) {
|
|
121
|
+
if (thumbnail.expiration && thumbnail.expiration < now) {
|
|
122
|
+
expiredCount++
|
|
123
|
+
}
|
|
124
|
+
totalSize += thumbnail.data?.length || 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
total: allThumbnails.length,
|
|
129
|
+
expired: expiredCount,
|
|
130
|
+
size: totalSize, // in bytes
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.warn('Failed to get cache stats:', error)
|
|
134
|
+
return { total: 0, expired: 0, size: 0 }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { ref } from "vue"
|
|
2
|
+
import { FFmpeg } from "@ffmpeg/ffmpeg"
|
|
3
|
+
import { fetchFile, toBlobURL } from "@ffmpeg/util"
|
|
4
|
+
|
|
5
|
+
// Use a specific version for stability and the single-threaded core
|
|
6
|
+
const FFMPEG_VERSION = "0.12.6"
|
|
7
|
+
const CORE_URL = `https://unpkg.com/@ffmpeg/core-mt@${FFMPEG_VERSION}/dist/esm/ffmpeg-core.js`
|
|
8
|
+
const WASM_URL = `https://unpkg.com/@ffmpeg/core-mt@${FFMPEG_VERSION}/dist/esm/ffmpeg-core.wasm`
|
|
9
|
+
const WORKER_URL = `https://unpkg.com/@ffmpeg/core-mt@${FFMPEG_VERSION}/dist/esm/ffmpeg-core.worker.js`
|
|
10
|
+
|
|
11
|
+
// Queue system for multiple files
|
|
12
|
+
interface CompressionTask {
|
|
13
|
+
file: File
|
|
14
|
+
resolve: (file: File) => void
|
|
15
|
+
reject: (error: Error) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useVideoCompressor() {
|
|
19
|
+
const loading = ref(false)
|
|
20
|
+
const progress = ref(0)
|
|
21
|
+
const queue = ref<CompressionTask[]>([])
|
|
22
|
+
const isProcessing = ref(false)
|
|
23
|
+
const skipRemaining = ref(false) // Skip compression for remaining files
|
|
24
|
+
let ffmpeg: FFmpeg | null = null
|
|
25
|
+
let currentTask: CompressionTask | null = null
|
|
26
|
+
|
|
27
|
+
const abort = () => {
|
|
28
|
+
try {
|
|
29
|
+
if (ffmpeg) {
|
|
30
|
+
ffmpeg.terminate()
|
|
31
|
+
ffmpeg = null
|
|
32
|
+
}
|
|
33
|
+
// Mark to skip compression for remaining files
|
|
34
|
+
skipRemaining.value = true
|
|
35
|
+
|
|
36
|
+
// Resolve all pending tasks with original files (skip compression)
|
|
37
|
+
while (queue.value.length > 0) {
|
|
38
|
+
const task = queue.value.shift()
|
|
39
|
+
if (task) {
|
|
40
|
+
task.resolve(task.file) // Return original file
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Reject current task only (it was being compressed)
|
|
44
|
+
if (currentTask) {
|
|
45
|
+
currentTask.reject(new Error("terminated"))
|
|
46
|
+
currentTask = null
|
|
47
|
+
}
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error("Error aborting FFmpeg:", e)
|
|
50
|
+
} finally {
|
|
51
|
+
loading.value = false
|
|
52
|
+
progress.value = 0
|
|
53
|
+
isProcessing.value = false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const resetSkip = () => {
|
|
58
|
+
skipRemaining.value = false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const processQueue = async () => {
|
|
62
|
+
if (isProcessing.value || queue.value.length === 0) return
|
|
63
|
+
|
|
64
|
+
isProcessing.value = true
|
|
65
|
+
|
|
66
|
+
while (queue.value.length > 0) {
|
|
67
|
+
const task = queue.value.shift()
|
|
68
|
+
if (!task) continue
|
|
69
|
+
|
|
70
|
+
currentTask = task
|
|
71
|
+
try {
|
|
72
|
+
// If skip was requested, return original file without compression
|
|
73
|
+
if (skipRemaining.value) {
|
|
74
|
+
task.resolve(task.file)
|
|
75
|
+
} else {
|
|
76
|
+
const compressedFile = await compressVideoInternal(task.file)
|
|
77
|
+
task.resolve(compressedFile)
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
task.reject(error as Error)
|
|
81
|
+
}
|
|
82
|
+
currentTask = null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
isProcessing.value = false
|
|
86
|
+
// Reset skip flag after processing all files
|
|
87
|
+
skipRemaining.value = false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const compressVideoInternal = async (videoFile: File): Promise<File> => {
|
|
91
|
+
ffmpeg = new FFmpeg()
|
|
92
|
+
let tempOutputFileName: string | null = null
|
|
93
|
+
let inputFileName: string | null = null
|
|
94
|
+
|
|
95
|
+
// ffmpeg.on("log", ({ message }) => {
|
|
96
|
+
// console.log(message)
|
|
97
|
+
// })
|
|
98
|
+
|
|
99
|
+
ffmpeg.on("progress", ({ progress: p, time }) => {
|
|
100
|
+
progress.value = p
|
|
101
|
+
// console.log(`Progress: ${p * 100}%, time: ${time / 1000000}s`)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
loading.value = true
|
|
106
|
+
progress.value = 0
|
|
107
|
+
|
|
108
|
+
await ffmpeg.load({
|
|
109
|
+
coreURL: await toBlobURL(CORE_URL, "text/javascript"),
|
|
110
|
+
wasmURL: await toBlobURL(WASM_URL, "application/wasm"),
|
|
111
|
+
workerURL: await toBlobURL(WORKER_URL, "text/javascript"),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Use unique filename to avoid conflicts
|
|
115
|
+
inputFileName = `input_${Date.now()}_${videoFile.name}`
|
|
116
|
+
tempOutputFileName = `temp_output_${Date.now()}.mp4`
|
|
117
|
+
const finalOutputName = `${videoFile.name.split(".").slice(0, -1).join(".")}.mp4`
|
|
118
|
+
|
|
119
|
+
await ffmpeg.writeFile(inputFileName, await fetchFile(videoFile))
|
|
120
|
+
|
|
121
|
+
// Execute FFmpeg command for compression
|
|
122
|
+
await ffmpeg.exec([
|
|
123
|
+
"-i",
|
|
124
|
+
inputFileName,
|
|
125
|
+
"-vcodec",
|
|
126
|
+
"libx264",
|
|
127
|
+
"-crf",
|
|
128
|
+
"28", // Adjust CRF for quality/size balance (lower is better quality)
|
|
129
|
+
"-preset",
|
|
130
|
+
"fast", // 'fast' provides a good balance of speed and compression
|
|
131
|
+
"-movflags",
|
|
132
|
+
"+faststart",
|
|
133
|
+
tempOutputFileName,
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
const data = await ffmpeg.readFile(tempOutputFileName)
|
|
137
|
+
|
|
138
|
+
// Create a new File object from the compressed data with a NEW instance
|
|
139
|
+
// This ensures the original file is not referenced
|
|
140
|
+
const blob = new Blob([data as any], { type: "video/mp4" })
|
|
141
|
+
const compressedFile = new File(
|
|
142
|
+
[blob],
|
|
143
|
+
finalOutputName,
|
|
144
|
+
{
|
|
145
|
+
type: "video/mp4",
|
|
146
|
+
lastModified: Date.now()
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return compressedFile
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (String(error).includes("terminate")) {
|
|
153
|
+
throw new Error("terminated")
|
|
154
|
+
}
|
|
155
|
+
console.error("Video compression failed:", error)
|
|
156
|
+
return videoFile // Return original file on other errors
|
|
157
|
+
} finally {
|
|
158
|
+
loading.value = false
|
|
159
|
+
progress.value = 0
|
|
160
|
+
if (ffmpeg) {
|
|
161
|
+
try {
|
|
162
|
+
if (inputFileName) await ffmpeg.deleteFile(inputFileName)
|
|
163
|
+
if (tempOutputFileName) await ffmpeg.deleteFile(tempOutputFileName)
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
console.warn("Could not cleanup files, ffmpeg may have been terminated.", e)
|
|
167
|
+
}
|
|
168
|
+
// Terminate FFmpeg instance to free resources
|
|
169
|
+
try {
|
|
170
|
+
await ffmpeg.terminate()
|
|
171
|
+
ffmpeg = null
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.warn("Could not terminate ffmpeg:", e)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const compressVideo = async (videoFile: File): Promise<File> => {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
queue.value.push({ file: videoFile, resolve, reject })
|
|
182
|
+
processQueue()
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
compressVideo,
|
|
188
|
+
loading,
|
|
189
|
+
progress,
|
|
190
|
+
abort,
|
|
191
|
+
resetSkip,
|
|
192
|
+
queueLength: computed(() => queue.value.length)
|
|
193
|
+
}
|
|
194
|
+
}
|
package/app/error.vue
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Offline / no-data bootstrap failures render OUTSIDE the layout chain.
|
|
4
|
+
When this error is registered during the very first navigation (e.g.
|
|
5
|
+
the database middleware fails before anything is cached), Nuxt's root
|
|
6
|
+
<Suspense> never settles while error.vue's lazy layout chain
|
|
7
|
+
(dashboard → default → LazyHeader…) resolves, so the app stays blank.
|
|
8
|
+
A synchronous panel here renders instantly instead.
|
|
9
|
+
-->
|
|
10
|
+
<div v-if="rendersOffline" class="offline-page">
|
|
11
|
+
<NFlex vertical justify="center" align="center" style="min-height: 60vh">
|
|
12
|
+
<NResult :status="code" :title="message" />
|
|
13
|
+
</NFlex>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<NuxtLayout v-else :name="layoutName">
|
|
17
|
+
<NFlex vertical justify="center" align="center" style="min-height: 60vh">
|
|
18
|
+
<NResult :status="code" :title="message" />
|
|
19
|
+
</NFlex>
|
|
20
|
+
</NuxtLayout>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import type { NuxtError } from "#app";
|
|
25
|
+
|
|
26
|
+
type errorCodes =
|
|
27
|
+
| "info"
|
|
28
|
+
| "success"
|
|
29
|
+
| "warning"
|
|
30
|
+
| "error"
|
|
31
|
+
| "404"
|
|
32
|
+
| "403"
|
|
33
|
+
| "500"
|
|
34
|
+
| "418";
|
|
35
|
+
const { error } = defineProps({
|
|
36
|
+
error: Object as () => NuxtError & { message: "database" | "table" | "item" },
|
|
37
|
+
});
|
|
38
|
+
const code = ref<errorCodes>();
|
|
39
|
+
const message = ref();
|
|
40
|
+
|
|
41
|
+
const route = useRoute();
|
|
42
|
+
|
|
43
|
+
const isOffline = typeof navigator !== "undefined" && !navigator.onLine;
|
|
44
|
+
|
|
45
|
+
// Network failures while offline (mapped to 503/"offline" by the database
|
|
46
|
+
// middleware) get a friendly offline page — not a raw fetch stack trace.
|
|
47
|
+
const rendersOffline = computed(
|
|
48
|
+
() => !!error && (isOffline || error.statusMessage === "offline"),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (error && !rendersOffline.value) {
|
|
52
|
+
code.value = String(error.statusCode) as errorCodes;
|
|
53
|
+
switch (error.statusCode) {
|
|
54
|
+
case 404:
|
|
55
|
+
code.value = "warning";
|
|
56
|
+
message.value = `${t(error.message)} ${t("notFound")}`;
|
|
57
|
+
break;
|
|
58
|
+
case 403:
|
|
59
|
+
code.value = "warning";
|
|
60
|
+
message.value = t("accessDenied");
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
code.value = "error";
|
|
64
|
+
message.value = t("internalServerError");
|
|
65
|
+
console.clear();
|
|
66
|
+
console.error(error);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
} else if (!error) {
|
|
70
|
+
code.value = "error";
|
|
71
|
+
message.value = t("internalServerError");
|
|
72
|
+
} else {
|
|
73
|
+
// offline branch
|
|
74
|
+
code.value = "warning";
|
|
75
|
+
message.value = t("offline");
|
|
76
|
+
// Match the app's other pages (function form — evaluated reactively and
|
|
77
|
+
// applied after the suspense-resolved mount, unlike a plain object).
|
|
78
|
+
useHead(() => ({
|
|
79
|
+
title: `${t("inicontent")} | ${t("offline")}`,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Pages that run inside the "table" layout (with the sidebar menu) are
|
|
84
|
+
// /admin/tables/:slug/... (incl. backups/assets), /admin/dashboards and
|
|
85
|
+
// /admin/settings. Errors thrown there — like a 404 for a non-existing
|
|
86
|
+
// table — should render inside that layout so the user keeps the sidebar
|
|
87
|
+
// navigation instead of being dropped to the bare dashboard shell.
|
|
88
|
+
const tableErrorMessages = ["table", "item"];
|
|
89
|
+
const layoutName = computed(() => {
|
|
90
|
+
if (error && tableErrorMessages.includes(error.statusMessage ?? ""))
|
|
91
|
+
return "table";
|
|
92
|
+
const segments = route.path.split("/").filter(Boolean);
|
|
93
|
+
const tablesIndex = segments.indexOf("tables");
|
|
94
|
+
if (tablesIndex !== -1 && segments[tablesIndex + 1]) return "table";
|
|
95
|
+
if (
|
|
96
|
+
route.path.includes("/admin/dashboards") ||
|
|
97
|
+
route.path.includes("/admin/settings")
|
|
98
|
+
)
|
|
99
|
+
return "table";
|
|
100
|
+
return "dashboard";
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<style scoped>
|
|
105
|
+
.offline-page {
|
|
106
|
+
min-height: 100vh;
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
padding: 24px;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NuxtLayout name="dashboard">
|
|
3
|
+
<NLayout position="absolute" has-sider>
|
|
4
|
+
<NLayoutSider v-if="database?.slug" :collapsed="!isMenuOpen"
|
|
5
|
+
@update-collapsed="(collapsed) => (isMenuOpen = !collapsed)" style="z-index: 1000" bordered
|
|
6
|
+
show-trigger="bar" collapse-mode="width" :collapsed-width="$device.isMobile ? 0 : 64" width="240"
|
|
7
|
+
:native-scrollbar="false">
|
|
8
|
+
<NMenu :collapsed="!isMenuOpen" :collapsed-icon-size="22" :collapsed-width="$device.isMobile ? 0 : 64"
|
|
9
|
+
:options="menuOptions" :default-value="defaultValue" :watch-props="['defaultValue']" accordion />
|
|
10
|
+
</NLayoutSider>
|
|
11
|
+
<NLayoutContent id="pageContent" position="absolute" :content-style="{
|
|
12
|
+
padding: $device.isMobile
|
|
13
|
+
? '24px 12px'
|
|
14
|
+
: Language === 'ar'
|
|
15
|
+
? '0 88px 24px 24px'
|
|
16
|
+
: '0 24px 24px 88px',
|
|
17
|
+
}" :native-scrollbar="false">
|
|
18
|
+
<div v-if="isMenuOpen" @mouseover="() => (isMenuOpen = false)"
|
|
19
|
+
style="width: 100%; height: 100%; right: 0; top: 0; position: absolute; background-color: #0000006e; z-index: 99; cursor: pointer;">
|
|
20
|
+
</div>
|
|
21
|
+
<slot></slot>
|
|
22
|
+
</NLayoutContent>
|
|
23
|
+
</NLayout>
|
|
24
|
+
</NuxtLayout>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import type { MenuOption } from "naive-ui"
|
|
29
|
+
import { Icon, LazyTableIcon, NuxtLink, NIcon } from "#components"
|
|
30
|
+
|
|
31
|
+
const database = useState<Database>("database");
|
|
32
|
+
const Language = useScopedCookie<LanguagesType>("language", database.value?.slug)
|
|
33
|
+
|
|
34
|
+
onBeforeUpdate(() => {
|
|
35
|
+
clearNuxtState("isMenuOpen")
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const route = useRoute()
|
|
39
|
+
const isMenuOpen = useState("isMenuOpen", () => false)
|
|
40
|
+
const table = useState<Table>("table")
|
|
41
|
+
|
|
42
|
+
const defaultValue = computed(() => {
|
|
43
|
+
const rawMethodKey = route.query.method as string | undefined
|
|
44
|
+
const normalizedMethod = rawMethodKey
|
|
45
|
+
? methodAliases[rawMethodKey.toLowerCase()] ?? rawMethodKey
|
|
46
|
+
: undefined
|
|
47
|
+
const methodKey = normalizedMethod ?? "overview"
|
|
48
|
+
if (table.value?.slug) return `${table.value.slug}-${methodKey}`
|
|
49
|
+
const lastPathInRoute = decodeURIComponent(
|
|
50
|
+
route.path.split("/").filter(Boolean).at(-1) ?? "",
|
|
51
|
+
)
|
|
52
|
+
return lastPathInRoute
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const methodAliases: Record<string, string> = {
|
|
56
|
+
create: "post",
|
|
57
|
+
read: "get",
|
|
58
|
+
update: "put",
|
|
59
|
+
post: "post",
|
|
60
|
+
get: "get",
|
|
61
|
+
put: "put",
|
|
62
|
+
delete: "delete",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const methodEntries = [
|
|
66
|
+
{
|
|
67
|
+
key: "overview",
|
|
68
|
+
icon: "tabler:book",
|
|
69
|
+
label: () => t("overview"),
|
|
70
|
+
show: () => true,
|
|
71
|
+
to: (baseUrl: string) => baseUrl,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: "post",
|
|
75
|
+
icon: "tabler:plus",
|
|
76
|
+
label: () => t("methods.post"),
|
|
77
|
+
show: (allowed?: string) => allowed?.includes("c"),
|
|
78
|
+
to: (baseUrl: string) => ({ path: baseUrl, query: { method: "post" } }),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: "get",
|
|
82
|
+
icon: "tabler:eye",
|
|
83
|
+
label: () => t("methods.get"),
|
|
84
|
+
show: (allowed?: string) => allowed?.includes("r"),
|
|
85
|
+
to: (baseUrl: string) => ({ path: baseUrl, query: { method: "get" } }),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "put",
|
|
89
|
+
icon: "tabler:pencil",
|
|
90
|
+
label: () => t("methods.put"),
|
|
91
|
+
show: (allowed?: string) => allowed?.includes("u"),
|
|
92
|
+
to: (baseUrl: string) => ({ path: baseUrl, query: { method: "put" } }),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "delete",
|
|
96
|
+
icon: "tabler:trash",
|
|
97
|
+
label: () => t("methods.delete"),
|
|
98
|
+
show: (allowed?: string) => allowed?.includes("d"),
|
|
99
|
+
to: (baseUrl: string) => ({ path: baseUrl, query: { method: "delete" } }),
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
function buildMethodChildren(currentTable: Table) {
|
|
104
|
+
const baseUrl = `${route.params.database ? `/${route.params.database}` : ""}/admin/api/tables/${currentTable.slug}`
|
|
105
|
+
return methodEntries
|
|
106
|
+
.filter((entry) => entry.show(currentTable.allowedMethods))
|
|
107
|
+
.map((entry) => ({
|
|
108
|
+
label: () =>
|
|
109
|
+
h(
|
|
110
|
+
NuxtLink,
|
|
111
|
+
{
|
|
112
|
+
to: entry.to(baseUrl),
|
|
113
|
+
},
|
|
114
|
+
{ default: () => entry.label() },
|
|
115
|
+
),
|
|
116
|
+
key: `${currentTable.slug}-${entry.key}`,
|
|
117
|
+
icon: () => h(NIcon, () => h(Icon, { name: entry.icon })),
|
|
118
|
+
}))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildMenuOption(currentTable: Table): MenuOption {
|
|
122
|
+
const children = buildMethodChildren(currentTable)
|
|
123
|
+
return {
|
|
124
|
+
label: () =>
|
|
125
|
+
h(
|
|
126
|
+
NuxtLink,
|
|
127
|
+
{
|
|
128
|
+
to: `${route.params.database ? `/${route.params.database}` : ""}/admin/api/tables/${currentTable.slug}`,
|
|
129
|
+
},
|
|
130
|
+
{ default: () => t(currentTable.slug) },
|
|
131
|
+
),
|
|
132
|
+
key: `${currentTable.slug}Group`,
|
|
133
|
+
icon: () => h(LazyTableIcon, { table: currentTable }),
|
|
134
|
+
children: [
|
|
135
|
+
{
|
|
136
|
+
type: "group",
|
|
137
|
+
label: t(currentTable.slug),
|
|
138
|
+
key: currentTable.slug,
|
|
139
|
+
children,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const primaryTables = computed(() =>
|
|
146
|
+
(database.value?.tables ?? []).filter(
|
|
147
|
+
({ slug, allowedMethods, show }) =>
|
|
148
|
+
![
|
|
149
|
+
"users",
|
|
150
|
+
"sessions",
|
|
151
|
+
"assets",
|
|
152
|
+
"translations",
|
|
153
|
+
"pages",
|
|
154
|
+
"blocks",
|
|
155
|
+
"templates",
|
|
156
|
+
].includes(slug) &&
|
|
157
|
+
allowedMethods?.includes("r") &&
|
|
158
|
+
show !== false,
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
const secondaryTables = computed(() =>
|
|
163
|
+
(database.value?.tables ?? []).filter(
|
|
164
|
+
({ slug, allowedMethods, show }) =>
|
|
165
|
+
[
|
|
166
|
+
"users",
|
|
167
|
+
"sessions",
|
|
168
|
+
"assets",
|
|
169
|
+
"translations",
|
|
170
|
+
"pages",
|
|
171
|
+
"blocks",
|
|
172
|
+
"templates",
|
|
173
|
+
].includes(slug) &&
|
|
174
|
+
allowedMethods?.includes("r") &&
|
|
175
|
+
show !== false,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const menuOptions = computed(() => {
|
|
180
|
+
if (!database.value?.tables) return [] as MenuOption[]
|
|
181
|
+
const options: MenuOption[] = []
|
|
182
|
+
options.push(...primaryTables.value.map(buildMenuOption))
|
|
183
|
+
if (secondaryTables.value.length)
|
|
184
|
+
options.push({ key: "divider-1", type: "divider" })
|
|
185
|
+
options.push(...secondaryTables.value.map(buildMenuOption))
|
|
186
|
+
return options
|
|
187
|
+
})
|
|
188
|
+
</script>
|