darktable-inspired reskin + metadata/collections, map, easy mode, select/export

UI overhaul to a darktable tone-and-manner and a set of features adapted from
darktable's proven patterns (reimplemented in our Electron/TS stack; no GPL code).

Design reskin:
- Dark neutral-gray palette + amber accent, flat/squared corners, no card shadows,
  compact darktable-style top bar (logo + pipe-separated view tabs), denser 15px base
- Done via design tokens (Tailwind slate/brand/radius/shadow remap) — minimal churn

Metadata & collections (Phase A/B):
- exifr now captures GPS + camera; asset table ALTER-migrated (gpsLat/gpsLon/camera,
  metaVersion backfill on re-index)
- Collection facet bar (year timeline / camera / color-label) filters the grid

Map & relation finder (Phase C):
- Leaflet + online OSM map tab; geotagged photos as markers
- relationService: related photos by place (GPS<1km) + time (+/-2d) + CLIP similarity

Easy mode (Phase D):
- easyMode setting (menu / onboarding); scales the whole UI (rem) + bigger thumbnails
  + large icon nav with plain labels (4050 accessibility)

Library usability:
- Video thumbnails (representative frame capture in the inference worker)
- Media filter (All / Photos / Videos) to separate them
- Clearer culling labels ("Good shots" / "To cull") + explanation tooltip
- Multi-select tiles -> Export selected to a folder (copy, best-cut extraction) and
  Delete to Recycle Bin (shell.trashItem) behind a confirm dialog
- ONNX Runtime wasm bundled locally (offline) via copy-ort-wasm + asarUnpack

Docs: DARKTABLE_REVIEW (feasibility + roadmap A->D). All typecheck/tests/build green;
boot smoke verified each phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 19:22:19 +09:00
parent 72c41ae834
commit 3e73967c7b
33 changed files with 1670 additions and 96 deletions
+84 -4
View File
@@ -33,6 +33,9 @@ export const MESSAGES: Table = {
'onboard.theme': { ko: '테마', en: 'Theme' },
'onboard.dark': { ko: '다크', en: 'Dark' },
'onboard.light': { ko: '라이트', en: 'Light' },
'onboard.easy': { ko: '화면 크기', en: 'Display size' },
'onboard.easyOn': { ko: '크게 (쉬운 모드)', en: 'Large (Easy)' },
'onboard.easyOff': { ko: '보통', en: 'Normal' },
'onboard.start': { ko: '시작하기', en: 'Get Started' },
// 1. 프로필
@@ -145,6 +148,52 @@ export const MESSAGES: Table = {
'nav.organize': { ko: '정리', en: 'Organize' },
'nav.library': { ko: '라이브러리', en: 'Library' },
'nav.search': { ko: '검색', en: 'Search' },
'nav.groups': { ko: '그룹·정화', en: 'Groups' },
'nav.map': { ko: '지도', en: 'Map' },
// 쉬운 모드 대형 네비(구어체)
'easynav.organize': { ko: '사진 정리', en: 'Organize' },
'easynav.library': { ko: '내 사진', en: 'My Photos' },
'easynav.search': { ko: '사진 찾기', en: 'Find' },
'easynav.map': { ko: '지도로 보기', en: 'Map' },
'easynav.groups': { ko: '중복 정리', en: 'Cleanup' },
// 지도 / 연관 탐색 (Phase C)
'map.section': { ko: '지도 (촬영 장소)', en: 'Map (photo locations)' },
'map.count': { ko: 'GPS 사진 {n}장', en: '{n} geotagged' },
'map.empty': {
ko: 'GPS 정보가 있는 사진이 없습니다. (라이브러리에서 재색인하면 GPS가 채워집니다)',
en: 'No geotagged photos yet. (Re-index in Library to populate GPS)'
},
'map.related': { ko: '연관 사진', en: 'Related photos' },
'map.relatedHint': {
ko: '지도의 사진을 클릭하면 같은 장소·시기·비슷한 장면의 사진을 모아 보여줍니다.',
en: 'Click a photo on the map to see ones from the same place, time, and similar scenes.'
},
'map.selectHint': { ko: '지도에서 사진을 클릭하세요.', en: 'Click a photo on the map.' },
'map.loading': { ko: '불러오는 중…', en: 'Loading…' },
// 그룹화 / 자가정화 (Phase 3)
'groups.section': { ko: '유사 사진 그룹 · 자가정화', en: 'Similar groups · Cleanup' },
'groups.hint': {
ko: '검색 색인(임베딩)을 기반으로 비슷한 사진을 묶습니다. 각 그룹의 보관 추천(가장 선명)만 남기고 나머지를 휴지통으로 보낼 수 있습니다.',
en: 'Groups similar photos using the search embeddings. Keep the recommended (sharpest) one per group and send the rest to the trash.'
},
'groups.threshold': { ko: '유사도 ({v})', en: 'Similarity ({v})' },
'groups.find': { ko: '그룹 찾기', en: 'Find groups' },
'groups.finding': { ko: '찾는 중…', en: 'Finding…' },
'groups.empty': {
ko: '유사 그룹이 없습니다. (먼저 검색 탭에서 임베딩 색인을 생성하세요)',
en: 'No similar groups. (Build the embedding index in the Search tab first)'
},
'groups.count': { ko: '{n}개 그룹', en: '{n} groups' },
'groups.keep': { ko: '추천 보관', en: 'Keep' },
'groups.groupSize': { ko: '유사 {n}장', en: '{n} similar' },
'groups.trashSelected': { ko: '선택 {n}개 휴지통으로', en: 'Trash {n} selected' },
'groups.confirmTrash': {
ko: '선택한 {n}개 사진을 휴지통(복구 가능)으로 보냅니다. 계속할까요?',
en: 'Move {n} selected photos to the trash (recoverable)? Continue?'
},
'groups.trashed': { ko: '{n}개를 휴지통으로 이동했습니다.', en: 'Moved {n} to trash.' },
// 검색 (Phase 2)
'search.section': { ko: '검색 색인', en: 'Search index' },
@@ -191,11 +240,37 @@ export const MESSAGES: Table = {
},
'lib.loadMore': { ko: '더 보기', en: 'Load more' },
// 선택 / 내보내기 / 삭제 (Library)
'sel.count': { ko: '{n}개 선택', en: '{n} selected' },
'sel.selectAll': { ko: '전체 선택', en: 'Select all' },
'sel.clear': { ko: '선택 해제', en: 'Clear' },
'sel.export': { ko: '폴더로 내보내기', en: 'Export to folder' },
'sel.delete': { ko: '삭제', en: 'Delete' },
'sel.hint': { ko: '사진을 클릭해 선택하세요.', en: 'Click photos to select.' },
'sel.confirmDelete': {
ko: '선택한 {n}개를 휴지통으로 보냅니다.\n(원본 파일이 휴지통으로 이동되며 복구할 수 있습니다)\n계속할까요?',
en: 'Move {n} selected items to the Recycle Bin?\n(Originals are moved to the trash and can be restored.)\nContinue?'
},
'sel.deleted': { ko: '{n}개를 휴지통으로 이동했습니다.', en: 'Moved {n} to the trash.' },
'sel.exported': {
ko: '{n}개를 내보냈습니다.\n{dest}',
en: 'Exported {n} items.\n{dest}'
},
// 미디어 종류 (사진/영상 분리)
'media.all': { ko: '전체', en: 'All' },
'media.image': { ko: '사진', en: 'Photos' },
'media.video': { ko: '영상', en: 'Videos' },
// 컬링 필터 / 품질 플래그 (Phase 1)
'cull.all': { ko: '전체', en: 'All' },
'cull.candidate': { ko: '고품질 후보', en: 'Candidates' },
'cull.rejected': { ko: '제외 후보', en: 'Rejected' },
'flag.candidate': { ko: '후보', en: 'Keep' },
'cull.all': { ko: '품질 전체', en: 'All' },
'cull.candidate': { ko: '잘 나온 사진', en: 'Good shots' },
'cull.rejected': { ko: '걸러낼 사진', en: 'To cull' },
'cull.help': {
ko: '흐리거나 눈 감은·노출 나쁜 사진을 자동으로 표시해, 잘 나온 사진만 빠르게 고를 수 있게 도와줍니다.',
en: 'Auto-flags blurry / closed-eye / badly-exposed shots so you can quickly keep the good ones.'
},
'flag.candidate': { ko: '좋음', en: 'Good' },
'flag.blurry': { ko: '흐림', en: 'Blurry' },
'flag.eyesClosed': { ko: '눈감음', en: 'Eyes closed' },
'flag.badExposure': { ko: '노출', en: 'Exposure' },
@@ -209,6 +284,10 @@ export const MESSAGES: Table = {
},
'cull.reset': { ko: '기본값', en: 'Reset' },
'cull.ratingMin': { ko: '별점', en: 'Rating' },
// 컬렉션 패싯 (Phase B)
'col.year': { ko: '연도', en: 'Year' },
'col.camera': { ko: '카메라', en: 'Camera' },
'col.label': { ko: '색라벨', en: 'Label' },
// 메뉴
'menu.file': { ko: '파일', en: 'File' },
@@ -235,6 +314,7 @@ export const MESSAGES: Table = {
'menu.theme.dark': { ko: '다크 모드', en: 'Dark' },
'menu.theme.light': { ko: '라이트 모드', en: 'Light' },
'menu.language': { ko: '언어', en: 'Language' },
'menu.easyMode': { ko: '쉬운 모드 (큰 화면)', en: 'Easy mode (large)' },
'menu.guide': { ko: '사용 방법 가이드', en: 'User Guide' },
'menu.about': { ko: '정보', en: 'About' }
}