-
-
-
-
-
- IPTC-IIM
-
+
+
+
+
+
+ IPTC-IIM
+
-
-
-
-
-
-
-
-
-
-
-
- No files selected!
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/Editor/Field.vue b/app/components/Editor/Field.vue
index f9c86b3..1b94c14 100644
--- a/app/components/Editor/Field.vue
+++ b/app/components/Editor/Field.vue
@@ -1,27 +1,95 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/app/components/Editor/Field/Date.vue b/app/components/Editor/Field/Date.vue
index 8e05a7c..7148a2f 100644
--- a/app/components/Editor/Field/Date.vue
+++ b/app/components/Editor/Field/Date.vue
@@ -2,7 +2,9 @@
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'
import { CalendarDate } from '@internationalized/date'
-defineProps<{
+const props = defineProps<{
+ fileId: string
+ original?: string
disabled?: boolean
required?: boolean
}>()
@@ -51,11 +53,12 @@ function updateDate(newDate?: CalendarDate) {
}
}
-const hasChanged = useHasChanged(field)
+const fileId = computed(() => props.fileId)
+const hasChanged = useHasChanged(fileId, field)
-
+
diff --git a/app/components/Editor/Field/Language.vue b/app/components/Editor/Field/Language.vue
index 175dcdb..ec52824 100644
--- a/app/components/Editor/Field/Language.vue
+++ b/app/components/Editor/Field/Language.vue
@@ -1,9 +1,15 @@
-
+
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'
+const props = defineProps<{
+ fileId: string
+ originalCode?: string
+ originalName?: string
+}>()
+
const code = defineModel('code', { required: true })
const name = defineModel('name', { required: true })
-const hasChanged = useHasChanged(code)
+const fileId = computed(() => props.fileId)
+const hasChanged = useHasChanged(fileId, code)
const selectedCountry = computed({
get: () => ({
@@ -29,7 +36,7 @@ const formattedTitle = computed(() => {
-
+
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'
+const props = defineProps<{
+ fileId: string
+ original?: string
+}>()
+
const field = defineModel({ required: true })
const rawValue = computed({
@@ -17,11 +22,12 @@ const rawValue = computed({
},
})
-const hasChanged = useHasChanged(field)
+const fileId = computed(() => props.fileId)
+const hasChanged = useHasChanged(fileId, field)
-
+
()
+
const field = defineModel({ required: true })
const objectData = computed(() => {
@@ -21,11 +26,12 @@ const rawValue = computed({
},
})
-const hasChanged = useHasChanged(field)
+const fileId = computed(() => props.fileId)
+const hasChanged = useHasChanged(fileId, field)
-
+
(), {
alignment: 'horizontal',
@@ -21,8 +25,24 @@ watchEffect(() => {
-
-
-
+
+
+
diff --git a/app/components/Editor/Field/Select.vue b/app/components/Editor/Field/Select.vue
index 53eb7ae..6342b76 100644
--- a/app/components/Editor/Field/Select.vue
+++ b/app/components/Editor/Field/Select.vue
@@ -1,6 +1,11 @@
-
+
{{ rawValue?.label }}
diff --git a/app/components/Editor/Field/Slider.vue b/app/components/Editor/Field/Slider.vue
index c636a6d..bf5603b 100644
--- a/app/components/Editor/Field/Slider.vue
+++ b/app/components/Editor/Field/Slider.vue
@@ -1,6 +1,11 @@
-
+
{{ field.minLabel }}
(), {
alignment: 'horizontal',
@@ -100,21 +102,33 @@ const selectedSubjectDetail = computed({
})
const originalSubject = computed(() => {
- const parts = field.value.original.split(':')
+ if (!props.original) {
+ return undefined
+ }
+
+ const parts = props.original.split(':')
const original = parts.length === 4 ? parts[1] : ''
return original === '00' ? undefined : original
})
const originalSubjectMatter = computed(() => {
- const parts = field.value.original.split(':')
+ if (!props.original) {
+ return undefined
+ }
+
+ const parts = props.original.split(':')
const original = parts.length === 4 ? parts[2] : ''
return original === '000' ? undefined : original
})
const originalSubjectDetail = computed(() => {
- const parts = field.value.original.split(':')
+ if (!props.original) {
+ return undefined
+ }
+
+ const parts = props.original.split(':')
const original = parts.length === 4 ? parts[3] : ''
return original === '000' ? undefined : original
diff --git a/app/components/Editor/Field/Text.vue b/app/components/Editor/Field/Text.vue
index db903af..04aa01a 100644
--- a/app/components/Editor/Field/Text.vue
+++ b/app/components/Editor/Field/Text.vue
@@ -1,21 +1,24 @@
-
+
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'
+const props = defineProps<{
+ fileId: string
+ original?: string
+}>()
+
const field = defineModel({ required: true })
-const hasChanged = useHasChanged(field)
+const fileId = computed(() => props.fileId)
+const hasChanged = useHasChanged(fileId, field)
const currentValue = computed(() => field.value.value || '')
const { limits, characterCountWidth, characterCountText } = useCharacterLimit(currentValue, field.value.octets)
-
+
()
+
const field = defineModel({ required: true })
const timeValue = ref<{ time: string, offset: string }>({ time: '', offset: '' })
@@ -53,11 +58,12 @@ const offsetSelectValues = computed(() => {
})
})
-const hasChanged = useHasChanged(field)
+const fileId = computed(() => props.fileId)
+const hasChanged = useHasChanged(fileId, field)
function reset() {
- const originalTime = field.value.original.slice(0, 6)
- const originalOffset = field.value.original.slice(6)
+ const originalTime = props.original?.slice(0, 6) ?? ''
+ const originalOffset = props.original?.slice(6) ?? ''
updateTime(originalTime)
updateOffset(originalOffset)
diff --git a/app/components/Editor/FileInformation.vue b/app/components/Editor/FileInformation.vue
index 922fecd..aaaecfd 100644
--- a/app/components/Editor/FileInformation.vue
+++ b/app/components/Editor/FileInformation.vue
@@ -1,23 +1,37 @@
-
-
-
-
-
+
+
+
+
File Properties
diff --git a/app/components/File/Display.vue b/app/components/File/Display.vue
deleted file mode 100644
index 2fefc9c..0000000
--- a/app/components/File/Display.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- This will be image :)
-
-
![Uploaded Image]()
-
-
-
diff --git a/app/components/File/List.vue b/app/components/File/List.vue
index 6ac1329..81d33a3 100644
--- a/app/components/File/List.vue
+++ b/app/components/File/List.vue
@@ -2,18 +2,18 @@
import type { FileWithMetadata } from '~/shared/types'
defineProps<{
- files: FileWithMetadata[]
+ files: FilesIdb
}>()
const emit = defineEmits<{
(e: 'select', file: FileWithMetadata): void
- (e: 'remove', file: FileWithMetadata): void
+ (e: 'remove', fileId: string): void
}>()
-
+
()
const file = computed(() => props.image.file)
const fileUrl = URL.createObjectURL(file.value)
const fileSize = (file.value.size / 1024).toFixed(2)
const altText = `${file.value.name} - ${fileSize} KB`
+
+const { isSelected } = useFileSelection()
-
+
@@ -37,6 +39,6 @@ const altText = `${file.value.name} - ${fileSize} KB`
{{ formatDate(file.lastModified).value }}
-
+
diff --git a/app/components/File/Upload.vue b/app/components/File/Upload.vue
deleted file mode 100644
index 6e0212f..0000000
--- a/app/components/File/Upload.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/composables/helpers.ts b/app/composables/helpers.ts
new file mode 100644
index 0000000..7c17bcb
--- /dev/null
+++ b/app/composables/helpers.ts
@@ -0,0 +1,52 @@
+import type { FileWithHandle } from 'browser-fs-access'
+import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval'
+
+/**
+ * Loads data from IndexedDB for a given key.
+ * @param key The key to load data for.
+ * @param defaultValue The default value to return if no data is found.
+ * @returns The data associated with the key or the default value.
+ */
+export async function loadFromIdb
(key: string, defaultValue: T): Promise {
+ const { data, isFinished } = useIDBKeyval(key, defaultValue)
+
+ await until(isFinished).toBe(true)
+
+ return data.value
+}
+
+/**
+ * Generates a unique ID for a given file.
+ * @param file The file for which to generate an ID.
+ * @returns A unique string ID for the file.
+ */
+export function getFileId(file: FileWithHandle): string {
+ return file.name + file.lastModified + file.size
+}
+
+/**
+ * Deeploy converts a reactive object to its raw form so it can savely be stored in IndexedDB.
+ * @param obj The object to convert. Can be anything.
+ * @returns The raw version of the object.
+ */
+function toRawDeep(obj: any): any {
+ if (Array.isArray(obj) && isReactive(obj)) {
+ return toRaw(obj.map(item => toRawDeep(item)))
+ }
+
+ return isReactive(obj) ? toRaw(obj) : obj
+}
+
+/**
+ * Updates data in IndexedDB for a given key.
+ * @param key The key to update data for.
+ * @param data The data to be stored.
+ */
+export async function updateIdb(key: string, data: T) {
+ const { set, data: storedData } = useIDBKeyval(key, data)
+ const rawData = toRawDeep(data)
+
+ await set(rawData)
+
+ return storedData
+}
diff --git a/app/composables/useFileSelection.ts b/app/composables/useFileSelection.ts
new file mode 100644
index 0000000..bc3c0a1
--- /dev/null
+++ b/app/composables/useFileSelection.ts
@@ -0,0 +1,88 @@
+import type { FileWithMetadata } from '~/shared/types'
+
+type FileSelection = Record
+
+const ALLOW_MULTIPLE_SELECTION = false
+const IDB_KEY_SELECTED_FILES = 'selected-files'
+
+const selections = ref({})
+
+export default function useFileSelection() {
+ async function loadSelectedFileIdsFromIndexedDB() {
+ if (import.meta.env.SSR) {
+ return
+ }
+
+ const files = await loadFromIdb(IDB_KEY_SELECTED_FILES, {})
+ selections.value = files ?? {}
+ }
+
+ function getSelectedIds(): string[] {
+ return Object.entries(selections.value).filter(([_, isSelected]) => isSelected).map(([fileId, _]) => fileId)
+ }
+
+ function toggleSelection(file: FileWithMetadata, modifier: 'shift' | 'ctrl' | undefined = undefined) {
+ if (ALLOW_MULTIPLE_SELECTION) {
+ if (modifier === 'shift') {
+ if (getSelectedIds().length === 0) {
+ handleNormalSelect(file.id)
+ }
+ else {
+ // TODO: reimplement shift selection
+ // const lastSelectedIndex = selectedIndexes.value[selectedIndexes.value.length - 1] ?? 0
+ // handleShiftSelect(selectedFileIndex, lastSelectedIndex)
+ }
+ }
+ else if (modifier === 'ctrl') {
+ handleCtrlSelect(file.id)
+ }
+ }
+ else {
+ handleNormalSelect(file.id)
+ }
+ }
+
+ /* TODO: Reimplement shift selection
+ function handleShiftSelect(fileIndex: number, lastSelectedIndex: number) {
+ const [start, end] = fileIndex < lastSelectedIndex ? [fileIndex, lastSelectedIndex] : [lastSelectedIndex, fileIndex]
+ selectedIndexes.value.push(...Array.from({ length: end - start + 1 }, (_, i) => start + i))
+ }
+ */
+
+ function handleCtrlSelect(fileId: string) {
+ selections.value[fileId] = !selections.value[fileId]
+ }
+
+ function handleNormalSelect(fileId: string) {
+ const areMoreSelected = getSelectedIds().length > 1
+ const isSelectedBefore = isSelected(fileId)
+
+ deselectAll()
+
+ if ((isSelectedBefore && areMoreSelected) || !isSelectedBefore) {
+ selections.value[fileId] = true
+ }
+ }
+
+ function deselectAll() {
+ Object.keys(selections.value).forEach((key) => {
+ selections.value[key] = false
+ })
+ }
+
+ function isSelected(fileId: string): boolean {
+ return selections.value[fileId] ?? false
+ }
+
+ const firstSelectedId = computed(() => {
+ const selectedIds = getSelectedIds()
+ return selectedIds.length > 0 ? selectedIds[0] : null
+ })
+
+ const firstSelectedFile = computed(() => {
+ const { loadedFiles } = useFiles()
+ return firstSelectedId.value ? loadedFiles.value[firstSelectedId.value] : null
+ })
+
+ return { toggleSelection, selections, isSelected, firstSelectedId, firstSelectedFile, loadSelectedFileIdsFromIndexedDB, getSelectedIds }
+}
diff --git a/app/composables/useFileState.ts b/app/composables/useFileState.ts
index 0c7da94..2817f96 100644
--- a/app/composables/useFileState.ts
+++ b/app/composables/useFileState.ts
@@ -1,5 +1,4 @@
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'
-import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval.mjs'
import { supported } from 'browser-fs-access'
import { iptcIimFields } from '~/utils/iptc-iim/mapping'
@@ -9,38 +8,29 @@ const fileStates = ref({})
const isLoading = ref(true)
export default function useFileState() {
- const { loadedFiles, updateMetadata, reloadFiles, markAsDownloaded } = useFiles()
-
- if (!import.meta.env.SSR) {
- loadFileStatesFromIndexedDB()
- }
+ const { loadedFiles, updateMetadata, markAsDownloaded } = useFiles()
async function loadFileStatesFromIndexedDB() {
- const { data: states, isFinished } = useIDBKeyval('file-states', {})
-
- await until(isFinished).toBe(true)
+ if (import.meta.env.SSR) {
+ return
+ }
- fileStates.value = states.value
+ const states = await loadFromIdb('file-states', {})
+ fileStates.value = states ?? {}
isLoading.value = false
}
- async function updateIdb() {
- const { set } = useIDBKeyval('file-states', fileStates.value)
- await set(toRaw(fileStates.value))
- }
+ function setupFileState(fileId: string) {
+ const file = loadedFiles.value[fileId]
+
+ if (!file) {
+ throw new Error('File not found for setting up state')
+ }
- function setupState() {
- const state = iptcIimFields.map(field => ({
+ fileStates.value[fileId] = iptcIimFields.map(field => ({
...field,
- value: '',
- original: '',
+ value: file.metadata[field.key] || '',
}))
-
- return state
- }
-
- function setupFileState(fileId: string) {
- fileStates.value[fileId] = setupState()
}
function removeFileState(fileId: string) {
@@ -59,21 +49,13 @@ export default function useFileState() {
fileStates.value[fileId]!.map((field) => {
if (field.key === key) {
field.value = newValue ?? ''
- if (!field.original) {
- field.original = field.value
- }
}
return field
})
}
- function hasFieldChanged(metadata: Record, key: string, currentValue: string) {
- const originalValue = metadata[key] || ''
- return originalValue !== currentValue
- }
-
function fileChanges(fileId: string) {
- const file = loadedFiles.value.find(file => file.id === fileId)
+ const file = loadedFiles.value[fileId]
if (!file) {
return 0
}
@@ -82,8 +64,7 @@ export default function useFileState() {
let changes = 0
state.forEach((field) => {
- const originalValue = file.metadata[field.key] || ''
- if (originalValue !== field.value) {
+ if (file.metadata[field.key] !== field.value) {
changes++
}
})
@@ -91,17 +72,7 @@ export default function useFileState() {
return changes
}
- const filesChanged = ref(0)
-
- function stateIndexByKey(state: IPTCFieldWithValue[], key: string) {
- const field = state.find(f => f.key === key)
-
- if (!field) {
- throw new Error(`Field with key ${key} not found in state`)
- }
-
- return state.indexOf(field)
- }
+ const filesChanged = computed(() => Object.keys(fileStates.value).filter(fileId => fileChanges(fileId) > 0).length)
async function saveAll() {
if (!filesChanged.value) {
@@ -114,7 +85,7 @@ export default function useFileState() {
}))
statesToSave.forEach(async ({ fileId, state }) => {
- const file = loadedFiles.value.find(file => file.id === fileId)
+ const file = loadedFiles.value[fileId]
if (!file) {
throw new Error('File not found for saving metadata')
}
@@ -132,31 +103,21 @@ export default function useFileState() {
color: 'success',
})
- if (supported) {
- reloadFiles()
- }
- else {
+ if (!supported) {
markAsDownloaded(statesToSave.map(s => s.fileId))
}
}
- watchDeep(() => fileStates.value, async (updatedStates) => {
- const totalChanges = Object.keys(updatedStates).filter(fileId => fileChanges(fileId) > 0).length
- filesChanged.value = totalChanges
-
- await updateIdb()
- })
-
return {
fileStates,
setupFileState,
removeFileState,
getFileState,
updateFileData,
- hasFieldChanged,
fileChanges,
filesChanged,
saveAll,
- stateIndexByKey,
+ isLoading,
+ loadFileStatesFromIndexedDB,
}
}
diff --git a/app/composables/useFiles.ts b/app/composables/useFiles.ts
index 25b0787..0fe7e32 100644
--- a/app/composables/useFiles.ts
+++ b/app/composables/useFiles.ts
@@ -1,287 +1,159 @@
import type { FileWithHandle } from 'browser-fs-access'
import type { FileWithMetadata } from '~/shared/types'
-import { useIDBKeyval } from '@vueuse/integrations/useIDBKeyval'
import { parseMetadata, writeMetadata } from 'iptc-parser'
-const loadedFiles = ref([])
+export type FilesIdb = Record
+
+const loadedFiles = ref({})
const isLoading = ref(true)
const fileAmount = ref(0)
-const selectedIndexes = ref([])
-
-const ALLOW_MULTIPLE_SELECTION = false
-
-function loadFilesFromIndexedDB() {
- const { data: files, isFinished } = useIDBKeyval('uploaded-images', [])
- const { data: indexes, isFinished: areIndexesFinished } = useIDBKeyval('selected-indexes', [])
-
- watch(() => isFinished.value, (newVal) => {
- if (newVal) {
- loadedFiles.value = files.value
- isLoading.value = false
-
- const amountCookie = useCookie('file-amount')
- amountCookie.value = String(loadedFiles.value.length)
- }
- })
-
- watch(() => areIndexesFinished.value, (newVal) => {
- if (newVal) {
- selectedIndexes.value = indexes.value
- }
- })
-}
-
-function loadAmountFromCookies() {
- const amountCookie = useCookie('file-amount')
- fileAmount.value = Number.parseInt(amountCookie.value ?? '0')
-}
-function deduplicateFiles(files: FileWithMetadata[]) {
- const dedupedIds = new Set()
+const IDB_KEY_FILES = 'uploaded-images'
+const COOKIE_KEY_FILE_AMOUNT = 'file-amount'
- return files.filter((file) => {
- const id = file.file.name + file.file.lastModified + file.file.size
- if (dedupedIds.has(id)) {
- return false
+export default function useFiles() {
+ async function loadFilesFromIndexedDB() {
+ if (import.meta.env.SSR) {
+ return
}
- dedupedIds.add(id)
- return true
- })
-}
-async function updateIdb(updatedFiles: FileWithMetadata[]) {
- const { set } = useIDBKeyval('uploaded-images', loadedFiles.value)
- await set(updatedFiles)
-
- const { set: setIndexes } = useIDBKeyval('selected-indexes', selectedIndexes.value)
- await setIndexes(toRaw(selectedIndexes.value))
+ const files = await loadFromIdb(IDB_KEY_FILES, {})
+ loadedFiles.value = files ?? {}
+ isLoading.value = false
+ }
- loadFilesFromIndexedDB()
-}
+ function loadAmountFromCookies() {
+ const amountCookie = useCookie(COOKIE_KEY_FILE_AMOUNT)
+ fileAmount.value = Number.parseInt(amountCookie.value ?? '0')
+ }
-async function addFiles(files: FileWithHandle[]) {
- const metadataMapping: FileWithMetadata[] = await Promise.all(files.map(async (file) => {
- const buffer = await file.arrayBuffer()
- const fileId = file.name + file.lastModified + file.size
+ function deduplicateFiles(files: FileWithMetadata[]) {
+ const dedupedIds = new Set()
- try {
- const metadata = parseMetadata(new Uint8Array(buffer))
- return {
- id: fileId,
- file,
- handle: file.handle,
- metadata,
- }
- }
- catch (e) {
- console.warn('Failed to find metadata for file: ', file.name, ' - ', e)
- return {
- id: fileId,
- file,
- handle: file.handle,
- metadata: {},
+ return files.filter((file) => {
+ if (dedupedIds.has(file.id)) {
+ return false
}
- }
- }))
-
- const rawLoadedFiles = toRaw(loadedFiles.value)
- const dedupedUpdatedFiles = deduplicateFiles([...rawLoadedFiles, ...metadataMapping])
+ dedupedIds.add(file.id)
+ return true
+ })
+ }
- updateIdb(dedupedUpdatedFiles)
-}
+ async function addFiles(files: FileWithHandle[]) {
+ const metadataMapping: FileWithMetadata[] = await Promise.all(files.map(async (file) => {
+ const buffer = await file.arrayBuffer()
+ const fileId = getFileId(file)
-async function reloadFiles() {
- const rawLoadedFiles = toRaw(loadedFiles.value)
+ try {
+ const metadata = parseMetadata(new Uint8Array(buffer))
- const filesWithUpdatedMetadata = await Promise.all(rawLoadedFiles.map(async (file) => {
- const buffer = await file.file.arrayBuffer()
- try {
- const metadata = parseMetadata(new Uint8Array(buffer))
- return {
- ...file,
- metadata,
+ return {
+ id: fileId,
+ file,
+ handle: file.handle,
+ metadata,
+ }
}
- }
- catch (e) {
- console.warn('Failed to reload metadata for file: ', file.file.name, ' - ', e)
- return {
- ...file,
- metadata: {},
+ catch {
+ return {
+ id: fileId,
+ file,
+ handle: file.handle,
+ metadata: {
+ '2:00': '\u0000\u0004',
+ },
+ }
}
- }
- }))
-
- updateIdb(filesWithUpdatedMetadata)
-}
+ }))
-function markAsDownloaded(fileIds: string[]) {
- loadedFiles.value = loadedFiles.value.map((file) => {
- if (fileIds.includes(file.id)) {
- return {
- ...file,
- isDownloaded: true,
- }
- }
- return file
- })
-}
+ const dedupedUpdatedFiles = deduplicateFiles([...Object.values(loadedFiles.value), ...metadataMapping])
-async function removeFile(fileToRemove: FileWithMetadata) {
- if (fileToRemove.isSelected) {
- const indexToRemove = loadedFiles.value.findIndex(file => file.file === fileToRemove.file)
- if (indexToRemove === -1) {
- return
- }
- selectedIndexes.value = selectedIndexes.value.filter(index => index !== indexToRemove)
- }
+ const { setupFileState } = useFileState()
- const updatedFiles = loadedFiles.value.filter(file => file.file !== fileToRemove.file).map(file => toRaw(file))
- await updateIdb(updatedFiles)
-}
+ dedupedUpdatedFiles.forEach((file) => {
+ loadedFiles.value[file.id] = file
+ setupFileState(file.id)
+ })
-async function toggleSelection(file: FileWithMetadata, modifier: 'shift' | 'ctrl' | undefined = undefined) {
- const selectedFileIndex = loadedFiles.value.findIndex(f => f.file === file.file)
+ fileAmount.value = Object.keys(loadedFiles.value).length
- if (selectedFileIndex === -1) {
- return
+ const amountCookie = useCookie(COOKIE_KEY_FILE_AMOUNT)
+ amountCookie.value = String(fileAmount.value)
}
- if (ALLOW_MULTIPLE_SELECTION) {
- if (modifier === 'shift') {
- if (selectedIndexes.value.length === 0) {
- handleNormalSelect(selectedFileIndex)
- }
- else {
- const lastSelectedIndex = selectedIndexes.value[selectedIndexes.value.length - 1] ?? 0
- handleShiftSelect(selectedFileIndex, lastSelectedIndex)
+ function markAsDownloaded(fileIds: string[]) {
+ fileIds.forEach((fileId) => {
+ if (!loadedFiles.value[fileId]) {
+ return
}
- }
- else if (modifier === 'ctrl') {
- handleCtrlSelect(selectedFileIndex)
- }
- }
- else {
- handleNormalSelect(selectedFileIndex)
- }
-
- const rawFiles = loadedFiles.value.map(file => toRaw(file))
- await updateIdb(rawFiles)
-}
-
-function handleShiftSelect(fileIndex: number, lastSelectedIndex: number) {
- const [start, end] = fileIndex < lastSelectedIndex ? [fileIndex, lastSelectedIndex] : [lastSelectedIndex, fileIndex]
- loadedFiles.value = loadedFiles.value.map((loadedFile, index) => {
- loadedFile.isSelected = index >= start && index <= end
- return loadedFile
- })
-}
-
-function handleCtrlSelect(fileIndex: number) {
- if (!loadedFiles.value[fileIndex]) {
- return
+ loadedFiles.value[fileId] = {
+ ...loadedFiles.value[fileId],
+ isDownloaded: true,
+ }
+ })
}
- const isSelected = loadedFiles.value[fileIndex].isSelected
+ function removeFile(fileToRemoveId: string) {
+ const { selections } = useFileSelection()
- if (!isSelected) {
- selectedIndexes.value.push(fileIndex)
- }
- else {
- selectedIndexes.value = selectedIndexes.value.filter(i => i !== fileIndex)
+ delete selections.value[fileToRemoveId]
+ delete loadedFiles.value[fileToRemoveId]
}
- loadedFiles.value[fileIndex].isSelected = !isSelected
-}
+ async function updateMetadata(file: FileWithMetadata, metadata: Array<{ key: string, value?: string }>) {
+ const mappedMetadata = metadata.reduce>((acc, { key, value }) => {
+ acc[key] = value
+ return acc
+ }, {})
-function handleNormalSelect(fileIndex: number) {
- if (!loadedFiles.value[fileIndex]) {
- return
- }
+ const originatingProgram: string | undefined = import.meta.env.VITE_APP_NAME
+ const programVersion: string | undefined = import.meta.env.VITE_APP_VERSION
- const otherIndexes = selectedIndexes.value.filter(i => i !== fileIndex)
- const isSelectedBefore = !loadedFiles.value[fileIndex].isSelected
+ if (!originatingProgram || !programVersion) {
+ console.warn('Could not set originating program and version in metadata update')
+ }
- loadedFiles.value = loadedFiles.value.map((loadedFile, index) => {
- if (index !== fileIndex) {
- loadedFile.isSelected = false
- return loadedFile
+ const updatedMetadata = {
+ ...file.metadata,
+ ...mappedMetadata,
+ // Automatically set originating program and version using values from env file if defined
+ ...(originatingProgram ? { '2:65': originatingProgram } : {}),
+ ...(programVersion ? { '2:70': programVersion } : {}),
}
- if (otherIndexes.length > 0) {
- loadedFile.isSelected = true
+ try {
+ await writeMetadata(file.file, updatedMetadata, undefined, file.handle)
}
- else {
- loadedFile.isSelected = !loadedFile.isSelected
+ catch (e) {
+ console.warn('Failed to save metadata for file: ', file.file.name, ' - ', e)
}
- return loadedFile
- })
-
- if (isSelectedBefore || otherIndexes.length > 0) {
- selectedIndexes.value = [fileIndex]
- }
- else {
- selectedIndexes.value = []
- }
-}
-
-async function updateMetadata(file: FileWithMetadata, metadata: Array<{ key: string, value?: string }>) {
- const mappedMetadata = metadata.reduce>((acc, { key, value }) => {
- if (!value) {
- return acc
+ loadedFiles.value[file.id] = {
+ ...file,
+ metadata: updatedMetadata,
}
-
- acc[key] = value
- return acc
- }, {})
-
- const originatingProgram: string | undefined = import.meta.env.VITE_APP_NAME
- const programVersion: string | undefined = import.meta.env.VITE_APP_VERSION
-
- if (!originatingProgram || !programVersion) {
- console.warn('Could not set originating program and version in metadata update')
}
- const updatedMetadata = {
- ...file.metadata,
- ...mappedMetadata,
- // Automatically set originating program and version using values from env file
- ...(originatingProgram ? { '2:65': originatingProgram } : {}),
- ...(programVersion ? { '2:70': programVersion } : {}),
- }
-
- try {
- await writeMetadata(file.file, updatedMetadata, undefined, file.handle)
- }
- catch (e) {
- console.warn('Failed to save metadata for file: ', file.file.name, ' - ', e)
- }
-
- const updatedFiles = loadedFiles.value.map((loadedFile) => {
- if (loadedFile.file === file.file) {
- return {
- ...toRaw(loadedFile),
- metadata: updatedMetadata,
- }
+ function getOriginal(fileId: string, key: string): string | undefined {
+ const file = loadedFiles.value[fileId]
+ if (!file) {
+ return undefined
}
- return toRaw(loadedFile)
- })
-
- updateIdb(updatedFiles)
-}
-
-const selectedFiles = computed(() => {
- return loadedFiles.value.filter(file => file.isSelected)
-})
-
-export default function () {
- if (!import.meta.env.SSR) {
- loadFilesFromIndexedDB()
+ return file.metadata[key]
}
- loadAmountFromCookies()
-
- return { loadedFiles, selectedFiles, isLoading, loadFilesFromIndexedDB, addFiles, removeFile, fileAmount, toggleSelection, updateMetadata, reloadFiles, markAsDownloaded }
+ return {
+ loadedFiles,
+ isLoading,
+ loadFilesFromIndexedDB,
+ loadAmountFromCookies,
+ addFiles,
+ removeFile,
+ fileAmount,
+ updateMetadata,
+ markAsDownloaded,
+ getOriginal,
+ }
}
diff --git a/app/composables/useHasChanged.ts b/app/composables/useHasChanged.ts
index 6a093fa..c8833a9 100644
--- a/app/composables/useHasChanged.ts
+++ b/app/composables/useHasChanged.ts
@@ -1,8 +1,9 @@
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'
-export default function useHasChanged(field: Ref) {
- const originalValue = computed(() => field.value.original)
+export default function useHasChanged(fileId: Ref, field: Ref) {
const currentValue = computed(() => field.value.value)
+ const { getOriginal } = useFiles()
+ const originalValue = computed(() => getOriginal(fileId.value, field.value.key))
return computed(() => {
if (!originalValue.value && !currentValue.value) {
diff --git a/app/layouts/default.vue b/app/layouts/default.vue
index f5f22a1..ce6d820 100644
--- a/app/layouts/default.vue
+++ b/app/layouts/default.vue
@@ -1,5 +1,11 @@
+
+
-
+