Add video sorting, reference thumbnails, theme/i18n, menu, DnD/paste, presets
Feature work on top of the initial organizer: - Videos: .mp4/.mov/.avi/.mkv/.webm/.m4v sorted into output/Movie/YYYY/MM - Profiles: reference-image thumbnail cards via a secure photoai-media:// protocol (serves only registered reference images); per-thumbnail delete; add via click, drag & drop, or paste (Ctrl+V) using webUtils.getPathForFile + a byte-based addReferenceData path for clipboard images - Presets: local person library (max 5) saved to userData; one-click apply into active profiles; reusing stored descriptors (no recompute) - Theme: dark/light with dark default (Tailwind class strategy) - i18n: ko/en table-based localization; first-run onboarding to pick language + theme; English-neutral "Unsorted" folder (was [미정]) - App menu: File/Edit/View/Window/Help, localized; Help opens a detailed, theme-aware user guide window - UI: History block scrolls internally (no whole-window scroll); threshold/concurrency tooltips; generic example name - Settings persisted to userData/settings.json; menu + renderer kept in sync - Docs: NextGen Photo AI feasibility review + Phase 0/1 roadmap All typecheck/tests (12) /build green; boot smoke verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { protocol } from 'electron'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { extname } from 'node:path'
|
||||
import { MEDIA_SCHEME } from '@shared/constants'
|
||||
import { profileStore } from './profileStore'
|
||||
import { presetStore } from './presetStore'
|
||||
import { logger } from './logger'
|
||||
|
||||
const MIME: Record<string, string> = {
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.webp': 'image/webp'
|
||||
}
|
||||
|
||||
/**
|
||||
* 로컬 참조 이미지를 UI 렌더러에 안전하게 표시하기 위한 커스텀 프로토콜.
|
||||
*
|
||||
* URL 형식: photoai-media://img/?p=<encodeURIComponent(절대경로)>
|
||||
*
|
||||
* 보안: 렌더러가 임의 파일을 읽지 못하도록, **현재 등록된 참조 이미지 경로**에
|
||||
* 한해서만 파일을 제공한다. (그 외 경로는 403)
|
||||
*/
|
||||
|
||||
/** app.whenReady 이전에 호출해야 함 — 스킴을 권한 있는 표준 스킴으로 등록 */
|
||||
export function registerMediaScheme(): void {
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: MEDIA_SCHEME,
|
||||
privileges: {
|
||||
standard: true,
|
||||
secure: true,
|
||||
supportFetchAPI: true,
|
||||
stream: true
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
/** app.whenReady 이후에 호출 — 실제 요청 핸들러 등록 */
|
||||
export function handleMediaProtocol(): void {
|
||||
protocol.handle(MEDIA_SCHEME, async (request) => {
|
||||
try {
|
||||
// request.url = photoai-media://img/?p=<encodeURIComponent(절대경로)>
|
||||
// searchParams의 자동 디코딩과의 이중 디코딩을 피하려고 raw 문자열을 직접 파싱한다.
|
||||
const marker = 'p='
|
||||
const i = request.url.indexOf(marker)
|
||||
if (i < 0) return new Response('missing path', { status: 400 })
|
||||
const filePath = decodeURIComponent(request.url.slice(i + marker.length))
|
||||
|
||||
// 등록된 참조 이미지(활성 프로필 또는 프리셋)에 한해 제공 — 임의 파일 읽기 차단
|
||||
const allowed =
|
||||
(await profileStore.isReferenceImage(filePath)) ||
|
||||
(await presetStore.isReferenceImage(filePath))
|
||||
if (!allowed) return new Response('forbidden', { status: 403 })
|
||||
|
||||
// net.fetch(file://) 대신 fs로 직접 읽어 바이트 반환 → 한글/공백 경로에도 안전
|
||||
const data = await readFile(filePath)
|
||||
const mime = MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream'
|
||||
return new Response(new Uint8Array(data), {
|
||||
status: 200,
|
||||
headers: { 'content-type': mime, 'cache-control': 'no-cache' }
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error('media 프로토콜 처리 실패', { message: (err as Error).message })
|
||||
return new Response('not found', { status: 404 })
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user