Refactor photo set customization
This commit is contained in:
parent
21ed815cba
commit
6738ffc28e
@ -79,7 +79,7 @@ _⚠️ READ BEFORE PROCEEDING_
|
||||
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
|
||||
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:
|
||||
- `all`
|
||||
- `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_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_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_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
|
||||
- `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?
|
||||
> 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?
|
||||
> 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
37
__tests__/set.test.ts
Normal 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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -63,7 +63,7 @@
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"eslint": "9.21.0",
|
||||
"eslint": "9.22.0",
|
||||
"eslint-config-next": "15.2.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"jest": "^29.7.0",
|
||||
|
||||
121
pnpm-lock.yaml
generated
121
pnpm-lock.yaml
generated
@ -166,14 +166,14 @@ importers:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
eslint:
|
||||
specifier: 9.21.0
|
||||
version: 9.21.0(jiti@2.4.2)
|
||||
specifier: 9.22.0
|
||||
version: 9.22.0(jiti@2.4.2)
|
||||
eslint-config-next:
|
||||
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:
|
||||
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:
|
||||
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))
|
||||
@ -609,6 +609,10 @@ packages:
|
||||
resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==}
|
||||
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':
|
||||
resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@ -617,8 +621,8 @@ packages:
|
||||
resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/js@9.21.0':
|
||||
resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==}
|
||||
'@eslint/js@9.22.0':
|
||||
resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/object-schema@2.1.6':
|
||||
@ -2425,8 +2429,8 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
|
||||
|
||||
eslint-scope@8.2.0:
|
||||
resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==}
|
||||
eslint-scope@8.3.0:
|
||||
resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
eslint-visitor-keys@3.4.3:
|
||||
@ -2437,8 +2441,8 @@ packages:
|
||||
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
eslint@9.21.0:
|
||||
resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==}
|
||||
eslint@9.22.0:
|
||||
resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@ -5035,9 +5039,9 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
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:
|
||||
eslint: 9.21.0(jiti@2.4.2)
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.1': {}
|
||||
@ -5050,6 +5054,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@eslint/config-helpers@0.1.0': {}
|
||||
|
||||
'@eslint/core@0.12.0':
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
@ -5068,7 +5074,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@eslint/js@9.21.0': {}
|
||||
'@eslint/js@9.22.0': {}
|
||||
|
||||
'@eslint/object-schema@2.1.6': {}
|
||||
|
||||
@ -6271,15 +6277,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@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:
|
||||
'@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/type-utils': 8.24.1(eslint@9.21.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/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.22.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
'@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
|
||||
ignore: 5.3.2
|
||||
natural-compare: 1.4.0
|
||||
@ -6288,14 +6294,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@typescript-eslint/scope-manager': 8.24.1
|
||||
'@typescript-eslint/types': 8.24.1
|
||||
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.2)
|
||||
'@typescript-eslint/visitor-keys': 8.24.1
|
||||
debug: 4.4.0
|
||||
eslint: 9.21.0(jiti@2.4.2)
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -6305,12 +6311,12 @@ snapshots:
|
||||
'@typescript-eslint/types': 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:
|
||||
'@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
|
||||
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)
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
@ -6332,13 +6338,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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/types': 8.24.1
|
||||
'@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
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -7025,19 +7031,19 @@ snapshots:
|
||||
optionalDependencies:
|
||||
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:
|
||||
'@next/eslint-plugin-next': 15.2.1
|
||||
'@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/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-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.22.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
eslint: 9.22.0(jiti@2.4.2)
|
||||
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-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-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@2.4.2))
|
||||
eslint-plugin-react: 7.37.4(eslint@9.21.0(jiti@2.4.2))
|
||||
eslint-plugin-react-hooks: 5.2.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.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.22.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.22.0(jiti@2.4.2))
|
||||
optionalDependencies:
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
@ -7053,33 +7059,33 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0
|
||||
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
|
||||
is-bun-module: 1.3.0
|
||||
stable-hash: 0.0.4
|
||||
tinyglobby: 0.2.11
|
||||
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:
|
||||
- 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:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@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-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)
|
||||
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:
|
||||
- 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:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
@ -7088,9 +7094,9 @@ snapshots:
|
||||
array.prototype.flatmap: 1.3.3
|
||||
debug: 3.2.7
|
||||
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-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
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@ -7102,13 +7108,13 @@ snapshots:
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
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:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- 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:
|
||||
aria-query: 5.3.2
|
||||
array-includes: 3.1.8
|
||||
@ -7118,7 +7124,7 @@ snapshots:
|
||||
axobject-query: 4.1.0
|
||||
damerau-levenshtein: 1.0.8
|
||||
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
|
||||
jsx-ast-utils: 3.3.5
|
||||
language-tags: 1.0.9
|
||||
@ -7127,11 +7133,11 @@ snapshots:
|
||||
safe-regex-test: 1.1.0
|
||||
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:
|
||||
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:
|
||||
array-includes: 3.1.8
|
||||
array.prototype.findlast: 1.2.5
|
||||
@ -7139,7 +7145,7 @@ snapshots:
|
||||
array.prototype.tosorted: 1.1.4
|
||||
doctrine: 2.1.0
|
||||
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
|
||||
hasown: 2.0.2
|
||||
jsx-ast-utils: 3.3.5
|
||||
@ -7153,7 +7159,7 @@ snapshots:
|
||||
string.prototype.matchall: 4.0.12
|
||||
string.prototype.repeat: 1.0.0
|
||||
|
||||
eslint-scope@8.2.0:
|
||||
eslint-scope@8.3.0:
|
||||
dependencies:
|
||||
esrecurse: 4.3.0
|
||||
estraverse: 5.3.0
|
||||
@ -7162,14 +7168,15 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@4.2.0: {}
|
||||
|
||||
eslint@9.21.0(jiti@2.4.2):
|
||||
eslint@9.22.0(jiti@2.4.2):
|
||||
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/config-array': 0.19.2
|
||||
'@eslint/config-helpers': 0.1.0
|
||||
'@eslint/core': 0.12.0
|
||||
'@eslint/eslintrc': 3.3.0
|
||||
'@eslint/js': 9.21.0
|
||||
'@eslint/js': 9.22.0
|
||||
'@eslint/plugin-kit': 0.2.7
|
||||
'@humanfs/node': 0.16.6
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
@ -7181,7 +7188,7 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.2.0
|
||||
eslint-scope: 8.3.0
|
||||
eslint-visitor-keys: 4.2.0
|
||||
espree: 10.3.0
|
||||
esquery: 1.6.0
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
ComponentProps,
|
||||
Fragment,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import ChecklistRow from '../components/ChecklistRow';
|
||||
@ -28,6 +29,8 @@ import { CgDebug } from 'react-icons/cg';
|
||||
import EnvVar from '@/components/EnvVar';
|
||||
import AdminLink from './AdminLink';
|
||||
import ScoreCardContainer from '@/components/ScoreCardContainer';
|
||||
import { capitalize } from '@/utility/string';
|
||||
import { DEFAULT_CATEGORY_KEYS, getHiddenDefaultCategories } from '@/photo/set';
|
||||
|
||||
export default function AdminAppConfigurationClient({
|
||||
// Storage
|
||||
@ -60,8 +63,8 @@ export default function AdminAppConfigurationClient({
|
||||
arePhotoCategoriesStaticallyOptimized,
|
||||
arePhotoCategoryOgImagesStaticallyOptimized,
|
||||
areOriginalUploadsPreserved,
|
||||
imageQuality,
|
||||
hasImageQuality,
|
||||
imageQuality,
|
||||
isBlurEnabled,
|
||||
// Visual
|
||||
hasDefaultTheme,
|
||||
@ -72,10 +75,9 @@ export default function AdminAppConfigurationClient({
|
||||
showZoomControls,
|
||||
showTakenAtTimeHidden,
|
||||
showSocial,
|
||||
showFilmSimulations,
|
||||
showRecipes,
|
||||
showRepoLink,
|
||||
showSidebarCamerasFirst,
|
||||
hasCategoryVisibility,
|
||||
categoryVisibility,
|
||||
// Grid
|
||||
isGridHomepageEnabled,
|
||||
gridAspectRatio,
|
||||
@ -375,7 +377,7 @@ export default function AdminAppConfigurationClient({
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
// eslint-disable-next-line max-len
|
||||
title={`Auto-generated fields: ${aiTextAutoGeneratedFields.join(', ')}`}
|
||||
title={`Auto-generated fields: ${aiTextAutoGeneratedFields.join(',')}`}
|
||||
status={hasAiTextAutoGeneratedFields}
|
||||
optional
|
||||
>
|
||||
@ -383,7 +385,7 @@ export default function AdminAppConfigurationClient({
|
||||
uploading photos. Accepted values: title, caption,
|
||||
tags, description, all, or none
|
||||
{' '}
|
||||
(default: {'"title, tags, semantic"'}):
|
||||
(default: {'"title,tags,semantic"'}):
|
||||
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
|
||||
</ChecklistRow>
|
||||
</ChecklistGroup>
|
||||
@ -517,25 +519,6 @@ export default function AdminAppConfigurationClient({
|
||||
X (formerly Twitter) button from share modal:
|
||||
{renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])}
|
||||
</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
|
||||
title="Show repo link"
|
||||
status={showRepoLink}
|
||||
@ -545,13 +528,40 @@ export default function AdminAppConfigurationClient({
|
||||
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title="Show cameras first"
|
||||
status={showSidebarCamerasFirst}
|
||||
title={hasCategoryVisibility
|
||||
? `Category visibility: ${categoryVisibility.join(',')}`
|
||||
: 'Category visibility'}
|
||||
status={hasCategoryVisibility}
|
||||
optional
|
||||
>
|
||||
Set environment variable to {'"1"'} to show cameras
|
||||
above tags in grid sidebar:
|
||||
{renderEnvVars(['NEXT_PUBLIC_CAMERAS_FIRST'])}
|
||||
{categoryVisibility.map((category, index) =>
|
||||
<Fragment key={category}>
|
||||
{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>
|
||||
</ChecklistGroup>
|
||||
<ChecklistGroup
|
||||
|
||||
@ -3,15 +3,16 @@ import {
|
||||
getUniqueCameras,
|
||||
getUniqueFilmSimulations,
|
||||
getUniqueFocalLengths,
|
||||
getUniqueRecipes,
|
||||
getUniqueTags,
|
||||
} from '@/photo/db/query';
|
||||
import AdminAppInsightsClient from './AdminAppInsightsClient';
|
||||
import {
|
||||
APP_CONFIGURATION,
|
||||
CATEGORY_VISIBILITY,
|
||||
GRID_HOMEPAGE_ENABLED,
|
||||
HAS_STATIC_OPTIMIZATION,
|
||||
MATTE_PHOTOS,
|
||||
SHOW_SIDEBAR_CAMERAS_FIRST,
|
||||
} from '@/app/config';
|
||||
import { getGitHubMetaForCurrentApp, getSignificantInsights } from '.';
|
||||
import { getOutdatedPhotosCount } from '@/photo/db/query';
|
||||
@ -27,6 +28,7 @@ export default async function AdminAppInsights() {
|
||||
{ count: photosCountPortrait },
|
||||
tags,
|
||||
cameras,
|
||||
recipes,
|
||||
filmSimulations,
|
||||
focalLengths,
|
||||
codeMeta,
|
||||
@ -37,6 +39,7 @@ export default async function AdminAppInsights() {
|
||||
getPhotosMeta({ maximumAspectRatio: 0.9 }),
|
||||
getUniqueTags(),
|
||||
getUniqueCameras(),
|
||||
getUniqueRecipes(),
|
||||
getUniqueFilmSimulations(),
|
||||
getUniqueFocalLengths(),
|
||||
getGitHubMetaForCurrentApp(),
|
||||
@ -67,7 +70,7 @@ export default async function AdminAppInsights() {
|
||||
photoMatting: photosCountPortrait > 0 && !MATTE_PHOTOS,
|
||||
camerasFirst: (
|
||||
tags.length > TAG_COUNT_THRESHOLD &&
|
||||
!SHOW_SIDEBAR_CAMERAS_FIRST
|
||||
CATEGORY_VISIBILITY[0] !== 'cameras'
|
||||
),
|
||||
gridFirst: (
|
||||
photosCount >= BASIC_PHOTO_INSTALLATION_COUNT &&
|
||||
@ -81,6 +84,7 @@ export default async function AdminAppInsights() {
|
||||
photosCountOutdated,
|
||||
tagsCount: tags.length,
|
||||
camerasCount: cameras.length,
|
||||
recipesCount: recipes.length,
|
||||
filmSimulationsCount: filmSimulations.length,
|
||||
focalLengthsCount: focalLengths.length,
|
||||
dateRange,
|
||||
|
||||
@ -11,7 +11,7 @@ import { HiMiniArrowsUpDown } from 'react-icons/hi2';
|
||||
import { HiOutlinePhotograph } from 'react-icons/hi';
|
||||
import { MdAspectRatio } from 'react-icons/md';
|
||||
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 {
|
||||
TEMPLATE_REPO_BRANCH,
|
||||
@ -21,6 +21,7 @@ import {
|
||||
VERCEL_GIT_COMMIT_MESSAGE,
|
||||
TEMPLATE_REPO_URL_FORK,
|
||||
TEMPLATE_REPO_URL_README,
|
||||
CATEGORY_VISIBILITY,
|
||||
} from '@/app/config';
|
||||
import {
|
||||
AdminAppInsights,
|
||||
@ -89,6 +90,7 @@ export default function AdminAppInsightsClient({
|
||||
photosCountOutdated,
|
||||
tagsCount,
|
||||
camerasCount,
|
||||
recipesCount,
|
||||
filmSimulationsCount,
|
||||
focalLengthsCount,
|
||||
dateRange,
|
||||
@ -134,10 +136,10 @@ export default function AdminAppInsightsClient({
|
||||
className={TEXT_COLOR_WARNING}
|
||||
/>}
|
||||
content={<>
|
||||
Could not analyze source code
|
||||
<span>Could not analyze source code</span>
|
||||
<Tooltip
|
||||
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
|
||||
{' '}
|
||||
<EnvVar
|
||||
variable="SHOW_SIDEBAR_CAMERAS_FIRST"
|
||||
value="1"
|
||||
variable="NEXT_PUBLIC_CATEGORY_VISIBILITY"
|
||||
value="cameras, tags, recipes, films"
|
||||
trailingContent="."
|
||||
/>
|
||||
</>}
|
||||
@ -411,30 +413,52 @@ export default function AdminAppInsightsClient({
|
||||
{photosCountHidden > 0 && ` (${photosCountHidden} hidden)`}
|
||||
</>}
|
||||
/>
|
||||
<ScoreCardRow
|
||||
{CATEGORY_VISIBILITY.map(category => {
|
||||
switch (category) {
|
||||
case 'tags':
|
||||
return <ScoreCardRow
|
||||
key={category}
|
||||
icon={<FaTag
|
||||
size={12}
|
||||
className="translate-y-[3px]"
|
||||
/>}
|
||||
content={pluralize(tagsCount, 'tag')}
|
||||
/>
|
||||
<ScoreCardRow
|
||||
/>;
|
||||
case 'cameras':
|
||||
return <ScoreCardRow
|
||||
key={category}
|
||||
icon={<FaCamera
|
||||
size={13}
|
||||
className="translate-y-[2px]"
|
||||
/>}
|
||||
content={pluralize(camerasCount, 'camera')}
|
||||
/>
|
||||
{filmSimulationsCount > 0 &&
|
||||
<ScoreCardRow
|
||||
/>;
|
||||
case 'recipes':
|
||||
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">
|
||||
<PhotoFilmSimulationIcon
|
||||
className="shrink-0 translate-x-[-1px] translate-y-[-0.5px]"
|
||||
className="shrink-0 translate-x-[-2px] translate-y-[-0.5px]"
|
||||
height={18}
|
||||
/>
|
||||
</span>}
|
||||
content={pluralize(filmSimulationsCount, 'film simulation')}
|
||||
/>}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
<ScoreCardRow
|
||||
icon={<TbCone className="rotate-[270deg] translate-x-[-2px]" />}
|
||||
content={pluralize(focalLengthsCount, 'focal length')}
|
||||
|
||||
@ -50,6 +50,7 @@ export interface PhotoStats {
|
||||
photosCountOutdated: number
|
||||
tagsCount: number
|
||||
camerasCount: number
|
||||
recipesCount: number
|
||||
filmSimulationsCount: number
|
||||
focalLengthsCount: number
|
||||
dateRange?: PhotoDateRange
|
||||
|
||||
@ -1,33 +1,25 @@
|
||||
import CommandKClient, {
|
||||
CommandKSection,
|
||||
} from '@/components/cmdk/CommandKClient';
|
||||
import CommandKClient from '@/components/cmdk/CommandKClient';
|
||||
import {
|
||||
getPhotosMetaCached,
|
||||
getUniqueCamerasCached,
|
||||
getUniqueFilmSimulationsCached,
|
||||
getUniqueRecipesCached,
|
||||
getUniqueTagsCached,
|
||||
} from '@/photo/cache';
|
||||
import {
|
||||
pathForCamera,
|
||||
pathForFilmSimulation,
|
||||
pathForFocalLength,
|
||||
} from './paths';
|
||||
import { formatCameraText } from '@/camera';
|
||||
import { photoQuantityText } from '@/photo';
|
||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { ADMIN_DEBUG_TOOLS_ENABLED, SHOW_FILM_SIMULATIONS } from './config';
|
||||
import { labelForFilmSimulation } from '@/platforms/fujifilm/simulation';
|
||||
import {
|
||||
ADMIN_DEBUG_TOOLS_ENABLED,
|
||||
SHOW_FILM_SIMULATIONS,
|
||||
SHOW_RECIPES,
|
||||
} from './config';
|
||||
import { getUniqueFocalLengths } from '@/photo/db/query';
|
||||
import { formatFocalLength } from '@/focal';
|
||||
import { TbCone } from 'react-icons/tb';
|
||||
|
||||
export default async function CommandK() {
|
||||
const [
|
||||
count,
|
||||
tags,
|
||||
cameras,
|
||||
recipes,
|
||||
filmSimulations,
|
||||
focalLengths,
|
||||
] = await Promise.all([
|
||||
@ -36,56 +28,21 @@ export default async function CommandK() {
|
||||
.catch(() => 0),
|
||||
getUniqueTagsCached().catch(() => []),
|
||||
getUniqueCamerasCached().catch(() => []),
|
||||
SHOW_RECIPES
|
||||
? getUniqueRecipesCached().catch(() => [])
|
||||
: [],
|
||||
SHOW_FILM_SIMULATIONS
|
||||
? getUniqueFilmSimulationsCached().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
|
||||
tags={tags}
|
||||
serverSections={[
|
||||
SECTION_CAMERAS,
|
||||
SECTION_FILM,
|
||||
SECTION_FOCAL,
|
||||
]}
|
||||
cameras={cameras}
|
||||
simulations={filmSimulations}
|
||||
recipes={recipes}
|
||||
focalLengths={focalLengths}
|
||||
showDebugTools={ADMIN_DEBUG_TOOLS_ENABLED}
|
||||
footer={photoQuantityText(count, false)}
|
||||
/>;
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
AI_AUTO_GENERATED_FIELDS_DEFAULT,
|
||||
parseAiAutoGeneratedFieldsString,
|
||||
} from '@/photo/ai';
|
||||
import { getOrderedCategoriesFromString } from '@/photo/set';
|
||||
import type { StorageType } from '@/platforms/storage';
|
||||
import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
|
||||
|
||||
@ -207,6 +208,12 @@ export const MATTE_PHOTOS =
|
||||
|
||||
// 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 =
|
||||
process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1';
|
||||
export const SHOW_ZOOM_CONTROLS =
|
||||
@ -215,14 +222,8 @@ export const SHOW_TAKEN_AT_TIME =
|
||||
process.env.NEXT_PUBLIC_HIDE_TAKEN_AT_TIME !== '1';
|
||||
export const SHOW_SOCIAL =
|
||||
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 =
|
||||
process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
|
||||
export const SHOW_SIDEBAR_CAMERAS_FIRST =
|
||||
process.env.NEXT_PUBLIC_CAMERAS_FIRST === '1';
|
||||
|
||||
// GRID
|
||||
|
||||
@ -304,22 +305,22 @@ export const APP_CONFIGURATION = {
|
||||
// eslint-disable-next-line max-len
|
||||
arePhotoCategoryOgImagesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES,
|
||||
areOriginalUploadsPreserved: PRESERVE_ORIGINAL_UPLOADS,
|
||||
imageQuality: IMAGE_QUALITY,
|
||||
hasImageQuality: Boolean(process.env.NEXT_PUBLIC_IMAGE_QUALITY),
|
||||
imageQuality: IMAGE_QUALITY,
|
||||
isBlurEnabled: BLUR_ENABLED,
|
||||
// Visual
|
||||
hasDefaultTheme: Boolean(process.env.NEXT_PUBLIC_DEFAULT_THEME),
|
||||
defaultTheme: DEFAULT_THEME,
|
||||
arePhotosMatted: MATTE_PHOTOS,
|
||||
// Display
|
||||
hasCategoryVisibility:
|
||||
Boolean(process.env.NEXT_PUBLIC_CATEGORY_VISIBILITY),
|
||||
categoryVisibility: CATEGORY_VISIBILITY,
|
||||
showExifInfo: SHOW_EXIF_DATA,
|
||||
showZoomControls: SHOW_ZOOM_CONTROLS,
|
||||
showTakenAtTimeHidden: SHOW_TAKEN_AT_TIME,
|
||||
showSocial: SHOW_SOCIAL,
|
||||
showFilmSimulations: SHOW_FILM_SIMULATIONS,
|
||||
showRecipes: SHOW_RECIPES,
|
||||
showRepoLink: SHOW_REPO_LINK,
|
||||
showSidebarCamerasFirst: SHOW_SIDEBAR_CAMERAS_FIRST,
|
||||
// Grid
|
||||
isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED,
|
||||
gridAspectRatio: GRID_ASPECT_RATIO,
|
||||
|
||||
@ -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 { Camera } from '@/camera';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { absolutePathForCamera } from '@/app/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import { PhotoSetAttributes } from '../photo/set';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import CameraOGTile from './CameraOGTile';
|
||||
import { Camera } from '.';
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { blobToImage } from '@/utility/blob';
|
||||
import { useRef, RefObject } from 'react';
|
||||
import { CopyExif } from '@/lib/CopyExif';
|
||||
import { CopyExif } from '@/utility/exif';
|
||||
import exifr from 'exifr';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';
|
||||
|
||||
@ -23,6 +23,9 @@ import {
|
||||
PATH_GRID_INFERRED,
|
||||
PATH_ROOT,
|
||||
PATH_SIGN_IN,
|
||||
pathForCamera,
|
||||
pathForFilmSimulation,
|
||||
pathForFocalLength,
|
||||
pathForPhoto,
|
||||
pathForTag,
|
||||
} from '../../app/paths';
|
||||
@ -40,19 +43,25 @@ import { RiToolsFill } from 'react-icons/ri';
|
||||
import { BiLockAlt, BiSolidUser } from 'react-icons/bi';
|
||||
import { HiDocumentText } from 'react-icons/hi';
|
||||
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 PhotoDate from '@/photo/PhotoDate';
|
||||
import PhotoSmall from '@/photo/PhotoSmall';
|
||||
import { FaCheck } from 'react-icons/fa6';
|
||||
import { Tags, addHiddenToTags, formatTag } from '@/tag';
|
||||
import { addHiddenToTags, formatTag } from '@/tag';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||
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 * as VisuallyHidden from '@radix-ui/react-visually-hidden';
|
||||
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_DESCRIPTION = 'For searching photos, views, and settings';
|
||||
@ -70,7 +79,7 @@ type CommandKItem = {
|
||||
action?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
export type CommandKSection = {
|
||||
type CommandKSection = {
|
||||
heading: string
|
||||
accessory?: ReactNode
|
||||
items: CommandKItem[]
|
||||
@ -88,15 +97,16 @@ const renderToggle = (
|
||||
|
||||
export default function CommandKClient({
|
||||
tags,
|
||||
serverSections = [],
|
||||
cameras,
|
||||
recipes,
|
||||
simulations,
|
||||
focalLengths,
|
||||
showDebugTools,
|
||||
footer,
|
||||
}: {
|
||||
tags: Tags
|
||||
serverSections?: CommandKSection[]
|
||||
showDebugTools?: boolean
|
||||
footer?: string
|
||||
}) {
|
||||
} & PhotoSetCategories) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
@ -234,7 +244,11 @@ export default function CommandKClient({
|
||||
addHiddenToTags(tags, photosCountHidden)
|
||||
, [tags, photosCountHidden]);
|
||||
|
||||
const SECTION_TAGS: CommandKSection = {
|
||||
const categorySections: CommandKSection[] = useMemo(() =>
|
||||
CATEGORY_VISIBILITY
|
||||
.map(category => {
|
||||
switch (category) {
|
||||
case 'tags': return {
|
||||
heading: 'Tags',
|
||||
accessory: <FaTag
|
||||
size={10}
|
||||
@ -247,6 +261,56 @@ export default function CommandKClient({
|
||||
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[] = [{
|
||||
heading: 'Theme',
|
||||
@ -465,8 +529,7 @@ export default function CommandKClient({
|
||||
{isLoading ? 'Searching ...' : 'No results found'}
|
||||
</Command.Empty>
|
||||
{queriedSections
|
||||
.concat(SECTION_TAGS)
|
||||
.concat(serverSections)
|
||||
.concat(categorySections)
|
||||
.concat(sectionPages)
|
||||
.concat(adminSection)
|
||||
.concat(clientSections)
|
||||
|
||||
@ -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 (
|
||||
<Tooltip.Provider delayDuration={100}>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { absolutePathForFocalLength } from '@/app/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import { PhotoSetAttributes } from '../photo/set';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import FocalLengthOGTile from './FocalLengthOGTile';
|
||||
import { shareTextFocalLength } from '.';
|
||||
|
||||
@ -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);
|
||||
});
|
||||
@ -11,7 +11,8 @@ import {
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import { getPhotosCachedAction, getPhotosAction } from '@/photo/actions';
|
||||
import { Photo, PhotoSetCategory } from '.';
|
||||
import { Photo } from '.';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { GetPhotosOptions } from './db';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { Photo, PhotoDateRange, PhotoSetCategory } from '.';
|
||||
import { Photo, PhotoDateRange } from '.';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import PhotoLarge from './PhotoLarge';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import PhotoGrid from './PhotoGrid';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Photo, PhotoSetCategory } from '.';
|
||||
import { Photo } from '.';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import PhotoMedium from './PhotoMedium';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
|
||||
@ -15,7 +15,7 @@ import FavsTag from '../tag/FavsTag';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { useMemo } from 'react';
|
||||
import HiddenTag from '@/tag/HiddenTag';
|
||||
import { SHOW_SIDEBAR_CAMERAS_FIRST, SITE_ABOUT } from '@/app/config';
|
||||
import { CATEGORY_VISIBILITY, SITE_ABOUT } from '@/app/config';
|
||||
import {
|
||||
htmlHasBrParagraphBreaks,
|
||||
safelyParseFormattedHtml,
|
||||
@ -184,11 +184,14 @@ export default function PhotoGridSidebar({
|
||||
}}
|
||||
/>]}
|
||||
/>}
|
||||
{SHOW_SIDEBAR_CAMERAS_FIRST
|
||||
? <>{camerasContent}{tagsContent}</>
|
||||
: <>{tagsContent}{camerasContent}</>}
|
||||
{recipesContent}
|
||||
{filmsContent}
|
||||
{CATEGORY_VISIBILITY.map(category => {
|
||||
switch (category) {
|
||||
case 'cameras': return camerasContent;
|
||||
case 'tags': return tagsContent;
|
||||
case 'recipes': return recipesContent;
|
||||
case 'films': return filmsContent;
|
||||
}
|
||||
})}
|
||||
{photoStatsContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -4,10 +4,10 @@ import { clsx } from 'clsx/lite';
|
||||
import {
|
||||
Photo,
|
||||
PhotoDateRange,
|
||||
PhotoSetCategory,
|
||||
dateRangeForPhotos,
|
||||
titleForPhoto,
|
||||
} from '.';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import ShareButton from '@/share/ShareButton';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { Photo, PhotoSetCategory } from '.';
|
||||
import { Photo } from '.';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import PhotoGrid from './PhotoGrid';
|
||||
import Link from 'next/link';
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
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 { AnimationConfig } from '../components/AnimateItems';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
|
||||
import {
|
||||
Photo,
|
||||
PhotoSetCategory,
|
||||
altTextForPhoto,
|
||||
doesPhotoNeedBlurCompatibility,
|
||||
} from '.';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import ImageMedium from '@/components/image/ImageMedium';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { pathForPhoto } from '@/app/paths';
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import {
|
||||
Photo,
|
||||
PhotoSetCategory,
|
||||
descriptionForPhoto,
|
||||
titleForPhoto,
|
||||
} from '@/photo';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import { absolutePathForPhotoImage, pathForPhoto } from '@/app/paths';
|
||||
import OGTile from '@/components/OGTile';
|
||||
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
Photo,
|
||||
PhotoSetCategory,
|
||||
getNextPhoto,
|
||||
getPreviousPhoto,
|
||||
} from '@/photo';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import PhotoLink from './PhotoLink';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { pathForPhoto } from '@/app/paths';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import PhotoOGTile from '@/photo/PhotoOGTile';
|
||||
import { absolutePathForPhoto } from '@/app/paths';
|
||||
import { Photo, PhotoSetCategory } from '.';
|
||||
import { Photo } from '.';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
|
||||
export default function PhotoShareModal(
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import {
|
||||
Photo,
|
||||
PhotoSetCategory,
|
||||
altTextForPhoto,
|
||||
doesPhotoNeedBlurCompatibility,
|
||||
} from '.';
|
||||
import { PhotoSetCategory } from './set';
|
||||
import ImageSmall from '@/components/image/ImageSmall';
|
||||
import Link from 'next/link';
|
||||
import { clsx } from 'clsx/lite';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
getUniqueCamerasCached,
|
||||
getUniqueFilmSimulationsCached,
|
||||
getUniqueRecipesCached,
|
||||
getUniqueTagsCached,
|
||||
} from '@/photo/cache';
|
||||
import {
|
||||
@ -27,4 +28,5 @@ export const getPhotoSidebarDataCached = () => [
|
||||
getUniqueTagsCached().then(sortTagsObject),
|
||||
getUniqueCamerasCached(),
|
||||
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
|
||||
SHOW_RECIPES ? getUniqueRecipesCached() : [],
|
||||
] as const;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PRIORITY_ORDER_ENABLED } from '@/app/config';
|
||||
import { parameterize } from '@/utility/string';
|
||||
import { PhotoSetCategory } from '..';
|
||||
import { PhotoSetCategory } from '../set';
|
||||
import { Camera } from '@/camera';
|
||||
|
||||
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { Camera } from '@/camera';
|
||||
import { formatFocalLength } from '@/focal';
|
||||
import { Lens } from '@/lens';
|
||||
import { getNextImageUrlForRequest } from '@/platforms/next-image';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import {
|
||||
@ -109,21 +107,6 @@ export interface Photo extends Omit<PhotoDb, 'recipeData'> {
|
||||
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 => {
|
||||
const photoDb = camelcaseKeys(
|
||||
photoDbRaw as unknown as Record<string, unknown>,
|
||||
|
||||
64
src/photo/set.ts
Normal file
64
src/photo/set.ts
Normal 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;
|
||||
@ -1,5 +1,5 @@
|
||||
import { absolutePathForRecipe } from '@/app/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import { PhotoSetAttributes } from '../photo/set';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import { shareTextForRecipe } from '.';
|
||||
import RecipeOGTile from './RecipeOGTile';
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Photo, PhotoSetAttributes, PhotoSetCategory } from '@/photo';
|
||||
import { Photo } from '@/photo';
|
||||
import { PhotoSetAttributes, PhotoSetCategory } from '@/photo/set';
|
||||
import {
|
||||
absolutePathForCameraImage,
|
||||
absolutePathForFilmSimulationImage,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { absolutePathForFilmSimulation } from '@/app/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import { PhotoSetAttributes } from '../photo/set';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import FilmSimulationOGTile from './FilmSimulationOGTile';
|
||||
import { FilmSimulation, shareTextForFilmSimulation } from '.';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { absolutePathForTag } from '@/app/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import { PhotoSetAttributes } from '../photo/set';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import TagOGTile from './TagOGTile';
|
||||
import { shareTextForTag } from '.';
|
||||
|
||||
@ -86,3 +86,41 @@ export const formatExposureCompensation = (exposureCompensation?: number) => {
|
||||
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 });
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user