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
2022-12-16 22:43:20 +01:00
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 ] {
let text = searchString
. trimmingCharacters ( in : . whitespacesAndNewlines )
. lowercased ( )
guard text != " " else {
return Array ( database . caps . values )
}
let textParts = text . components ( separatedBy : " " ) . filter { $0 != " " }
return database . caps . values . compactMap { cap -> Cap ? in
// F o r e a c h p a r t o f t e x t , c h e c k i f n a m e c o n t a i n s i t
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 ,
2022-12-16 22:43:20 +01:00
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 ( )
2023-04-17 14:20:13 +02:00
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 {
2022-12-16 22:43:20 +01:00
selectedCapId = cap . id
2022-06-10 21:20:49 +02:00
openCamera ( )
return
}
database . save ( image , for : cap . id )
database . image = nil
2022-12-16 22:43:20 +01:00
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
2022-12-16 22:43:20 +01:00
selectedCapId = cap . id
2022-12-11 19:26:11 +01:00
showUpdateCapNameAlert = true
}
private func saveNewCapName ( ) {
defer {
2022-12-16 22:43:20 +01:00
selectedCapId = nil
2022-12-11 19:26:11 +01:00
updatedCapName = " "
}
2022-12-16 22:43:20 +01:00
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 )
}
}