diff --git a/eslint.config.mjs b/eslint.config.mjs index 82cce0f7..9eaf8694 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,7 @@ import { dirname } from 'path'; import { fileURLToPath } from 'url'; import { FlatCompat } from '@eslint/eslintrc'; +import stylistic from '@stylistic/eslint-plugin'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -11,10 +12,14 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends('next/core-web-vitals', 'next/typescript'), { + plugins: { + '@stylistic': stylistic, + }, rules: { '@next/next/no-img-element': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-require-imports': 'off', + '@stylistic/indent': ['warn', 2], 'no-unused-expressions': ['warn'], 'no-duplicate-imports': ['warn'], '@typescript-eslint/no-unused-vars': [ @@ -27,10 +32,6 @@ const eslintConfig = [ 'warn', 'always-multiline', ], - 'indent': [ - 'warn', - 2, - ], 'linebreak-style': [ 'warn', 'unix', diff --git a/package.json b/package.json index b0008d94..cde5f990 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@eslint/eslintrc": "^3.3.1", "@next/bundle-analyzer": "15.4.5", "@next/eslint-plugin-next": "^15.4.5", + "@stylistic/eslint-plugin": "^5.2.3", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81102c31..c5279cdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,6 +132,9 @@ importers: '@next/eslint-plugin-next': specifier: ^15.4.5 version: 15.4.5 + '@stylistic/eslint-plugin': + specifier: ^5.2.3 + version: 5.2.3(eslint@9.32.0(jiti@2.5.1)) '@tailwindcss/container-queries': specifier: ^0.1.1 version: 0.1.1(tailwindcss@4.1.11) @@ -1687,6 +1690,12 @@ packages: resolution: {integrity: sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg==} engines: {node: '>=18.0.0'} + '@stylistic/eslint-plugin@5.2.3': + resolution: {integrity: sha512-oY7GVkJGVMI5benlBDCaRrSC1qPasafyv5dOBLLv5MTilMGnErKhO6ziEfodDDIZbo5QxPUNW360VudJOFODMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=9.0.0' + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -6576,6 +6585,16 @@ snapshots: '@smithy/types': 4.3.1 tslib: 2.8.1 + '@stylistic/eslint-plugin@5.2.3(eslint@9.32.0(jiti@2.5.1))': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.5.1)) + '@typescript-eslint/types': 8.39.0 + eslint: 9.32.0(jiti@2.5.1) + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.3 + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 diff --git a/src/admin/AdminShowRecipeButton.tsx b/src/admin/AdminShowRecipeButton.tsx index c4ead286..ab7f8d15 100644 --- a/src/admin/AdminShowRecipeButton.tsx +++ b/src/admin/AdminShowRecipeButton.tsx @@ -16,7 +16,7 @@ export default function AdminShowRecipeButton(props: RecipeProps) { />} onClick={() => setRecipeModalProps?.(props)} > - Preview + Preview ); -} \ No newline at end of file +} diff --git a/src/admin/config/AdminAppConfigurationClient.tsx b/src/admin/config/AdminAppConfigurationClient.tsx index ea53f715..ea4d88b1 100644 --- a/src/admin/config/AdminAppConfigurationClient.tsx +++ b/src/admin/config/AdminAppConfigurationClient.tsx @@ -134,9 +134,9 @@ export default function AdminAppConfigurationClient({ isAnalyzingConfiguration, }: AppConfiguration & Partial>> & { - simplifiedView?: boolean - isAnalyzingConfiguration?: boolean -}) { + simplifiedView?: boolean + isAnalyzingConfiguration?: boolean + }) { const [hasScrolled, setHasScrolled] = useState(false); useEffect(() => { @@ -216,235 +216,236 @@ export default function AdminAppConfigurationClient({ const renderGroupContent = (key: ConfigSectionKey): JSX.Element => { switch (key) { - case 'Storage': - return <> - - {databaseError && renderError({ - connection: { provider: 'Database', error: databaseError}, - })} - {hasVercelPostgres - ? renderSubStatus('checked', 'Vercel Postgres: connected') - : renderSubStatus('optional', <> - Vercel Postgres: - {' '} - + + {databaseError && renderError({ + connection: { provider: 'Database', error: databaseError}, + })} + {hasVercelPostgres + ? renderSubStatus('checked', 'Vercel Postgres: connected') + : renderSubStatus('optional', <> + Vercel Postgres: + {' '} + - create store - - {' '} - and connect to project - )} - {hasDatabase && !hasVercelPostgres && + href="https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database" + externalIcon + > + create store + + {' '} + and connect to project + )} + {hasDatabase && !hasVercelPostgres && renderSubStatus('checked', <> Postgres-compatible: connected {' '} (SSL {isPostgresSslEnabled ? 'enabled' : 'disabled'}) )} - - + - {storageError && renderError({ - connection: { provider: 'Storage', error: storageError}, - })} -
- {hasVercelBlobStorage - ? renderSubStatus('checked', 'Vercel Blob: connected') - : renderSubStatus('optional', <> - {labelForStorage('vercel-blob')}: - {' '} - + {storageError && renderError({ + connection: { provider: 'Storage', error: storageError}, + })} +
+ {hasVercelBlobStorage + ? renderSubStatus('checked', 'Vercel Blob: connected') + : renderSubStatus('optional', <> + {labelForStorage('vercel-blob')}: + {' '} + - create store - - {' '} - and connect to project - , - )} - {hasCloudflareR2Storage - ? renderSubStatus('checked', 'Cloudflare R2: connected') - : renderSubStatus('optional', <> - {labelForStorage('cloudflare-r2')}: - {' '} - + create store + + {' '} + and connect to project + , + )} + {hasCloudflareR2Storage + ? renderSubStatus('checked', 'Cloudflare R2: connected') + : renderSubStatus('optional', <> + {labelForStorage('cloudflare-r2')}: + {' '} + - create/configure bucket - - )} - {hasAwsS3Storage - ? renderSubStatus('checked', 'AWS S3: connected') - : renderSubStatus('optional', <> - {labelForStorage('aws-s3')}: - {' '} - - create/configure bucket - - )} -
- - ; - case 'Authentication': - return <> - - Store auth secret in environment variable: - {!hasAuthSecret && + href="https://github.com/sambecker/exif-photo-blog#cloudflare-r2" + externalIcon + > + create/configure bucket +
+ )} + {hasAwsS3Storage + ? renderSubStatus('checked', 'AWS S3: connected') + : renderSubStatus('optional', <> + {labelForStorage('aws-s3')}: + {' '} + + create/configure bucket + + )} +
+
+ ; + case 'Authentication': + return <> + + Store auth secret in environment variable: + {!hasAuthSecret &&
} - {renderEnvVars(['AUTH_SECRET'])} -
- - Store admin email/password - {' '} - in environment variables: - {renderEnvVars([ - 'ADMIN_EMAIL', - 'ADMIN_PASSWORD', - ])} - - ; - case 'Content': - return <> - - {renderContent(locale)} - Store in environment variable - (check README for - {' '} - - supported languages - - ): - {renderEnvVars(['NEXT_PUBLIC_LOCALE'])} - - - {renderContent(domain)} - Store in environment variable - (used in explicit share urls, seen in nav if no title is defined): - {renderEnvVars(['NEXT_PUBLIC_DOMAIN'])} - - - {renderContent(metaTitle)} - Store in environment variable - (seen in search results and browser tab): - {renderEnvVars(['NEXT_PUBLIC_META_TITLE'])} - - {!simplifiedView && <> + {renderEnvVars(['AUTH_SECRET'])} +
+ Store admin email/password + {' '} + in environment variables: + {renderEnvVars([ + 'ADMIN_EMAIL', + 'ADMIN_PASSWORD', + ])} + + ; + case 'Content': + return <> + - {renderContent(metaDescription)} + {renderContent(locale)} Store in environment variable - (seen in search results): - {renderEnvVars(['NEXT_PUBLIC_META_DESCRIPTION'])} + (check README for + {' '} + + supported languages + + ): + {renderEnvVars(['NEXT_PUBLIC_LOCALE'])} - {renderContent(navTitle)} - Store in environment variable (replaces domain in top-right nav): - {renderEnvVars(['NEXT_PUBLIC_NAV_TITLE'])} + {renderContent(domain)} + Store in environment variable + (used in explicit share urls, seen in nav if no title is defined): + {renderEnvVars(['NEXT_PUBLIC_DOMAIN'])} + {renderContent(metaTitle)} + Store in environment variable + (seen in search results and browser tab): + {renderEnvVars(['NEXT_PUBLIC_META_TITLE'])} + + {!simplifiedView && <> + + {renderContent(metaDescription)} + Store in environment variable + (seen in search results): + {renderEnvVars(['NEXT_PUBLIC_META_DESCRIPTION'])} + + + {renderContent(navTitle)} + Store in environment variable (replaces domain in top-right nav): + {renderEnvVars(['NEXT_PUBLIC_NAV_TITLE'])} + + + {hasNavCaption && renderContent(navCaption)} + Store in environment variable + (seen in top-right nav, under title): + {renderEnvVars(['NEXT_PUBLIC_NAV_CAPTION'])} + + + {hasPageAbout && renderContent(pageAbout)} + Store in environment variable (seen in sidebar): + {renderEnvVars(['NEXT_PUBLIC_PAGE_ABOUT'])} + + } + ; + case 'AI Content Generation': + return <> + - {hasNavCaption && renderContent(navCaption)} - Store in environment variable (seen in top-right nav, under title): - {renderEnvVars(['NEXT_PUBLIC_NAV_CAPTION'])} + {aiError && renderError({ + connection: { provider: 'OpenAI', error: aiError}, + })} + Store your OpenAI secret key in order to enable AI-generated + text descriptions and optionally leverage an invisible field + called {'"Semantic Description"'} used to support CMD-K search + and improve accessibility: + {renderEnvVars(['OPENAI_SECRET_KEY'])} - {hasPageAbout && renderContent(pageAbout)} - Store in environment variable (seen in sidebar): - {renderEnvVars(['NEXT_PUBLIC_PAGE_ABOUT'])} - - } - ; - case 'AI Content Generation': - return <> - - {aiError && renderError({ - connection: { provider: 'OpenAI', error: aiError}, - })} - Store your OpenAI secret key in order to enable AI-generated - text descriptions and optionally leverage an invisible field - called {'"Semantic Description"'} used to support CMD-K search - and improve accessibility: - {renderEnvVars(['OPENAI_SECRET_KEY'])} - - -
- {hasAiTextAutoGeneratedFields && +
+ {hasAiTextAutoGeneratedFields && AI_AUTO_GENERATED_FIELDS_ALL.map(field => {renderSubStatus( @@ -454,453 +455,453 @@ export default function AdminAppConfigurationClient({ field, )} )} -
- Comma-separated fields to auto-generate when - uploading photos. Accepted values: title, caption, - tags, description, all, or none - {' '} - (default: {'"title,tags,semantic"'}): - {renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])} - - - {redisError && renderError({ - connection: { provider: 'Redis', error: redisError}, - })} - Create Upstash Redis store from storage tab - on Vercel dashboard and connect to this project - to enable rate limiting - - - Store base URL in environment variable to use - alternate OpenAI-compatible providers: - {renderEnvVars(['OPENAI_BASE_URL'])} - - ; - case 'Performance': - return <> - - Set environment variable to {'"1"'} to make site more responsive - by enabling static optimization - (i.e., rendering pages and images at build time): -
- {renderSubStatusWithEnvVar( - arePhotosStaticallyOptimized ? 'checked' : 'optional', - 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS', - )} - {renderSubStatusWithEnvVar( - arePhotoOGImagesStaticallyOptimized ? 'checked' : 'optional', - 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES', - )} - {renderSubStatusWithEnvVar( - arePhotoCategoriesStaticallyOptimized ? 'checked' : 'optional', - 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES', - )} - {renderSubStatusWithEnvVar( +
+ Comma-separated fields to auto-generate when + uploading photos. Accepted values: title, caption, + tags, description, all, or none + {' '} + (default: {'"title,tags,semantic"'}): + {renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])} +
+ + {redisError && renderError({ + connection: { provider: 'Redis', error: redisError}, + })} + Create Upstash Redis store from storage tab + on Vercel dashboard and connect to this project + to enable rate limiting + + + Store base URL in environment variable to use + alternate OpenAI-compatible providers: + {renderEnvVars(['OPENAI_BASE_URL'])} + + ; + case 'Performance': + return <> + + Set environment variable to {'"1"'} to make site more responsive + by enabling static optimization + (i.e., rendering pages and images at build time): +
+ {renderSubStatusWithEnvVar( + arePhotosStaticallyOptimized ? 'checked' : 'optional', + 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS', + )} + {renderSubStatusWithEnvVar( + arePhotoOGImagesStaticallyOptimized ? 'checked' : 'optional', + 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES', + )} + {renderSubStatusWithEnvVar( + arePhotoCategoriesStaticallyOptimized ? 'checked' : 'optional', + 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES', + )} + {renderSubStatusWithEnvVar( // eslint-disable-next-line max-len - arePhotoCategoryOgImagesStaticallyOptimized ? 'checked' : 'optional', - 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORY_OG_IMAGES', - )} -
-
- - Set environment variable to {'"1"'} to prevent - image uploads being compressed before storing: - {renderEnvVars(['NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS'])} - - - Set environment variable from {'"1-100"'} - {' '} - to control the quality of large photos - ({'"100"'} represents highest quality/largest size): - {renderEnvVars(['NEXT_PUBLIC_IMAGE_QUALITY'])} - - - Set environment variable to {'"1"'} to prevent - image blur data being stored and displayed: - {renderEnvVars(['NEXT_PUBLIC_BLUR_DISABLED'])} - - ; - case 'Categories': - return <> - -
- {categoryVisibility.map((category, index) => - - {renderSubStatus( - 'checked', - <> - {index + 1} - {'.'} - {category} - , - )} - )} - {getHiddenCategories(categoryVisibility) - .map(category => + arePhotoCategoryOgImagesStaticallyOptimized ? 'checked' : 'optional', + 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORY_OG_IMAGES', + )} +
+
+ + Set environment variable to {'"1"'} to prevent + image uploads being compressed before storing: + {renderEnvVars(['NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS'])} + + + Set environment variable from {'"1-100"'} + {' '} + to control the quality of large photos + ({'"100"'} represents highest quality/largest size): + {renderEnvVars(['NEXT_PUBLIC_IMAGE_QUALITY'])} + + + Set environment variable to {'"1"'} to prevent + image blur data being stored and displayed: + {renderEnvVars(['NEXT_PUBLIC_BLUR_DISABLED'])} + + ; + case 'Categories': + return <> + +
+ {categoryVisibility.map((category, index) => {renderSubStatus( - 'optional', - - {'* '} + 'checked', + <> + {index + 1} + {'.'} {category} - , + , )} )} -
- Configure order and visibility of categories - (seen in grid sidebar and CMD-K results) - by storing comma-separated values - (default: {`"${DEFAULT_CATEGORY_KEYS.join(',')}"`}): - {renderEnvVars(['NEXT_PUBLIC_CATEGORY_VISIBILITY'])} -
- -
-
- Set environment variable to {'"1"'} to prevent images - displaying when hovering over category links: - {renderEnvVars(['NEXT_PUBLIC_HIDE_CATEGORY_IMAGE_HOVERS'])} + {getHiddenCategories(categoryVisibility) + .map(category => + + {renderSubStatus( + 'optional', + + {'* '} + {category} + , + )} + )}
-
-
- - Set environment variable to {'"1"'} to always show - expanded category content - {renderEnvVars(['NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES'])} - - - Set environment variable to {'"1"'} to only show tags - with 2 or more photos - {renderEnvVars(['NEXT_PUBLIC_HIDE_TAGS_WITH_ONE_PHOTO'])} - - ; - case 'Sorting': - return <> - -
- {DEFAULT_SORT_BY_OPTIONS - .map(({sortBy, configKey }) => - - {renderSubStatus( - sortBy === defaultSortBy ? 'checked' : 'optional', - `${configKey}${sortBy === APP_DEFAULT_SORT_BY - ? ' (default)' - : ''}`, - )} - )} -
- Change default sort on grid/full homepages - {renderEnvVars(['NEXT_PUBLIC_DEFAULT_SORT'])} -
- - Set environment variable to {'"none"'}, {'"toggle"'} (default), - or {'"menu"'}, to control sort UI on grid/full homepages: - {renderEnvVars(['NEXT_PUBLIC_NAV_SORT_CONTROL'])} - - - Set environment variable to {'"1"'} to enable color-based sorting - (forces nav sort control to {'"menu,"'} flags photos missing - color data in admin dashboard)—color identification - benefits greatly from AI being enabled: - {renderEnvVars([ - 'NEXT_PUBLIC_COLOR_SORT', - ])} - - - Configure which colors start first - (accepts a hue of 0 to 360, default: 80) - and which are considered sufficiently vibrant - (accepts a chroma of 0 to 0.37, default: 0.05): -
- - -
-
- - Set environment variable to {'"1"'} to take priority field - into account when sorting photos (enabling may have - performance consequences): - {renderEnvVars(['NEXT_PUBLIC_PRIORITY_BASED_SORTING'])} - - ; - case 'Display': - return <> - - Set environment variable to {'"1"'} to hide keyboard shortcut - tooltips in areas like the main nav, and previous/next photo links: - {renderEnvVars(['NEXT_PUBLIC_HIDE_KEYBOARD_SHORTCUT_TOOLTIPS'])} - - - Set environment variable to {'"1"'} to hide EXIF data: - {renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])} - - - Set environment variable to {'"1"'} to hide - fullscreen photo zoom controls: - {renderEnvVars(['NEXT_PUBLIC_HIDE_ZOOM_CONTROLS'])} - - - Set environment variable to {'"1"'} to hide - taken at time from photo meta: - {renderEnvVars(['NEXT_PUBLIC_HIDE_TAKEN_AT_TIME'])} - - - Set environment variable to {'"1"'} to hide - {' '} - X (formerly Twitter) button from share modal: - {renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])} - - - Set environment variable to {'"1"'} to hide footer link: - {renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])} - - ; - case 'Grid': - return <> - - Set environment variable to {'"1"'} to show grid layout - on homepage: - {renderEnvVars(['NEXT_PUBLIC_GRID_HOMEPAGE'])} - - - Set environment variable to any number to enforce aspect ratio - {' '} - (default is {'"1"'}, i.e., square)—set to {'"0"'} to disable: - {renderEnvVars(['NEXT_PUBLIC_GRID_ASPECT_RATIO'])} - - - Set environment variable to {'"1"'} to ensure large thumbnails - on photo grid views (if not configured, density is based on - aspect ratio): - {renderEnvVars(['NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS'])} - - ; - case 'Design': - return <> - - {'Set environment variable to \'light\' or \'dark\''} - {' '} - to configure initial theme - {' '} - (defaults to {'\'system\''}): - {renderEnvVars(['NEXT_PUBLIC_DEFAULT_THEME'])} - - - Set environment variable to {'"1"'} to constrain the size - {' '} - of each photo, and display a surrounding border: -
- -
-
- - Set environment variable hex values (e.g., #cccccc) - to override matte colors: -
- - -
-
- ; - case 'Settings': - return <> - - Set environment variable to {'"1"'} to disable - collection/display of location-based data: - {renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])} - - - Set environment variable to {'"1"'} to enable - public photo downloads for all visitors: - {renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])} - - - Set environment variable to {'"1"'} to enable feeds at - {' '} - {renderLink(PATH_FEED_JSON)} and {renderLink(PATH_RSS_XML)}: - {renderEnvVars(['NEXT_PUBLIC_SITE_FEEDS'])} - - - Set environment variable to {'"BOTTOM"'} to - keep OG image text bottom aligned (default is {'"top"'}): - {renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])} - - ; - case 'Internal': - return <> - - Set environment variable to {'"1"'} to temporarily enable - features like photo matting, baseline grid, etc.: - {renderEnvVars(['ADMIN_DEBUG_TOOLS'])} - - - Set environment variable to {'"1"'} to prevent - homepages from seeding infinite scroll on load: - {renderEnvVars(['ADMIN_DB_OPTIMIZE'])} - - - Set environment variable to {'"1"'} to enable - console output for all sql queries: - {renderEnvVars(['ADMIN_SQL_DEBUG'])} - - ; + Configure order and visibility of categories + (seen in grid sidebar and CMD-K results) + by storing comma-separated values + (default: {`"${DEFAULT_CATEGORY_KEYS.join(',')}"`}): + {renderEnvVars(['NEXT_PUBLIC_CATEGORY_VISIBILITY'])} + + +
+
+ Set environment variable to {'"1"'} to prevent images + displaying when hovering over category links: + {renderEnvVars(['NEXT_PUBLIC_HIDE_CATEGORY_IMAGE_HOVERS'])} +
+
+
+ + Set environment variable to {'"1"'} to always show + expanded category content + {renderEnvVars(['NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES'])} + + + Set environment variable to {'"1"'} to only show tags + with 2 or more photos + {renderEnvVars(['NEXT_PUBLIC_HIDE_TAGS_WITH_ONE_PHOTO'])} + + ; + case 'Sorting': + return <> + +
+ {DEFAULT_SORT_BY_OPTIONS + .map(({sortBy, configKey }) => + + {renderSubStatus( + sortBy === defaultSortBy ? 'checked' : 'optional', + `${configKey}${sortBy === APP_DEFAULT_SORT_BY + ? ' (default)' + : ''}`, + )} + )} +
+ Change default sort on grid/full homepages + {renderEnvVars(['NEXT_PUBLIC_DEFAULT_SORT'])} +
+ + Set environment variable to {'"none"'}, {'"toggle"'} (default), + or {'"menu"'}, to control sort UI on grid/full homepages: + {renderEnvVars(['NEXT_PUBLIC_NAV_SORT_CONTROL'])} + + + Set environment variable to {'"1"'} to enable color-based sorting + (forces nav sort control to {'"menu,"'} flags photos missing + color data in admin dashboard)—color identification + benefits greatly from AI being enabled: + {renderEnvVars([ + 'NEXT_PUBLIC_COLOR_SORT', + ])} + + + Configure which colors start first + (accepts a hue of 0 to 360, default: 80) + and which are considered sufficiently vibrant + (accepts a chroma of 0 to 0.37, default: 0.05): +
+ + +
+
+ + Set environment variable to {'"1"'} to take priority field + into account when sorting photos (enabling may have + performance consequences): + {renderEnvVars(['NEXT_PUBLIC_PRIORITY_BASED_SORTING'])} + + ; + case 'Display': + return <> + + Set environment variable to {'"1"'} to hide keyboard shortcut + tooltips in areas like the main nav, and previous/next photo links: + {renderEnvVars(['NEXT_PUBLIC_HIDE_KEYBOARD_SHORTCUT_TOOLTIPS'])} + + + Set environment variable to {'"1"'} to hide EXIF data: + {renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])} + + + Set environment variable to {'"1"'} to hide + fullscreen photo zoom controls: + {renderEnvVars(['NEXT_PUBLIC_HIDE_ZOOM_CONTROLS'])} + + + Set environment variable to {'"1"'} to hide + taken at time from photo meta: + {renderEnvVars(['NEXT_PUBLIC_HIDE_TAKEN_AT_TIME'])} + + + Set environment variable to {'"1"'} to hide + {' '} + X (formerly Twitter) button from share modal: + {renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])} + + + Set environment variable to {'"1"'} to hide footer link: + {renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])} + + ; + case 'Grid': + return <> + + Set environment variable to {'"1"'} to show grid layout + on homepage: + {renderEnvVars(['NEXT_PUBLIC_GRID_HOMEPAGE'])} + + + Set environment variable to any number to enforce aspect ratio + {' '} + (default is {'"1"'}, i.e., square)—set to {'"0"'} to disable: + {renderEnvVars(['NEXT_PUBLIC_GRID_ASPECT_RATIO'])} + + + Set environment variable to {'"1"'} to ensure large thumbnails + on photo grid views (if not configured, density is based on + aspect ratio): + {renderEnvVars(['NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS'])} + + ; + case 'Design': + return <> + + {'Set environment variable to \'light\' or \'dark\''} + {' '} + to configure initial theme + {' '} + (defaults to {'\'system\''}): + {renderEnvVars(['NEXT_PUBLIC_DEFAULT_THEME'])} + + + Set environment variable to {'"1"'} to constrain the size + {' '} + of each photo, and display a surrounding border: +
+ +
+
+ + Set environment variable hex values (e.g., #cccccc) + to override matte colors: +
+ + +
+
+ ; + case 'Settings': + return <> + + Set environment variable to {'"1"'} to disable + collection/display of location-based data: + {renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])} + + + Set environment variable to {'"1"'} to enable + public photo downloads for all visitors: + {renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])} + + + Set environment variable to {'"1"'} to enable feeds at + {' '} + {renderLink(PATH_FEED_JSON)} and {renderLink(PATH_RSS_XML)}: + {renderEnvVars(['NEXT_PUBLIC_SITE_FEEDS'])} + + + Set environment variable to {'"BOTTOM"'} to + keep OG image text bottom aligned (default is {'"top"'}): + {renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])} + + ; + case 'Internal': + return <> + + Set environment variable to {'"1"'} to temporarily enable + features like photo matting, baseline grid, etc.: + {renderEnvVars(['ADMIN_DEBUG_TOOLS'])} + + + Set environment variable to {'"1"'} to prevent + homepages from seeding infinite scroll on load: + {renderEnvVars(['ADMIN_DB_OPTIMIZE'])} + + + Set environment variable to {'"1"'} to enable + console output for all sql queries: + {renderEnvVars(['ADMIN_SQL_DEBUG'])} + + ; } }; diff --git a/src/admin/insights/AdminAppInsightsClient.tsx b/src/admin/insights/AdminAppInsightsClient.tsx index 27ef8f9e..8793e12a 100644 --- a/src/admin/insights/AdminAppInsightsClient.tsx +++ b/src/admin/insights/AdminAppInsightsClient.tsx @@ -440,58 +440,58 @@ export default function AdminAppInsightsClient({ /> {CATEGORY_VISIBILITY.map(category => { switch (category) { - case 'cameras': - return } - content={pluralize(camerasCount, 'camera')} - />; - case 'lenses': - return } - content={pluralize(lensesCount, 'lens', 'lenses')} - />; - case 'tags': - return } - content={pluralize(tagsCount, 'tag')} - />; - case 'recipes': - return recipesCount > 0 - ? } - content={pluralize(recipesCount, 'recipe')} - /> - : null; - case 'films': - return filmsCount > 0 - ? ; + case 'lenses': + return } - content={pluralize(filmsCount, 'film')} - /> - : null; - case 'focal-lengths': - return } - content={pluralize(focalLengthsCount, 'focal length')} - />; + icon={} + content={pluralize(lensesCount, 'lens', 'lenses')} + />; + case 'tags': + return } + content={pluralize(tagsCount, 'tag')} + />; + case 'recipes': + return recipesCount > 0 + ? } + content={pluralize(recipesCount, 'recipe')} + /> + : null; + case 'films': + return filmsCount > 0 + ? } + content={pluralize(filmsCount, 'film')} + /> + : null; + case 'focal-lengths': + return } + content={pluralize(focalLengthsCount, 'focal length')} + />; } })} {descriptionWithSpaces && { switch (size) { - case 'small': return 6; - case 'medium': return 7; - case 'large': return 8; + case 'small': return 6; + case 'medium': return 7; + case 'large': return 8; } }; diff --git a/src/app/AppState.ts b/src/app/AppState.ts index 54783a1c..2c5e29e5 100644 --- a/src/app/AppState.ts +++ b/src/app/AppState.ts @@ -28,8 +28,9 @@ export type AppStateContextType = { clearNextPhotoAnimation?: (id?: string) => void shouldRespondToKeyboardCommands?: boolean setShouldRespondToKeyboardCommands?: Dispatch> - categoriesWithCounts?: - Awaited> + categoriesWithCounts?: Awaited> // MODAL isCommandKOpen?: boolean setIsCommandKOpen?: Dispatch> diff --git a/src/app/AppViewSwitcher.tsx b/src/app/AppViewSwitcher.tsx index 71479fc3..d54093c7 100644 --- a/src/app/AppViewSwitcher.tsx +++ b/src/app/AppViewSwitcher.tsx @@ -86,15 +86,15 @@ export default function AppViewSwitcher({ const onKeyDown = useCallback((e: KeyboardEvent) => { if (!e.metaKey) { switch (e.key.toLocaleUpperCase()) { - case KEY_COMMANDS.full: - if (pathname !== PATH_FULL_INFERRED) { refHrefFull.current?.click(); } - break; - case KEY_COMMANDS.grid: - if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); } - break; - case KEY_COMMANDS.admin: - if (isUserSignedIn) { setIsAdminMenuOpen(true); } - break; + case KEY_COMMANDS.full: + if (pathname !== PATH_FULL_INFERRED) { refHrefFull.current?.click(); } + break; + case KEY_COMMANDS.grid: + if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); } + break; + case KEY_COMMANDS.admin: + if (isUserSignedIn) { setIsAdminMenuOpen(true); } + break; } } }, [pathname, isUserSignedIn]); diff --git a/src/app/static.ts b/src/app/static.ts index 3c9d0e43..4a20acd6 100644 --- a/src/app/static.ts +++ b/src/app/static.ts @@ -47,24 +47,24 @@ export const staticallyGenerateCategoryIfConfigured = ( getData: () => Promise, formatData: (data: T[]) => K[], ): (() => Promise) | undefined => - CATEGORY_VISIBILITY.includes(key) && - IS_PRODUCTION && ( - (type === 'page' && STATICALLY_OPTIMIZED_PHOTO_CATEGORIES) || - (type === 'image' && STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES) - ) - ? async () => { - const data = (await getData() - .catch(e => { - console.error(`Error fetching static ${key} data: ${e}`); - return []; - })) - .slice(0, GENERATE_STATIC_PARAMS_LIMIT); - if (IS_BUILDING) { - logStaticGenerationDetails( - data.length, - `${depluralize(key)} ${type}`, - ); - } - return formatData(data); + CATEGORY_VISIBILITY.includes(key) && + IS_PRODUCTION && ( + (type === 'page' && STATICALLY_OPTIMIZED_PHOTO_CATEGORIES) || + (type === 'image' && STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES) + ) + ? async () => { + const data = (await getData() + .catch(e => { + console.error(`Error fetching static ${key} data: ${e}`); + return []; + })) + .slice(0, GENERATE_STATIC_PARAMS_LIMIT); + if (IS_BUILDING) { + logStaticGenerationDetails( + data.length, + `${depluralize(key)} ${type}`, + ); } - : undefined; + return formatData(data); + } + : undefined; diff --git a/src/camera/index.ts b/src/camera/index.ts index 457ce945..706e7b98 100644 --- a/src/camera/index.ts +++ b/src/camera/index.ts @@ -74,22 +74,22 @@ export const formatCameraText = ( model.toLocaleLowerCase().startsWith(makeSimple.toLocaleLowerCase()) ); switch (length) { - case 'long': - return `${make} ${model}`; - case 'medium': - return doesModelStartWithMake || isCameraMakeApple(make) - ? model - : `${make} ${model}`; - case 'short': - model = doesModelStartWithMake - ? model.replace(makeSimple, '').trim() - : model; - if ( - model.includes('iPhone') && + case 'long': + return `${make} ${model}`; + case 'medium': + return doesModelStartWithMake || isCameraMakeApple(make) + ? model + : `${make} ${model}`; + case 'short': + model = doesModelStartWithMake + ? model.replace(makeSimple, '').trim() + : model; + if ( + model.includes('iPhone') && model.length > 9 - ) { - model = model.replace(/iPhone\s*/i, ''); - } - return model; + ) { + model = model.replace(/iPhone\s*/i, ''); + } + return model; } }; diff --git a/src/cmdk/CommandKClient.tsx b/src/cmdk/CommandKClient.tsx index 7f4ce69b..d79fe6ca 100644 --- a/src/cmdk/CommandKClient.tsx +++ b/src/cmdk/CommandKClient.tsx @@ -351,107 +351,107 @@ export default function CommandKClient({ CATEGORY_VISIBILITY .map(category => { switch (category) { - case 'recents': return { - heading: appText.category.recentPlural, - accessory: , - items: recentsStatus ? [{ - label: recentsStatus.subhead, - annotation: formatCount(recentsStatus.count), - annotationAria: formatCountDescriptive(recentsStatus.count), - path: PREFIX_RECENTS, - }] : [], - }; - case 'years': return { - heading: appText.category.yearPlural, - accessory: , - items: years.map(({ year, count }) => ({ - label: year, - annotation: formatCount(count), - annotationAria: formatCountDescriptive(count), - path: pathForYear(year), - })), - }; - case 'cameras': return { - heading: appText.category.cameraPlural, - accessory: , - items: cameras.map(({ camera, count }) => ({ - label: formatCameraText(camera), - annotation: formatCount(count), - annotationAria: formatCountDescriptive(count), - path: pathForCamera(camera), - })), - }; - case 'lenses': return { - heading: appText.category.lensPlural, - accessory: , - items: lenses.map(({ lens, count }) => ({ - label: formatLensText(lens, 'medium'), - explicitKey: formatLensText(lens, 'long'), - annotation: formatCount(count), - annotationAria: formatCountDescriptive(count), - path: pathForLens(lens), - })), - }; - case 'tags': return { - heading: appText.category.tagPlural, - accessory: , - items: tags.map(({ tag, count }) => ({ - explicitKey: formatTag(tag), - label: - {formatTag(tag)} - {isTagFavs(tag) && - } - {isTagPrivate(tag) && - } - , - annotation: formatCount(count), - annotationAria: formatCountDescriptive(count), - path: pathForTag(tag), - })), - }; - case 'recipes': return { - heading: appText.category.recipePlural, - accessory: , - items: recipes.map(({ recipe, count }) => ({ - label: formatRecipe(recipe), - annotation: formatCount(count), - annotationAria: formatCountDescriptive(count), - path: pathForRecipe(recipe), - })), - }; - case 'films': return { - heading: appText.category.filmPlural, - accessory: , - items: films.map(({ film, count }) => ({ - label: labelForFilm(film).medium, - annotation: formatCount(count), - annotationAria: formatCountDescriptive(count), - path: pathForFilm(film), - })), - }; - case 'focal-lengths': return { - heading: appText.category.focalLengthPlural, - accessory: , - items: focalLengths.map(({ focal, count }) => ({ - label: formatFocalLength(focal), - annotation: formatCount(count), - annotationAria: formatCountDescriptive(count), - path: pathForFocalLength(focal), - })), - }; + case 'recents': return { + heading: appText.category.recentPlural, + accessory: , + items: recentsStatus ? [{ + label: recentsStatus.subhead, + annotation: formatCount(recentsStatus.count), + annotationAria: formatCountDescriptive(recentsStatus.count), + path: PREFIX_RECENTS, + }] : [], + }; + case 'years': return { + heading: appText.category.yearPlural, + accessory: , + items: years.map(({ year, count }) => ({ + label: year, + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + path: pathForYear(year), + })), + }; + case 'cameras': return { + heading: appText.category.cameraPlural, + accessory: , + items: cameras.map(({ camera, count }) => ({ + label: formatCameraText(camera), + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + path: pathForCamera(camera), + })), + }; + case 'lenses': return { + heading: appText.category.lensPlural, + accessory: , + items: lenses.map(({ lens, count }) => ({ + label: formatLensText(lens, 'medium'), + explicitKey: formatLensText(lens, 'long'), + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + path: pathForLens(lens), + })), + }; + case 'tags': return { + heading: appText.category.tagPlural, + accessory: , + items: tags.map(({ tag, count }) => ({ + explicitKey: formatTag(tag), + label: + {formatTag(tag)} + {isTagFavs(tag) && + } + {isTagPrivate(tag) && + } + , + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + path: pathForTag(tag), + })), + }; + case 'recipes': return { + heading: appText.category.recipePlural, + accessory: , + items: recipes.map(({ recipe, count }) => ({ + label: formatRecipe(recipe), + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + path: pathForRecipe(recipe), + })), + }; + case 'films': return { + heading: appText.category.filmPlural, + accessory: , + items: films.map(({ film, count }) => ({ + label: labelForFilm(film).medium, + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + path: pathForFilm(film), + })), + }; + case 'focal-lengths': return { + heading: appText.category.focalLengthPlural, + accessory: , + items: focalLengths.map(({ focal, count }) => ({ + label: formatFocalLength(focal), + annotation: formatCount(count), + annotationAria: formatCountDescriptive(count), + path: pathForFocalLength(focal), + })), + }; } }) .filter(Boolean) as CommandKSection[] diff --git a/src/components/AnimateItems.tsx b/src/components/AnimateItems.tsx index abd0e139..b300f554 100644 --- a/src/components/AnimateItems.tsx +++ b/src/components/AnimateItems.tsx @@ -75,22 +75,22 @@ function AnimateItems({ const getInitialVariant = (): Variant => { switch (typeResolved) { - case 'left': return { - opacity: 0, - transform: `translateX(${distanceOffset}px)`, - }; - case 'right': return { - opacity: 0, - transform: `translateX(${-distanceOffset}px)`, - }; - case 'bottom': return { - opacity: 0, - transform: `translateY(${distanceOffset}px)`, - }; - default: return { - opacity: 0, - transform: `translateY(${distanceOffset}px) scale(${scaleOffset})`, - }; + case 'left': return { + opacity: 0, + transform: `translateX(${distanceOffset}px)`, + }; + case 'right': return { + opacity: 0, + transform: `translateX(${-distanceOffset}px)`, + }; + case 'bottom': return { + opacity: 0, + transform: `translateY(${distanceOffset}px)`, + }; + default: return { + opacity: 0, + transform: `translateY(${distanceOffset}px) scale(${scaleOffset})`, + }; } }; diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 7dbdfb5a..1cddc7ce 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -19,33 +19,33 @@ export default function Badge({ }) { const stylesForType = () => { switch (type) { - case 'large': - return clsx( - 'px-1.5 h-[26px]', - 'rounded-md', - 'bg-gray-100/40 dark:bg-gray-900/60', - 'border border-medium', - ); - case 'small': - return clsx( - 'px-[5px] h-[17px] md:h-[18px]', - 'text-[0.7rem] font-medium rounded-[0.25rem]', - contrast === 'high' - ? 'text-invert bg-invert' - : contrast === 'frosted' - ? 'text-black bg-neutral-100/30 border border-neutral-200/40' - : 'text-medium bg-gray-300/30 dark:bg-gray-700/50', - interactive && (contrast === 'high' - ? 'hover:opacity-70' - : contrast === 'frosted' - ? 'hover:text-black dark:hover:text-black' - : 'hover:text-gray-900 dark:hover:text-gray-100'), - interactive && (contrast === 'high' - ? 'active:opacity-90' - : contrast === 'frosted' - ? 'active:bg-neutral-100/50 dark:active:bg-neutral-900/10' - : 'active:bg-gray-200 dark:active:bg-gray-700/60'), - ); + case 'large': + return clsx( + 'px-1.5 h-[26px]', + 'rounded-md', + 'bg-gray-100/40 dark:bg-gray-900/60', + 'border border-medium', + ); + case 'small': + return clsx( + 'px-[5px] h-[17px] md:h-[18px]', + 'text-[0.7rem] font-medium rounded-[0.25rem]', + contrast === 'high' + ? 'text-invert bg-invert' + : contrast === 'frosted' + ? 'text-black bg-neutral-100/30 border border-neutral-200/40' + : 'text-medium bg-gray-300/30 dark:bg-gray-700/50', + interactive && (contrast === 'high' + ? 'hover:opacity-70' + : contrast === 'frosted' + ? 'hover:text-black dark:hover:text-black' + : 'hover:text-gray-900 dark:hover:text-gray-100'), + interactive && (contrast === 'high' + ? 'active:opacity-90' + : contrast === 'frosted' + ? 'active:bg-neutral-100/50 dark:active:bg-neutral-900/10' + : 'active:bg-gray-200 dark:active:bg-gray-700/60'), + ); } }; return ( diff --git a/src/components/Container.tsx b/src/components/Container.tsx index 1e8b95ac..5d916c18 100644 --- a/src/components/Container.tsx +++ b/src/components/Container.tsx @@ -25,37 +25,37 @@ export default function Container({ } & HTMLAttributes) { const getColorClasses = () => { switch (color) { - case 'gray': return [ - 'text-medium', - 'bg-dim', - ]; - case 'gray-border': return [ - 'text-medium', - 'bg-extra-dim', - 'border-medium', - ]; - case 'blue': return [ - 'text-blue-800 dark:text-blue-400', - 'bg-blue-50 dark:bg-blue-950/50', - ]; - case 'red': return [ - 'text-red-700 dark:text-red-400', - 'bg-red-100/50 dark:bg-red-950/55', - ]; - case 'yellow': return [ - 'text-amber-700 dark:text-amber-500', - 'bg-amber-100/55 dark:bg-amber-950/55', - ]; + case 'gray': return [ + 'text-medium', + 'bg-dim', + ]; + case 'gray-border': return [ + 'text-medium', + 'bg-extra-dim', + 'border-medium', + ]; + case 'blue': return [ + 'text-blue-800 dark:text-blue-400', + 'bg-blue-50 dark:bg-blue-950/50', + ]; + case 'red': return [ + 'text-red-700 dark:text-red-400', + 'bg-red-100/50 dark:bg-red-950/55', + ]; + case 'yellow': return [ + 'text-amber-700 dark:text-amber-500', + 'bg-amber-100/55 dark:bg-amber-950/55', + ]; } }; const getPaddingClasses = () => { switch (padding) { - case 'loose': return 'p-4 md:p-24'; - case 'normal': return 'p-4 md:p-8'; - case 'tight': return 'py-1.5 px-2.5'; - case 'tight-cta-right': return 'py-1.5 pl-2.5 pr-1.5'; - case 'tight-cta-right-left': return 'py-1.5 px-1.5'; + case 'loose': return 'p-4 md:p-24'; + case 'normal': return 'p-4 md:p-8'; + case 'tight': return 'py-1.5 px-2.5'; + case 'tight-cta-right': return 'py-1.5 pl-2.5 pr-1.5'; + case 'tight-cta-right-left': return 'py-1.5 px-1.5'; } }; diff --git a/src/components/ImageInput.tsx b/src/components/ImageInput.tsx index b1a820f2..bd32f181 100644 --- a/src/components/ImageInput.tsx +++ b/src/components/ImageInput.tsx @@ -145,14 +145,14 @@ export default function ImageInput({ // Reverse engineer orientation // so preserved EXIF data can be copied switch (orientation) { - case 1: orientation = 1; break; - case 2: orientation = 1; break; - case 3: orientation = 3; break; - case 4: orientation = 1; break; - case 5: orientation = 1; break; - case 6: orientation = 8; break; - case 7: orientation = 1; break; - case 8: orientation = 6; break; + case 1: orientation = 1; break; + case 2: orientation = 1; break; + case 3: orientation = 3; break; + case 4: orientation = 1; break; + case 5: orientation = 1; break; + case 6: orientation = 8; break; + case 7: orientation = 1; break; + case 8: orientation = 6; break; } } @@ -171,43 +171,43 @@ export default function ImageInput({ // https://gist.github.com/SagiMedina/f00a57de4e211456225d3114fd10b0d0 switch(orientation) { - case 2: - ctx.translate(width, 0); - ctx.scale(-1, 1); - break; - case 3: - ctx.translate(width, height); - ctx.rotate((180 / 180) * Math.PI); - break; - case 4: - ctx.translate(0, height); - ctx.scale(1, -1); - break; - case 5: - canvas.width = height; - canvas.height = width; - ctx.rotate((90 / 180) * Math.PI); - ctx.scale(1, -1); - break; - case 6: - canvas.width = height; - canvas.height = width; - ctx.rotate((90 / 180) * Math.PI); - ctx.translate(0, -height); - break; - case 7: - canvas.width = height; - canvas.height = width; - ctx.rotate((270 / 180) * Math.PI); - ctx.translate(-width, height); - ctx.scale(1, -1); - break; - case 8: - canvas.width = height; - canvas.height = width; - ctx.translate(0, width); - ctx.rotate((270 / 180) * Math.PI); - break; + case 2: + ctx.translate(width, 0); + ctx.scale(-1, 1); + break; + case 3: + ctx.translate(width, height); + ctx.rotate((180 / 180) * Math.PI); + break; + case 4: + ctx.translate(0, height); + ctx.scale(1, -1); + break; + case 5: + canvas.width = height; + canvas.height = width; + ctx.rotate((90 / 180) * Math.PI); + ctx.scale(1, -1); + break; + case 6: + canvas.width = height; + canvas.height = width; + ctx.rotate((90 / 180) * Math.PI); + ctx.translate(0, -height); + break; + case 7: + canvas.width = height; + canvas.height = width; + ctx.rotate((270 / 180) * Math.PI); + ctx.translate(-width, height); + ctx.scale(1, -1); + break; + case 8: + canvas.width = height; + canvas.height = width; + ctx.translate(0, width); + ctx.rotate((270 / 180) * Math.PI); + break; } ctx.drawImage(image, 0, 0, width, height); diff --git a/src/components/SelectMenu.tsx b/src/components/SelectMenu.tsx index 9156b62e..863d7fcd 100644 --- a/src/components/SelectMenu.tsx +++ b/src/components/SelectMenu.tsx @@ -59,58 +59,58 @@ export default function SelectMenu({ const listener = (e: KeyboardEvent) => { // Keys which always trap focus switch (e.key) { - case 'ArrowDown': - case 'ArrowUp': - case 'Escape': - setShouldHighlightOnHover(false); - e.stopImmediatePropagation(); - e.preventDefault(); + case 'ArrowDown': + case 'ArrowUp': + case 'Escape': + setShouldHighlightOnHover(false); + e.stopImmediatePropagation(); + e.preventDefault(); } // Navigate options switch (e.key) { - case 'ArrowDown': - if (isOpen) { - setSelectedOptionIndex(i => { - if (i === undefined) { - return options.length > 1 ? 1 : 0; - } else if (i >= options.length - 1) { - return 0; - } else { - return i + 1; - } - }); - } else { - setIsOpen(true); - setSelectedOptionIndex(0); - } - break; - case 'ArrowUp': - if (isOpen) { - setSelectedOptionIndex((i = 0) => { - if (options.length > 1) { - if (i === 0) { - return options.length - 1; + case 'ArrowDown': + if (isOpen) { + setSelectedOptionIndex(i => { + if (i === undefined) { + return options.length > 1 ? 1 : 0; + } else if (i >= options.length - 1) { + return 0; } else { - return i - 1; + return i + 1; } - } - }); - } else { - setIsOpen(true); - setSelectedOptionIndex(Math.max(0, options.length - 1)); - } - break; - case 'Enter': - if (isOpen) { - if (selectedOptionIndex !== undefined) { - onChange?.(options[selectedOptionIndex].value); + }); + } else { + setIsOpen(true); + setSelectedOptionIndex(0); } + break; + case 'ArrowUp': + if (isOpen) { + setSelectedOptionIndex((i = 0) => { + if (options.length > 1) { + if (i === 0) { + return options.length - 1; + } else { + return i - 1; + } + } + }); + } else { + setIsOpen(true); + setSelectedOptionIndex(Math.max(0, options.length - 1)); + } + break; + case 'Enter': + if (isOpen) { + if (selectedOptionIndex !== undefined) { + onChange?.(options[selectedOptionIndex].value); + } + setIsOpen(false); + } + break; + case 'Escape': setIsOpen(false); - } - break; - case 'Escape': - setIsOpen(false); - break; + break; } }; diff --git a/src/components/StatusIcon.tsx b/src/components/StatusIcon.tsx index 8e9c880e..ce6d391c 100644 --- a/src/components/StatusIcon.tsx +++ b/src/components/StatusIcon.tsx @@ -17,26 +17,26 @@ export default function StatusIcon({ }) { const getIcon = () => { switch (type) { - case 'checked': - return ; - case 'missing': - return ; - case 'warning': - return ; - case 'optional': - return ; + case 'checked': + return ; + case 'missing': + return ; + case 'warning': + return ; + case 'optional': + return ; } }; diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index 02881295..1f2b8bda 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -172,68 +172,66 @@ export default function TagInput({ const listener = (e: KeyboardEvent) => { // Keys which always trap focus switch (e.key) { - case 'ArrowDown': - case 'ArrowUp': - case 'Escape': - e.stopImmediatePropagation(); - e.preventDefault(); - } - switch (e.key) { - case 'Enter': - // Only trap focus if there are options to select - // otherwise allow form to submit - if ( - shouldShowMenu && - optionsFiltered.length > 0 - ) { + case 'ArrowDown': + case 'ArrowUp': + case 'Escape': e.stopImmediatePropagation(); e.preventDefault(); - if (!hasReachedLimit) { - addOptions([optionsFiltered[selectedOptionIndex ?? 0].value]); + } + switch (e.key) { + case 'Enter': + // Only trap focus if there are options to select + // otherwise allow form to submit + if ( + shouldShowMenu && + optionsFiltered.length > 0 + ) { + e.stopImmediatePropagation(); + e.preventDefault(); + if (!hasReachedLimit) { + addOptions([optionsFiltered[selectedOptionIndex ?? 0].value]); + } } - } - break; - case 'ArrowDown': - if (shouldShowMenu) { + break; + case 'ArrowDown': + if (shouldShowMenu) { + setSelectedOptionIndex(i => { + if (i === undefined) { + return optionsFiltered.length > 1 ? 1 : 0; + } else if (i >= optionsFiltered.length - 1) { + return 0; + } else { + return i + 1; + } + }); + } else { + setShouldShowMenu(true); + } + break; + case 'ArrowUp': setSelectedOptionIndex(i => { - if (i === undefined) { - return optionsFiltered.length > 1 ? 1 : 0; - } else if (i >= optionsFiltered.length - 1) { - return 0; + if ( + document.activeElement === inputRef.current && + optionsFiltered.length > 0 + ) { + return optionsFiltered.length - 1; + } else if (i === undefined || i === 0) { + inputRef.current?.focus(); + return undefined; } else { - return i + 1; + return i - 1; } }); - } else { - setShouldShowMenu(true); - } - break; - case 'ArrowUp': - setSelectedOptionIndex(i => { - if ( - document.activeElement === inputRef.current && - optionsFiltered.length > 0 - ) { - return optionsFiltered.length - 1; - } else if (i === undefined || i === 0) { - inputRef.current?.focus(); - return undefined; - } else { - return i - 1; + break; + case 'Backspace': + if (inputText === '' && selectedOptions.length > 0) { + removeOption(selectedOptions[selectedOptions.length - 1]); + if (!showMenuOnDelete) { hideMenu(); } } - }); - break; - case 'Backspace': - if (inputText === '' && selectedOptions.length > 0) { - removeOption(selectedOptions[selectedOptions.length - 1]); - if (!showMenuOnDelete) { - hideMenu(); - } - } - break; - case 'Escape': - hideMenu(true); - break; + break; + case 'Escape': + hideMenu(true); + break; } }; diff --git a/src/components/entity/EntityLink.tsx b/src/components/entity/EntityLink.tsx index 3b58e1d0..670e87d9 100644 --- a/src/components/entity/EntityLink.tsx +++ b/src/components/entity/EntityLink.tsx @@ -74,14 +74,14 @@ export default function EntityLink({ const classForContrast = () => { switch (contrast) { - case 'low': - return 'text-dim'; - case 'high': - return 'text-main'; - case 'frosted': - return 'text-black'; - default: - return 'text-medium'; + case 'low': + return 'text-dim'; + case 'high': + return 'text-main'; + case 'frosted': + return 'text-black'; + default: + return 'text-medium'; } }; diff --git a/src/components/image/ImageWithFallback.tsx b/src/components/image/ImageWithFallback.tsx index ad265c11..2cd4e9de 100644 --- a/src/components/image/ImageWithFallback.tsx +++ b/src/components/image/ImageWithFallback.tsx @@ -41,11 +41,11 @@ export default function ImageWithFallback({ const getBlurClass = () => { switch (blurCompatibilityLevel) { - case 'high': + case 'high': // Fix poorly blurred placeholder data generated on client - return 'blur-[4px] @xs:blue-md scale-[1.05]'; - case 'low': - return 'blur-[2px] @xs:blue-md scale-[1.01]'; + return 'blur-[4px] @xs:blue-md scale-[1.05]'; + case 'low': + return 'blur-[2px] @xs:blue-md scale-[1.01]'; } }; diff --git a/src/components/image/ZoomControls.tsx b/src/components/image/ZoomControls.tsx index 2a61daff..24e2a811 100644 --- a/src/components/image/ZoomControls.tsx +++ b/src/components/image/ZoomControls.tsx @@ -16,8 +16,8 @@ export default function ZoomControls({ }: { ref?: RefObject children: ReactNode - selectImageElement?: - (container: HTMLElement | null) => HTMLImageElement | null + selectImageElement?: (container: HTMLElement | null) => + HTMLImageElement | null isEnabled?: boolean }) { const refImageContainer = useRef(null); diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index 4458281c..6e054f6c 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -45,14 +45,14 @@ export default function MoreMenuItem({ const getColorClasses = () => { switch (color) { - case 'grey': return clsx( - 'hover:bg-gray-100/90 active:bg-gray-200/75', - 'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80', - ); - case 'red': return clsx( - 'hover:bg-red-100/50 active:bg-red-100/75', - 'dark:hover:bg-red-950/55 dark:active:bg-red-950/80', - ); + case 'grey': return clsx( + 'hover:bg-gray-100/90 active:bg-gray-200/75', + 'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80', + ); + case 'red': return clsx( + 'hover:bg-red-100/50 active:bg-red-100/75', + 'dark:hover:bg-red-950/55 dark:active:bg-red-950/80', + ); } }; diff --git a/src/feed/rss.ts b/src/feed/rss.ts index cecdb5b1..128ad743 100644 --- a/src/feed/rss.ts +++ b/src/feed/rss.ts @@ -39,8 +39,8 @@ const feedPhotoToXml = (photo: FeedPhotoRss): string => { ${photo.link} ${photo.description - ? `` - : ''} + ? `` + : ''} - - - ; - case 'monochrome-ye': return - - - - ; - case 'monochrome-r': return - - - - ; - case 'monochrome-g': return - - - - ; - case 'sepia': return - - - - ; - case 'acros': return - - - ; - case 'acros-ye': return - - - - ; - case 'acros-r': return - - - - ; - case 'acros-g': return - - - - ; - case 'provia': return - - - - ; - case 'portrait': return - - - ; - case 'portrait-saturation': return - - - - ; - case 'astia': return - - - ; - case 'portrait-sharpness': return - - - - ; - case 'portrait-ex': return - - - - ; - case 'velvia': return - - - ; - case 'pro-neg-std': return - - - - ; - case 'pro-neg-hi': return - - - - ; - case 'classic-chrome': return - - - - ; - case 'eterna': return - - - ; - case 'classic-neg': return - - - - ; - case 'eterna-bleach-bypass': return - - - - ; - case 'nostalgic-neg': return - - - - ; - case 'reala': return - - - ; + case 'monochrome': return + + + ; + case 'monochrome-ye': return + + + + ; + case 'monochrome-r': return + + + + ; + case 'monochrome-g': return + + + + ; + case 'sepia': return + + + + ; + case 'acros': return + + + ; + case 'acros-ye': return + + + + ; + case 'acros-r': return + + + + ; + case 'acros-g': return + + + + ; + case 'provia': return + + + + ; + case 'portrait': return + + + ; + case 'portrait-saturation': return + + + + ; + case 'astia': return + + + ; + case 'portrait-sharpness': return + + + + ; + case 'portrait-ex': return + + + + ; + case 'velvia': return + + + ; + case 'pro-neg-std': return + + + + ; + case 'pro-neg-hi': return + + + + ; + case 'classic-chrome': return + + + + ; + case 'eterna': return + + + ; + case 'classic-neg': return + + + + ; + case 'eterna-bleach-bypass': return + + + + ; + case 'nostalgic-neg': return + + + + ; + case 'reala': return + + + ; } })(); diff --git a/src/lens/index.ts b/src/lens/index.ts index 83f43ed7..80a540de 100644 --- a/src/lens/index.ts +++ b/src/lens/index.ts @@ -113,10 +113,10 @@ export const formatLensText = ( : modelRaw; switch (length) { - case 'long': - return make ? `${make} ${modelRaw}` : modelRaw; - case 'medium': - case 'short': - return model; + case 'long': + return make ? `${make} ${modelRaw}` : modelRaw; + case 'medium': + case 'short': + return model; } }; diff --git a/src/photo/PhotoDate.tsx b/src/photo/PhotoDate.tsx index e06b7e5e..b885cc0f 100644 --- a/src/photo/PhotoDate.tsx +++ b/src/photo/PhotoDate.tsx @@ -35,12 +35,12 @@ export default function PhotoDate({ const getTitleLabel = () => { switch (dateType) { - case 'takenAt': - return appText.photo.taken; - case 'createdAt': - return appText.photo.created; - case 'updatedAt': - return appText.photo.updated; + case 'takenAt': + return appText.photo.taken; + case 'createdAt': + return appText.photo.created; + case 'updatedAt': + return appText.photo.updated; } }; diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index c4c4108f..66188bb8 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -199,34 +199,34 @@ export default function PhotoGridSidebar({ items={tagsIncludingHidden .map(({ tag, count }) => { switch (tag) { - case TAG_FAVS: - return ; - case TAG_PRIVATE: - return ; - default: - return ; + case TAG_FAVS: + return ; + case TAG_PRIVATE: + return ; + default: + return ; } })} /> @@ -315,14 +315,14 @@ export default function PhotoGridSidebar({ />} {CATEGORY_VISIBILITY.map(category => { switch (category) { - case 'recents': return recentsContent; - case 'years': return yearsContent; - case 'cameras': return camerasContent; - case 'lenses': return lensesContent; - case 'tags': return tagsContent; - case 'recipes': return recipesContent; - case 'films': return filmsContent; - case 'focal-lengths': return focalLengthsContent; + case 'recents': return recentsContent; + case 'years': return yearsContent; + case 'cameras': return camerasContent; + case 'lenses': return lensesContent; + case 'tags': return tagsContent; + case 'recipes': return recipesContent; + case 'films': return filmsContent; + case 'focal-lengths': return focalLengthsContent; } })} {photoStatsContent} diff --git a/src/photo/PhotoPrevNextActions.tsx b/src/photo/PhotoPrevNextActions.tsx index a2e7c105..8343a288 100644 --- a/src/photo/PhotoPrevNextActions.tsx +++ b/src/photo/PhotoPrevNextActions.tsx @@ -129,72 +129,72 @@ export default function PhotoPrevNextActions({ const onKeyDown = useCallback((e: KeyboardEvent) => { if (e.metaKey) { switch (e.key.toUpperCase()) { - case KEY_COMMANDS.delete[1]: - if (isUserSignedIn) { - deletePhoto(); - } - break; + case KEY_COMMANDS.delete[1]: + if (isUserSignedIn) { + deletePhoto(); + } + break; } } else { switch (e.key.toUpperCase()) { // Public commands - case KEY_COMMANDS.prev[0]: - case KEY_COMMANDS.prev[1]: - if (pathPrevious) { - setNextPhotoAnimation?.(ANIMATION_RIGHT); - refPrevious.current?.click(); - } - break; - case KEY_COMMANDS.next[0]: - case KEY_COMMANDS.next[1]: - if (pathNext) { - setNextPhotoAnimation?.(ANIMATION_LEFT); - refNext.current?.click(); - } - break; - // Admin commands - case KEY_COMMANDS.edit: - if (isUserSignedIn) { - navigateToPhotoEdit(); - } - break; - case KEY_COMMANDS.favorite: - if (isUserSignedIn && photo && !isPhotoFav(photo)) { - favoritePhoto(); - } - break; - case KEY_COMMANDS.unfavorite: - if (isUserSignedIn && photo && isPhotoFav(photo)) { - unfavoritePhoto(); - } - break; - case KEY_COMMANDS.togglePrivate: - if (isUserSignedIn && photo) { - if (photo.hidden) { - unhidePhoto(); - } else { - hidePhoto(); + case KEY_COMMANDS.prev[0]: + case KEY_COMMANDS.prev[1]: + if (pathPrevious) { + setNextPhotoAnimation?.(ANIMATION_RIGHT); + refPrevious.current?.click(); } - } - break; - case KEY_COMMANDS.download: - if ( - (isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) && + break; + case KEY_COMMANDS.next[0]: + case KEY_COMMANDS.next[1]: + if (pathNext) { + setNextPhotoAnimation?.(ANIMATION_LEFT); + refNext.current?.click(); + } + break; + // Admin commands + case KEY_COMMANDS.edit: + if (isUserSignedIn) { + navigateToPhotoEdit(); + } + break; + case KEY_COMMANDS.favorite: + if (isUserSignedIn && photo && !isPhotoFav(photo)) { + favoritePhoto(); + } + break; + case KEY_COMMANDS.unfavorite: + if (isUserSignedIn && photo && isPhotoFav(photo)) { + unfavoritePhoto(); + } + break; + case KEY_COMMANDS.togglePrivate: + if (isUserSignedIn && photo) { + if (photo.hidden) { + unhidePhoto(); + } else { + hidePhoto(); + } + } + break; + case KEY_COMMANDS.download: + if ( + (isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) && downloadUrl && downloadFileName - ) { - downloadFileFromBrowser(downloadUrl, downloadFileName); - } - break; - case KEY_COMMANDS.sync: - if ( - isUserSignedIn && - photo && - window.confirm(syncPhotoConfirmText(photo, hasAiTextGeneration)) - ) { - syncPhoto(); - } - break; + ) { + downloadFileFromBrowser(downloadUrl, downloadFileName); + } + break; + case KEY_COMMANDS.sync: + if ( + isUserSignedIn && + photo && + window.confirm(syncPhotoConfirmText(photo, hasAiTextGeneration)) + ) { + syncPhoto(); + } + break; }; } }, [ diff --git a/src/photo/ai/AiButton.tsx b/src/photo/ai/AiButton.tsx index 01938386..357852ef 100644 --- a/src/photo/ai/AiButton.tsx +++ b/src/photo/ai/AiButton.tsx @@ -18,16 +18,16 @@ export default function AiButton({ const isLoading = useMemo(() => (requestFields ?? []).map(field => { switch (field) { - case 'title': - return aiContent.isLoadingTitle; - case 'caption': - return aiContent.isLoadingCaption; - case 'tags': - return aiContent.isLoadingTags; - case 'semantic': - return aiContent.isLoadingSemantic; - default: - return false; + case 'title': + return aiContent.isLoadingTitle; + case 'caption': + return aiContent.isLoadingCaption; + case 'tags': + return aiContent.isLoadingTags; + case 'semantic': + return aiContent.isLoadingSemantic; + default: + return false; } }).some(Boolean) , [ diff --git a/src/photo/ai/index.ts b/src/photo/ai/index.ts index 619c14a0..d0288863 100644 --- a/src/photo/ai/index.ts +++ b/src/photo/ai/index.ts @@ -56,21 +56,21 @@ export const getAiImageQuery = ( existingTitle?: string, ): string => { switch (query) { - case 'title': return 'Write a compelling title for this image in 3 words or less'; - case 'caption': return existingTitle - ? `Write a pithy caption for this image in 6 words or less and no punctuation that complements the existing title: "${existingTitle}"` - : 'Write a pithy caption for this image in 6 words or less and no punctuation'; - case 'title-and-caption': return 'Write a compelling title and pithy caption of 8 words or less for this image, using the format Title: "title" Caption: "caption"'; - case 'tags': - const tagQuery = 'Describe this image in 1-2 comma-separated unique keywords, with no adjective or adverbs. Avoid using general terms like "nature," "travel," "architecture," or "sky." Use terms that are highly specific to the image and not redundant.'; - const tags = existingTags.map(({ tag }) => tag).join(', '); - return tags - ? `${tagQuery}. Consider using some of these existing tags, but only if they are relevant: ${tags}.` - : tagQuery; - case 'description-small': return 'Describe this image succinctly without the initial text "This image shows" or "This is a picture of"'; - case 'description': return 'Describe this image'; - case 'description-large': return 'Describe this image in detail'; - case 'description-semantic': return 'List up to 5 things in this image without description as a comma-separated list'; + case 'title': return 'Write a compelling title for this image in 3 words or less'; + case 'caption': return existingTitle + ? `Write a pithy caption for this image in 6 words or less and no punctuation that complements the existing title: "${existingTitle}"` + : 'Write a pithy caption for this image in 6 words or less and no punctuation'; + case 'title-and-caption': return 'Write a compelling title and pithy caption of 8 words or less for this image, using the format Title: "title" Caption: "caption"'; + case 'tags': + const tagQuery = 'Describe this image in 1-2 comma-separated unique keywords, with no adjective or adverbs. Avoid using general terms like "nature," "travel," "architecture," or "sky." Use terms that are highly specific to the image and not redundant.'; + const tags = existingTags.map(({ tag }) => tag).join(', '); + return tags + ? `${tagQuery}. Consider using some of these existing tags, but only if they are relevant: ${tags}.` + : tagQuery; + case 'description-small': return 'Describe this image succinctly without the initial text "This image shows" or "This is a picture of"'; + case 'description': return 'Describe this image'; + case 'description-large': return 'Describe this image in detail'; + case 'description-semantic': return 'List up to 5 things in this image without description as a comma-separated list'; } }; diff --git a/src/photo/cache.ts b/src/photo/cache.ts index 5991a920..116b3ae3 100644 --- a/src/photo/cache.ts +++ b/src/photo/cache.ts @@ -60,24 +60,24 @@ const getCacheKeyForPhotoQueryOptions = ( ): string | null => { switch (option) { // Complex keys - case 'camera': { - const value = options[option]; - return value ? `${option}-${createCameraKey(value)}` : null; - } - case 'lens': { - const value = options[option]; - return value ? `${option}-${createLensKey(value)}` : null; - } - case 'takenBefore': - case 'takenAfterInclusive': - case 'updatedBefore': { - const value = options[option]; - return value ? `${option}-${value.toISOString()}` : null; - } - // Primitive keys - default: - const value = options[option]; - return value !== undefined ? `${option}-${value}` : null; + case 'camera': { + const value = options[option]; + return value ? `${option}-${createCameraKey(value)}` : null; + } + case 'lens': { + const value = options[option]; + return value ? `${option}-${createLensKey(value)}` : null; + } + case 'takenBefore': + case 'takenAfterInclusive': + case 'updatedBefore': { + const value = options[option]; + return value ? `${option}-${value.toISOString()}` : null; + } + // Primitive keys + default: + const value = options[option]; + return value !== undefined ? `${option}-${value}` : null; } }; diff --git a/src/photo/db/index.ts b/src/photo/db/index.ts index 624aaa5a..f8adc6ba 100644 --- a/src/photo/db/index.ts +++ b/src/photo/db/index.ts @@ -67,12 +67,12 @@ export const getWheresFromOptions = ( let valuesIndex = initialValuesIndex; switch (hidden) { - case 'exclude': - wheres.push('hidden IS NOT TRUE'); - break; - case 'only': - wheres.push('hidden IS TRUE'); - break; + case 'exclude': + wheres.push('hidden IS NOT TRUE'); + break; + case 'only': + wheres.push('hidden IS TRUE'); + break; } if (excludeFromFeeds) { @@ -162,31 +162,31 @@ export const getOrderByFromOptions = (options: PhotoQueryOptions) => { } = options; switch (sortBy) { - case 'takenAt': - return sortWithPriority - ? 'ORDER BY priority_order ASC, taken_at DESC' - : 'ORDER BY taken_at DESC'; - case 'takenAtAsc': - return sortWithPriority - ? 'ORDER BY priority_order ASC, taken_at ASC' - : 'ORDER BY taken_at ASC'; - case 'createdAt': - return sortWithPriority - ? 'ORDER BY priority_order ASC, created_at DESC' - : 'ORDER BY created_at DESC'; - case 'createdAtAsc': - return sortWithPriority - ? 'ORDER BY priority_order ASC, created_at ASC' - : 'ORDER BY created_at ASC'; - // Add date sort to account for photos with same color sort - case 'color': - return sortWithPriority - ? 'ORDER BY priority_order ASC, color_sort DESC, taken_at DESC' - : 'ORDER BY color_sort DESC, taken_at DESC'; - case 'colorAsc': - return sortWithPriority - ? 'ORDER BY priority_order ASC, color_sort ASC, taken_at ASC' - : 'ORDER BY color_sort ASC, taken_at ASC'; + case 'takenAt': + return sortWithPriority + ? 'ORDER BY priority_order ASC, taken_at DESC' + : 'ORDER BY taken_at DESC'; + case 'takenAtAsc': + return sortWithPriority + ? 'ORDER BY priority_order ASC, taken_at ASC' + : 'ORDER BY taken_at ASC'; + case 'createdAt': + return sortWithPriority + ? 'ORDER BY priority_order ASC, created_at DESC' + : 'ORDER BY created_at DESC'; + case 'createdAtAsc': + return sortWithPriority + ? 'ORDER BY priority_order ASC, created_at ASC' + : 'ORDER BY created_at ASC'; + // Add date sort to account for photos with same color sort + case 'color': + return sortWithPriority + ? 'ORDER BY priority_order ASC, color_sort DESC, taken_at DESC' + : 'ORDER BY color_sort DESC, taken_at DESC'; + case 'colorAsc': + return sortWithPriority + ? 'ORDER BY priority_order ASC, color_sort ASC, taken_at ASC' + : 'ORDER BY color_sort ASC, taken_at ASC'; } }; diff --git a/src/photo/db/query.ts b/src/photo/db/query.ts index a519eb54..8967cdcd 100644 --- a/src/photo/db/query.ts +++ b/src/photo/db/query.ts @@ -644,10 +644,10 @@ const needsAiTextWhereClauses = ? AI_TEXT_AUTO_GENERATED_FIELDS .map(field => { switch (field) { - case 'title': return `(title <> '') IS NOT TRUE`; - case 'caption': return `(caption <> '') IS NOT TRUE`; - case 'tags': return `(tags IS NULL OR array_length(tags, 1) = 0)`; - case 'semantic': return `(semantic_description <> '') IS NOT TRUE`; + case 'title': return `(title <> '') IS NOT TRUE`; + case 'caption': return `(caption <> '') IS NOT TRUE`; + case 'tags': return `(tags IS NULL OR array_length(tags, 1) = 0)`; + case 'semantic': return `(semantic_description <> '') IS NOT TRUE`; } }) : []; @@ -703,7 +703,7 @@ export const getColorDataForPhotos = () => SELECT id, url, color_data FROM photos LIMIT ${UPDATE_QUERY_LIMIT} `.then(({ rows }) => rows.map(({ id, url, color_data }) => - ({ id, url, colorData: color_data }))) + ({ id, url, colorData: color_data }))) , 'getColorDataForPhotos'); export const updateColorDataForPhoto = ( diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 49e8ac33..907890df 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -193,61 +193,64 @@ export default function PhotoForm({ const isFieldGeneratingAi = (key: keyof PhotoFormData) => { switch (key) { - case 'title': - return aiContent?.isLoadingTitle; - case 'caption': - return aiContent?.isLoadingCaption; - case 'tags': - return aiContent?.isLoadingTags; - case 'semanticDescription': - return aiContent?.isLoadingSemantic; - default: - return false; + case 'title': + return aiContent?.isLoadingTitle; + case 'caption': + return aiContent?.isLoadingCaption; + case 'tags': + return aiContent?.isLoadingTags; + case 'semanticDescription': + return aiContent?.isLoadingSemantic; + default: + return false; } }; const accessoryForField = (key: keyof PhotoFormData) => { if (aiContent) { switch (key) { - case 'title': - return ; - case 'caption': - return ; - case 'tags': - return ; - case 'semanticDescription': - return ; - case 'blurData': - return shouldDebugImageFallbacks && type === 'edit' && formData.url - ? - setFormData(data => ({ ...data, blurData }))} - /> - : null; + case 'title': + return ; + case 'caption': + return ; + case 'tags': + return ; + case 'semanticDescription': + return ; + case 'blurData': + return shouldDebugImageFallbacks && type === 'edit' && formData.url + ? + setFormData(data => ({ ...data, blurData }))} + /> + : null; } } }; @@ -429,60 +432,60 @@ export default function PhotoForm({ }; switch (key) { - case 'film': - return - - } - />; - case 'applyRecipeTitleGlobally': - return ; - case 'colorData': - return } - />; - case 'visibility': - return ; - case 'favorite': - return ; - default: - return ; + case 'film': + return + + } + />; + case 'applyRecipeTitleGlobally': + return ; + case 'colorData': + return } + />; + case 'visibility': + return ; + case 'favorite': + return ; + default: + return ; } } })} diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index a7787251..10283f93 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -258,22 +258,22 @@ export const formHasTextContent = ({ export const convertPhotoToFormData = (photo: Photo): PhotoFormData => { const valueForKey = (key: keyof Photo, value: any) => { switch (key) { - case 'tags': - return (value ?? []) - .filter((tag: string) => tag !== TAG_FAVS) - .join(', '); - case 'takenAt': - return value?.toISOString ? value.toISOString() : value; - case 'hidden': - return value ? 'true' : 'false'; - case 'recipeData': - return JSON.stringify(value); - case 'colorData': - return JSON.stringify(value); - default: - return value !== undefined && value !== null - ? value.toString() - : undefined; + case 'tags': + return (value ?? []) + .filter((tag: string) => tag !== TAG_FAVS) + .join(', '); + case 'takenAt': + return value?.toISOString ? value.toISOString() : value; + case 'hidden': + return value ? 'true' : 'false'; + case 'recipeData': + return JSON.stringify(value); + case 'colorData': + return JSON.stringify(value); + default: + return value !== undefined && value !== null + ? value.toString() + : undefined; } }; return Object.entries(photo).reduce((photoForm, [key, value]) => ({ diff --git a/src/photo/server.ts b/src/photo/server.ts index eaac7425..c2f5627a 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -221,7 +221,7 @@ export const removeGpsData = async (image: ArrayBuffer) => export const convertFormDataToPhotoDbInsertAndLookupRecipeTitle = async (...args: Parameters): - Promise> => { + Promise> => { const photo = convertFormDataToPhotoDbInsert(...args); if (photo.recipeData && !photo.recipeTitle && photo.film) { diff --git a/src/photo/sort/index.ts b/src/photo/sort/index.ts index 295bae05..cf72ff0f 100644 --- a/src/photo/sort/index.ts +++ b/src/photo/sort/index.ts @@ -6,10 +6,10 @@ export const getNavSortControlFromString = ( navSortControl = '', ): NavSortControl => { switch (navSortControl.toLocaleLowerCase()) { - case 'none': return 'none'; - case 'toggle': return 'toggle'; - case 'menu': return 'menu'; - default: return NAV_SORT_CONTROL_DEFAULT; + case 'none': return 'none'; + case 'toggle': return 'toggle'; + case 'menu': return 'menu'; + default: return NAV_SORT_CONTROL_DEFAULT; } }; @@ -51,13 +51,13 @@ export interface SortProps { export const getSortByFromString = (sortBy = ''): SortBy => { switch (sortBy) { - case 'taken-at': return 'takenAt'; - case 'taken-at-oldest-first': return 'takenAtAsc'; - case 'uploaded-at': return 'createdAt'; - case 'uploaded-at-oldest-first': return 'createdAtAsc'; - case 'color': return 'color'; - case 'color-ascending': return 'colorAsc'; - default: return 'takenAt'; + case 'taken-at': return 'takenAt'; + case 'taken-at-oldest-first': return 'takenAtAsc'; + case 'uploaded-at': return 'createdAt'; + case 'uploaded-at-oldest-first': return 'createdAtAsc'; + case 'color': return 'color'; + case 'color-ascending': return 'colorAsc'; + default: return 'takenAt'; } }; diff --git a/src/photo/sort/path.ts b/src/photo/sort/path.ts index 378fbf50..66e92227 100644 --- a/src/photo/sort/path.ts +++ b/src/photo/sort/path.ts @@ -24,30 +24,30 @@ const getSortByComponents = (sortBy: SortBy): { sortOrder: string } => { switch (sortBy) { - case 'takenAt': return { - sortType: PARAM_SORT_TYPE_TAKEN_AT, - sortOrder: PARAM_SORT_ORDER_DESCENDING, - }; - case 'takenAtAsc': return { - sortType: PARAM_SORT_TYPE_TAKEN_AT, - sortOrder: PARAM_SORT_ORDER_ASCENDING, - }; - case 'createdAt': return { - sortType: PARAM_SORT_TYPE_UPLOADED_AT, - sortOrder: PARAM_SORT_ORDER_DESCENDING, - }; - case 'createdAtAsc': return { - sortType: PARAM_SORT_TYPE_UPLOADED_AT, - sortOrder: PARAM_SORT_ORDER_ASCENDING, - }; - case 'color': return { - sortType: PARAM_SORT_TYPE_COLOR, - sortOrder: PARAM_SORT_ORDER_DESCENDING, - }; - case 'colorAsc': return { - sortType: PARAM_SORT_TYPE_COLOR, - sortOrder: PARAM_SORT_ORDER_ASCENDING, - }; + case 'takenAt': return { + sortType: PARAM_SORT_TYPE_TAKEN_AT, + sortOrder: PARAM_SORT_ORDER_DESCENDING, + }; + case 'takenAtAsc': return { + sortType: PARAM_SORT_TYPE_TAKEN_AT, + sortOrder: PARAM_SORT_ORDER_ASCENDING, + }; + case 'createdAt': return { + sortType: PARAM_SORT_TYPE_UPLOADED_AT, + sortOrder: PARAM_SORT_ORDER_DESCENDING, + }; + case 'createdAtAsc': return { + sortType: PARAM_SORT_TYPE_UPLOADED_AT, + sortOrder: PARAM_SORT_ORDER_ASCENDING, + }; + case 'color': return { + sortType: PARAM_SORT_TYPE_COLOR, + sortOrder: PARAM_SORT_ORDER_DESCENDING, + }; + case 'colorAsc': return { + sortType: PARAM_SORT_TYPE_COLOR, + sortOrder: PARAM_SORT_ORDER_ASCENDING, + }; } }; @@ -66,24 +66,24 @@ const _getSortOptionsFromParams = ( let sortBy: SortBy = 'takenAt'; const isAscending = sortOrder === PARAM_SORT_ORDER_ASCENDING; switch (sortType) { - case PARAM_SORT_TYPE_TAKEN_AT: { - sortBy = isAscending - ? 'takenAtAsc' - : 'takenAt'; - break; - } - case PARAM_SORT_TYPE_UPLOADED_AT: { - sortBy = isAscending - ? 'createdAtAsc' - : 'createdAt'; - break; - } - case PARAM_SORT_TYPE_COLOR: { - sortBy = isAscending - ? 'colorAsc' - : 'color'; - break; - } + case PARAM_SORT_TYPE_TAKEN_AT: { + sortBy = isAscending + ? 'takenAtAsc' + : 'takenAt'; + break; + } + case PARAM_SORT_TYPE_UPLOADED_AT: { + sortBy = isAscending + ? 'createdAtAsc' + : 'createdAt'; + break; + } + case PARAM_SORT_TYPE_COLOR: { + sortBy = isAscending + ? 'colorAsc' + : 'color'; + break; + } } return { sortBy, diff --git a/src/photo/update/index.ts b/src/photo/update/index.ts index 67187208..5af47a80 100644 --- a/src/photo/update/index.ts +++ b/src/photo/update/index.ts @@ -35,14 +35,14 @@ const getMissingAiTextFields = ({ AI_CONTENT_GENERATION_ENABLED ? AI_TEXT_AUTO_GENERATED_FIELDS.reduce((fields, field) => { switch (field) { - case 'title': - return !title ? [...fields, 'title'] : fields; - case 'caption': - return !caption ? [...fields, 'caption'] : fields; - case 'tags': - return (tags ?? []).length === 0 ? [...fields, 'tags'] : fields; - case 'semantic': - return !semanticDescription ? [...fields, 'semantic'] : fields; + case 'title': + return !title ? [...fields, 'title'] : fields; + case 'caption': + return !caption ? [...fields, 'caption'] : fields; + case 'tags': + return (tags ?? []).length === 0 ? [...fields, 'tags'] : fields; + case 'semantic': + return !semanticDescription ? [...fields, 'semantic'] : fields; } }, [] as AiAutoGeneratedField[]) : []; diff --git a/src/platforms/apple.ts b/src/platforms/apple.ts index eca96a76..b36b4001 100644 --- a/src/platforms/apple.ts +++ b/src/platforms/apple.ts @@ -49,110 +49,110 @@ export const formatAppleLensText = ( } else if (side?.toLocaleUpperCase() === 'BACK') { switch (phoneName?.toLocaleUpperCase()) { // X + XS - case 'X': - case 'XS': - case 'XS MAX': - switch (aperture) { - case '1.8': return format('Main'); - case '2.4': return format('Telephoto'); - } - // XR + SE (single lens) - case 'XR': - case 'SE': - return format('Main'); - // 11 - case '11': - switch (aperture) { - case '2.4': return format('Wide'); - case '1.8': return format('Main'); - } - case '11 PRO': - case '11 PRO MAX': - switch (aperture) { - case '2.4': return format('Wide'); - case '1.8': return format('Main'); - case '2.0': return format('Telephoto'); - } - // 12 - case '12': - case '12 MINI': - switch (aperture) { - case '2.4': return format('Wide'); - case '1.6': return format('Main'); - } - case '12 PRO': - switch (aperture) { - case '2.4': return format('Wide'); - case '1.6': return format('Main'); - case '2.0': return format('Telephoto'); - } - case '12 PRO MAX': - switch (aperture) { - case '2.4': return format('Wide'); - case '1.6': return format('Main'); - case '2.2': return format('Telephoto'); - } - // 13 - case '13': - case '13 MINI': - case '13 PLUS': - switch (aperture) { - case '2.4': return format('Wide'); - case '1.6': return format('Main'); - } - case '13 PRO': - case '13 PRO MAX': - switch (aperture) { - case '1.8': return format('Wide'); - case '1.5': return format('Main'); - case '2.8': return format('Telephoto'); - } - // 14 - case '14': - case '14 PLUS': - switch (aperture) { - case '2.4': return format('Wide'); - case '1.5': return format('Main'); - } - case '14 PRO': - case '14 PRO MAX': - switch (aperture) { - case '2.2': return format('Wide'); - case '1.78': return format('Main'); - case '2.8': return format('Telephoto'); - } - // 15 - case '15': - case '15 PLUS': - switch (aperture) { - case '2.4': return format('Wide'); - case '1.6': return format('Main'); - } - case '15 PRO': - case '15 PRO MAX': - switch (aperture) { - case '2.2': return format('Wide'); - case '1.78': return format('Main'); - case '2.8': return format('Telephoto'); - } - // 16 (single lens) - case '16E': - return format('Main'); - case '16': - case '16 PLUS': - switch (aperture) { - case '2.2': return format('Wide'); - case '1.6': return format('Main'); - } - case '16 PRO': - case '16 PRO MAX': - switch (aperture) { - case '2.2': return format('Wide'); - case '1.78': return format('Main'); - case '2.8': return format('Telephoto'); - } - default: - return format('Back', true); + case 'X': + case 'XS': + case 'XS MAX': + switch (aperture) { + case '1.8': return format('Main'); + case '2.4': return format('Telephoto'); + } + // XR + SE (single lens) + case 'XR': + case 'SE': + return format('Main'); + // 11 + case '11': + switch (aperture) { + case '2.4': return format('Wide'); + case '1.8': return format('Main'); + } + case '11 PRO': + case '11 PRO MAX': + switch (aperture) { + case '2.4': return format('Wide'); + case '1.8': return format('Main'); + case '2.0': return format('Telephoto'); + } + // 12 + case '12': + case '12 MINI': + switch (aperture) { + case '2.4': return format('Wide'); + case '1.6': return format('Main'); + } + case '12 PRO': + switch (aperture) { + case '2.4': return format('Wide'); + case '1.6': return format('Main'); + case '2.0': return format('Telephoto'); + } + case '12 PRO MAX': + switch (aperture) { + case '2.4': return format('Wide'); + case '1.6': return format('Main'); + case '2.2': return format('Telephoto'); + } + // 13 + case '13': + case '13 MINI': + case '13 PLUS': + switch (aperture) { + case '2.4': return format('Wide'); + case '1.6': return format('Main'); + } + case '13 PRO': + case '13 PRO MAX': + switch (aperture) { + case '1.8': return format('Wide'); + case '1.5': return format('Main'); + case '2.8': return format('Telephoto'); + } + // 14 + case '14': + case '14 PLUS': + switch (aperture) { + case '2.4': return format('Wide'); + case '1.5': return format('Main'); + } + case '14 PRO': + case '14 PRO MAX': + switch (aperture) { + case '2.2': return format('Wide'); + case '1.78': return format('Main'); + case '2.8': return format('Telephoto'); + } + // 15 + case '15': + case '15 PLUS': + switch (aperture) { + case '2.4': return format('Wide'); + case '1.6': return format('Main'); + } + case '15 PRO': + case '15 PRO MAX': + switch (aperture) { + case '2.2': return format('Wide'); + case '1.78': return format('Main'); + case '2.8': return format('Telephoto'); + } + // 16 (single lens) + case '16E': + return format('Main'); + case '16': + case '16 PLUS': + switch (aperture) { + case '2.2': return format('Wide'); + case '1.6': return format('Main'); + } + case '16 PRO': + case '16 PRO MAX': + switch (aperture) { + case '2.2': return format('Wide'); + case '1.78': return format('Main'); + case '2.8': return format('Telephoto'); + } + default: + return format('Back', true); } } diff --git a/src/platforms/fujifilm/recipe.ts b/src/platforms/fujifilm/recipe.ts index edcfeebc..c90eb962 100644 --- a/src/platforms/fujifilm/recipe.ts +++ b/src/platforms/fujifilm/recipe.ts @@ -73,12 +73,12 @@ export const processDynamicRangeSettings = ( value: number, ): FujifilmRecipe['dynamicRange']['setting'] => { switch (value) { - case 0x001: return 'manual'; - case 0x100: return 'standard'; - case 0x200: return 'wide-1'; - case 0x201: return 'wide-1'; - case 0x8000: return 'film-simulation'; - default: return 'auto'; + case 0x001: return 'manual'; + case 0x100: return 'standard'; + case 0x200: return 'wide-1'; + case 0x201: return 'wide-1'; + case 0x8000: return 'film-simulation'; + default: return 'auto'; } }; @@ -87,51 +87,51 @@ export const processTone = (value: number) => export const processSaturation = (value: number) => { switch (value) { - case 0x4e0: return -4; - case 0x4c0: return -3; - case 0x400: return -2; - case 0x180: return -1; - case 0x80: return 1; - case 0x100: return 2; - case 0xc0: return 3; - case 0xe0: return 4; - default: return 0; + case 0x4e0: return -4; + case 0x4c0: return -3; + case 0x400: return -2; + case 0x180: return -1; + case 0x80: return 1; + case 0x100: return 2; + case 0xc0: return 3; + case 0xe0: return 4; + default: return 0; } }; export const processNoiseReductionLegacy = (value: number) => { switch (value) { - case 0x40: return 'low'; - case 0x80: return 'normal'; - default: return 'n/a'; + case 0x40: return 'low'; + case 0x80: return 'normal'; + default: return 'n/a'; } }; export const processNoiseReduction = (value: number) => { switch (value) { - case 0x2e0: return -4; - case 0x2c0: return -3; - case 0x200: return -2; - case 0x280: return -1; - case 0x180: return 1; - case 0x100: return 2; - case 0x1c0: return 3; - case 0x1e0: return 4; - default: return 0; + case 0x2e0: return -4; + case 0x2c0: return -3; + case 0x200: return -2; + case 0x280: return -1; + case 0x180: return 1; + case 0x100: return 2; + case 0x1c0: return 3; + case 0x1e0: return 4; + default: return 0; } }; export const processSharpness = (value: number) => { switch (value) { - case 0x0: return -4; - case 0x1: return -3; - case 0x2: return -2; - case 0x82: return -1; - case 0x84: return 1; - case 0x4: return 2; - case 0x5: return 3; - case 0x6: return 4; - default: return 0; + case 0x0: return -4; + case 0x1: return -3; + case 0x2: return -2; + case 0x82: return -1; + case 0x84: return 1; + case 0x4: return 2; + case 0x5: return 3; + case 0x6: return 4; + default: return 0; } }; @@ -139,9 +139,9 @@ export const processClarity = (value: number) => value / 1000; export const processWeakStrong = (value: number): WeakStrong => { switch (value) { - case 32: return 'weak'; - case 64: return 'strong'; - default: return 'off'; + case 32: return 'weak'; + case 64: return 'strong'; + default: return 'off'; } }; @@ -149,33 +149,33 @@ export const processGrainEffectSize = ( value: number, ): Required['grainEffect']['size'] => { switch (value) { - case 16: return 'small'; - case 32: return 'large'; - default: return 'off'; + case 16: return 'small'; + case 32: return 'large'; + default: return 'off'; } }; export const processWhiteBalanceType = (value: number) => { switch (value) { - case 0x1: return 'auto-white-priority'; - case 0x2: return 'auto-ambiance-priority'; - case 0x100: return 'daylight'; - case 0x200: return 'cloudy'; - case 0x300: return 'daylight-fluorescent'; - case 0x301: return 'day-white-fluorescent'; - case 0x302: return 'white-fluorescent'; - case 0x303: return 'warm-white-fluorescent'; - case 0x304: return 'living-room-warm-white-fluorescent'; - case 0x400: return 'incandescent'; - case 0x500: return 'flash'; - case 0x600: return 'underwater'; - case 0xf00: return 'custom'; - case 0xf01: return 'custom-2'; - case 0xf02: return 'custom-3'; - case 0xf03: return 'custom-4'; - case 0xf04: return 'custom-5'; - case 0xff0: return 'kelvin'; - default: return 'auto'; + case 0x1: return 'auto-white-priority'; + case 0x2: return 'auto-ambiance-priority'; + case 0x100: return 'daylight'; + case 0x200: return 'cloudy'; + case 0x300: return 'daylight-fluorescent'; + case 0x301: return 'day-white-fluorescent'; + case 0x302: return 'white-fluorescent'; + case 0x303: return 'warm-white-fluorescent'; + case 0x304: return 'living-room-warm-white-fluorescent'; + case 0x400: return 'incandescent'; + case 0x500: return 'flash'; + case 0x600: return 'underwater'; + case 0xf00: return 'custom'; + case 0xf01: return 'custom-2'; + case 0xf02: return 'custom-3'; + case 0xf03: return 'custom-4'; + case 0xf04: return 'custom-5'; + case 0xff0: return 'kelvin'; + default: return 'auto'; } }; @@ -194,66 +194,66 @@ export const getFujifilmRecipeFromMakerNote = ( bytes, (tag, numbers) => { switch (tag) { - case TAG_ID_DYNAMIC_RANGE: - recipe.dynamicRange.range = numbers[0] === 3 - ? 'wide' - : 'standard'; - break; - case TAG_ID_DYNAMIC_RANGE_SETTING: - recipe.dynamicRange.setting = processDynamicRangeSettings(numbers[0]); - break; - case TAG_ID_DEVELOPMENT_DYNAMIC_RANGE: - recipe.dynamicRange.development = numbers[0]; - break; - case TAG_ID_WHITE_BALANCE: - recipe.whiteBalance.type = processWhiteBalanceType(numbers[0]); - break; - case TAG_ID_WHITE_BALANCE_FINE_TUNE: - recipe.whiteBalance.red = processWhiteBalanceComponent(numbers[0]); - recipe.whiteBalance.blue = processWhiteBalanceComponent(numbers[1]); - break; - case TAG_ID_WHITE_BALANCE_COLOR_TEMPERATURE: - recipe.whiteBalance.colorTemperature = numbers[0]; - break; - case TAG_ID_NOISE_REDUCTION: - recipe.highISONoiseReduction = processNoiseReduction(numbers[0]); - break; - case TAG_ID_NOISE_REDUCTION_BASIC: - recipe.noiseReductionBasic = processNoiseReductionLegacy(numbers[0]); - break; - case TAG_ID_HIGHLIGHT: - recipe.highlight = processTone(numbers[0]); - break; - case TAG_ID_SHADOW: - recipe.shadow = processTone(numbers[0]); - break; - case TAG_ID_SATURATION: - recipe.color = processSaturation(numbers[0]); - break; - case TAG_ID_SHARPNESS: - recipe.sharpness = processSharpness(numbers[0]); - break; - case TAG_ID_CLARITY: - recipe.clarity = processClarity(numbers[0]); - break; - case TAG_ID_COLOR_CHROME_EFFECT: - recipe.colorChromeEffect = processWeakStrong(numbers[0]); - break; - case TAG_ID_COLOR_CHROME_FX_BLUE: - recipe.colorChromeFXBlue = processWeakStrong(numbers[0]); - break; - case TAG_ID_GRAIN_EFFECT_ROUGHNESS: - recipe.grainEffect.roughness = processWeakStrong(numbers[0]); - break; - case TAG_ID_GRAIN_EFFECT_SIZE: - recipe.grainEffect.size = processGrainEffectSize(numbers[0]); - break; - case TAG_ID_BW_ADJUSTMENT: - recipe.bwAdjustment = numbers[0]; - break; - case TAG_ID_BW_MAGENTA_GREEN: - recipe.bwMagentaGreen = numbers[0]; - break; + case TAG_ID_DYNAMIC_RANGE: + recipe.dynamicRange.range = numbers[0] === 3 + ? 'wide' + : 'standard'; + break; + case TAG_ID_DYNAMIC_RANGE_SETTING: + recipe.dynamicRange.setting = processDynamicRangeSettings(numbers[0]); + break; + case TAG_ID_DEVELOPMENT_DYNAMIC_RANGE: + recipe.dynamicRange.development = numbers[0]; + break; + case TAG_ID_WHITE_BALANCE: + recipe.whiteBalance.type = processWhiteBalanceType(numbers[0]); + break; + case TAG_ID_WHITE_BALANCE_FINE_TUNE: + recipe.whiteBalance.red = processWhiteBalanceComponent(numbers[0]); + recipe.whiteBalance.blue = processWhiteBalanceComponent(numbers[1]); + break; + case TAG_ID_WHITE_BALANCE_COLOR_TEMPERATURE: + recipe.whiteBalance.colorTemperature = numbers[0]; + break; + case TAG_ID_NOISE_REDUCTION: + recipe.highISONoiseReduction = processNoiseReduction(numbers[0]); + break; + case TAG_ID_NOISE_REDUCTION_BASIC: + recipe.noiseReductionBasic = processNoiseReductionLegacy(numbers[0]); + break; + case TAG_ID_HIGHLIGHT: + recipe.highlight = processTone(numbers[0]); + break; + case TAG_ID_SHADOW: + recipe.shadow = processTone(numbers[0]); + break; + case TAG_ID_SATURATION: + recipe.color = processSaturation(numbers[0]); + break; + case TAG_ID_SHARPNESS: + recipe.sharpness = processSharpness(numbers[0]); + break; + case TAG_ID_CLARITY: + recipe.clarity = processClarity(numbers[0]); + break; + case TAG_ID_COLOR_CHROME_EFFECT: + recipe.colorChromeEffect = processWeakStrong(numbers[0]); + break; + case TAG_ID_COLOR_CHROME_FX_BLUE: + recipe.colorChromeFXBlue = processWeakStrong(numbers[0]); + break; + case TAG_ID_GRAIN_EFFECT_ROUGHNESS: + recipe.grainEffect.roughness = processWeakStrong(numbers[0]); + break; + case TAG_ID_GRAIN_EFFECT_SIZE: + recipe.grainEffect.size = processGrainEffectSize(numbers[0]); + break; + case TAG_ID_BW_ADJUSTMENT: + recipe.bwAdjustment = numbers[0]; + break; + case TAG_ID_BW_MAGENTA_GREEN: + recipe.bwMagentaGreen = numbers[0]; + break; } }, ); diff --git a/src/platforms/fujifilm/server.ts b/src/platforms/fujifilm/server.ts index fdec1b36..7e21a2cd 100644 --- a/src/platforms/fujifilm/server.ts +++ b/src/platforms/fujifilm/server.ts @@ -59,21 +59,21 @@ export const parseFujifilmMakerNote = ( switch (tagType) { // Int8 (UInt8 read as Int8 according to spec) - case 1: - sendNumbersForDataType(offset => bytes.readInt8(offset), 1); - break; - // UInt16 - case 3: - sendNumbersForDataType(offset => bytes.readUInt16LE(offset), 2); - break; - // UInt32 - case 4: - sendNumbersForDataType(offset => bytes.readUInt32LE(offset), 4); - break; - // Int32 - case 9: - sendNumbersForDataType(offset => bytes.readInt32LE(offset), 4); - break; + case 1: + sendNumbersForDataType(offset => bytes.readInt8(offset), 1); + break; + // UInt16 + case 3: + sendNumbersForDataType(offset => bytes.readUInt16LE(offset), 2); + break; + // UInt32 + case 4: + sendNumbersForDataType(offset => bytes.readUInt32LE(offset), 4); + break; + // Int32 + case 9: + sendNumbersForDataType(offset => bytes.readInt32LE(offset), 4); + break; } } } diff --git a/src/platforms/fujifilm/simulation.ts b/src/platforms/fujifilm/simulation.ts index a4ef3b96..d0770c9d 100644 --- a/src/platforms/fujifilm/simulation.ts +++ b/src/platforms/fujifilm/simulation.ts @@ -39,15 +39,15 @@ const getFujifilmSimulationFromSaturation = ( value?: number, ): FujifilmSimulationFromSaturation | undefined => { switch (value) { - case 0x300: return 'monochrome'; - case 0x301: return 'monochrome-r'; - case 0x302: return 'monochrome-ye'; - case 0x303: return 'monochrome-g'; - case 0x310: return 'sepia'; - case 0x500: return 'acros'; - case 0x501: return 'acros-r'; - case 0x502: return 'acros-ye'; - case 0x503: return 'acros-g'; + case 0x300: return 'monochrome'; + case 0x301: return 'monochrome-r'; + case 0x302: return 'monochrome-ye'; + case 0x303: return 'monochrome-g'; + case 0x310: return 'sepia'; + case 0x500: return 'acros'; + case 0x501: return 'acros-r'; + case 0x502: return 'acros-ye'; + case 0x503: return 'acros-g'; } }; @@ -55,22 +55,22 @@ const getFujifilmMode = ( value?: number, ): FujifilmMode | undefined => { switch (value) { - case 0x000: return 'provia'; - case 0x100: return 'portrait'; - case 0x110: return 'portrait-saturation'; - case 0x120: return 'astia'; // can be encoded as 'portrait-skin-tone' - case 0x130: return 'portrait-sharpness'; - case 0x300: return 'portrait-ex'; - case 0x200: - case 0x400: return 'velvia'; - case 0x500: return 'pro-neg-std'; - case 0x501: return 'pro-neg-hi'; - case 0x600: return 'classic-chrome'; - case 0x700: return 'eterna'; - case 0x800: return 'classic-neg'; - case 0x900: return 'eterna-bleach-bypass'; - case 0xa00: return 'nostalgic-neg'; - case 0xb00: return 'reala'; + case 0x000: return 'provia'; + case 0x100: return 'portrait'; + case 0x110: return 'portrait-saturation'; + case 0x120: return 'astia'; // can be encoded as 'portrait-skin-tone' + case 0x130: return 'portrait-sharpness'; + case 0x300: return 'portrait-ex'; + case 0x200: + case 0x400: return 'velvia'; + case 0x500: return 'pro-neg-std'; + case 0x501: return 'pro-neg-hi'; + case 0x600: return 'classic-chrome'; + case 0x700: return 'eterna'; + case 0x800: return 'classic-neg'; + case 0x900: return 'eterna-bleach-bypass'; + case 0xa00: return 'nostalgic-neg'; + case 0xb00: return 'reala'; } }; @@ -242,14 +242,14 @@ export const getFujifilmSimulationFromMakerNote = ( bytes, (tag, numbers) => { switch (tag) { - case TAG_ID_SATURATION: - filmModeFromSaturation = + case TAG_ID_SATURATION: + filmModeFromSaturation = getFujifilmSimulationFromSaturation(numbers[0]); - break; - case TAG_ID_FILM_MODE: - filmMode = + break; + case TAG_ID_FILM_MODE: + filmMode = getFujifilmMode(numbers[0]); - break; + break; } }, ); diff --git a/src/platforms/storage/index.ts b/src/platforms/storage/index.ts index e7be288a..7613f691 100644 --- a/src/platforms/storage/index.ts +++ b/src/platforms/storage/index.ts @@ -49,17 +49,17 @@ export type StorageType = export const labelForStorage = (type: StorageType): string => { switch (type) { - case 'vercel-blob': return 'Vercel Blob'; - case 'cloudflare-r2': return 'Cloudflare R2'; - case 'aws-s3': return 'AWS S3'; + case 'vercel-blob': return 'Vercel Blob'; + case 'cloudflare-r2': return 'Cloudflare R2'; + case 'aws-s3': return 'AWS S3'; } }; export const baseUrlForStorage = (type: StorageType) => { switch (type) { - case 'vercel-blob': return VERCEL_BLOB_BASE_URL; - case 'cloudflare-r2': return CLOUDFLARE_R2_BASE_URL_PUBLIC; - case 'aws-s3': return AWS_S3_BASE_URL; + case 'vercel-blob': return VERCEL_BLOB_BASE_URL; + case 'cloudflare-r2': return CLOUDFLARE_R2_BASE_URL_PUBLIC; + case 'aws-s3': return AWS_S3_BASE_URL; } }; @@ -91,12 +91,12 @@ const REGEX_UPLOAD_ID = new RegExp( export const fileNameForStorageUrl = (url: string) => { switch (storageTypeFromUrl(url)) { - case 'vercel-blob': - return url.replace(`${VERCEL_BLOB_BASE_URL}/`, ''); - case 'cloudflare-r2': - return url.replace(`${CLOUDFLARE_R2_BASE_URL_PUBLIC}/`, ''); - case 'aws-s3': - return url.replace(`${AWS_S3_BASE_URL}/`, ''); + case 'vercel-blob': + return url.replace(`${VERCEL_BLOB_BASE_URL}/`, ''); + case 'cloudflare-r2': + return url.replace(`${CLOUDFLARE_R2_BASE_URL_PUBLIC}/`, ''); + case 'aws-s3': + return url.replace(`${AWS_S3_BASE_URL}/`, ''); } }; @@ -144,12 +144,12 @@ export const putFile = ( fileName: string, ) => { switch (CURRENT_STORAGE) { - case 'vercel-blob': - return vercelBlobPut(file, fileName); - case 'cloudflare-r2': - return cloudflareR2Put(file, fileName); - case 'aws-s3': - return awsS3Put(file, fileName); + case 'vercel-blob': + return vercelBlobPut(file, fileName); + case 'cloudflare-r2': + return cloudflareR2Put(file, fileName); + case 'aws-s3': + return awsS3Put(file, fileName); } }; @@ -158,35 +158,35 @@ export const copyFile = ( destinationFileName: string, ): Promise => { switch (storageTypeFromUrl(originUrl)) { - case 'vercel-blob': - return vercelBlobCopy( - originUrl, - destinationFileName, - false, - ); - case 'cloudflare-r2': - return cloudflareR2Copy( - getFileNameFromStorageUrl(originUrl), - destinationFileName, - false, - ); - case 'aws-s3': - return awsS3Copy( - originUrl, - destinationFileName, - false, - ); + case 'vercel-blob': + return vercelBlobCopy( + originUrl, + destinationFileName, + false, + ); + case 'cloudflare-r2': + return cloudflareR2Copy( + getFileNameFromStorageUrl(originUrl), + destinationFileName, + false, + ); + case 'aws-s3': + return awsS3Copy( + originUrl, + destinationFileName, + false, + ); } }; export const deleteFile = (url: string) => { switch (storageTypeFromUrl(url)) { - case 'vercel-blob': - return vercelBlobDelete(url); - case 'cloudflare-r2': - return cloudflareR2Delete(getFileNameFromStorageUrl(url)); - case 'aws-s3': - return awsS3Delete(getFileNameFromStorageUrl(url)); + case 'vercel-blob': + return vercelBlobDelete(url); + case 'cloudflare-r2': + return cloudflareR2Delete(getFileNameFromStorageUrl(url)); + case 'aws-s3': + return awsS3Delete(getFileNameFromStorageUrl(url)); } }; diff --git a/src/utility/date.ts b/src/utility/date.ts index 14baaac8..b27afd23 100644 --- a/src/utility/date.ts +++ b/src/utility/date.ts @@ -50,26 +50,26 @@ export const formatDate = ({ : DATE_FORMAT_SHORT_PLACEHOLDER; switch (length) { - case 'rss': - formatString = DATE_FORMAT_RSS; - placeholderString = DATE_FORMAT_RSS_PLACEHOLDER; - break; - case 'tiny': - formatString = DATE_FORMAT_TINY; - placeholderString = DATE_FORMAT_TINY_PLACEHOLDER; - break; - case 'short': - formatString = DATE_FORMAT_SHORT; - placeholderString = DATE_FORMAT_SHORT_PLACEHOLDER; - break; - case 'medium': - formatString = !hideTime - ? DATE_FORMAT_MEDIUM - : DATE_FORMAT_SHORT; - placeholderString = !hideTime - ? DATE_FORMAT_MEDIUM_PLACEHOLDER - : DATE_FORMAT_SHORT_PLACEHOLDER; - break; + case 'rss': + formatString = DATE_FORMAT_RSS; + placeholderString = DATE_FORMAT_RSS_PLACEHOLDER; + break; + case 'tiny': + formatString = DATE_FORMAT_TINY; + placeholderString = DATE_FORMAT_TINY_PLACEHOLDER; + break; + case 'short': + formatString = DATE_FORMAT_SHORT; + placeholderString = DATE_FORMAT_SHORT_PLACEHOLDER; + break; + case 'medium': + formatString = !hideTime + ? DATE_FORMAT_MEDIUM + : DATE_FORMAT_SHORT; + placeholderString = !hideTime + ? DATE_FORMAT_MEDIUM_PLACEHOLDER + : DATE_FORMAT_SHORT_PLACEHOLDER; + break; } return showPlaceholder diff --git a/src/utility/exif.ts b/src/utility/exif.ts index c0fb077a..ff3de60d 100644 --- a/src/utility/exif.ts +++ b/src/utility/exif.ts @@ -17,16 +17,16 @@ export const getAspectRatioFromExif = (data: ExifData): number => { const height = data.imageSize?.height ?? 2.0; switch (orientation) { - case OrientationTypes.TOP_LEFT: - case OrientationTypes.TOP_RIGHT: - case OrientationTypes.BOTTOM_RIGHT: - case OrientationTypes.BOTTOM_LEFT: - case OrientationTypes.LEFT_TOP: - case OrientationTypes.RIGHT_BOTTOM: - return width / height; - case OrientationTypes.RIGHT_TOP: - case OrientationTypes.LEFT_BOTTOM: - return height / width; + case OrientationTypes.TOP_LEFT: + case OrientationTypes.TOP_RIGHT: + case OrientationTypes.BOTTOM_RIGHT: + case OrientationTypes.BOTTOM_LEFT: + case OrientationTypes.LEFT_TOP: + case OrientationTypes.RIGHT_BOTTOM: + return width / height; + case OrientationTypes.RIGHT_TOP: + case OrientationTypes.LEFT_BOTTOM: + return height / width; } }; @@ -37,17 +37,17 @@ export const convertApertureValueToFNumber = ( const aperture = parseInt(apertureValue); if (aperture <= 10) { switch (aperture) { - case 0: return '1'; - case 1: return '1.4'; - case 2: return '2'; - case 3: return '2.8'; - case 4: return '4'; - case 5: return '5.6'; - case 6: return '8'; - case 7: return '11'; - case 8: return '16'; - case 9: return '22'; - case 10: return '32'; + case 0: return '1'; + case 1: return '1.4'; + case 2: return '2'; + case 3: return '2.8'; + case 4: return '4'; + case 5: return '5.6'; + case 6: return '8'; + case 7: return '11'; + case 8: return '16'; + case 9: return '22'; + case 10: return '32'; } } else { const value = Math.round(Math.pow(2, aperture / 2.0) * 10) / 10;