From 65e22979060e03b22d556e69226dbff2e777c327 Mon Sep 17 00:00:00 2001 From: yoshikazu Date: Mon, 27 Jul 2026 15:43:08 +0000 Subject: [PATCH] Add group list view --- Sources/Photochk/Views/GroupListView.swift | 114 +++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 Sources/Photochk/Views/GroupListView.swift diff --git a/Sources/Photochk/Views/GroupListView.swift b/Sources/Photochk/Views/GroupListView.swift new file mode 100644 index 0000000..5cb51aa --- /dev/null +++ b/Sources/Photochk/Views/GroupListView.swift @@ -0,0 +1,114 @@ +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) + } +}