Caps-iOS/Caps/ContentView.swift

439 lines
15 KiB
Swift
Raw Normal View History

2022-06-10 21:20:49 +02:00
import SwiftUI
import SFSafeSymbols
import BottomSheet
struct ContentView: View {
private let bottomIconSize: CGFloat = 25
private let bottomIconPadding: CGFloat = 7
private let capturedImageSize: CGFloat = 80
private let plusIconSize: CGFloat = 20
private var scale: CGFloat {
UIScreen.main.scale
}
@EnvironmentObject
var database: Database
@State
var searchString = ""
@State
var sortType: SortCriteria = .id
@State
var sortAscending = false
@State
var showSortPopover = false
@State
var showCameraSheet = false
@State
var showSettingsSheet = false
@State
var showGridView = false
@State
var showNewClassifierAlert = false
2022-12-11 19:26:11 +01:00
@State
var showUpdateCapNameAlert = false
2023-03-13 11:07:22 +01:00
@State
var showDeleteCapAlert = false
2022-12-11 19:26:11 +01:00
@State
var updatedCapName = ""
2022-06-10 21:20:49 +02:00
@State
var isEnteringNewCapName = false
@State
private var selectedCapId: Int?
2023-02-19 00:38:52 +01:00
@State
var showImageOverviewForCap = false
@State
private var selectedCapToShowImages: Cap?
2022-06-10 21:20:49 +02:00
var filteredCaps: [Cap] {
2023-07-28 13:20:33 +02:00
let text = searchString.clean
2022-06-10 21:20:49 +02:00
guard text != "" else {
return Array(database.caps.values)
}
let textParts = text.components(separatedBy: " ").filter { $0 != "" }
return database.caps.values.compactMap { cap -> Cap? in
// For each part of text, check if name contains it
for textItem in textParts {
if !cap.cleanName.contains(textItem) {
return nil
}
}
return cap
}
}
var shownCaps: [Cap] {
let caps = filteredCaps
if sortAscending {
switch sortType {
case .id:
return caps.sorted { $0.id < $1.id }
case .count:
return caps.sorted {
$0.imageCount < $1.imageCount
}
case .name:
return caps.sorted {
$0.name < $1.name
}
case .match:
return caps.sorted {
match(for: $0.id) ?? 0 < match(for: $1.id) ?? 0
}
}
} else {
switch sortType {
case .id:
return caps.sorted { $0.id > $1.id }
case .count:
return caps.sorted {
$0.imageCount > $1.imageCount
}
case .name:
return caps.sorted {
$0.name > $1.name
}
case .match:
return caps.sorted {
match(for: $0.id) ?? 0 > match(for: $1.id) ?? 0
}
}
}
}
func match(for cap: Int) -> Float? {
database.matches[cap]
}
var body: some View {
NavigationView {
ZStack {
List(shownCaps) { cap in
CapRowView(cap: cap, match: match(for: cap.id))
.onTapGesture {
didTap(cap: cap)
}
2023-02-19 00:38:52 +01:00
.swipeActions(edge: .trailing) {
2022-12-11 19:26:11 +01:00
Button {
showRenameWindow(for: cap)
} label: {
Label("Rename", systemSymbol: .pencil)
}
.tint(.purple)
2023-03-13 11:07:22 +01:00
Button {
selectedCapId = cap.id
showDeleteCapAlert = true
2023-02-26 18:01:00 +01:00
} label: {
Label("Delete", systemSymbol: .trashCircleFill)
}
2023-03-13 11:07:22 +01:00
.tint(.red)
2022-12-11 19:26:11 +01:00
}
2023-02-19 00:38:52 +01:00
.swipeActions(edge: .leading) {
Button {
showAllImages(for: cap)
} label: {
Label("Images", systemSymbol: .photoStack)
}
2023-03-13 11:07:22 +01:00
.tint(.blue)
2023-02-19 00:38:52 +01:00
}
2022-06-10 21:20:49 +02:00
}
.refreshable {
refresh()
}
.navigationTitle("Caps")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: showSettings) {
Image(systemSymbol: .gearshape)
}
}
}
VStack(spacing: 0) {
Spacer()
if let image = database.image {
HStack(alignment: .bottom, spacing: 0) {
Spacer()
if isEnteringNewCapName {
Text(String(format: "ID: %d", database.nextCapId))
.font(.headline)
.padding(.vertical, 3)
.padding(.horizontal, 8)
.background(.regularMaterial)
.cornerRadius(5)
.padding(5)
}
Button(action: didTapClassifiedImage) {
ZStack(alignment: Alignment.bottomTrailing) {
Image(uiImage: image.resize(to: CGSize(width: capturedImageSize, height: capturedImageSize)))
.frame(width: capturedImageSize, height: capturedImageSize)
.background(.gray)
.cornerRadius(capturedImageSize/2)
.padding(5)
.background(.regularMaterial)
.cornerRadius(capturedImageSize/2 + 5)
.padding(5)
if !isEnteringNewCapName {
Image(systemSymbol: .plus)
.frame(width: plusIconSize, height: plusIconSize)
.padding(6)
.background(.regularMaterial)
.cornerRadius(plusIconSize/2 + 6)
.padding(5)
}
}
}
}
}
HStack(spacing: 0) {
if isEnteringNewCapName {
Button(action: removeCapturedImage) {
Image(systemSymbol: .xmark)
.resizable()
.frame(width: bottomIconSize-6,
height: bottomIconSize-6)
.padding()
.padding(3)
}
} else {
Button(action: filter) {
Image(systemSymbol: .line3HorizontalDecreaseCircle)
.resizable()
.frame(width: bottomIconSize,
height: bottomIconSize)
.padding()
}
}
if isEnteringNewCapName {
CapNameEntryView(name: $searchString)
.disableAutocorrection(true)
} else {
SearchField(searchString: $searchString)
.disableAutocorrection(true)
}
if isEnteringNewCapName {
Button(action: saveNewCap) {
Image(systemSymbol: .squareAndArrowDown)
.resizable()
.frame(width: bottomIconSize-3,
height: bottomIconSize)
.padding()
.padding(.horizontal, (bottomIconPadding-3)/2)
}
} else if database.image != nil {
Button(action: removeCapturedImage) {
Image(systemSymbol: .xmark)
.resizable()
.frame(width: bottomIconSize-6,
height: bottomIconSize-6)
.padding(3)
.padding(bottomIconPadding)
}
} else {
Button(action: openCamera) {
Image(systemSymbol: .camera)
.resizable()
.frame(width: bottomIconSize+bottomIconPadding,
height: bottomIconSize)
.padding()
}
}
}
.background(.regularMaterial)
}
}
}
.onAppear {
UIScrollView.appearance().keyboardDismissMode = .onDrag
}
.bottomSheet(isPresented: $showSortPopover, height: 280) {
SortSelectionView(
hasMatches: !database.matches.isEmpty,
isPresented: $showSortPopover,
sortType: $sortType,
sortAscending: $sortAscending,
showGridView: $showGridView)
}
.sheet(isPresented: $showCameraSheet) {
CameraView(isPresented: $showCameraSheet,
image: $database.image,
capId: $selectedCapId)
2022-06-10 21:20:49 +02:00
}
2022-06-11 11:27:56 +02:00
.sheet(isPresented: $showSettingsSheet) {
2022-06-10 21:20:49 +02:00
SettingsView(isPresented: $showSettingsSheet)
}
.sheet(isPresented: $showGridView) {
2022-12-11 19:25:58 +01:00
GridView(isPresented: $showGridView, database: database)
2023-02-19 00:38:52 +01:00
}
.bottomSheet(isPresented: $showImageOverviewForCap, height: 400) {
CapImagesView(cap: $selectedCapToShowImages, database: database, isPresented: $showImageOverviewForCap)
}
.alert(isPresented: $showNewClassifierAlert) {
2022-06-10 21:20:49 +02:00
Alert(title: Text("New classifier available"),
message: Text("Classifier \(database.serverClassifierVersion) is available. You have version \(database.classifierVersion). Do you want to download it now?"),
primaryButton: .default(Text("Download"), action: downloadClassifier),
secondaryButton: .cancel())
}
2023-03-13 11:07:22 +01:00
.alert(Text("Delete cap"),
isPresented: $showDeleteCapAlert,
actions: {
Button("Delete", role: .destructive, action: saveNewCapName)
Button("Cancel", role: .cancel, action: {})
}, message: {
Text("Confirm the deletion of cap \(selectedCapId ?? 0)")
})
2022-12-11 19:26:11 +01:00
.alert("Update name", isPresented: $showUpdateCapNameAlert, actions: {
TextField("Name", text: $updatedCapName)
Button("Update", action: saveNewCapName)
Button("Cancel", role: .cancel, action: {})
}, message: {
Text("Please enter the new name for the cap")
})
2022-06-10 21:20:49 +02:00
.onChange(of: database.image) { newImage in
if newImage != nil {
sortType = .id
sortAscending = false
return
}
}.onChange(of: database.matches) { newMatches in
if newMatches.isEmpty {
sortType = .id
sortAscending = false
} else {
sortType = .match
sortAscending = false
}
2022-06-11 11:27:56 +02:00
}.onAppear {
database.startRegularUploads()
2022-06-10 21:20:49 +02:00
}
}
private func refresh() {
Task {
await database.downloadCaps()
let hasNewClassifier = await database.serverHasNewClassifier()
guard hasNewClassifier else {
return
}
DispatchQueue.main.async {
self.showNewClassifierAlert = true
}
}
}
private func filter() {
showSortPopover.toggle()
}
private func openCamera() {
removeCapturedImage()
showCameraSheet.toggle()
}
private func showSettings() {
showSettingsSheet.toggle()
}
private func downloadClassifier() {
Task {
await database.downloadClassifier()
await database.downloadClassifierClasses()
2022-06-10 21:20:49 +02:00
}
}
private func didTapClassifiedImage() {
isEnteringNewCapName = true
}
private func removeCapturedImage() {
database.image = nil
}
private func didTap(cap: Cap) {
guard let image = database.image else {
selectedCapId = cap.id
2022-06-10 21:20:49 +02:00
openCamera()
return
}
database.save(image, for: cap.id)
database.image = nil
selectedCapId = nil
2022-06-10 21:20:49 +02:00
}
private func saveNewCap() {
guard let image = database.image else {
return
}
let name = searchString.trimmingCharacters(in: .whitespacesAndNewlines)
let newCap = database.save(newCap: name)
database.save(image, for: newCap.id)
removeCapturedImage()
isEnteringNewCapName = false
}
2022-12-11 19:26:11 +01:00
private func showRenameWindow(for cap: Cap) {
updatedCapName = cap.name
selectedCapId = cap.id
2022-12-11 19:26:11 +01:00
showUpdateCapNameAlert = true
}
private func saveNewCapName() {
defer {
selectedCapId = nil
2022-12-11 19:26:11 +01:00
updatedCapName = ""
}
guard let capId = selectedCapId else {
2022-12-11 19:26:11 +01:00
return
}
let name = updatedCapName.trimmingCharacters(in: .whitespacesAndNewlines)
guard name != "" else {
return
}
guard database.update(name: name, for: capId) else {
return
}
}
2023-02-19 00:38:52 +01:00
private func showAllImages(for cap: Cap) {
selectedCapToShowImages = cap
showImageOverviewForCap = true
}
2023-02-26 18:01:00 +01:00
2023-03-13 11:07:22 +01:00
private func startDeleteCap() {
guard let cap = selectedCapId else {
return
}
Task {
guard await database.delete(cap: cap) else {
return
}
}
2023-02-26 18:01:00 +01:00
}
2022-06-10 21:20:49 +02:00
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Database.mock)
}
}