Refactor photo set customization

This commit is contained in:
Sam Becker 2025-03-07 18:07:08 -06:00
parent 21ed815cba
commit 6738ffc28e
38 changed files with 464 additions and 294 deletions

View File

@ -79,7 +79,7 @@ _⚠ READ BEFORE PROCEEDING_
2. Add rate limiting (_recommended_) 2. Add rate limiting (_recommended_)
- As an additional precaution, create an Upstash Redis store from the storage tab of the Vercel dashboard and link it to your project in order to enable rate limiting—no further configuration necessary - As an additional precaution, create an Upstash Redis store from the storage tab of the Vercel dashboard and link it to your project in order to enable rate limiting—no further configuration necessary
3. Configure auto-generated fields (optional) 3. Configure auto-generated fields (optional)
- Set which text fields auto-generate when uploading a photo by storing a comma-separated list, e.g., `AI_TEXT_AUTO_GENERATED_FIELDS = title, semantic` - Set which text fields auto-generate when uploading a photo by storing a comma-separated list, e.g., `AI_TEXT_AUTO_GENERATED_FIELDS = title,semantic`
- Accepted values: - Accepted values:
- `all` - `all`
- `title` (default) - `title` (default)
@ -129,10 +129,8 @@ Application behavior can be changed by configuring the following environment var
- `NEXT_PUBLIC_HIDE_ZOOM_CONTROLS = 1` hides fullscreen photo zoom controls - `NEXT_PUBLIC_HIDE_ZOOM_CONTROLS = 1` hides fullscreen photo zoom controls
- `NEXT_PUBLIC_HIDE_TAKEN_AT_TIME = 1` hides taken at time from photo meta - `NEXT_PUBLIC_HIDE_TAKEN_AT_TIME = 1` hides taken at time from photo meta
- `NEXT_PUBLIC_HIDE_SOCIAL = 1` removes X (formerly Twitter) button from share modal - `NEXT_PUBLIC_HIDE_SOCIAL = 1` removes X (formerly Twitter) button from share modal
- `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar and CMD-K search results
- `NEXT_PUBLIC_HIDE_RECIPES = 1` prevents Fujifilm recipe button showing up in photo meta
- `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo - `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo
- `NEXT_PUBLIC_CAMERAS_FIRST = 1` shows cameras above tags in grid sidebar - `NEXT_PUBLIC_CATEGORY_VISIBILITY` controls which photos sets appear in the grid sidebar and CMD-K menu, and in what order. Default value is `tags,cameras,recipes,films`. As an example, you could move cameras above tags, and hide film simulations, by updating this value to `cameras,tags,recipes`.
#### Grid #### Grid
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage - `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage
@ -286,6 +284,9 @@ Vercel Postgres can be switched to another Postgres-compatible, pooling provider
#### My Fujifilm recipes are missing/displaying incorrect data. What should I do? #### My Fujifilm recipes are missing/displaying incorrect data. What should I do?
> Fujifilm file specifications have evolved over time. Open an issue with the file in question attached in order for it to be investigated. > Fujifilm file specifications have evolved over time. Open an issue with the file in question attached in order for it to be investigated.
#### How do I hide Fujifilm content such as a recipes and film simulations?
> This can be accomplished by setting `NEXT_PUBLIC_CATEGORY_VISIBILITY` (which has a default value of `tags, cameras, recipes, simulations`) to simply `tags, cameras`.
#### Why do my images appear flipped/rotated incorrectly? #### Why do my images appear flipped/rotated incorrectly?
> For a number of reasons, only EXIF orientations: 1, 3, 6, and 8 are supported. Orientations 2, 4, 5, and 7—which make use of mirroring—are not supported. > For a number of reasons, only EXIF orientations: 1, 3, 6, and 8 are supported. Orientations 2, 4, 5, and 7—which make use of mirroring—are not supported.

37
__tests__/set.test.ts Normal file
View File

@ -0,0 +1,37 @@
import {
DEFAULT_CATEGORY_KEYS,
getOrderedCategoriesFromString,
} from '@/photo/set';
describe('set', () => {
it('parses from string', () => {
expect(getOrderedCategoriesFromString())
.toStrictEqual(DEFAULT_CATEGORY_KEYS);
expect(getOrderedCategoriesFromString(
'cameras,recipes,tags,films,focal-lengths,lenses',
)).toStrictEqual([
'cameras',
'recipes',
'tags',
'films',
'focal-lengths',
'lenses',
]);
expect(getOrderedCategoriesFromString(
'cameras, recipes, tags, films',
)).toStrictEqual([
'cameras',
'recipes',
'tags',
'films',
]);
expect(getOrderedCategoriesFromString(
'cameras',
)).toStrictEqual([
'cameras',
]);
});
});

View File

@ -63,7 +63,7 @@
"@types/sanitize-html": "^2.13.0", "@types/sanitize-html": "^2.13.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cross-fetch": "^4.1.0", "cross-fetch": "^4.1.0",
"eslint": "9.21.0", "eslint": "9.22.0",
"eslint-config-next": "15.2.1", "eslint-config-next": "15.2.1",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"jest": "^29.7.0", "jest": "^29.7.0",

121
pnpm-lock.yaml generated
View File

@ -166,14 +166,14 @@ importers:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
eslint: eslint:
specifier: 9.21.0 specifier: 9.22.0
version: 9.21.0(jiti@2.4.2) version: 9.22.0(jiti@2.4.2)
eslint-config-next: eslint-config-next:
specifier: 15.2.1 specifier: 15.2.1
version: 15.2.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) version: 15.2.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
eslint-plugin-react-hooks: eslint-plugin-react-hooks:
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.2.0(eslint@9.21.0(jiti@2.4.2)) version: 5.2.0(eslint@9.22.0(jiti@2.4.2))
jest: jest:
specifier: ^29.7.0 specifier: ^29.7.0
version: 29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2)) version: 29.7.0(@types/node@22.13.9)(ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2))
@ -609,6 +609,10 @@ packages:
resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/config-helpers@0.1.0':
resolution: {integrity: sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@0.12.0': '@eslint/core@0.12.0':
resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -617,8 +621,8 @@ packages:
resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.21.0': '@eslint/js@9.22.0':
resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==} resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.6': '@eslint/object-schema@2.1.6':
@ -2425,8 +2429,8 @@ packages:
peerDependencies: peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
eslint-scope@8.2.0: eslint-scope@8.3.0:
resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@3.4.3: eslint-visitor-keys@3.4.3:
@ -2437,8 +2441,8 @@ packages:
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.21.0: eslint@9.22.0:
resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==} resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -5035,9 +5039,9 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@eslint-community/eslint-utils@4.4.1(eslint@9.21.0(jiti@2.4.2))': '@eslint-community/eslint-utils@4.4.1(eslint@9.22.0(jiti@2.4.2))':
dependencies: dependencies:
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {} '@eslint-community/regexpp@4.12.1': {}
@ -5050,6 +5054,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@eslint/config-helpers@0.1.0': {}
'@eslint/core@0.12.0': '@eslint/core@0.12.0':
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
@ -5068,7 +5074,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@eslint/js@9.21.0': {} '@eslint/js@9.22.0': {}
'@eslint/object-schema@2.1.6': {} '@eslint/object-schema@2.1.6': {}
@ -6271,15 +6277,15 @@ snapshots:
dependencies: dependencies:
'@types/yargs-parser': 21.0.3 '@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': '@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.1 '@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/parser': 8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
'@typescript-eslint/scope-manager': 8.24.1 '@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/type-utils': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/type-utils': 8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
'@typescript-eslint/utils': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/utils': 8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
'@typescript-eslint/visitor-keys': 8.24.1 '@typescript-eslint/visitor-keys': 8.24.1
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
graphemer: 1.4.0 graphemer: 1.4.0
ignore: 5.3.2 ignore: 5.3.2
natural-compare: 1.4.0 natural-compare: 1.4.0
@ -6288,14 +6294,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': '@typescript-eslint/parser@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.24.1 '@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/types': 8.24.1 '@typescript-eslint/types': 8.24.1
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.2) '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.2)
'@typescript-eslint/visitor-keys': 8.24.1 '@typescript-eslint/visitor-keys': 8.24.1
debug: 4.4.0 debug: 4.4.0
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
typescript: 5.8.2 typescript: 5.8.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -6305,12 +6311,12 @@ snapshots:
'@typescript-eslint/types': 8.24.1 '@typescript-eslint/types': 8.24.1
'@typescript-eslint/visitor-keys': 8.24.1 '@typescript-eslint/visitor-keys': 8.24.1
'@typescript-eslint/type-utils@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': '@typescript-eslint/type-utils@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)':
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.2) '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.2)
'@typescript-eslint/utils': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/utils': 8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
debug: 4.4.0 debug: 4.4.0
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
ts-api-utils: 2.0.1(typescript@5.8.2) ts-api-utils: 2.0.1(typescript@5.8.2)
typescript: 5.8.2 typescript: 5.8.2
transitivePeerDependencies: transitivePeerDependencies:
@ -6332,13 +6338,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': '@typescript-eslint/utils@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@2.4.2)) '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.24.1 '@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/types': 8.24.1 '@typescript-eslint/types': 8.24.1
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.2) '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.2)
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
typescript: 5.8.2 typescript: 5.8.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -7025,19 +7031,19 @@ snapshots:
optionalDependencies: optionalDependencies:
source-map: 0.6.1 source-map: 0.6.1
eslint-config-next@15.2.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2): eslint-config-next@15.2.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2):
dependencies: dependencies:
'@next/eslint-plugin-next': 15.2.1 '@next/eslint-plugin-next': 15.2.1
'@rushstack/eslint-patch': 1.10.5 '@rushstack/eslint-patch': 1.10.5
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
'@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/parser': 8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.1)(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-react: 7.37.4(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-react: 7.37.4(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-react-hooks: 5.2.0(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.22.0(jiti@2.4.2))
optionalDependencies: optionalDependencies:
typescript: 5.8.2 typescript: 5.8.2
transitivePeerDependencies: transitivePeerDependencies:
@ -7053,33 +7059,33 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)): eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2)):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.0 debug: 4.4.0
enhanced-resolve: 5.18.1 enhanced-resolve: 5.18.1
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
get-tsconfig: 4.10.0 get-tsconfig: 4.10.0
is-bun-module: 1.3.0 is-bun-module: 1.3.0
stable-hash: 0.0.4 stable-hash: 0.0.4
tinyglobby: 0.2.11 tinyglobby: 0.2.11
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.1)(eslint@9.22.0(jiti@2.4.2))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)): eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.22.0(jiti@2.4.2)):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/parser': 8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)): eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.1)(eslint@9.22.0(jiti@2.4.2)):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.8 array-includes: 3.1.8
@ -7088,9 +7094,9 @@ snapshots:
array.prototype.flatmap: 1.3.3 array.prototype.flatmap: 1.3.3
debug: 3.2.7 debug: 3.2.7
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)) eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.22.0(jiti@2.4.2))
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@ -7102,13 +7108,13 @@ snapshots:
string.prototype.trimend: 1.0.9 string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0 tsconfig-paths: 3.15.0
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/parser': 8.24.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
transitivePeerDependencies: transitivePeerDependencies:
- eslint-import-resolver-typescript - eslint-import-resolver-typescript
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
- supports-color - supports-color
eslint-plugin-jsx-a11y@6.10.2(eslint@9.21.0(jiti@2.4.2)): eslint-plugin-jsx-a11y@6.10.2(eslint@9.22.0(jiti@2.4.2)):
dependencies: dependencies:
aria-query: 5.3.2 aria-query: 5.3.2
array-includes: 3.1.8 array-includes: 3.1.8
@ -7118,7 +7124,7 @@ snapshots:
axobject-query: 4.1.0 axobject-query: 4.1.0
damerau-levenshtein: 1.0.8 damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2 emoji-regex: 9.2.2
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
hasown: 2.0.2 hasown: 2.0.2
jsx-ast-utils: 3.3.5 jsx-ast-utils: 3.3.5
language-tags: 1.0.9 language-tags: 1.0.9
@ -7127,11 +7133,11 @@ snapshots:
safe-regex-test: 1.1.0 safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1 string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@5.2.0(eslint@9.21.0(jiti@2.4.2)): eslint-plugin-react-hooks@5.2.0(eslint@9.22.0(jiti@2.4.2)):
dependencies: dependencies:
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
eslint-plugin-react@7.37.4(eslint@9.21.0(jiti@2.4.2)): eslint-plugin-react@7.37.4(eslint@9.22.0(jiti@2.4.2)):
dependencies: dependencies:
array-includes: 3.1.8 array-includes: 3.1.8
array.prototype.findlast: 1.2.5 array.prototype.findlast: 1.2.5
@ -7139,7 +7145,7 @@ snapshots:
array.prototype.tosorted: 1.1.4 array.prototype.tosorted: 1.1.4
doctrine: 2.1.0 doctrine: 2.1.0
es-iterator-helpers: 1.2.1 es-iterator-helpers: 1.2.1
eslint: 9.21.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
estraverse: 5.3.0 estraverse: 5.3.0
hasown: 2.0.2 hasown: 2.0.2
jsx-ast-utils: 3.3.5 jsx-ast-utils: 3.3.5
@ -7153,7 +7159,7 @@ snapshots:
string.prototype.matchall: 4.0.12 string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0 string.prototype.repeat: 1.0.0
eslint-scope@8.2.0: eslint-scope@8.3.0:
dependencies: dependencies:
esrecurse: 4.3.0 esrecurse: 4.3.0
estraverse: 5.3.0 estraverse: 5.3.0
@ -7162,14 +7168,15 @@ snapshots:
eslint-visitor-keys@4.2.0: {} eslint-visitor-keys@4.2.0: {}
eslint@9.21.0(jiti@2.4.2): eslint@9.22.0(jiti@2.4.2):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@2.4.2)) '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0(jiti@2.4.2))
'@eslint-community/regexpp': 4.12.1 '@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.19.2 '@eslint/config-array': 0.19.2
'@eslint/config-helpers': 0.1.0
'@eslint/core': 0.12.0 '@eslint/core': 0.12.0
'@eslint/eslintrc': 3.3.0 '@eslint/eslintrc': 3.3.0
'@eslint/js': 9.21.0 '@eslint/js': 9.22.0
'@eslint/plugin-kit': 0.2.7 '@eslint/plugin-kit': 0.2.7
'@humanfs/node': 0.16.6 '@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/module-importer': 1.0.1
@ -7181,7 +7188,7 @@ snapshots:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
debug: 4.4.0 debug: 4.4.0
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint-scope: 8.2.0 eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0 eslint-visitor-keys: 4.2.0
espree: 10.3.0 espree: 10.3.0
esquery: 1.6.0 esquery: 1.6.0

View File

@ -2,6 +2,7 @@
import { import {
ComponentProps, ComponentProps,
Fragment,
ReactNode, ReactNode,
} from 'react'; } from 'react';
import ChecklistRow from '../components/ChecklistRow'; import ChecklistRow from '../components/ChecklistRow';
@ -28,6 +29,8 @@ import { CgDebug } from 'react-icons/cg';
import EnvVar from '@/components/EnvVar'; import EnvVar from '@/components/EnvVar';
import AdminLink from './AdminLink'; import AdminLink from './AdminLink';
import ScoreCardContainer from '@/components/ScoreCardContainer'; import ScoreCardContainer from '@/components/ScoreCardContainer';
import { capitalize } from '@/utility/string';
import { DEFAULT_CATEGORY_KEYS, getHiddenDefaultCategories } from '@/photo/set';
export default function AdminAppConfigurationClient({ export default function AdminAppConfigurationClient({
// Storage // Storage
@ -60,8 +63,8 @@ export default function AdminAppConfigurationClient({
arePhotoCategoriesStaticallyOptimized, arePhotoCategoriesStaticallyOptimized,
arePhotoCategoryOgImagesStaticallyOptimized, arePhotoCategoryOgImagesStaticallyOptimized,
areOriginalUploadsPreserved, areOriginalUploadsPreserved,
imageQuality,
hasImageQuality, hasImageQuality,
imageQuality,
isBlurEnabled, isBlurEnabled,
// Visual // Visual
hasDefaultTheme, hasDefaultTheme,
@ -72,10 +75,9 @@ export default function AdminAppConfigurationClient({
showZoomControls, showZoomControls,
showTakenAtTimeHidden, showTakenAtTimeHidden,
showSocial, showSocial,
showFilmSimulations,
showRecipes,
showRepoLink, showRepoLink,
showSidebarCamerasFirst, hasCategoryVisibility,
categoryVisibility,
// Grid // Grid
isGridHomepageEnabled, isGridHomepageEnabled,
gridAspectRatio, gridAspectRatio,
@ -375,7 +377,7 @@ export default function AdminAppConfigurationClient({
</ChecklistRow> </ChecklistRow>
<ChecklistRow <ChecklistRow
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
title={`Auto-generated fields: ${aiTextAutoGeneratedFields.join(', ')}`} title={`Auto-generated fields: ${aiTextAutoGeneratedFields.join(',')}`}
status={hasAiTextAutoGeneratedFields} status={hasAiTextAutoGeneratedFields}
optional optional
> >
@ -383,7 +385,7 @@ export default function AdminAppConfigurationClient({
uploading photos. Accepted values: title, caption, uploading photos. Accepted values: title, caption,
tags, description, all, or none tags, description, all, or none
{' '} {' '}
(default: {'"title, tags, semantic"'}): (default: {'"title,tags,semantic"'}):
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])} {renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
</ChecklistRow> </ChecklistRow>
</ChecklistGroup> </ChecklistGroup>
@ -517,25 +519,6 @@ export default function AdminAppConfigurationClient({
X (formerly Twitter) button from share modal: X (formerly Twitter) button from share modal:
{renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])} {renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])}
</ChecklistRow> </ChecklistRow>
<ChecklistRow
title="Show Fujifilm simulations"
status={showFilmSimulations}
optional
>
Set environment variable to {'"1"'} to prevent
simulations showing up in /grid sidebar and
CMD-K results:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
</ChecklistRow>
<ChecklistRow
title="Show Fujifilm recipes"
status={showRecipes}
optional
>
Set environment variable to {'"1"'} to prevent
Fujifilm recipe button showing up in photo meta:
{renderEnvVars(['NEXT_PUBLIC_HIDE_RECIPES'])}
</ChecklistRow>
<ChecklistRow <ChecklistRow
title="Show repo link" title="Show repo link"
status={showRepoLink} status={showRepoLink}
@ -545,13 +528,40 @@ export default function AdminAppConfigurationClient({
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])} {renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
</ChecklistRow> </ChecklistRow>
<ChecklistRow <ChecklistRow
title="Show cameras first" title={hasCategoryVisibility
status={showSidebarCamerasFirst} ? `Category visibility: ${categoryVisibility.join(',')}`
: 'Category visibility'}
status={hasCategoryVisibility}
optional optional
> >
Set environment variable to {'"1"'} to show cameras {categoryVisibility.map((category, index) =>
above tags in grid sidebar: <Fragment key={category}>
{renderEnvVars(['NEXT_PUBLIC_CAMERAS_FIRST'])} {renderSubStatus(
'checked',
<>
{index + 1}
{'.'}
{capitalize(category)}
</>,
)}
</Fragment>)}
{getHiddenDefaultCategories(categoryVisibility)
.map((category, index) =>
<Fragment key={category}>
{renderSubStatus(
'optional',
<span className="text-dim">
{categoryVisibility.length + index + 1}
{'.'}
{capitalize(category)}
</span>,
)}
</Fragment>)}
Configure photo category visibility and order
(seen in grid sidebar and CMD-K results)
by storing comma-separated values
(default: {`"${DEFAULT_CATEGORY_KEYS.join(',')}"`}):
{renderEnvVars(['NEXT_PUBLIC_CATEGORY_VISIBILITY'])}
</ChecklistRow> </ChecklistRow>
</ChecklistGroup> </ChecklistGroup>
<ChecklistGroup <ChecklistGroup

View File

@ -3,15 +3,16 @@ import {
getUniqueCameras, getUniqueCameras,
getUniqueFilmSimulations, getUniqueFilmSimulations,
getUniqueFocalLengths, getUniqueFocalLengths,
getUniqueRecipes,
getUniqueTags, getUniqueTags,
} from '@/photo/db/query'; } from '@/photo/db/query';
import AdminAppInsightsClient from './AdminAppInsightsClient'; import AdminAppInsightsClient from './AdminAppInsightsClient';
import { import {
APP_CONFIGURATION, APP_CONFIGURATION,
CATEGORY_VISIBILITY,
GRID_HOMEPAGE_ENABLED, GRID_HOMEPAGE_ENABLED,
HAS_STATIC_OPTIMIZATION, HAS_STATIC_OPTIMIZATION,
MATTE_PHOTOS, MATTE_PHOTOS,
SHOW_SIDEBAR_CAMERAS_FIRST,
} from '@/app/config'; } from '@/app/config';
import { getGitHubMetaForCurrentApp, getSignificantInsights } from '.'; import { getGitHubMetaForCurrentApp, getSignificantInsights } from '.';
import { getOutdatedPhotosCount } from '@/photo/db/query'; import { getOutdatedPhotosCount } from '@/photo/db/query';
@ -27,6 +28,7 @@ export default async function AdminAppInsights() {
{ count: photosCountPortrait }, { count: photosCountPortrait },
tags, tags,
cameras, cameras,
recipes,
filmSimulations, filmSimulations,
focalLengths, focalLengths,
codeMeta, codeMeta,
@ -37,6 +39,7 @@ export default async function AdminAppInsights() {
getPhotosMeta({ maximumAspectRatio: 0.9 }), getPhotosMeta({ maximumAspectRatio: 0.9 }),
getUniqueTags(), getUniqueTags(),
getUniqueCameras(), getUniqueCameras(),
getUniqueRecipes(),
getUniqueFilmSimulations(), getUniqueFilmSimulations(),
getUniqueFocalLengths(), getUniqueFocalLengths(),
getGitHubMetaForCurrentApp(), getGitHubMetaForCurrentApp(),
@ -67,7 +70,7 @@ export default async function AdminAppInsights() {
photoMatting: photosCountPortrait > 0 && !MATTE_PHOTOS, photoMatting: photosCountPortrait > 0 && !MATTE_PHOTOS,
camerasFirst: ( camerasFirst: (
tags.length > TAG_COUNT_THRESHOLD && tags.length > TAG_COUNT_THRESHOLD &&
!SHOW_SIDEBAR_CAMERAS_FIRST CATEGORY_VISIBILITY[0] !== 'cameras'
), ),
gridFirst: ( gridFirst: (
photosCount >= BASIC_PHOTO_INSTALLATION_COUNT && photosCount >= BASIC_PHOTO_INSTALLATION_COUNT &&
@ -81,6 +84,7 @@ export default async function AdminAppInsights() {
photosCountOutdated, photosCountOutdated,
tagsCount: tags.length, tagsCount: tags.length,
camerasCount: cameras.length, camerasCount: cameras.length,
recipesCount: recipes.length,
filmSimulationsCount: filmSimulations.length, filmSimulationsCount: filmSimulations.length,
focalLengthsCount: focalLengths.length, focalLengthsCount: focalLengths.length,
dateRange, dateRange,

View File

@ -11,7 +11,7 @@ import { HiMiniArrowsUpDown } from 'react-icons/hi2';
import { HiOutlinePhotograph } from 'react-icons/hi'; import { HiOutlinePhotograph } from 'react-icons/hi';
import { MdAspectRatio } from 'react-icons/md'; import { MdAspectRatio } from 'react-icons/md';
import { PiWarningBold } from 'react-icons/pi'; import { PiWarningBold } from 'react-icons/pi';
import { TbCone, TbSparkles } from 'react-icons/tb'; import { TbChecklist, TbCone, TbSparkles } from 'react-icons/tb';
import { BiGitBranch, BiGitCommit, BiLogoGithub } from 'react-icons/bi'; import { BiGitBranch, BiGitCommit, BiLogoGithub } from 'react-icons/bi';
import { import {
TEMPLATE_REPO_BRANCH, TEMPLATE_REPO_BRANCH,
@ -21,6 +21,7 @@ import {
VERCEL_GIT_COMMIT_MESSAGE, VERCEL_GIT_COMMIT_MESSAGE,
TEMPLATE_REPO_URL_FORK, TEMPLATE_REPO_URL_FORK,
TEMPLATE_REPO_URL_README, TEMPLATE_REPO_URL_README,
CATEGORY_VISIBILITY,
} from '@/app/config'; } from '@/app/config';
import { import {
AdminAppInsights, AdminAppInsights,
@ -89,6 +90,7 @@ export default function AdminAppInsightsClient({
photosCountOutdated, photosCountOutdated,
tagsCount, tagsCount,
camerasCount, camerasCount,
recipesCount,
filmSimulationsCount, filmSimulationsCount,
focalLengthsCount, focalLengthsCount,
dateRange, dateRange,
@ -134,10 +136,10 @@ export default function AdminAppInsightsClient({
className={TEXT_COLOR_WARNING} className={TEXT_COLOR_WARNING}
/>} />}
content={<> content={<>
Could not analyze source code <span>Could not analyze source code</span>
<Tooltip <Tooltip
content="Could not connect to GitHub API. Try refreshing." content="Could not connect to GitHub API. Try refreshing."
classNameTrigger="translate-y-[4.5px] ml-2 h-3" classNameTrigger="translate-y-[-1.5px] ml-2 h-3"
/> />
</>} </>}
/>} />}
@ -357,8 +359,8 @@ export default function AdminAppInsightsClient({
showing cameras first in the sidebar by setting showing cameras first in the sidebar by setting
{' '} {' '}
<EnvVar <EnvVar
variable="SHOW_SIDEBAR_CAMERAS_FIRST" variable="NEXT_PUBLIC_CATEGORY_VISIBILITY"
value="1" value="cameras, tags, recipes, films"
trailingContent="." trailingContent="."
/> />
</>} </>}
@ -411,30 +413,52 @@ export default function AdminAppInsightsClient({
{photosCountHidden > 0 && ` (${photosCountHidden} hidden)`} {photosCountHidden > 0 && ` (${photosCountHidden} hidden)`}
</>} </>}
/> />
<ScoreCardRow {CATEGORY_VISIBILITY.map(category => {
switch (category) {
case 'tags':
return <ScoreCardRow
key={category}
icon={<FaTag icon={<FaTag
size={12} size={12}
className="translate-y-[3px]" className="translate-y-[3px]"
/>} />}
content={pluralize(tagsCount, 'tag')} content={pluralize(tagsCount, 'tag')}
/> />;
<ScoreCardRow case 'cameras':
return <ScoreCardRow
key={category}
icon={<FaCamera icon={<FaCamera
size={13} size={13}
className="translate-y-[2px]" className="translate-y-[2px]"
/>} />}
content={pluralize(camerasCount, 'camera')} content={pluralize(camerasCount, 'camera')}
/> />;
{filmSimulationsCount > 0 && case 'recipes':
<ScoreCardRow if (recipesCount > 0) {
return <ScoreCardRow
key={category}
icon={<TbChecklist
size={18}
className="translate-y-[-0.5px]"
/>}
content={pluralize(recipesCount, 'recipe')}
/>;
}
case 'films':
if (filmSimulationsCount > 0) {
return <ScoreCardRow
key={category}
icon={<span className="inline-flex w-3"> icon={<span className="inline-flex w-3">
<PhotoFilmSimulationIcon <PhotoFilmSimulationIcon
className="shrink-0 translate-x-[-1px] translate-y-[-0.5px]" className="shrink-0 translate-x-[-2px] translate-y-[-0.5px]"
height={18} height={18}
/> />
</span>} </span>}
content={pluralize(filmSimulationsCount, 'film simulation')} content={pluralize(filmSimulationsCount, 'film simulation')}
/>} />;
}
}
})}
<ScoreCardRow <ScoreCardRow
icon={<TbCone className="rotate-[270deg] translate-x-[-2px]" />} icon={<TbCone className="rotate-[270deg] translate-x-[-2px]" />}
content={pluralize(focalLengthsCount, 'focal length')} content={pluralize(focalLengthsCount, 'focal length')}

View File

@ -50,6 +50,7 @@ export interface PhotoStats {
photosCountOutdated: number photosCountOutdated: number
tagsCount: number tagsCount: number
camerasCount: number camerasCount: number
recipesCount: number
filmSimulationsCount: number filmSimulationsCount: number
focalLengthsCount: number focalLengthsCount: number
dateRange?: PhotoDateRange dateRange?: PhotoDateRange

View File

@ -1,33 +1,25 @@
import CommandKClient, { import CommandKClient from '@/components/cmdk/CommandKClient';
CommandKSection,
} from '@/components/cmdk/CommandKClient';
import { import {
getPhotosMetaCached, getPhotosMetaCached,
getUniqueCamerasCached, getUniqueCamerasCached,
getUniqueFilmSimulationsCached, getUniqueFilmSimulationsCached,
getUniqueRecipesCached,
getUniqueTagsCached, getUniqueTagsCached,
} from '@/photo/cache'; } from '@/photo/cache';
import {
pathForCamera,
pathForFilmSimulation,
pathForFocalLength,
} from './paths';
import { formatCameraText } from '@/camera';
import { photoQuantityText } from '@/photo'; import { photoQuantityText } from '@/photo';
import { formatCount, formatCountDescriptive } from '@/utility/string'; import {
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon'; ADMIN_DEBUG_TOOLS_ENABLED,
import { IoMdCamera } from 'react-icons/io'; SHOW_FILM_SIMULATIONS,
import { ADMIN_DEBUG_TOOLS_ENABLED, SHOW_FILM_SIMULATIONS } from './config'; SHOW_RECIPES,
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation'; } from './config';
import { getUniqueFocalLengths } from '@/photo/db/query'; import { getUniqueFocalLengths } from '@/photo/db/query';
import { formatFocalLength } from '@/focal';
import { TbCone } from 'react-icons/tb';
export default async function CommandK() { export default async function CommandK() {
const [ const [
count, count,
tags, tags,
cameras, cameras,
recipes,
filmSimulations, filmSimulations,
focalLengths, focalLengths,
] = await Promise.all([ ] = await Promise.all([
@ -36,56 +28,21 @@ export default async function CommandK() {
.catch(() => 0), .catch(() => 0),
getUniqueTagsCached().catch(() => []), getUniqueTagsCached().catch(() => []),
getUniqueCamerasCached().catch(() => []), getUniqueCamerasCached().catch(() => []),
SHOW_RECIPES
? getUniqueRecipesCached().catch(() => [])
: [],
SHOW_FILM_SIMULATIONS SHOW_FILM_SIMULATIONS
? getUniqueFilmSimulationsCached().catch(() => []) ? getUniqueFilmSimulationsCached().catch(() => [])
: [], : [],
getUniqueFocalLengths().catch(() => []), getUniqueFocalLengths().catch(() => []),
]); ]);
const SECTION_CAMERAS: CommandKSection = {
heading: 'Cameras',
accessory: <IoMdCamera />,
items: cameras.map(({ camera, count }) => ({
label: formatCameraText(camera),
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForCamera(camera),
})),
};
const SECTION_FILM: CommandKSection = {
heading: 'Film Simulations',
accessory: <span className="w-3">
<PhotoFilmSimulationIcon className="translate-y-[0.5px]" />
</span>,
items: filmSimulations.map(({ simulation, count }) => ({
label: labelForFilmSimulation(simulation).medium,
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForFilmSimulation(simulation),
})),
};
const SECTION_FOCAL: CommandKSection = {
heading: 'Focal Lengths',
accessory: <TbCone
className="rotate-[270deg] text-[14px]"
/>,
items: focalLengths.map(({ focal, count }) => ({
label: formatFocalLength(focal)!,
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForFocalLength(focal),
})),
};
return <CommandKClient return <CommandKClient
tags={tags} tags={tags}
serverSections={[ cameras={cameras}
SECTION_CAMERAS, simulations={filmSimulations}
SECTION_FILM, recipes={recipes}
SECTION_FOCAL, focalLengths={focalLengths}
]}
showDebugTools={ADMIN_DEBUG_TOOLS_ENABLED} showDebugTools={ADMIN_DEBUG_TOOLS_ENABLED}
footer={photoQuantityText(count, false)} footer={photoQuantityText(count, false)}
/>; />;

View File

@ -2,6 +2,7 @@ import {
AI_AUTO_GENERATED_FIELDS_DEFAULT, AI_AUTO_GENERATED_FIELDS_DEFAULT,
parseAiAutoGeneratedFieldsString, parseAiAutoGeneratedFieldsString,
} from '@/photo/ai'; } from '@/photo/ai';
import { getOrderedCategoriesFromString } from '@/photo/set';
import type { StorageType } from '@/platforms/storage'; import type { StorageType } from '@/platforms/storage';
import { makeUrlAbsolute, shortenUrl } from '@/utility/url'; import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
@ -207,6 +208,12 @@ export const MATTE_PHOTOS =
// DISPLAY // DISPLAY
export const CATEGORY_VISIBILITY = getOrderedCategoriesFromString(
process.env.NEXT_PUBLIC_CATEGORY_VISIBILITY);
export const SHOW_RECIPES =
CATEGORY_VISIBILITY.includes('recipes');
export const SHOW_FILM_SIMULATIONS =
CATEGORY_VISIBILITY.includes('films');
export const SHOW_EXIF_DATA = export const SHOW_EXIF_DATA =
process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1'; process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1';
export const SHOW_ZOOM_CONTROLS = export const SHOW_ZOOM_CONTROLS =
@ -215,14 +222,8 @@ export const SHOW_TAKEN_AT_TIME =
process.env.NEXT_PUBLIC_HIDE_TAKEN_AT_TIME !== '1'; process.env.NEXT_PUBLIC_HIDE_TAKEN_AT_TIME !== '1';
export const SHOW_SOCIAL = export const SHOW_SOCIAL =
process.env.NEXT_PUBLIC_HIDE_SOCIAL !== '1'; process.env.NEXT_PUBLIC_HIDE_SOCIAL !== '1';
export const SHOW_FILM_SIMULATIONS =
process.env.NEXT_PUBLIC_HIDE_FILM_SIMULATIONS !== '1';
export const SHOW_RECIPES =
process.env.NEXT_PUBLIC_HIDE_RECIPES !== '1';
export const SHOW_REPO_LINK = export const SHOW_REPO_LINK =
process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1'; process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
export const SHOW_SIDEBAR_CAMERAS_FIRST =
process.env.NEXT_PUBLIC_CAMERAS_FIRST === '1';
// GRID // GRID
@ -304,22 +305,22 @@ export const APP_CONFIGURATION = {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
arePhotoCategoryOgImagesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES, arePhotoCategoryOgImagesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES,
areOriginalUploadsPreserved: PRESERVE_ORIGINAL_UPLOADS, areOriginalUploadsPreserved: PRESERVE_ORIGINAL_UPLOADS,
imageQuality: IMAGE_QUALITY,
hasImageQuality: Boolean(process.env.NEXT_PUBLIC_IMAGE_QUALITY), hasImageQuality: Boolean(process.env.NEXT_PUBLIC_IMAGE_QUALITY),
imageQuality: IMAGE_QUALITY,
isBlurEnabled: BLUR_ENABLED, isBlurEnabled: BLUR_ENABLED,
// Visual // Visual
hasDefaultTheme: Boolean(process.env.NEXT_PUBLIC_DEFAULT_THEME), hasDefaultTheme: Boolean(process.env.NEXT_PUBLIC_DEFAULT_THEME),
defaultTheme: DEFAULT_THEME, defaultTheme: DEFAULT_THEME,
arePhotosMatted: MATTE_PHOTOS, arePhotosMatted: MATTE_PHOTOS,
// Display // Display
hasCategoryVisibility:
Boolean(process.env.NEXT_PUBLIC_CATEGORY_VISIBILITY),
categoryVisibility: CATEGORY_VISIBILITY,
showExifInfo: SHOW_EXIF_DATA, showExifInfo: SHOW_EXIF_DATA,
showZoomControls: SHOW_ZOOM_CONTROLS, showZoomControls: SHOW_ZOOM_CONTROLS,
showTakenAtTimeHidden: SHOW_TAKEN_AT_TIME, showTakenAtTimeHidden: SHOW_TAKEN_AT_TIME,
showSocial: SHOW_SOCIAL, showSocial: SHOW_SOCIAL,
showFilmSimulations: SHOW_FILM_SIMULATIONS,
showRecipes: SHOW_RECIPES,
showRepoLink: SHOW_REPO_LINK, showRepoLink: SHOW_REPO_LINK,
showSidebarCamerasFirst: SHOW_SIDEBAR_CAMERAS_FIRST,
// Grid // Grid
isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED, isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED,
gridAspectRatio: GRID_ASPECT_RATIO, gridAspectRatio: GRID_ASPECT_RATIO,

View File

@ -1,4 +1,5 @@
import { Photo, PhotoSetCategory } from '@/photo'; import { Photo } from '@/photo';
import { PhotoSetCategory } from '@/photo/set';
import { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config'; import { BASE_URL, GRID_HOMEPAGE_ENABLED } from './config';
import { Camera } from '@/camera'; import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation'; import { FilmSimulation } from '@/simulation';

View File

@ -1,5 +1,5 @@
import { absolutePathForCamera } from '@/app/paths'; import { absolutePathForCamera } from '@/app/paths';
import { PhotoSetAttributes } from '../photo'; import { PhotoSetAttributes } from '../photo/set';
import ShareModal from '@/share/ShareModal'; import ShareModal from '@/share/ShareModal';
import CameraOGTile from './CameraOGTile'; import CameraOGTile from './CameraOGTile';
import { Camera } from '.'; import { Camera } from '.';

View File

@ -2,7 +2,7 @@
import { blobToImage } from '@/utility/blob'; import { blobToImage } from '@/utility/blob';
import { useRef, RefObject } from 'react'; import { useRef, RefObject } from 'react';
import { CopyExif } from '@/lib/CopyExif'; import { CopyExif } from '@/utility/exif';
import exifr from 'exifr'; import exifr from 'exifr';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo'; import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';

View File

@ -23,6 +23,9 @@ import {
PATH_GRID_INFERRED, PATH_GRID_INFERRED,
PATH_ROOT, PATH_ROOT,
PATH_SIGN_IN, PATH_SIGN_IN,
pathForCamera,
pathForFilmSimulation,
pathForFocalLength,
pathForPhoto, pathForPhoto,
pathForTag, pathForTag,
} from '../../app/paths'; } from '../../app/paths';
@ -40,19 +43,25 @@ import { RiToolsFill } from 'react-icons/ri';
import { BiLockAlt, BiSolidUser } from 'react-icons/bi'; import { BiLockAlt, BiSolidUser } from 'react-icons/bi';
import { HiDocumentText } from 'react-icons/hi'; import { HiDocumentText } from 'react-icons/hi';
import { signOutAction } from '@/auth/actions'; import { signOutAction } from '@/auth/actions';
import { TbPhoto } from 'react-icons/tb'; import { TbChecklist, TbCone, TbPhoto } from 'react-icons/tb';
import { getKeywordsForPhoto, titleForPhoto } from '@/photo'; import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
import PhotoDate from '@/photo/PhotoDate'; import PhotoDate from '@/photo/PhotoDate';
import PhotoSmall from '@/photo/PhotoSmall'; import PhotoSmall from '@/photo/PhotoSmall';
import { FaCheck } from 'react-icons/fa6'; import { FaCheck } from 'react-icons/fa6';
import { Tags, addHiddenToTags, formatTag } from '@/tag'; import { addHiddenToTags, formatTag } from '@/tag';
import { FaTag } from 'react-icons/fa'; import { FaTag } from 'react-icons/fa';
import { formatCount, formatCountDescriptive } from '@/utility/string'; import { formatCount, formatCountDescriptive } from '@/utility/string';
import CommandKItem from './CommandKItem'; import CommandKItem from './CommandKItem';
import { GRID_HOMEPAGE_ENABLED } from '@/app/config'; import { CATEGORY_VISIBILITY, GRID_HOMEPAGE_ENABLED } from '@/app/config';
import { DialogDescription, DialogTitle } from '@radix-ui/react-dialog'; import { DialogDescription, DialogTitle } from '@radix-ui/react-dialog';
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'; import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot'; import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot';
import { PhotoSetCategories } from '@/photo/set';
import { formatCameraText } from '@/camera';
import { IoMdCamera } from 'react-icons/io';
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import { formatFocalLength } from '@/focal';
const DIALOG_TITLE = 'Global Command-K Menu'; const DIALOG_TITLE = 'Global Command-K Menu';
const DIALOG_DESCRIPTION = 'For searching photos, views, and settings'; const DIALOG_DESCRIPTION = 'For searching photos, views, and settings';
@ -70,7 +79,7 @@ type CommandKItem = {
action?: () => void | Promise<void> action?: () => void | Promise<void>
} }
export type CommandKSection = { type CommandKSection = {
heading: string heading: string
accessory?: ReactNode accessory?: ReactNode
items: CommandKItem[] items: CommandKItem[]
@ -88,15 +97,16 @@ const renderToggle = (
export default function CommandKClient({ export default function CommandKClient({
tags, tags,
serverSections = [], cameras,
recipes,
simulations,
focalLengths,
showDebugTools, showDebugTools,
footer, footer,
}: { }: {
tags: Tags
serverSections?: CommandKSection[]
showDebugTools?: boolean showDebugTools?: boolean
footer?: string footer?: string
}) { } & PhotoSetCategories) {
const pathname = usePathname(); const pathname = usePathname();
const { const {
@ -234,7 +244,11 @@ export default function CommandKClient({
addHiddenToTags(tags, photosCountHidden) addHiddenToTags(tags, photosCountHidden)
, [tags, photosCountHidden]); , [tags, photosCountHidden]);
const SECTION_TAGS: CommandKSection = { const categorySections: CommandKSection[] = useMemo(() =>
CATEGORY_VISIBILITY
.map(category => {
switch (category) {
case 'tags': return {
heading: 'Tags', heading: 'Tags',
accessory: <FaTag accessory: <FaTag
size={10} size={10}
@ -247,6 +261,56 @@ export default function CommandKClient({
path: pathForTag(tag), path: pathForTag(tag),
})), })),
}; };
case 'cameras': return {
heading: 'Cameras',
accessory: <IoMdCamera />,
items: cameras.map(({ camera, count }) => ({
label: formatCameraText(camera),
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForCamera(camera),
})),
};
case 'recipes': return {
heading: 'Recipes',
accessory: <TbChecklist
size={15}
className="translate-x-[-1px]"
/>,
items: recipes.map(({ recipe, count }) => ({
label: recipe,
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
})),
};
case 'films': return {
heading: 'Film Simulations',
accessory: <span className="w-3">
<PhotoFilmSimulationIcon className="translate-y-[0.5px]" />
</span>,
items: simulations.map(({ simulation, count }) => ({
label: labelForFilmSimulation(simulation).medium,
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForFilmSimulation(simulation),
})),
};
case 'focal-lengths': return {
heading: 'Focal Lengths',
accessory: <TbCone
className="rotate-[270deg] text-[14px]"
/>,
items: focalLengths.map(({ focal, count }) => ({
label: formatFocalLength(focal)!,
annotation: formatCount(count),
annotationAria: formatCountDescriptive(count),
path: pathForFocalLength(focal),
})),
};
}
})
.filter(Boolean) as CommandKSection[]
, [tagsIncludingHidden, cameras, recipes, simulations, focalLengths]);
const clientSections: CommandKSection[] = [{ const clientSections: CommandKSection[] = [{
heading: 'Theme', heading: 'Theme',
@ -465,8 +529,7 @@ export default function CommandKClient({
{isLoading ? 'Searching ...' : 'No results found'} {isLoading ? 'Searching ...' : 'No results found'}
</Command.Empty> </Command.Empty>
{queriedSections {queriedSections
.concat(SECTION_TAGS) .concat(categorySections)
.concat(serverSections)
.concat(sectionPages) .concat(sectionPages)
.concat(adminSection) .concat(adminSection)
.concat(clientSections) .concat(clientSections)

View File

@ -38,7 +38,10 @@ export default function TooltipPrimitive({
}, },
}); });
const classNameTrigger = clsx('link cursor-default', classNameTriggerProp); const classNameTrigger = clsx(
'link cursor-default inline-block',
classNameTriggerProp,
);
return ( return (
<Tooltip.Provider delayDuration={100}> <Tooltip.Provider delayDuration={100}>

View File

@ -1,5 +1,5 @@
import { absolutePathForFocalLength } from '@/app/paths'; import { absolutePathForFocalLength } from '@/app/paths';
import { PhotoSetAttributes } from '../photo'; import { PhotoSetAttributes } from '../photo/set';
import ShareModal from '@/share/ShareModal'; import ShareModal from '@/share/ShareModal';
import FocalLengthOGTile from './FocalLengthOGTile'; import FocalLengthOGTile from './FocalLengthOGTile';
import { shareTextFocalLength } from '.'; import { shareTextFocalLength } from '.';

View File

@ -1,36 +0,0 @@
export async function CopyExif(
src: Blob,
dest: Blob,
type = 'image/jpeg',
) {
const exif = await retrieveExif(src);
return new Blob([dest.slice(0, 2), exif, dest.slice(2)], { type });
};
const SOS = 0xffda;
const APP1 = 0xffe1;
const EXIF = 0x45786966;
const retrieveExif = (blob: Blob): Promise<Blob> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', e => {
const buffer = e.target!.result as ArrayBuffer;
const view = new DataView(buffer);
let offset = 0;
if (view.getUint16(offset) !== 0xffd8)
return reject('not a valid jpeg');
offset += 2;
while (true) {
const marker = view.getUint16(offset);
if (marker === SOS) break;
const size = view.getUint16(offset + 2);
if (marker === APP1 && view.getUint32(offset + 4) === EXIF)
return resolve(blob.slice(offset, offset + 2 + size));
offset += 2 + size;
}
return resolve(new Blob());
});
reader.readAsArrayBuffer(blob);
});

View File

@ -11,7 +11,8 @@ import {
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import Spinner from '@/components/Spinner'; import Spinner from '@/components/Spinner';
import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions'; import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions';
import { Photo, PhotoSetCategory } from '.'; import { Photo } from '.';
import { PhotoSetCategory } from './set';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import { GetPhotosOptions } from './db'; import { GetPhotosOptions } from './db';

View File

@ -1,5 +1,6 @@
import AnimateItems from '@/components/AnimateItems'; import AnimateItems from '@/components/AnimateItems';
import { Photo, PhotoDateRange, PhotoSetCategory } from '.'; import { Photo, PhotoDateRange } from '.';
import { PhotoSetCategory } from './set';
import PhotoLarge from './PhotoLarge'; import PhotoLarge from './PhotoLarge';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import PhotoGrid from './PhotoGrid'; import PhotoGrid from './PhotoGrid';

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { Photo, PhotoSetCategory } from '.'; import { Photo } from '.';
import { PhotoSetCategory } from './set';
import PhotoMedium from './PhotoMedium'; import PhotoMedium from './PhotoMedium';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import AnimateItems from '@/components/AnimateItems'; import AnimateItems from '@/components/AnimateItems';

View File

@ -15,7 +15,7 @@ import FavsTag from '../tag/FavsTag';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';
import { useMemo } from 'react'; import { useMemo } from 'react';
import HiddenTag from '@/tag/HiddenTag'; import HiddenTag from '@/tag/HiddenTag';
import { SHOW_SIDEBAR_CAMERAS_FIRST, SITE_ABOUT } from '@/app/config'; import { CATEGORY_VISIBILITY, SITE_ABOUT } from '@/app/config';
import { import {
htmlHasBrParagraphBreaks, htmlHasBrParagraphBreaks,
safelyParseFormattedHtml, safelyParseFormattedHtml,
@ -184,11 +184,14 @@ export default function PhotoGridSidebar({
}} }}
/>]} />]}
/>} />}
{SHOW_SIDEBAR_CAMERAS_FIRST {CATEGORY_VISIBILITY.map(category => {
? <>{camerasContent}{tagsContent}</> switch (category) {
: <>{tagsContent}{camerasContent}</>} case 'cameras': return camerasContent;
{recipesContent} case 'tags': return tagsContent;
{filmsContent} case 'recipes': return recipesContent;
case 'films': return filmsContent;
}
})}
{photoStatsContent} {photoStatsContent}
</div> </div>
); );

View File

@ -4,10 +4,10 @@ import { clsx } from 'clsx/lite';
import { import {
Photo, Photo,
PhotoDateRange, PhotoDateRange,
PhotoSetCategory,
dateRangeForPhotos, dateRangeForPhotos,
titleForPhoto, titleForPhoto,
} from '.'; } from '.';
import { PhotoSetCategory } from './set';
import ShareButton from '@/share/ShareButton'; import ShareButton from '@/share/ShareButton';
import AnimateItems from '@/components/AnimateItems'; import AnimateItems from '@/components/AnimateItems';
import { ReactNode } from 'react'; import { ReactNode } from 'react';

View File

@ -1,5 +1,6 @@
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { Photo, PhotoSetCategory } from '.'; import { Photo } from '.';
import { PhotoSetCategory } from './set';
import PhotoGrid from './PhotoGrid'; import PhotoGrid from './PhotoGrid';
import Link from 'next/link'; import Link from 'next/link';

View File

@ -1,7 +1,8 @@
'use client'; 'use client';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Photo, PhotoSetCategory, titleForPhoto } from '@/photo'; import { Photo, titleForPhoto } from '@/photo';
import { PhotoSetCategory } from '@/photo/set';
import Link from 'next/link'; import Link from 'next/link';
import { AnimationConfig } from '../components/AnimateItems'; import { AnimationConfig } from '../components/AnimateItems';
import { useAppState } from '@/state/AppState'; import { useAppState } from '@/state/AppState';

View File

@ -2,10 +2,10 @@
import { import {
Photo, Photo,
PhotoSetCategory,
altTextForPhoto, altTextForPhoto,
doesPhotoNeedBlurCompatibility, doesPhotoNeedBlurCompatibility,
} from '.'; } from '.';
import { PhotoSetCategory } from './set';
import ImageMedium from '@/components/image/ImageMedium'; import ImageMedium from '@/components/image/ImageMedium';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';
import { pathForPhoto } from '@/app/paths'; import { pathForPhoto } from '@/app/paths';

View File

@ -1,9 +1,9 @@
import { import {
Photo, Photo,
PhotoSetCategory,
descriptionForPhoto, descriptionForPhoto,
titleForPhoto, titleForPhoto,
} from '@/photo'; } from '@/photo';
import { PhotoSetCategory } from './set';
import { absolutePathForPhotoImage, pathForPhoto } from '@/app/paths'; import { absolutePathForPhotoImage, pathForPhoto } from '@/app/paths';
import OGTile from '@/components/OGTile'; import OGTile from '@/components/OGTile';

View File

@ -3,10 +3,10 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { import {
Photo, Photo,
PhotoSetCategory,
getNextPhoto, getNextPhoto,
getPreviousPhoto, getPreviousPhoto,
} from '@/photo'; } from '@/photo';
import { PhotoSetCategory } from './set';
import PhotoLink from './PhotoLink'; import PhotoLink from './PhotoLink';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { pathForPhoto } from '@/app/paths'; import { pathForPhoto } from '@/app/paths';

View File

@ -1,6 +1,7 @@
import PhotoOGTile from '@/photo/PhotoOGTile'; import PhotoOGTile from '@/photo/PhotoOGTile';
import { absolutePathForPhoto } from '@/app/paths'; import { absolutePathForPhoto } from '@/app/paths';
import { Photo, PhotoSetCategory } from '.'; import { Photo } from '.';
import { PhotoSetCategory } from './set';
import ShareModal from '@/share/ShareModal'; import ShareModal from '@/share/ShareModal';
export default function PhotoShareModal( export default function PhotoShareModal(

View File

@ -1,9 +1,9 @@
import { import {
Photo, Photo,
PhotoSetCategory,
altTextForPhoto, altTextForPhoto,
doesPhotoNeedBlurCompatibility, doesPhotoNeedBlurCompatibility,
} from '.'; } from '.';
import { PhotoSetCategory } from './set';
import ImageSmall from '@/components/image/ImageSmall'; import ImageSmall from '@/components/image/ImageSmall';
import Link from 'next/link'; import Link from 'next/link';
import { clsx } from 'clsx/lite'; import { clsx } from 'clsx/lite';

View File

@ -1,6 +1,7 @@
import { import {
getUniqueCamerasCached, getUniqueCamerasCached,
getUniqueFilmSimulationsCached, getUniqueFilmSimulationsCached,
getUniqueRecipesCached,
getUniqueTagsCached, getUniqueTagsCached,
} from '@/photo/cache'; } from '@/photo/cache';
import { import {
@ -27,4 +28,5 @@ export const getPhotoSidebarDataCached = () => [
getUniqueTagsCached().then(sortTagsObject), getUniqueTagsCached().then(sortTagsObject),
getUniqueCamerasCached(), getUniqueCamerasCached(),
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [], SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
SHOW_RECIPES ? getUniqueRecipesCached() : [],
] as const; ] as const;

View File

@ -1,6 +1,6 @@
import { PRIORITY_ORDER_ENABLED } from '@/app/config'; import { PRIORITY_ORDER_ENABLED } from '@/app/config';
import { parameterize } from '@/utility/string'; import { parameterize } from '@/utility/string';
import { PhotoSetCategory } from '..'; import { PhotoSetCategory } from '../set';
import { Camera } from '@/camera'; import { Camera } from '@/camera';
export const GENERATE_STATIC_PARAMS_LIMIT = 1000; export const GENERATE_STATIC_PARAMS_LIMIT = 1000;

View File

@ -1,6 +1,4 @@
import { Camera } from '@/camera';
import { formatFocalLength } from '@/focal'; import { formatFocalLength } from '@/focal';
import { Lens } from '@/lens';
import { getNextImageUrlForRequest } from '@/platforms/next-image'; import { getNextImageUrlForRequest } from '@/platforms/next-image';
import { FilmSimulation } from '@/simulation'; import { FilmSimulation } from '@/simulation';
import { import {
@ -109,21 +107,6 @@ export interface Photo extends Omit<PhotoDb, 'recipeData'> {
recipeData?: FujifilmRecipe recipeData?: FujifilmRecipe
} }
export interface PhotoSetCategory {
tag?: string
camera?: Camera
simulation?: FilmSimulation
recipe?: string
focal?: number
lens?: Lens // Unimplemented as a set
}
export interface PhotoSetAttributes {
photos: Photo[]
count?: number
dateRange?: PhotoDateRange
}
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
const photoDb = camelcaseKeys( const photoDb = camelcaseKeys(
photoDbRaw as unknown as Record<string, unknown>, photoDbRaw as unknown as Record<string, unknown>,

64
src/photo/set.ts Normal file
View File

@ -0,0 +1,64 @@
import { Photo } from '.';
import { Camera, Cameras } from '@/camera';
import { PhotoDateRange } from '.';
import { FilmSimulation, FilmSimulations } from '@/simulation';
import { Lens } from '@/lens';
import { Tags } from '@/tag';
import { FocalLengths } from '@/focal';
import { Recipes } from '@/recipe';
const CATEGORY_KEYS = [
'tags',
'cameras',
'recipes',
'films',
'focal-lengths',
'lenses',
] as const;
type CategoryKey = (typeof CATEGORY_KEYS)[number];
type CategoryKeys = CategoryKey[];
export const DEFAULT_CATEGORY_KEYS: CategoryKeys = [
'tags',
'cameras',
'recipes',
'films',
];
export const getHiddenDefaultCategories = (keys: CategoryKeys): CategoryKeys =>
DEFAULT_CATEGORY_KEYS.filter(key => !keys.includes(key));
export interface PhotoSetCategory {
tag?: string
camera?: Camera
recipe?: string
simulation?: FilmSimulation
focal?: number
lens?: Lens // Unimplemented as a set
}
export interface PhotoSetCategories {
tags: Tags
cameras: Cameras
recipes: Recipes
simulations: FilmSimulations
focalLengths: FocalLengths
}
export interface PhotoSetAttributes {
photos: Photo[]
count?: number
dateRange?: PhotoDateRange
}
export const getOrderedCategoriesFromString = (
categories?: string,
): CategoryKeys =>
categories
? categories
.split(',')
.map(category => category.trim().toLocaleLowerCase() as CategoryKey)
.filter(category => CATEGORY_KEYS.includes(category))
: DEFAULT_CATEGORY_KEYS;

View File

@ -1,5 +1,5 @@
import { absolutePathForRecipe } from '@/app/paths'; import { absolutePathForRecipe } from '@/app/paths';
import { PhotoSetAttributes } from '../photo'; import { PhotoSetAttributes } from '../photo/set';
import ShareModal from '@/share/ShareModal'; import ShareModal from '@/share/ShareModal';
import { shareTextForRecipe } from '.'; import { shareTextForRecipe } from '.';
import RecipeOGTile from './RecipeOGTile'; import RecipeOGTile from './RecipeOGTile';

View File

@ -1,4 +1,5 @@
import { Photo, PhotoSetAttributes, PhotoSetCategory } from '@/photo'; import { Photo } from '@/photo';
import { PhotoSetAttributes, PhotoSetCategory } from '@/photo/set';
import { import {
absolutePathForCameraImage, absolutePathForCameraImage,
absolutePathForFilmSimulationImage, absolutePathForFilmSimulationImage,

View File

@ -1,5 +1,5 @@
import { absolutePathForFilmSimulation } from '@/app/paths'; import { absolutePathForFilmSimulation } from '@/app/paths';
import { PhotoSetAttributes } from '../photo'; import { PhotoSetAttributes } from '../photo/set';
import ShareModal from '@/share/ShareModal'; import ShareModal from '@/share/ShareModal';
import FilmSimulationOGTile from './FilmSimulationOGTile'; import FilmSimulationOGTile from './FilmSimulationOGTile';
import { FilmSimulation, shareTextForFilmSimulation } from '.'; import { FilmSimulation, shareTextForFilmSimulation } from '.';

View File

@ -1,5 +1,5 @@
import { absolutePathForTag } from '@/app/paths'; import { absolutePathForTag } from '@/app/paths';
import { PhotoSetAttributes } from '../photo'; import { PhotoSetAttributes } from '../photo/set';
import ShareModal from '@/share/ShareModal'; import ShareModal from '@/share/ShareModal';
import TagOGTile from './TagOGTile'; import TagOGTile from './TagOGTile';
import { shareTextForTag } from '.'; import { shareTextForTag } from '.';

View File

@ -86,3 +86,41 @@ export const formatExposureCompensation = (exposureCompensation?: number) => {
return undefined; return undefined;
} }
}; };
const SOS = 0xffda;
const APP1 = 0xffe1;
const EXIF = 0x45786966;
const retrieveExif = (blob: Blob): Promise<Blob> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', e => {
const buffer = e.target!.result as ArrayBuffer;
const view = new DataView(buffer);
let offset = 0;
if (view.getUint16(offset) !== 0xffd8)
return reject('not a valid jpeg');
offset += 2;
while (true) {
const marker = view.getUint16(offset);
if (marker === SOS) break;
const size = view.getUint16(offset + 2);
if (marker === APP1 && view.getUint32(offset + 4) === EXIF)
return resolve(blob.slice(offset, offset + 2 + size));
offset += 2 + size;
}
return resolve(new Blob());
});
reader.readAsArrayBuffer(blob);
});
export const CopyExif = async (
src: Blob,
dest: Blob,
type = 'image/jpeg',
) => {
const exif = await retrieveExif(src);
return new Blob([dest.slice(0, 2), exif, dest.slice(2)], { type });
};