Files
photochk/Sources/Photochk/Views/GroupListView.swift
T
yfoxjpandClaude Opus 5 c7f68b6cca Photochk 1.0.0 — 類似写真を探して整理する macOS アプリ
フォルダ内の写真から、中身が同じものと見た目が似ているものを検出して
グループにまとめ、残す1枚を選んでほかをゴミ箱へ送れるようにする。

検出は安い判定から順に4段:
- ファイルサイズが同じものだけ SHA-256 でバイト一致を確定
- dHash(知覚ハッシュ)のハミング距離で候補ペアを抽出
- 候補に出てくる画像だけ Vision の feature print で精査
- Union-Find で連結成分をグループ化

判定のしきい値は実写画像で距離を実測して決めた(docs/calibration.md)。
tools/release-update.sh で Gitea のリリースを更新する。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 08:02:14 +09:00

115 lines
3.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct GroupListView: View {
@Bindable var model: ScanViewModel
var body: some View {
List(selection: $model.selectedGroupID) {
if !model.groups.isEmpty {
Section {
ForEach(model.groups) { group in
GroupRow(group: group, markedCount: markedCount(in: group))
.tag(group.id)
}
} header: {
Text("\(model.groups.count) グループ・重複 \(model.totalDuplicateCount) 枚")
}
}
}
.listStyle(.sidebar)
.overlay {
if model.groups.isEmpty && !model.isScanning {
sidebarPlaceholder
}
}
.safeAreaInset(edge: .bottom) {
if !model.groups.isEmpty {
summaryBar
}
}
}
private func markedCount(in group: SimilarityGroup) -> Int {
group.members.filter { model.markedForDeletion.contains($0.id) }.count
}
private var sidebarPlaceholder: some View {
VStack(spacing: 6) {
if model.folder == nil {
Text("フォルダが未選択です")
} else if model.hasScanned {
Text("似た写真は見つかりませんでした")
} else {
Text("スキャンしていません")
}
}
.font(.callout)
.foregroundStyle(.secondary)
.padding()
}
private var summaryBar: some View {
VStack(alignment: .leading, spacing: 2) {
Divider()
Text("最大 \(model.totalReclaimableText) 削減できます")
.font(.callout)
if model.markedCount > 0 {
Text("\(model.markedCount) 枚を選択中(\(model.markedBytesText))")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.padding(.bottom, 8)
.background(.bar)
}
}
struct GroupRow: View {
let group: SimilarityGroup
let markedCount: Int
var body: some View {
HStack(spacing: 10) {
ThumbnailView(url: group.representativeID, size: 52)
VStack(alignment: .leading, spacing: 3) {
Text(group.representative.fileName)
.lineLimit(1)
.truncationMode(.middle)
HStack(spacing: 6) {
Text(group.kindText)
.font(.caption2)
.padding(.horizontal, 5)
.padding(.vertical, 1)
.background(
group.isAllExact ? Color.orange.opacity(0.22) : Color.accentColor.opacity(0.18),
in: Capsule()
)
Text("\(group.members.count) 枚")
.font(.caption)
.foregroundStyle(.secondary)
}
Text("\(group.reclaimableBytesText) 削減可")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
if markedCount > 0 {
Text("\(markedCount)")
.font(.caption.monospacedDigit())
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.red, in: Capsule())
}
}
.padding(.vertical, 3)
}
}