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) } } }