mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat(data-management): add data management module for photo asset maintenance
- Introduced the DataManagementModule, including a controller and service for managing photo asset records. - Implemented functionality to truncate photo asset records from the database, enhancing data management capabilities. - Updated existing photo asset deletion logic to support optional deletion from storage. - Added a new DataManagementPanel in the dashboard for user interaction with data management features. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'
|
||||
|
||||
import type { AfilmoryBuilder } from '../builder/builder.js'
|
||||
import type { StorageManager } from '../storage/index.js'
|
||||
import type { StorageConfig } from '../storage/interfaces.js'
|
||||
import type { GitHubConfig, S3Config, StorageConfig } from '../storage/interfaces.js'
|
||||
import type { PhotoProcessingLoggers } from './logger-adapter.js'
|
||||
|
||||
export interface PhotoExecutionContext {
|
||||
@@ -44,11 +44,11 @@ export function createStorageKeyNormalizer(storageConfig: StorageConfig): (key:
|
||||
|
||||
switch (storageConfig.provider) {
|
||||
case 's3': {
|
||||
basePrefix = sanitizeStoragePath(storageConfig.prefix)
|
||||
basePrefix = sanitizeStoragePath((storageConfig as S3Config).prefix)
|
||||
break
|
||||
}
|
||||
case 'github': {
|
||||
basePrefix = sanitizeStoragePath(storageConfig.path)
|
||||
basePrefix = sanitizeStoragePath((storageConfig as GitHubConfig).path)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function eagleStoragePlugin(options: EagleStoragePluginOptions =
|
||||
const eagleConfig = storage
|
||||
const key = payload.item.s3Key
|
||||
|
||||
const meta = await readImageMetadata(eagleConfig.libraryPath, key)
|
||||
const meta = await readImageMetadata((eagleConfig as EagleConfig).libraryPath, key)
|
||||
|
||||
// Append folder names as tags if enabled
|
||||
if (eagleConfig.folderAsTag) {
|
||||
@@ -36,7 +36,7 @@ export default function eagleStoragePlugin(options: EagleStoragePluginOptions =
|
||||
const indexCacheKey = 'afilmory:eagle:folderIndex'
|
||||
let folderIndex = runShared.get(indexCacheKey) as Map<string, string[]> | undefined
|
||||
if (!folderIndex) {
|
||||
folderIndex = await getEagleFolderIndex(eagleConfig.libraryPath)
|
||||
folderIndex = await getEagleFolderIndex((eagleConfig as EagleConfig).libraryPath)
|
||||
runShared.set(indexCacheKey, folderIndex)
|
||||
}
|
||||
const folderNames = (meta.folders ?? [])
|
||||
@@ -52,7 +52,7 @@ export default function eagleStoragePlugin(options: EagleStoragePluginOptions =
|
||||
}
|
||||
}
|
||||
// Apply omitTagNamesInMetadata filter
|
||||
const omit = new Set(eagleConfig.omitTagNamesInMetadata ?? [])
|
||||
const omit = new Set((eagleConfig as EagleConfig).omitTagNamesInMetadata ?? [])
|
||||
if (omit.size > 0 && meta.tags) {
|
||||
meta.tags = meta.tags.filter((t) => !omit.has(t))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { StorageManager } from '../../storage/index.js'
|
||||
import type { StorageConfig } from '../../storage/interfaces.js'
|
||||
import type { S3Config, StorageConfig } from '../../storage/interfaces.js'
|
||||
import type { BuilderPlugin } from '../types.js'
|
||||
import type { ThumbnailPluginData } from './shared.js'
|
||||
import {
|
||||
@@ -56,7 +56,7 @@ function joinSegments(...segments: Array<string | null | undefined>): string {
|
||||
function resolveRemotePrefix(config: UploadableStorageConfig, directory: string): string {
|
||||
switch (config.provider) {
|
||||
case 's3': {
|
||||
const base = trimSlashes(config.prefix)
|
||||
const base = trimSlashes((config as S3Config).prefix)
|
||||
return joinSegments(base, directory)
|
||||
}
|
||||
case 'github': {
|
||||
|
||||
@@ -85,6 +85,11 @@ export type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Con
|
||||
HTMLMotionProps<'div'> & {
|
||||
from?: FlipDirection
|
||||
transition?: Transition
|
||||
/**
|
||||
* Whether the dialog can be dismissed by clicking outside (on the overlay).
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
dismissOnOutsideClick?: boolean
|
||||
}
|
||||
|
||||
const contentTransition: Transition = {
|
||||
@@ -97,6 +102,8 @@ function DialogContent({
|
||||
children,
|
||||
from = 'top',
|
||||
transition = contentTransition,
|
||||
dismissOnOutsideClick = true,
|
||||
onInteractOutside,
|
||||
...props
|
||||
}: DialogContentProps) {
|
||||
const { isOpen } = useDialog()
|
||||
@@ -118,7 +125,17 @@ function DialogContent({
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
/>
|
||||
</DialogOverlay>
|
||||
<DialogPrimitive.Content asChild forceMount {...props}>
|
||||
<DialogPrimitive.Content
|
||||
asChild
|
||||
forceMount
|
||||
{...props}
|
||||
onInteractOutside={(event) => {
|
||||
if (!dismissOnOutsideClick) {
|
||||
event.preventDefault()
|
||||
}
|
||||
onInteractOutside?.(event)
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
key="dialog-content"
|
||||
data-slot="dialog-content"
|
||||
|
||||
@@ -60,14 +60,21 @@ function ModalWrapper({ item }: { item: ModalItem }) {
|
||||
|
||||
const { contentProps, contentClassName } = Component
|
||||
|
||||
const mergedContentConfig = {
|
||||
...contentProps,
|
||||
...item.modalContent,
|
||||
}
|
||||
|
||||
const { dismissOnOutsideClick = true, ...restContentConfig } = mergedContentConfig
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
className={clsxm('w-full max-w-md', contentClassName)}
|
||||
transition={Spring.presets.smooth}
|
||||
onAnimationComplete={handleAnimationComplete}
|
||||
{...contentProps}
|
||||
{...item.modalContent}
|
||||
dismissOnOutsideClick={item.dismissOnOutsideClick ?? dismissOnOutsideClick}
|
||||
{...restContentConfig}
|
||||
>
|
||||
<Component modalId={item.id} dismiss={dismiss} {...(item.props as any)} />
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { atom } from 'jotai'
|
||||
|
||||
import { modalStore } from './store'
|
||||
import type { ModalComponent, ModalContentConfig, ModalItem } from './types'
|
||||
import type { ModalComponent, ModalItem, ModalPresentConfig } from './types'
|
||||
|
||||
export const modalItemsAtom = atom<ModalItem[]>([])
|
||||
|
||||
const modalCloseRegistry = new Map<string, () => void>()
|
||||
|
||||
export const Modal = {
|
||||
present<P = unknown>(Component: ModalComponent<P>, props?: P, modalContent?: ModalContentConfig): string {
|
||||
present<P = unknown>(Component: ModalComponent<P>, props?: P, config?: ModalPresentConfig): string {
|
||||
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
||||
const items = modalStore.get(modalItemsAtom)
|
||||
modalStore.set(modalItemsAtom, [...items, { id, component: Component as ModalComponent<any>, props, modalContent }])
|
||||
const { dismissOnOutsideClick, ...modalContent } = config ?? {}
|
||||
|
||||
modalStore.set(modalItemsAtom, [
|
||||
...items,
|
||||
{
|
||||
id,
|
||||
component: Component as ModalComponent<any>,
|
||||
props,
|
||||
modalContent,
|
||||
dismissOnOutsideClick,
|
||||
},
|
||||
])
|
||||
return id
|
||||
},
|
||||
|
||||
|
||||
@@ -2,7 +2,14 @@ import type * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import type { HTMLMotionProps } from 'motion/react'
|
||||
import type { FC } from 'react'
|
||||
|
||||
export type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> & HTMLMotionProps<'div'>
|
||||
export type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> &
|
||||
HTMLMotionProps<'div'> & {
|
||||
/**
|
||||
* Whether the dialog can be dismissed by clicking outside (on the overlay).
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
dismissOnOutsideClick?: boolean
|
||||
}
|
||||
|
||||
export type ModalComponentProps = {
|
||||
modalId: string
|
||||
@@ -16,9 +23,22 @@ export type ModalComponent<P = unknown> = FC<ModalComponentProps & P> & {
|
||||
|
||||
export type ModalContentConfig = Partial<DialogContentProps>
|
||||
|
||||
export type ModalPresentConfig = ModalContentConfig & {
|
||||
/**
|
||||
* Control whether this modal can be dismissed by clicking outside.
|
||||
* Defaults to `true` when omitted.
|
||||
*/
|
||||
dismissOnOutsideClick?: boolean
|
||||
}
|
||||
|
||||
export type ModalItem = {
|
||||
id: string
|
||||
component: ModalComponent<any>
|
||||
props?: unknown
|
||||
modalContent?: ModalContentConfig
|
||||
/**
|
||||
* When `false`, prevent dismissing this modal via outside clicks.
|
||||
* `undefined` means "use default" (treated as `true`).
|
||||
*/
|
||||
dismissOnOutsideClick?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user