import { useState } from 'react' import { useT } from '../i18n' import { thumbUrl, baseName } from '../media' import type { AssetGroup, IndexedAsset } from '@shared/types' const VIDEO_EXTS = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v'] /** 스마트 그룹화 + 자가정화 (Phase 3) */ export function GroupsView(): JSX.Element { const t = useT() const [threshold, setThreshold] = useState(0.92) const [groups, setGroups] = useState([]) const [finding, setFinding] = useState(false) const [searched, setSearched] = useState(false) const [selected, setSelected] = useState>(new Set()) const [trashing, setTrashing] = useState(false) const find = async () => { setFinding(true) setSearched(true) try { const g = await window.api.groups.build(threshold) setGroups(g) // 기본 선택: 각 그룹에서 보관 추천을 제외한 나머지(중복 후보) const pre = new Set() for (const grp of g) { for (const m of grp.members) { if (m.id != null && m.id !== grp.bestId) pre.add(m.id) } } setSelected(pre) } finally { setFinding(false) } } const toggle = (id: number) => { setSelected((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const trash = async () => { if (selected.size === 0) return if (!window.confirm(t('groups.confirmTrash', { n: selected.size }))) return setTrashing(true) try { const ids = [...selected] const n = await window.api.groups.trash(ids) // 휴지통으로 보낸 항목 제거 const removed = new Set(ids) setGroups((prev) => prev .map((g) => ({ ...g, members: g.members.filter((m) => m.id == null || !removed.has(m.id)) })) .filter((g) => g.members.length > 1) ) setSelected(new Set()) window.alert(t('groups.trashed', { n })) } finally { setTrashing(false) } } return (

{t('groups.section')}

{searched && !finding && ( {t('groups.count', { n: groups.length })} )}

{t('groups.hint')}

{selected.size > 0 && ( )}
{searched && !finding && groups.length === 0 && (

{t('groups.empty')}

)} {groups.map((g, gi) => (
{t('groups.groupSize', { n: g.members.length })}
{g.members.map((m) => ( m.id != null && toggle(m.id)} /> ))}
))}
) } function GroupTile(props: { asset: IndexedAsset isBest: boolean checked: boolean keepLabel: string onToggle: () => void }): JSX.Element { const { asset: a, isBest, checked } = props const isVideo = VIDEO_EXTS.includes(a.ext) return (
{isVideo ? (
) : ( {baseName(a.path)} )} {isBest ? ( ★ {props.keepLabel} ) : ( )}
) }