Files
photochk/Sources/Photochk/Views/ContentView.swift
T
2026-07-27 15:43:30 +00:00

146 lines
4.9 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 ContentView: View {
@Bindable var model: ScanViewModel
@State private var isConfirmingTrash = false
var body: some View {
NavigationSplitView {
GroupListView(model: model)
.navigationSplitViewColumnWidth(min: 270, ideal: 310, max: 420)
} detail: {
if let group = model.selectedGroup {
GroupDetailView(model: model, group: group)
} else {
placeholder
}
}
.toolbar { ScanToolbar(model: model, isConfirmingTrash: $isConfirmingTrash) }
.overlay {
if let progress = model.progress {
Color.black.opacity(0.12).ignoresSafeArea()
ScanProgressView(progress: progress) { model.cancelScan() }
}
}
.confirmationDialog(
"\(model.markedCount) 枚をゴミ箱へ移動します",
isPresented: $isConfirmingTrash,
titleVisibility: .visible
) {
Button("ゴミ箱へ移動(\(model.markedBytesText))", role: .destructive) {
Task { await model.moveMarkedToTrash() }
}
Button("キャンセル", role: .cancel) {}
} message: {
Text("削除ではなくゴミ箱への移動です。Finder のゴミ箱からいつでも元に戻せます。")
}
.alert(
"エラー",
isPresented: Binding(
get: { model.errorMessage != nil },
set: { if !$0 { model.errorMessage = nil } }
)
) {
Button("OK") { model.errorMessage = nil }
} message: {
Text(model.errorMessage ?? "")
}
}
@ViewBuilder
private var placeholder: some View {
if model.folder == nil {
ContentUnavailableView {
Label("フォルダを選んでください", systemImage: "photo.stack")
} description: {
Text("選んだフォルダの中から、内容が同じ写真とよく似た写真を探します。")
} actions: {
Button("フォルダを選ぶ…") { model.chooseFolder() }
.buttonStyle(.borderedProminent)
}
} else if model.hasScanned && model.groups.isEmpty {
ContentUnavailableView {
Label("似た写真は見つかりませんでした", systemImage: "checkmark.circle")
} description: {
Text("判定をゆるくすると、より広く似た写真を拾えます。")
}
} else {
ContentUnavailableView(
"グループを選択してください",
systemImage: "square.grid.2x2"
)
}
}
}
struct ScanToolbar: ToolbarContent {
@Bindable var model: ScanViewModel
@Binding var isConfirmingTrash: Bool
var body: some ToolbarContent {
ToolbarItem(placement: .navigation) {
Button {
model.chooseFolder()
} label: {
Label("フォルダを選ぶ", systemImage: "folder")
}
.help(model.folder?.path ?? "調べるフォルダを選びます")
}
ToolbarItem {
Picker("類似度", selection: $model.settings.sensitivity) {
ForEach(Sensitivity.allCases) { level in
Text(level.label).tag(level)
}
}
.pickerStyle(.segmented)
.frame(width: 220)
.help(model.settings.sensitivity.help)
.disabled(model.isScanning)
}
ToolbarItem {
Toggle(isOn: $model.settings.includesSubfolders) {
Label("サブフォルダも含む", systemImage: "folder.badge.plus")
}
.help("サブフォルダの中まで再帰的に調べます")
.disabled(model.isScanning)
}
ToolbarItem {
Menu {
ForEach(KeepRule.allCases) { rule in
Button(rule.label) { model.applyKeepRule(rule) }
}
Divider()
Button("選択をすべて解除") { model.clearMarks() }
} label: {
Label("一括選択", systemImage: "checklist")
}
.disabled(model.groups.isEmpty)
}
ToolbarItem {
Button {
isConfirmingTrash = true
} label: {
Label(
model.markedCount > 0 ? "\(model.markedCount) 枚をゴミ箱へ" : "ゴミ箱へ",
systemImage: "trash"
)
}
.disabled(model.markedCount == 0)
}
ToolbarItem {
Button {
model.rescan()
} label: {
Label("再スキャン", systemImage: "arrow.clockwise")
}
.disabled(model.folder == nil || model.isScanning)
}
}
}