Offer explicit sync controls
This commit is contained in:
parent
7b115da8d4
commit
d2cca5ec9e
@ -26,7 +26,6 @@ import MoreMenu, { MoreMenuSection } from '@/components/more/MoreMenu';
|
|||||||
import { useAppState } from '@/app/AppState';
|
import { useAppState } from '@/app/AppState';
|
||||||
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
|
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
|
||||||
import { MdOutlineFileDownload } from 'react-icons/md';
|
import { MdOutlineFileDownload } from 'react-icons/md';
|
||||||
import MoreMenuItem from '@/components/more/MoreMenuItem';
|
|
||||||
import IconGrSync from '@/components/icons/IconGrSync';
|
import IconGrSync from '@/components/icons/IconGrSync';
|
||||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||||
import IconFavs from '@/components/icons/IconFavs';
|
import IconFavs from '@/components/icons/IconFavs';
|
||||||
@ -40,6 +39,7 @@ import IconUpload from '@/components/icons/IconUpload';
|
|||||||
import { uploadPhotoFromClient } from '@/photo/storage';
|
import { uploadPhotoFromClient } from '@/photo/storage';
|
||||||
import ImageInput from '@/components/ImageInput';
|
import ImageInput from '@/components/ImageInput';
|
||||||
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config';
|
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config';
|
||||||
|
import IconWarning from '@/components/icons/IconWarning';
|
||||||
|
|
||||||
export default function AdminPhotoMenu({
|
export default function AdminPhotoMenu({
|
||||||
photo,
|
photo,
|
||||||
@ -75,7 +75,7 @@ export default function AdminPhotoMenu({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const sectionMain = useMemo(() => {
|
const sectionMain = useMemo(() => {
|
||||||
const items: ComponentProps<typeof MoreMenuItem>[] = [{
|
const items: MoreMenuSection['items'] = [{
|
||||||
label: appText.admin.edit,
|
label: appText.admin.edit,
|
||||||
icon: <IconEdit
|
icon: <IconEdit
|
||||||
size={14}
|
size={14}
|
||||||
@ -144,9 +144,25 @@ export default function AdminPhotoMenu({
|
|||||||
icon: <IconGrSync
|
icon: <IconGrSync
|
||||||
className="translate-x-[-1px] translate-y-[0.5px]"
|
className="translate-x-[-1px] translate-y-[0.5px]"
|
||||||
/>,
|
/>,
|
||||||
action: () => syncPhotoAction(photo.id)
|
items: [{
|
||||||
.then(() => revalidatePhoto?.(photo.id)),
|
label: appText.admin.syncAutomatic,
|
||||||
...showKeyCommands && { keyCommand: KEY_COMMANDS.sync },
|
icon: <IconGrSync
|
||||||
|
className="translate-x-[-1px] translate-y-[0.5px]"
|
||||||
|
/>,
|
||||||
|
action: () => syncPhotoAction(photo.id)
|
||||||
|
.then(() => revalidatePhoto?.(photo.id)),
|
||||||
|
}, {
|
||||||
|
label: appText.admin.syncOverwrite,
|
||||||
|
icon: <IconWarning className="translate-x-[-1.5px]" />,
|
||||||
|
className: 'text-warning *:hover:text-warning *:active:text-warning',
|
||||||
|
color: 'yellow',
|
||||||
|
action: () => {
|
||||||
|
if(window.confirm(appText.admin.syncOverwriteConfirm)) {
|
||||||
|
syncPhotoAction(photo.id, { syncMode: 'only-missing' })
|
||||||
|
.then(() => revalidatePhoto?.(photo.id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}],
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
label: appText.admin.reupload,
|
label: appText.admin.reupload,
|
||||||
@ -184,7 +200,7 @@ export default function AdminPhotoMenu({
|
|||||||
icon: <IconTrash
|
icon: <IconTrash
|
||||||
className="translate-x-[-1px]"
|
className="translate-x-[-1px]"
|
||||||
/>,
|
/>,
|
||||||
className: 'text-error *:hover:text-error',
|
className: 'text-error *:hover:text-error *:active:text-error',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
action: () => {
|
action: () => {
|
||||||
if (confirm(deleteConfirmationTextForPhoto(photo, appText))) {
|
if (confirm(deleteConfirmationTextForPhoto(photo, appText))) {
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export default function AdminPhotosTable({
|
|||||||
hasAiTextGeneration,
|
hasAiTextGeneration,
|
||||||
dateType = 'createdAt',
|
dateType = 'createdAt',
|
||||||
canEdit = true,
|
canEdit = true,
|
||||||
|
canSync,
|
||||||
canDelete = true,
|
canDelete = true,
|
||||||
timezone,
|
timezone,
|
||||||
shouldScrollIntoViewOnExternalSync,
|
shouldScrollIntoViewOnExternalSync,
|
||||||
@ -42,6 +43,7 @@ export default function AdminPhotosTable({
|
|||||||
hasAiTextGeneration: boolean
|
hasAiTextGeneration: boolean
|
||||||
dateType?: 'createdAt' | 'updatedAt'
|
dateType?: 'createdAt' | 'updatedAt'
|
||||||
canEdit?: boolean
|
canEdit?: boolean
|
||||||
|
canSync?: boolean
|
||||||
canDelete?: boolean
|
canDelete?: boolean
|
||||||
timezone?: Timezone
|
timezone?: Timezone
|
||||||
shouldScrollIntoViewOnExternalSync?: boolean
|
shouldScrollIntoViewOnExternalSync?: boolean
|
||||||
@ -127,19 +129,20 @@ export default function AdminPhotosTable({
|
|||||||
)}>
|
)}>
|
||||||
{canEdit &&
|
{canEdit &&
|
||||||
<EditButton path={pathForAdminPhotoEdit(photo)} />}
|
<EditButton path={pathForAdminPhotoEdit(photo)} />}
|
||||||
<PhotoSyncButton
|
{canSync &&
|
||||||
photo={photo}
|
<PhotoSyncButton
|
||||||
onSyncComplete={invalidateSwr}
|
photo={photo}
|
||||||
isSyncingExternal={photoIdsSyncing.includes(photo.id)}
|
onSyncComplete={invalidateSwr}
|
||||||
hasAiTextGeneration={hasAiTextGeneration}
|
isSyncingExternal={photoIdsSyncing.includes(photo.id)}
|
||||||
disabled={photoIdsSyncing.length > 0}
|
hasAiTextGeneration={hasAiTextGeneration}
|
||||||
className={opacityForPhotoId(photo.id)}
|
disabled={photoIdsSyncing.length > 0}
|
||||||
shouldConfirm
|
className={opacityForPhotoId(photo.id)}
|
||||||
shouldToast
|
shouldConfirm
|
||||||
shouldScrollIntoViewOnExternalSync={
|
shouldToast
|
||||||
shouldScrollIntoViewOnExternalSync}
|
shouldScrollIntoViewOnExternalSync={
|
||||||
updateMode={updateMode}
|
shouldScrollIntoViewOnExternalSync}
|
||||||
/>
|
updateMode={updateMode}
|
||||||
|
/>}
|
||||||
{debugColorData &&
|
{debugColorData &&
|
||||||
<SyncColorButton photoId={photo.id} />}
|
<SyncColorButton photoId={photo.id} />}
|
||||||
<AdminPhotoMenu
|
<AdminPhotoMenu
|
||||||
|
|||||||
@ -151,6 +151,7 @@ export default function AdminPhotosUpdateClient({
|
|||||||
photoIdsSyncing={photoIdsSyncing}
|
photoIdsSyncing={photoIdsSyncing}
|
||||||
hasAiTextGeneration={hasAiTextGeneration}
|
hasAiTextGeneration={hasAiTextGeneration}
|
||||||
canEdit={false}
|
canEdit={false}
|
||||||
|
canSync={true}
|
||||||
canDelete={false}
|
canDelete={false}
|
||||||
dateType="updatedAt"
|
dateType="updatedAt"
|
||||||
shouldScrollIntoViewOnExternalSync
|
shouldScrollIntoViewOnExternalSync
|
||||||
|
|||||||
9
src/components/icons/IconWarning.tsx
Normal file
9
src/components/icons/IconWarning.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { IconBaseProps } from 'react-icons/lib';
|
||||||
|
import { MdWarningAmber } from 'react-icons/md';
|
||||||
|
|
||||||
|
export default function IconWarning(props: IconBaseProps) {
|
||||||
|
return <MdWarningAmber
|
||||||
|
{...props}
|
||||||
|
size={props.size ?? 17}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
@ -10,10 +10,36 @@ import { clsx } from 'clsx/lite';
|
|||||||
import { FiMoreHorizontal } from 'react-icons/fi';
|
import { FiMoreHorizontal } from 'react-icons/fi';
|
||||||
import MoreMenuItem from './MoreMenuItem';
|
import MoreMenuItem from './MoreMenuItem';
|
||||||
import { clearGlobalFocus } from '@/utility/dom';
|
import { clearGlobalFocus } from '@/utility/dom';
|
||||||
|
import { FaChevronRight } from 'react-icons/fa6';
|
||||||
|
|
||||||
|
const surfaceStyles = (className?: string) => clsx(
|
||||||
|
'z-10',
|
||||||
|
'min-w-[8rem]',
|
||||||
|
'component-surface',
|
||||||
|
'py-1',
|
||||||
|
'not-dark:shadow-lg not-dark:shadow-gray-900/10',
|
||||||
|
'data-[side=top]:dark:shadow-[0_0px_40px_rgba(0,0,0,0.6)]',
|
||||||
|
'data-[side=bottom]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
|
||||||
|
'data-[side=right]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
|
||||||
|
'data-[side=top]:animate-fade-in-from-bottom',
|
||||||
|
'data-[side=bottom]:animate-fade-in-from-top',
|
||||||
|
'data-[side=right]:animate-fade-in-from-top',
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
export type MoreMenuSection = {
|
export type MoreMenuSection = {
|
||||||
label?: string
|
label?: string
|
||||||
items: ComponentProps<typeof MoreMenuItem>[]
|
items: (
|
||||||
|
// Either a menu item
|
||||||
|
ComponentProps<typeof MoreMenuItem> |
|
||||||
|
// or a submenu
|
||||||
|
{
|
||||||
|
label: string
|
||||||
|
labelComplex?: ReactNode
|
||||||
|
icon?: ReactNode
|
||||||
|
items: ComponentProps<typeof MoreMenuItem>[]
|
||||||
|
}
|
||||||
|
)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MoreMenu({
|
export default function MoreMenu({
|
||||||
@ -89,20 +115,7 @@ export default function MoreMenu({
|
|||||||
onCloseAutoFocus={e => e.preventDefault()}
|
onCloseAutoFocus={e => e.preventDefault()}
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={clsx(
|
className={surfaceStyles(className)}
|
||||||
'z-10',
|
|
||||||
'min-w-[8rem]',
|
|
||||||
'component-surface',
|
|
||||||
'py-1',
|
|
||||||
'not-dark:shadow-lg not-dark:shadow-gray-900/10',
|
|
||||||
'data-[side=top]:dark:shadow-[0_0px_40px_rgba(0,0,0,0.6)]',
|
|
||||||
'data-[side=bottom]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
|
|
||||||
'data-[side=right]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
|
|
||||||
'data-[side=top]:animate-fade-in-from-bottom',
|
|
||||||
'data-[side=bottom]:animate-fade-in-from-top',
|
|
||||||
'data-[side=right]:animate-fade-in-from-top',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{header && <div className={clsx(
|
{header && <div className={clsx(
|
||||||
'px-3 pt-3 pb-2 text-dim uppercase',
|
'px-3 pt-3 pb-2 text-dim uppercase',
|
||||||
@ -126,13 +139,56 @@ export default function MoreMenu({
|
|||||||
{label}
|
{label}
|
||||||
</div>}
|
</div>}
|
||||||
{items.map(item =>
|
{items.map(item =>
|
||||||
<div key={item.label} className="px-1">
|
'items' in item
|
||||||
<MoreMenuItem
|
? <DropdownMenu.DropdownMenuSub key={item.label}>
|
||||||
{...item}
|
<DropdownMenu.SubTrigger asChild>
|
||||||
dismissMenu={dismissMenu}
|
<div className="mx-1 focus:outline-none">
|
||||||
/>
|
<div className={clsx(
|
||||||
</div>,
|
'link outline-none focus:outline-none',
|
||||||
)}
|
'inline-flex w-full items-center h-8.5',
|
||||||
|
'rounded-sm p-2.5',
|
||||||
|
'items-center gap-1.5',
|
||||||
|
'text-sm text-main hover:text-main',
|
||||||
|
'hover:bg-gray-100/90 active:bg-gray-200/75',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80',
|
||||||
|
'select-none',
|
||||||
|
'cursor-pointer',
|
||||||
|
'whitespace-nowrap',
|
||||||
|
)}>
|
||||||
|
{item.icon && <div className="w-4.5">
|
||||||
|
{item.icon}
|
||||||
|
</div>}
|
||||||
|
<span className="grow min-w-0 text-left">
|
||||||
|
{item.labelComplex ?? item.label}
|
||||||
|
</span>
|
||||||
|
<FaChevronRight
|
||||||
|
size={10}
|
||||||
|
className="text-dim"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.SubContent
|
||||||
|
className={surfaceStyles()}
|
||||||
|
>
|
||||||
|
{item.items.map(item =>
|
||||||
|
<div key={item.label} className="px-1">
|
||||||
|
<MoreMenuItem
|
||||||
|
{...item}
|
||||||
|
dismissMenu={dismissMenu}
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.DropdownMenuSub>
|
||||||
|
: <div key={item.label} className="px-1">
|
||||||
|
<MoreMenuItem
|
||||||
|
{...item}
|
||||||
|
dismissMenu={dismissMenu}
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
</div>,
|
</div>,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export default function MoreMenuItem({
|
|||||||
labelComplex?: ReactNode
|
labelComplex?: ReactNode
|
||||||
annotation?: ReactNode
|
annotation?: ReactNode
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
color?: 'grey' | 'red'
|
color?: 'grey' | 'red' | 'yellow'
|
||||||
href?: string
|
href?: string
|
||||||
hrefDownloadName?: string
|
hrefDownloadName?: string
|
||||||
className?: string
|
className?: string
|
||||||
@ -53,6 +53,10 @@ export default function MoreMenuItem({
|
|||||||
'hover:bg-red-100/50 active:bg-red-100/75',
|
'hover:bg-red-100/50 active:bg-red-100/75',
|
||||||
'dark:hover:bg-red-950/55 dark:active:bg-red-950/80',
|
'dark:hover:bg-red-950/55 dark:active:bg-red-950/80',
|
||||||
);
|
);
|
||||||
|
case 'yellow': return clsx(
|
||||||
|
'hover:bg-amber-100/50 active:bg-amber-100/75',
|
||||||
|
'dark:hover:bg-amber-950/55 dark:active:bg-amber-950/80',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export const TEXT: I18N = {
|
|||||||
public: 'সর্বজনীন করুন',
|
public: 'সর্বজনীন করুন',
|
||||||
download: 'ডাউনলোড',
|
download: 'ডাউনলোড',
|
||||||
sync: 'সিঙ্ক',
|
sync: 'সিঙ্ক',
|
||||||
|
syncAutomatic: 'স্বয়ংক্রিয়',
|
||||||
|
syncOverwrite: 'ওভাররাইট করুন',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'আপনি কি নিশ্চিত যে আপনি সমস্ত ফটো ফিল্ড ওভাররাইট করতে চান? কাস্টমাইজড ডেটা হারিয়ে যেতে পারে।',
|
||||||
reupload: 'পুনরায় আপলোড করুন',
|
reupload: 'পুনরায় আপলোড করুন',
|
||||||
delete: 'ডিলিট',
|
delete: 'ডিলিট',
|
||||||
deleteConfirm: 'আপনি কি "{{photoTitle}}" মুছে ফেলতে চান?',
|
deleteConfirm: 'আপনি কি "{{photoTitle}}" মুছে ফেলতে চান?',
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export const TEXT: I18N = {
|
|||||||
public: 'Make Public',
|
public: 'Make Public',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
sync: 'Sync',
|
sync: 'Sync',
|
||||||
|
syncAutomatic: 'Automatic',
|
||||||
|
syncOverwrite: 'Overwrite',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'Are you sure you want to overwrite all photo fields? Customised data may be lost.',
|
||||||
reupload: 'Reupload',
|
reupload: 'Reupload',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
deleteConfirm: 'Are you sure you want to delete "{{photoTitle}}?"',
|
deleteConfirm: 'Are you sure you want to delete "{{photoTitle}}?"',
|
||||||
|
|||||||
@ -134,6 +134,10 @@ export const TEXT = {
|
|||||||
public: 'Make Public',
|
public: 'Make Public',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
sync: 'Sync',
|
sync: 'Sync',
|
||||||
|
syncAutomatic: 'Automatic',
|
||||||
|
syncOverwrite: 'Overwrite',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'Are you sure you want to overwrite all photo fields? Customized data may be lost.',
|
||||||
reupload: 'Reupload',
|
reupload: 'Reupload',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
deleteConfirm: 'Are you sure you want to delete "{{photoTitle}}?"',
|
deleteConfirm: 'Are you sure you want to delete "{{photoTitle}}?"',
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export const TEXT: I18N = {
|
|||||||
public: 'सार्वजनिक बनाएं',
|
public: 'सार्वजनिक बनाएं',
|
||||||
download: 'डाउनलोड करें',
|
download: 'डाउनलोड करें',
|
||||||
sync: 'सिंक करें',
|
sync: 'सिंक करें',
|
||||||
|
syncAutomatic: 'स्वचालित',
|
||||||
|
syncOverwrite: 'अधिलेखित करें',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'क्या आप सुनिश्चित हैं कि आप सभी फोटो फ़ील्ड को अधिलेखित करना चाहते हैं? अनुकूलित डेटा खो सकता है।',
|
||||||
reupload: 'पुनः अपलोड करें',
|
reupload: 'पुनः अपलोड करें',
|
||||||
delete: 'हटाएं',
|
delete: 'हटाएं',
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export const TEXT: I18N = {
|
|||||||
public: 'Buat Publik',
|
public: 'Buat Publik',
|
||||||
download: 'Unduh',
|
download: 'Unduh',
|
||||||
sync: 'Sinkronkan',
|
sync: 'Sinkronkan',
|
||||||
|
syncAutomatic: 'Otomatis',
|
||||||
|
syncOverwrite: 'Timpa',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'Apakah Anda yakin ingin menimpa semua bidang foto? Data yang disesuaikan mungkin hilang.',
|
||||||
reupload: 'Unggah ulang',
|
reupload: 'Unggah ulang',
|
||||||
delete: 'Hapus',
|
delete: 'Hapus',
|
||||||
deleteConfirm: 'Apakah Anda yakin ingin menghapus "{{photoTitle}}"?',
|
deleteConfirm: 'Apakah Anda yakin ingin menghapus "{{photoTitle}}"?',
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export const TEXT: I18N = {
|
|||||||
public: 'Tornar Público',
|
public: 'Tornar Público',
|
||||||
download: 'Baixar',
|
download: 'Baixar',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
|
syncAutomatic: 'Automático',
|
||||||
|
syncOverwrite: 'Sobrescrever',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'Tem certeza de que deseja sobrescrever todos os campos da foto? Dados personalizados podem ser perdidos.',
|
||||||
reupload: 'Enviar novamente',
|
reupload: 'Enviar novamente',
|
||||||
delete: 'Excluir',
|
delete: 'Excluir',
|
||||||
deleteConfirm: 'Tem certeza de que deseja excluir "{{photoTitle}}"?',
|
deleteConfirm: 'Tem certeza de que deseja excluir "{{photoTitle}}"?',
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export const TEXT: I18N = {
|
|||||||
public: 'Tornar Público',
|
public: 'Tornar Público',
|
||||||
download: 'Descarregar',
|
download: 'Descarregar',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
|
syncAutomatic: 'Automático',
|
||||||
|
syncOverwrite: 'Sobrescrever',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'Tens certeza de que queres sobrescrever todos os campos da fotografia? Dados personalizados podem ser perdidos.',
|
||||||
reupload: 'Carregar novamente',
|
reupload: 'Carregar novamente',
|
||||||
delete: 'Excluir',
|
delete: 'Excluir',
|
||||||
deleteConfirm: 'Tens certeza de que deseja excluir "{{photoTitle}}"?',
|
deleteConfirm: 'Tens certeza de que deseja excluir "{{photoTitle}}"?',
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export const TEXT: I18N = {
|
|||||||
public: 'Herkese Açık Yap',
|
public: 'Herkese Açık Yap',
|
||||||
download: 'İndir',
|
download: 'İndir',
|
||||||
sync: 'Senkronize Et',
|
sync: 'Senkronize Et',
|
||||||
|
syncAutomatic: 'Otomatik',
|
||||||
|
syncOverwrite: 'Üzerine Yaz',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'Tüm fotoğraf alanlarının üzerine yazmak istediğinize emin misiniz? Özelleştirilmiş veriler kaybolabilir.',
|
||||||
reupload: 'Yeniden Yükle',
|
reupload: 'Yeniden Yükle',
|
||||||
delete: 'Sil',
|
delete: 'Sil',
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
|||||||
@ -135,6 +135,10 @@ export const TEXT: I18N = {
|
|||||||
public: 'Làm công khai',
|
public: 'Làm công khai',
|
||||||
download: 'Tải xuống',
|
download: 'Tải xuống',
|
||||||
sync: 'Đồng bộ',
|
sync: 'Đồng bộ',
|
||||||
|
syncAutomatic: 'Tự động',
|
||||||
|
syncOverwrite: 'Ghi đè',
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
syncOverwriteConfirm: 'Bạn có chắc chắn muốn ghi đè tất cả các trường ảnh? Dữ liệu tùy chỉnh có thể bị mất.',
|
||||||
reupload: 'Tải lên lại',
|
reupload: 'Tải lên lại',
|
||||||
delete: 'Xóa',
|
delete: 'Xóa',
|
||||||
deleteConfirm: 'Bạn có chắc chắn muốn xóa "{{photoTitle}}?"',
|
deleteConfirm: 'Bạn có chắc chắn muốn xóa "{{photoTitle}}?"',
|
||||||
|
|||||||
@ -135,6 +135,9 @@ export const TEXT: I18N = {
|
|||||||
public: '设为公开',
|
public: '设为公开',
|
||||||
download: '下载',
|
download: '下载',
|
||||||
sync: '同步',
|
sync: '同步',
|
||||||
|
syncAutomatic: '自动',
|
||||||
|
syncOverwrite: '覆盖',
|
||||||
|
syncOverwriteConfirm: '确定要覆盖所有照片字段吗?自定义数据可能会丢失。',
|
||||||
reupload: '重新上传',
|
reupload: '重新上传',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
deleteConfirm: '确定要删除 "{{photoTitle}}" 吗?',
|
deleteConfirm: '确定要删除 "{{photoTitle}}" 吗?',
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import {
|
|||||||
getPhotoOptionsCountForPath,
|
getPhotoOptionsCountForPath,
|
||||||
} from '@/db';
|
} from '@/db';
|
||||||
import {
|
import {
|
||||||
FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC,
|
|
||||||
PhotoFormData,
|
PhotoFormData,
|
||||||
convertFormDataToPhotoDbInsert,
|
convertFormDataToPhotoDbInsert,
|
||||||
convertPhotoToFormData,
|
convertPhotoToFormData,
|
||||||
@ -583,9 +582,11 @@ export const getExifDataAction = async (
|
|||||||
export const syncPhotoAction = async (
|
export const syncPhotoAction = async (
|
||||||
photoId: string, {
|
photoId: string, {
|
||||||
isBatch,
|
isBatch,
|
||||||
|
syncMode = 'auto',
|
||||||
updateMode,
|
updateMode,
|
||||||
}: {
|
}: {
|
||||||
isBatch?: boolean,
|
isBatch?: boolean,
|
||||||
|
syncMode?: 'auto' | 'only-missing' | 'overwrite',
|
||||||
updateMode?: boolean,
|
updateMode?: boolean,
|
||||||
} = {},
|
} = {},
|
||||||
) =>
|
) =>
|
||||||
@ -602,7 +603,7 @@ export const syncPhotoAction = async (
|
|||||||
includeInitialPhotoFields: false,
|
includeInitialPhotoFields: false,
|
||||||
generateBlurData: BLUR_ENABLED,
|
generateBlurData: BLUR_ENABLED,
|
||||||
generateResizedImage: AI_CONTENT_GENERATION_ENABLED,
|
generateResizedImage: AI_CONTENT_GENERATION_ENABLED,
|
||||||
// If in update mode, only update color fields if necessary
|
// In update mode, only update color fields if necessary
|
||||||
updateColorFields: !(
|
updateColorFields: !(
|
||||||
updateMode &&
|
updateMode &&
|
||||||
photo.colorData !== undefined &&
|
photo.colorData !== undefined &&
|
||||||
@ -643,10 +644,22 @@ export const syncPhotoAction = async (
|
|||||||
|
|
||||||
const formDataFromPhoto = convertPhotoToFormData(photo);
|
const formDataFromPhoto = convertPhotoToFormData(photo);
|
||||||
|
|
||||||
// Don't overwrite manually configured meta with null data
|
Object.entries(formDataFromExif).forEach(([field, value]) => {
|
||||||
FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC.forEach(field => {
|
const existingValue =
|
||||||
if (!formDataFromExif[field] && formDataFromPhoto[field]) {
|
formDataFromPhoto[field as keyof PhotoFormData];
|
||||||
delete formDataFromExif[field];
|
switch (syncMode) {
|
||||||
|
case 'auto':
|
||||||
|
// Remove all fields already present in formDataFromPhoto
|
||||||
|
if (existingValue !== undefined) {
|
||||||
|
delete formDataFromExif[field as keyof PhotoFormData];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'only-missing':
|
||||||
|
// Avoid overwriting fields with null data
|
||||||
|
if (existingValue !== undefined && !value) {
|
||||||
|
delete formDataFromExif[field as keyof PhotoFormData];
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,6 @@ export type FormMeta = {
|
|||||||
tagOptionsLimit?: number
|
tagOptionsLimit?: number
|
||||||
tagOptionsLimitValidationMessage?: string
|
tagOptionsLimitValidationMessage?: string
|
||||||
tagOptionsShouldParameterize?: boolean
|
tagOptionsShouldParameterize?: boolean
|
||||||
shouldNotOverwriteWithNullDataOnSync?: boolean
|
|
||||||
isJson?: boolean
|
isJson?: boolean
|
||||||
staticValue?: string
|
staticValue?: string
|
||||||
};
|
};
|
||||||
@ -89,7 +88,6 @@ const FORM_METADATA = (
|
|||||||
label: 'title',
|
label: 'title',
|
||||||
capitalize: true,
|
capitalize: true,
|
||||||
validateStringMaxLength: STRING_MAX_LENGTH_SHORT,
|
validateStringMaxLength: STRING_MAX_LENGTH_SHORT,
|
||||||
shouldNotOverwriteWithNullDataOnSync: true,
|
|
||||||
},
|
},
|
||||||
caption: {
|
caption: {
|
||||||
section: 'text',
|
section: 'text',
|
||||||
@ -155,7 +153,6 @@ const FORM_METADATA = (
|
|||||||
noteShort: 'Fujifilm / Nikon / analog scans',
|
noteShort: 'Fujifilm / Nikon / analog scans',
|
||||||
tagOptions: filmOptions,
|
tagOptions: filmOptions,
|
||||||
tagOptionsLimit: 1,
|
tagOptionsLimit: 1,
|
||||||
shouldNotOverwriteWithNullDataOnSync: true,
|
|
||||||
},
|
},
|
||||||
recipeTitle: {
|
recipeTitle: {
|
||||||
section: 'exif',
|
section: 'exif',
|
||||||
@ -187,7 +184,6 @@ const FORM_METADATA = (
|
|||||||
spellCheck: false,
|
spellCheck: false,
|
||||||
capitalize: false,
|
capitalize: false,
|
||||||
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||||
shouldNotOverwriteWithNullDataOnSync: true,
|
|
||||||
isJson: true,
|
isJson: true,
|
||||||
validate: value => {
|
validate: value => {
|
||||||
let validationMessage = undefined;
|
let validationMessage = undefined;
|
||||||
@ -299,11 +295,6 @@ export const FIELDS_WITH_JSON = Object.entries(FORM_METADATA())
|
|||||||
.filter(([_, meta]) => meta.isJson)
|
.filter(([_, meta]) => meta.isJson)
|
||||||
.map(([key]) => key as keyof PhotoFormData);
|
.map(([key]) => key as keyof PhotoFormData);
|
||||||
|
|
||||||
export const FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC =
|
|
||||||
Object.entries(FORM_METADATA())
|
|
||||||
.filter(([_, meta]) => meta.shouldNotOverwriteWithNullDataOnSync)
|
|
||||||
.map(([key]) => key as keyof PhotoFormData);
|
|
||||||
|
|
||||||
export const FORM_METADATA_ENTRIES = (
|
export const FORM_METADATA_ENTRIES = (
|
||||||
...args: Parameters<typeof FORM_METADATA>
|
...args: Parameters<typeof FORM_METADATA>
|
||||||
) =>
|
) =>
|
||||||
|
|||||||
@ -142,6 +142,10 @@ html {
|
|||||||
@apply
|
@apply
|
||||||
text-red-500 dark:text-red-400
|
text-red-500 dark:text-red-400
|
||||||
}
|
}
|
||||||
|
@utility text-warning {
|
||||||
|
@apply
|
||||||
|
text-amber-700 dark:text-amber-500
|
||||||
|
}
|
||||||
/* Rules */
|
/* Rules */
|
||||||
@utility border-main {
|
@utility border-main {
|
||||||
@apply
|
@apply
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user