From e79f0d5c164637023ce8a083ff4144b0dae86f3f Mon Sep 17 00:00:00 2001 From: Tony Freeman Date: Fri, 10 Jan 2025 00:29:59 +0300 Subject: [PATCH 01/16] [Archive] Move File Manager to Archive --- Flipper/iOS/UI/Archive/ArchiveView.swift | 14 ++++++++++++++ .../FileManager/FileManagerEditor.swift | 0 .../FileManager/FileManagerListing.swift | 0 .../FileManager/FileManagerView.swift | 0 Flipper/iOS/UI/Options/OptionsView.swift | 5 ----- 5 files changed, 14 insertions(+), 5 deletions(-) rename Flipper/iOS/UI/{Options => }/FileManager/FileManagerEditor.swift (100%) rename Flipper/iOS/UI/{Options => }/FileManager/FileManagerListing.swift (100%) rename Flipper/iOS/UI/{Options => }/FileManager/FileManagerView.swift (100%) diff --git a/Flipper/iOS/UI/Archive/ArchiveView.swift b/Flipper/iOS/UI/Archive/ArchiveView.swift index 8dfdedad..46aa9c72 100644 --- a/Flipper/iOS/UI/Archive/ArchiveView.swift +++ b/Flipper/iOS/UI/Archive/ArchiveView.swift @@ -26,6 +26,7 @@ struct ArchiveView: View { case importing(URL) case category(ArchiveItem.Kind) case categoryDeleted + case fileManager } var canPullToRefresh: Bool { @@ -70,6 +71,18 @@ struct ArchiveView: View { ) .padding(14) + NavigationLink(value: Destination.fileManager) { + Card { + HStack { + Text("File Manager") + } + .frame(maxWidth: .infinity) + .padding(14) + } + .padding(.horizontal, 14) + .padding(.bottom, 14) + } + if !favoriteItems.isEmpty { FavoritesSection(items: favoriteItems) .padding(.horizontal, 14) @@ -134,6 +147,7 @@ struct ArchiveView: View { case .importing(let url): ImportView(url: url) case .category(let kind): CategoryView(kind: kind) case .categoryDeleted: CategoryDeletedView() + case .fileManager: FileManagerView() } } } diff --git a/Flipper/iOS/UI/Options/FileManager/FileManagerEditor.swift b/Flipper/iOS/UI/FileManager/FileManagerEditor.swift similarity index 100% rename from Flipper/iOS/UI/Options/FileManager/FileManagerEditor.swift rename to Flipper/iOS/UI/FileManager/FileManagerEditor.swift diff --git a/Flipper/iOS/UI/Options/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift similarity index 100% rename from Flipper/iOS/UI/Options/FileManager/FileManagerListing.swift rename to Flipper/iOS/UI/FileManager/FileManagerListing.swift diff --git a/Flipper/iOS/UI/Options/FileManager/FileManagerView.swift b/Flipper/iOS/UI/FileManager/FileManagerView.swift similarity index 100% rename from Flipper/iOS/UI/Options/FileManager/FileManagerView.swift rename to Flipper/iOS/UI/FileManager/FileManagerView.swift diff --git a/Flipper/iOS/UI/Options/OptionsView.swift b/Flipper/iOS/UI/Options/OptionsView.swift index 12ddb6c7..cd59e0a0 100644 --- a/Flipper/iOS/UI/Options/OptionsView.swift +++ b/Flipper/iOS/UI/Options/OptionsView.swift @@ -25,7 +25,6 @@ struct OptionsView: View { case stressTest case speedTest case logs - case fileManager case reportBug case infrared } @@ -55,9 +54,6 @@ struct OptionsView: View { } Section(header: Text("Remote")) { - NavigationLink(value: Destination.fileManager) { - Text("File Manager") - } Button("Reboot Flipper") { device.reboot() } @@ -155,7 +151,6 @@ struct OptionsView: View { case .stressTest: StressTestView() case .speedTest: SpeedTestView() case .logs: LogsView() - case .fileManager: FileManagerView() case .reportBug: ReportBugView() case .infrared: InfraredDebugLayout() } From 5a8eb2accbe1161857455880db15831f2076a3eb Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:02:57 +0200 Subject: [PATCH 02/16] [UI] Redesign File Manager without new logic --- .../Storage/Patform/UserDefaultsStorage.swift | 3 + .../Sources/RPC/Model/Storage.swift | 4 +- .../Assets.xcassets/FileManager/Contents.json | 6 + .../FileManager/File.imageset/Contents.json | 21 ++ .../FileManager/File.imageset/File.svg | 3 + .../FileManagerIcon.imageset/Contents.json | 21 ++ .../FileManagerIcon.svg | 21 ++ .../FileManager/Folder.imageset/Contents.json | 21 ++ .../FileManager/Folder.imageset/Folder.svg | 3 + .../FileManager/Grid.imageset/Contents.json | 21 ++ .../FileManager/Grid.imageset/Grid.svg | 6 + .../FileManager/List.imageset/Contents.json | 21 ++ .../FileManager/List.imageset/List.svg | 5 + .../FileManager/SDCard.imageset/Contents.json | 21 ++ .../FileManager/SDCard.imageset/SDCard.svg | 3 + Flipper/iOS/UI/Archive/ArchiveView.swift | 18 +- .../UI/Archive/Components/FileManager.swift | 36 ++ .../FileManager/Components/ElementRow.swift | 110 ++++++ .../FileManager/Components/EmptyFolder.swift | 31 ++ .../Components/FileEditorOptions.swift | 45 +++ .../Components/FileListingOptions.swift | 106 ++++++ .../Components/FileManagerElements.swift | 62 ++++ .../Components/NavigationPathView.swift | 81 +++++ .../FileManager/Components/SDCardInfo.swift | 99 +++++ .../Components/SaveChangesContentAlert.swift | 49 +++ .../Components/SelectedElementSheet.swift | 98 +++++ .../UI/FileManager/FileManagerEditor.swift | 101 ++++-- .../UI/FileManager/FileManagerListing.swift | 337 +++++++----------- 28 files changed, 1121 insertions(+), 232 deletions(-) create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/File.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/File.imageset/File.svg create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/FileManagerIcon.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/FileManagerIcon.imageset/FileManagerIcon.svg create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/Folder.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/Folder.imageset/Folder.svg create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/Grid.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/Grid.imageset/Grid.svg create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/List.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/List.imageset/List.svg create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/SDCard.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/SDCard.imageset/SDCard.svg create mode 100644 Flipper/iOS/UI/Archive/Components/FileManager.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/ElementRow.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/EmptyFolder.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/FileEditorOptions.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/SDCardInfo.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/SaveChangesContentAlert.swift create mode 100644 Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift diff --git a/Flipper/Packages/Core/Sources/Storage/Patform/UserDefaultsStorage.swift b/Flipper/Packages/Core/Sources/Storage/Patform/UserDefaultsStorage.swift index 2b6e6f7d..790a56dc 100644 --- a/Flipper/Packages/Core/Sources/Storage/Patform/UserDefaultsStorage.swift +++ b/Flipper/Packages/Core/Sources/Storage/Patform/UserDefaultsStorage.swift @@ -109,5 +109,8 @@ public extension UserDefaults { case appsSortOrder = "appsSortOrder" case todayWidgetUpdated = "todayWidgetUpdated" + + case fileManagerDisplayType = "fileManagerDisplayType" + case fileManagerShowHiddenFiles = "fileManagerShowHiddenFiles" } } diff --git a/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift b/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift index 27652cb2..b9b05b4a 100644 --- a/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift +++ b/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift @@ -7,10 +7,12 @@ public struct StorageSpace: Equatable { public var used: Int { total - free } } -public enum Element: Equatable { +public enum Element: Equatable, Identifiable { case file(File) case directory(Directory) + public var id: String { name } + public var name: String { switch self { case .file(let file): return file.name diff --git a/Flipper/Shared/Assets.xcassets/FileManager/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/File.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/File.imageset/Contents.json new file mode 100644 index 00000000..671bd130 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/File.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "File.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/File.imageset/File.svg b/Flipper/Shared/Assets.xcassets/FileManager/File.imageset/File.svg new file mode 100644 index 00000000..b9bde621 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/File.imageset/File.svg @@ -0,0 +1,3 @@ + + + diff --git a/Flipper/Shared/Assets.xcassets/FileManager/FileManagerIcon.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/FileManagerIcon.imageset/Contents.json new file mode 100644 index 00000000..4a4ac044 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/FileManagerIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "FileManagerIcon.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/FileManagerIcon.imageset/FileManagerIcon.svg b/Flipper/Shared/Assets.xcassets/FileManager/FileManagerIcon.imageset/FileManagerIcon.svg new file mode 100644 index 00000000..a1f32dc9 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/FileManagerIcon.imageset/FileManagerIcon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Flipper/Shared/Assets.xcassets/FileManager/Folder.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/Folder.imageset/Contents.json new file mode 100644 index 00000000..b154a4f3 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/Folder.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Folder.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/Folder.imageset/Folder.svg b/Flipper/Shared/Assets.xcassets/FileManager/Folder.imageset/Folder.svg new file mode 100644 index 00000000..cae061b5 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/Folder.imageset/Folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/Flipper/Shared/Assets.xcassets/FileManager/Grid.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/Grid.imageset/Contents.json new file mode 100644 index 00000000..917df941 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/Grid.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Grid.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/Grid.imageset/Grid.svg b/Flipper/Shared/Assets.xcassets/FileManager/Grid.imageset/Grid.svg new file mode 100644 index 00000000..ff5fb7b7 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/Grid.imageset/Grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Flipper/Shared/Assets.xcassets/FileManager/List.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/List.imageset/Contents.json new file mode 100644 index 00000000..d75f53cb --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/List.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "List.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/List.imageset/List.svg b/Flipper/Shared/Assets.xcassets/FileManager/List.imageset/List.svg new file mode 100644 index 00000000..7c5a59fc --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/List.imageset/List.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Flipper/Shared/Assets.xcassets/FileManager/SDCard.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/SDCard.imageset/Contents.json new file mode 100644 index 00000000..181d25a0 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/SDCard.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "SDCard.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/SDCard.imageset/SDCard.svg b/Flipper/Shared/Assets.xcassets/FileManager/SDCard.imageset/SDCard.svg new file mode 100644 index 00000000..9b86dda4 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/SDCard.imageset/SDCard.svg @@ -0,0 +1,3 @@ + + + diff --git a/Flipper/iOS/UI/Archive/ArchiveView.swift b/Flipper/iOS/UI/Archive/ArchiveView.swift index 46aa9c72..fe5f5b55 100644 --- a/Flipper/iOS/UI/Archive/ArchiveView.swift +++ b/Flipper/iOS/UI/Archive/ArchiveView.swift @@ -34,6 +34,11 @@ struct ArchiveView: View { device.status == .synchronized } + var isFileManagerAvailable: Bool { + device.status == .connected || + device.status == .synchronized + } + var items: [ArchiveItem] { archive.items } @@ -72,16 +77,11 @@ struct ArchiveView: View { .padding(14) NavigationLink(value: Destination.fileManager) { - Card { - HStack { - Text("File Manager") - } - .frame(maxWidth: .infinity) - .padding(14) - } - .padding(.horizontal, 14) - .padding(.bottom, 14) + FileManagerSection() + .padding(.horizontal, 14) + .padding(.bottom, 14) } + .disabled(!isFileManagerAvailable) if !favoriteItems.isEmpty { FavoritesSection(items: favoriteItems) diff --git a/Flipper/iOS/UI/Archive/Components/FileManager.swift b/Flipper/iOS/UI/Archive/Components/FileManager.swift new file mode 100644 index 00000000..cb6b0e7a --- /dev/null +++ b/Flipper/iOS/UI/Archive/Components/FileManager.swift @@ -0,0 +1,36 @@ +import SwiftUI + +struct FileManagerSection: View { + @Environment(\.isEnabled) var isEnabled + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 12) { + Image("FileManagerIcon") + .resizable() + .renderingMode(.template) + .frame(width: 30, height: 30) + .foregroundColor(.primary) + + Text("File Manager") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.primary) + + Spacer() + + Image("ChevronRight") + .resizable() + .frame(width: 14, height: 14) + } + + Text("Manage files and assets on your Flipper Zero") + .font(.system(size: 14, weight: .medium)) + .multilineTextAlignment(.leading) + .foregroundColor(.black30) + } + .padding(12) + .opacity(isEnabled ? 1 : 0.4) + .background(Color.groupedBackground) + .cornerRadius(12) + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift new file mode 100644 index 00000000..015da7a2 --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift @@ -0,0 +1,110 @@ +import Core +import Peripheral + +import SwiftUI + +extension FileManagerView.FileManagerListing { + struct ElementRow: View { + let element: Element + let type: DisplayType + + let onAction: () -> Void + + var body: some View { + Group { + switch type { + case .grid: + VStack(alignment: .leading, spacing: 12) { + HStack { + Icon(for: element) + Spacer() + Action(onTap: onAction) + } + Title(for: element) + } + case .list: + HStack(spacing: 12) { + Icon(for: element) + Title(for: element) + Spacer() + Action(onTap: onAction) + } + } + } + .padding(12) + .background(Color.groupedBackground) + .cornerRadius(12) + } + } +} + +fileprivate extension FileManagerView.FileManagerListing.ElementRow { + struct Icon: View { + let element: Element + + private var image: Image { + switch element { + case .directory: + return .init("Folder") + case .file: + if let item = try? ArchiveItem.Kind(filename: element.name) { + return item.icon + } else { + return .init("File") + } + } + } + + init(for element: Element) { + self.element = element + } + + var body: some View { + image + .resizable() + .renderingMode(.template) + .frame(width: 24, height: 24) + .foregroundColor(.primary) + } + } + + struct Title: View { + let element: Element + + init(for element: Element) { + self.element = element + } + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(element.name) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.primary) + .lineLimit(1) + + if case let .file(file) = element { + Text(file.size.hr) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.black30) + } + } + .frame(height: 32) + } + } + + struct Action: View { + let onTap: () -> Void + + var body: some View { + Image(systemName: "ellipsis") + .resizable() + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + .foregroundColor(.black30) + .frame(width: 20) + .padding([.vertical, .leading], 12) + .contentShape(Rectangle()) + .onTapGesture { onTap() } + } + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/EmptyFolder.swift b/Flipper/iOS/UI/FileManager/Components/EmptyFolder.swift new file mode 100644 index 00000000..bccf59f1 --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/EmptyFolder.swift @@ -0,0 +1,31 @@ +import SwiftUI + +extension FileManagerView.FileManagerListing { + struct EmptyFolder: View { + let onUpload: () -> Void + + var body: some View { + VStack(alignment: .center, spacing: 24) { + Spacer() + + Text("No Files Yet") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.primary) + + Image("ReportFailed") + .renderingMode(.template) + .foregroundColor(.primary) + + Text("Upload Files") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.a2) + .onTapGesture { + onUpload() + } + + Spacer() + } + .frame(maxWidth: .infinity) + } + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/FileEditorOptions.swift b/Flipper/iOS/UI/FileManager/Components/FileEditorOptions.swift new file mode 100644 index 00000000..d2b9b302 --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/FileEditorOptions.swift @@ -0,0 +1,45 @@ +import SwiftUI + +extension FileManagerView.FileManagerEditor { + struct FileEditorOptions: View { + @Binding var isPresented: Bool + + let save: () -> Void + let saveAs: () -> Void + + var body: some View { + HStack { + Spacer() + Card { + VStack(alignment: .leading, spacing: 0) { + Option(text: "Save") { + isPresented = false + save() + } + } + } + .frame(width: 150) + } + .padding(.horizontal, 14) + .offset(y: 40) + } + } +} + +fileprivate extension FileManagerView.FileManagerEditor.FileEditorOptions { + struct Option: View { + let text: String + let onTap: () -> Void + + var body: some View { + HStack { + Text(text) + .font(.system(size: 16, weight: .medium)) + .padding(.horizontal, 16) + .padding(.vertical, 12) + Spacer() + } + .onTapGesture { onTap() } + } + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift b/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift new file mode 100644 index 00000000..beaee3c8 --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift @@ -0,0 +1,106 @@ +import SwiftUI + +extension FileManagerView.FileManagerListing { + struct FileListingOptions: View { + @Binding var isPresented: Bool + + let upload: () -> Void + let selectDisplayType: (DisplayType) -> Void + let toggleHidenFiles: (Bool) -> Void + + let isHiddenFilesShow: Bool + + var body: some View { + HStack { + Spacer() + Card { + VStack(alignment: .leading, spacing: 0) { + Option(title: "Upload", image: "Share") { + isPresented = false + upload() + } + + Divider() + + Option(title: "List", image: "List") { + isPresented = false + selectDisplayType(.list) + } + + Option(title: "Grid", image: "Grid") { + isPresented = false + selectDisplayType(.grid) + } + + Divider() + + ShowHiddenFilesOption( + isHiddenFilesShow: isHiddenFilesShow + ) { + isPresented = false + toggleHidenFiles(!isHiddenFilesShow) + } + } + } + .frame(width: 200) + } + .padding(.horizontal, 14) + .offset(y: 40) + } + } +} + +fileprivate extension FileManagerView.FileManagerListing.FileListingOptions { + struct Option: View { + let title: String + let image: String + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 8) { + Image(image) + .resizable() + .renderingMode(.template) + .frame(width: 24, height: 24) + .foregroundColor(.primary) + Text(title) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.primary) + Spacer() + } + } + .padding(12) + } + } + + struct ShowHiddenFilesOption: View { + let isHiddenFilesShow: Bool + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 8) { + ZStack { + Circle() + .stroke(Color.black30, lineWidth: 2) + + if isHiddenFilesShow { + Circle() + .fill(Color.a1) + .padding(4) + } + } + .frame(width: 20, height: 20) + .padding(2) + + Text("Show Hidden Files") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.primary) + Spacer() + } + } + .padding(12) + } + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift new file mode 100644 index 00000000..64efdadb --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift @@ -0,0 +1,62 @@ +import Peripheral + +import SwiftUI + +extension FileManagerView.FileManagerListing { + struct FileManagerElements: View { + let elements: [Element] + let displayType: DisplayType + + let onTap: (Element) -> Void + let onDelete: (Element) -> Void + let onAction: (Element) -> Void + + private let columns = [GridItem(.flexible()), GridItem(.flexible())] + + var body: some View { + Group { + switch displayType { + case .list: + ForEach(elements, id: \.description) { element in + ElementRow( + element: element, + type: displayType, + onAction: { onAction(element) } + ) + .onTapGesture { onTap(element) } + .swipeActions { + Button(role: .destructive) { + onDelete(element) + } label: { + Image("Delete") + .foregroundColor(.red) + } + .tint(.red.opacity(0.1)) + } + } + case .grid: + LazyVGrid(columns: columns, spacing: 12) { + ForEach(elements, id: \.description) { element in + ElementRow( + element: element, + type: displayType, + onAction: { onAction(element) } + ) + .onTapGesture { onTap(element) } + } + } + } + } + .listRowSeparator(.hidden) + .listRowInsets( + .init( + top: 0, + leading: 0, + bottom: 0, + trailing: 0 + ) + ) + .listRowBackground(Color.clear) + } + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift b/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift new file mode 100644 index 00000000..0e0dbba8 --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift @@ -0,0 +1,81 @@ +import SwiftUI +import Peripheral + +extension FileManagerView { + struct NavigationPathView: View { + @Environment(\.path) private var navigationPath + + let path: Peripheral.Path + + private var components: [String] { + path + .string + .split(separator: "/") + .map(String.init) + .filter { $0 != "ext" } + } + + var body: some View { + HStack(spacing: 8) { + Image("SDCard") + .resizable() + .renderingMode(.template) + .frame(width: 24, height: 24) + .foregroundColor(.primary) + .onTapGesture { navigate(to: 0) } + + Text("/") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black30) + + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(components.indices, id: \.self) { index in + Element( + component: components[index], + index: index, + onTap: navigate + ) + } + } + } + .onAppear { + if let lastIndex = components.indices.last { + proxy.scrollTo(lastIndex, anchor: .trailing) + } + } + } + } + } + + private func navigate(to index: Int) { + let stack = navigationPath.wrappedValue.count + navigationPath.wrappedValue.removeLast(stack - index - 1) + } + } +} + +fileprivate extension FileManagerView.NavigationPathView { + struct Element: View { + let component: String + let index: Int + let onTap: (Int) -> Void + + var body: some View { + HStack(spacing: 8) { + if index != 0 { + Text("/") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black30) + } + + Text(component) + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.primary) + .onTapGesture { onTap(index + 1) } + } + .id(index) + } + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/SDCardInfo.swift b/Flipper/iOS/UI/FileManager/Components/SDCardInfo.swift new file mode 100644 index 00000000..15d1fc42 --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/SDCardInfo.swift @@ -0,0 +1,99 @@ +import SwiftUI +import Peripheral + +extension FileManagerView { + struct SDCardInfo: View { + let storage: StorageSpace? + + init(_ storage: StorageSpace?) { + self.storage = storage + } + + var body: some View { + HStack(spacing: 32) { + VStack(alignment: .leading, spacing: 12) { + Text("SD Card") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.primary) + + ProgressBar(storage: storage) + + HStack { + Info(title: "Used", space: storage?.used) + Spacer() + Info(title: "Total", space: storage?.total) + } + .padding(.horizontal, 12) + } + + Image("SDCard") + .resizable() + .renderingMode(.template) + .frame(width: 84, height: 84) + .foregroundColor(.primary) + } + .padding(8) + .background(Color.groupedBackground) + .cornerRadius(12) + } + } +} + +fileprivate extension FileManagerView.SDCardInfo { + struct ProgressBar: View { + let storage: StorageSpace? + + var body: some View { + Group { + if let storage { + GeometryReader { geometry in + HStack(spacing: 0) { + Rectangle() + .fill(Color.orange) + .frame( + width: geometry.size.width + * storage.usedRatio + ) + + Rectangle() + .fill(Color.orange.opacity(0.3)) + } + } + } else { + AnimatedPlaceholder() + } + } + .frame(height: 8) + .cornerRadius(4) + } + } + + struct Info: View { + let title: String + let space: Int? + + var body: some View { + VStack { + Text(title) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.black30) + + if let space { + Text(space.hr) + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.primary) + } else { + AnimatedPlaceholder() + .frame(width: 50, height: 10) + } + } + } + } +} + +fileprivate extension StorageSpace { + var usedRatio: CGFloat { + guard total > 0 else { return 0 } + return CGFloat(used) / CGFloat(total) + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/SaveChangesContentAlert.swift b/Flipper/iOS/UI/FileManager/Components/SaveChangesContentAlert.swift new file mode 100644 index 00000000..fe79953c --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/SaveChangesContentAlert.swift @@ -0,0 +1,49 @@ +import SwiftUI + +extension FileManagerView.FileManagerEditor { + struct SaveChangesContentAlert: View { + @Binding var isPresented: Bool + let save: () -> Void + let saveAs: () -> Void + let dontSave: () -> Void + + var body: some View { + VStack(spacing: 24) { + VStack(spacing: 4) { + Text("Save Changes?") + .font(.system(size: 14, weight: .bold)) + .padding(.top, 5) + + Text("All unsaved changes will be lost") + .font(.system(size: 14, weight: .medium)) + .multilineTextAlignment(.center) + .foregroundColor(.black40) + .padding(.horizontal, 12) + } + + VStack(spacing: 14) { + Divider() + Button { + isPresented = false + save() + } label: { + Text("Save") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.a2) + } + + Divider() + Button { + isPresented = false + dontSave() + } label: { + Text("Don't save") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.primary) + } + } + } + } + } + +} diff --git a/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift b/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift new file mode 100644 index 00000000..952b8b6a --- /dev/null +++ b/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift @@ -0,0 +1,98 @@ +import Peripheral + +import SwiftUI + +extension FileManagerView.FileManagerListing { + struct SelectedElementSheet: View { + @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) private var dismiss + + let element: Element + + let onExport: (Element) -> Void + let onDelete: (Element) -> Void + + private var backgroundColor: Color { + colorScheme == .light ? .white : .black88 + } + + private var type: String { + switch element { + case .directory: "Folder" + case .file: "File" + } + } + + private var isDirectory: Bool { + return if case .directory = element { + true + } else { + false + } + } + + var body: some View { + VStack(spacing: 12) { + VStack(spacing: 2) { + Text(type) + .font(.system(size: 14, weight: .medium)) + + Text(element.name) + .font(.system(size: 14, weight: .bold)) + } + + Option( + image: "Share", + title: "Export" + ) { + dismiss() + onExport(element) + } + .disabled(isDirectory) + .foregroundColor(.primary) + + Option( + image: "Delete", + title: "Delete" + ) { + dismiss() + onDelete(element) + } + .disabled(isDirectory) + .foregroundColor(.red) + } + .padding(.horizontal, 14) + .background(backgroundColor) + .presentationDragIndicator(.visible) + .presentationDetents([.height(200)]) + .pickerStyle(.segmented) + } + } +} + +fileprivate extension FileManagerView.FileManagerListing.SelectedElementSheet { + struct Option: View { + @Environment(\.isEnabled) private var isEnabled + + let image: String + let title: String + let action: () -> Void + + var body: some View { + HStack(spacing: 8) { + Image(image) + .resizable() + .renderingMode(.template) + .frame(width: 24, height: 24) + + Text(title) + .font(.system(size: 14, weight: .medium)) + + Spacer() + } + .opacity(isEnabled ? 1 : 0.4) + .padding(12) + .onTapGesture { action() } + } + } +} diff --git a/Flipper/iOS/UI/FileManager/FileManagerEditor.swift b/Flipper/iOS/UI/FileManager/FileManagerEditor.swift index b1bd7614..d388664b 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerEditor.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerEditor.swift @@ -8,34 +8,49 @@ extension FileManagerView { let path: Peripheral.Path @EnvironmentObject var fileManager: RemoteFileManager + @Environment(\.dismiss) var dismiss + @Environment(\.popups) var popups + + @State private var current: String = "" + @State private var backup: String = "" - @State private var content: String = "" @State private var error: String? - @State private var isBusy = false + @State private var isLoading = false + + @State private var showSaveChanges = false + @State private var showOptions = false + + @FocusState private var textFieldFocus: Bool var body: some View { VStack { if let error = error { Text(error) - } else if isBusy { + } else if isLoading { ProgressView() } else { Card { - TextEditor(text: $content) + TextEditor(text: $current) + .focused($textFieldFocus) + .font(.system(size: 14, weight: .medium)) .hideScrollBackground() .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(4) } .padding(14) } } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.background) .navigationBarBackground(Color.a1) .navigationBarBackButtonHidden(true) .navigationBarTitleDisplayMode(.inline) .toolbar { LeadingToolbarItems { BackButton { - dismiss() + textFieldFocus = false + back() } } PrincipalToolbarItems(alignment: .leading) { @@ -43,36 +58,78 @@ extension FileManagerView { } TrailingToolbarItems { SaveButton { - Task { - await save() - } + textFieldFocus = false + save() + } + .disabled(isLoading || error != nil) + } + ToolbarItem(placement: .keyboard) { + HStack { + Spacer() + Text("Done") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.a1) + .onTapGesture { textFieldFocus = false } } } } - .padding(.bottom, 16) - .task { - await load() + .alert(isPresented: $showSaveChanges) { + SaveChangesContentAlert( + isPresented: $showSaveChanges, + save: save, + saveAs: saveAs, + dontSave: dontSave + ) + } + .popup(isPresented: $showOptions) { + FileEditorOptions( + isPresented: $showOptions, + save: save, + saveAs: saveAs + ) } + .task { await load() } } - func load() async { + private func load() async { + isLoading = true + defer { isLoading = false } + do { - isBusy = true - defer { isBusy = false } - content = try await fileManager.readFile(at: path) + current = try await fileManager.readFile(at: path) + backup = current } catch { self.error = String(describing: error) } } - func save() async { - isBusy = true - do { - try await fileManager.writeFile(content, at: path) - } catch { - self.error = String(describing: error) + private func back() { + if current != backup { + showSaveChanges = true + } else { + dismiss() } - isBusy = false + } + + private func save() { + Task { + isLoading = true + defer { isLoading = false } + + do { + try await fileManager.writeFile(current, at: path) + dismiss() + } catch { + self.error = String(describing: error) + } + } + } + + // TODO + private func saveAs() {} + + private func dontSave() { + dismiss() } } } diff --git a/Flipper/iOS/UI/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift index 60e8b120..5def34e2 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerListing.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerListing.swift @@ -1,91 +1,79 @@ import Core +import UniformTypeIdentifiers import Peripheral import SwiftUI -import UniformTypeIdentifiers extension FileManagerView { struct FileManagerListing: View { - @Environment(\.path) var navigationPath - - let path: Peripheral.Path - @EnvironmentObject var fileManager: RemoteFileManager + @EnvironmentObject var device: Device + + @Environment(\.path) var navigationPath @Environment(\.dismiss) var dismiss - @State private var elements: [Element] = [] + @State private var _elements: [Element] = [] + @State private var isLoading = true @State private var error: String? - @State private var isBusy = false - @State private var name = "" - @State private var isNewFile = false - @State private var isNewDirectory = false - @FocusState var isNameFocused: Bool - var namePlaceholder: String { - "\(isNewFile ? "file" : "directory") name" + @State private var isFileImporterPresented = false + @State private var showOptions = false + @State private var selectedElement: Element? + + @AppStorage(.fileManagerShowHiddenFiles) + private var isHiddenFilesShow: Bool = false + + @AppStorage(.fileManagerDisplayType) + private var displayType: DisplayType = .list + + let path: Peripheral.Path + + private var title: String { + path.isRoot ? "File Manager" : path.lastComponent ?? "/" } - @State private var selectedIndexSet: IndexSet? - @State private var isForceDeletePresented = false - @State private var isFileImporterPresented = false + enum DisplayType: String { + case list + case grid + } + + var elements: [Element] { + isHiddenFilesShow + ? _elements + : _elements.filter { !$0.name.hasPrefix(".") } + } var body: some View { VStack { - if isBusy { - ProgressView() - } else if let error = error { + if let error = error { Text(error) + } else if isLoading { + ProgressView() } else { List { - if !path.isEmpty { - Button("..") { - dismiss() - } - .foregroundColor(.primary) + if path.isRoot { + SDCardInfo(device.storageInfo?.external) + } else { + NavigationPathView(path: path) } - if isNewFile || isNewDirectory { - TextField(namePlaceholder, text: $name) - .onSubmit { - submitNewElement() - } - .focused($isNameFocused) - } - ForEach(elements, id: \.description) { - switch $0 { - case .directory(let directory): - NavigationLink(value: Destination.listing( - path.appending(directory.name) - )) { - DirectoryRow(directory: directory) - } - .foregroundColor(.primary) - case .file(let file): - HStack { - FileRow(file: file) - .onTapGesture { - navigationPath.append( - Destination.editor( - path.appending(file.name) - ) - ) - } - DownloadFileIcon() - .onTapGesture { - Task { - await downloadFile(file) - } - } - } - } - } - .onDelete { indexSet in - Task { - await delete(indexSet) - } + + if elements.isEmpty { + EmptyFolder(onUpload: showUpload) + } else { + FileManagerElements( + elements: elements, + displayType: displayType, + onTap: navigate, + onDelete: deleteFile, + onAction: { selectedElement = $0 } + ) } } + .listRowSpacing(12) } } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.background) .navigationBarBackground(Color.a1) .navigationBarBackButtonHidden(true) .navigationBarTitleDisplayMode(.inline) @@ -95,183 +83,132 @@ extension FileManagerView { dismiss() } } + PrincipalToolbarItems(alignment: .leading) { - Title(path.lastComponent ?? "/") + Title(title) } - TrailingToolbarItems { - if !path.isEmpty { - NavBarMenu { - Button { - newElement(isDirectory: false) - } label: { - Text("File") - } - - Button { - newElement(isDirectory: true) - } label: { - Text("Folder") - } - Button { - isFileImporterPresented = true - } label: { - Text("Import") - } - } label: { - Image(systemName: "plus") - } + TrailingToolbarItems { + EllipsisButton { + showOptions = true } + .disabled(isLoading || error != nil) } } - .alert( - "Directory is not empty", - isPresented: $isForceDeletePresented, - presenting: selectedIndexSet - ) { selectedIndexSet in - Button("Force Delete", role: .destructive) { - Task { - await delete(selectedIndexSet, force: true) - } - } + .popup(isPresented: $showOptions) { + FileListingOptions( + isPresented: $showOptions, + upload: showUpload, + selectDisplayType: { displayType = $0 }, + toggleHidenFiles: { isHiddenFilesShow = $0 }, + isHiddenFilesShow: isHiddenFilesShow + ) + } + .sheet(item: $selectedElement) { element in + SelectedElementSheet( + element: element, + onExport: downloadFile, + onDelete: deleteFile + ) } .fileImporter( isPresented: $isFileImporterPresented, - allowedContentTypes: [UTType.item] + allowedContentTypes: [UTType.item], + allowsMultipleSelection: true ) { result in - if case .success(let url) = result { - Task { - await importFile(url) - } + switch result { + case .success(let urls): + importFiles(urls) + case .failure(let error): + self.error = String(describing: error) } } - .task { - await list() - } + .task { await load() } + .refreshable { await load() } } - func showingProgress(_ task: () async throws -> Void) async throws { - isBusy = true - defer { isBusy = false } - try await task() - } + private func load() async { + isLoading = true + defer { isLoading = false } - func list() async { do { - try await showingProgress { - elements = try await fileManager.list(at: path) - } + _elements = try await fileManager.list(at: path) } catch { self.error = String(describing: error) } } - func delete(_ indexSet: IndexSet, force: Bool = false) async { - guard let index = indexSet.first else { return } - let element = elements.remove(at: index) - do { - try await fileManager.delete(element, at: path, force: force) - } catch let error as RemoteFileManager.Error - where error == .directoryIsNotEmpty && !force { - elements.insert(element, at: index) - self.selectedIndexSet = indexSet - self.isForceDeletePresented = true - } catch { - self.error = String(describing: error) - elements.insert(element, at: index) + private func navigate(_ element: Element) { + switch element { + case .directory(let directory): + let nextPath = path.appending(directory.name) + navigationPath.append(Destination.listing(nextPath)) + case .file(let file): + let nextPath = path.appending(file.name) + navigationPath.append(Destination.editor(nextPath)) } } - func newElement(isDirectory: Bool) { - name = "" - isNewFile = !isDirectory - isNewDirectory = isDirectory - isNameFocused = true - } + private func importFiles(_ urls: [URL]) { + isLoading = true + defer { isLoading = false } - func submitNewElement() { - if !name.isEmpty { - let path = path.appending(name) - let isDirectory = isNewDirectory - Task { - do { - try await fileManager.create( - path: path, - isDirectory: isDirectory) - await list() - } catch { - self.error = String(describing: error) + Task { + do { + try await urls.forEach { url in + try await fileManager.importFile(url: url, at: path) } + await load() + } catch { + self.error = String(describing: error) } } - name = "" - isNewFile = false - isNewDirectory = false } - func importFile(_ url: URL) async { - do { - try await showingProgress { - try await fileManager.importFile(url: url, at: path) - } - await list() - } catch { - self.error = String(describing: error) - } - } - - func downloadFile(_ file: File) async { - isBusy = true - defer { isBusy = false } - do { - let bytes = try await fileManager.readRaw( - at: path.appending(file.name)) - let url = try FileManager.default.createTempFile( - name: file.name, - data: .init(bytes)) - share(url) { - try? FileManager.default.removeItem(at: url) + private func downloadFile(_ element: Element) { + isLoading = true + defer { isLoading = false } + + guard case let .file(file) = element else { return } + + Task { + do { + let bytes = try await fileManager.readRaw( + at: path.appending(file.name)) + let url = try FileManager.default.createTempFile( + name: file.name, + data: .init(bytes)) + share(url) { + try? FileManager.default.removeItem(at: url) + } + } catch { + self.error = String(describing: error) } - } catch { - self.error = String(describing: error) } } - } -} - -extension FileManagerView.FileManagerListing { - struct DirectoryRow: View { - let directory: Directory - var body: some View { - HStack { - Image(systemName: "folder.fill") - .frame(width: 20) + private func deleteFile(_ element: Element) { + isLoading = true + defer { isLoading = false } - Text(directory.name) + Task { + do { + try await fileManager.delete(element, at: path) + await load() + } catch { + self.error = String(describing: error) + } } } - } - - struct FileRow: View { - let file: File - var body: some View { - HStack { - Image(systemName: "doc") - .frame(width: 20) - Text(file.name) - Spacer() - Text("\(file.size) bytes") - } - .contentShape(Rectangle()) + private func showUpload() { + isFileImporterPresented = true } } +} - struct DownloadFileIcon: View { - var body: some View { - Image(systemName: "icloud.and.arrow.down") - .frame(width: 20, height: 20) - } +fileprivate extension Peripheral.Path { + var isRoot: Bool { + self == "/ext" } } From af3befe7c099da5cd17fac933efbe5a0b2ed4ac2 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:57:28 +0200 Subject: [PATCH 03/16] [Core] Extended file manager element --- .../Sources/FileManager/ExtendedElement.swift | 19 ++++++++++++++++ .../RemoteFileManager.swift | 6 +++-- .../Sources/RPC/Model/Storage.swift | 4 +--- .../FileManager/Components/ElementRow.swift | 15 ++++++------- .../Components/FileManagerElements.swift | 14 ++++++------ .../Components/SelectedElementSheet.swift | 12 +++++----- .../UI/FileManager/FileManagerListing.swift | 22 +++++++++---------- 7 files changed, 55 insertions(+), 37 deletions(-) create mode 100644 Flipper/Packages/Core/Sources/FileManager/ExtendedElement.swift rename Flipper/Packages/Core/Sources/{Model => FileManager}/RemoteFileManager.swift (94%) diff --git a/Flipper/Packages/Core/Sources/FileManager/ExtendedElement.swift b/Flipper/Packages/Core/Sources/FileManager/ExtendedElement.swift new file mode 100644 index 00000000..bacda331 --- /dev/null +++ b/Flipper/Packages/Core/Sources/FileManager/ExtendedElement.swift @@ -0,0 +1,19 @@ +import Peripheral + +public struct ExtendedElement: Equatable, Identifiable { + public var id: String { path.string } + + public let path: Path + public let type: Element + + public var name: String { + type.name + } +} + +extension ExtendedElement { + init(element: Element, relativeTo path: Path) { + self.path = path.appending(element.name) + self.type = element + } +} diff --git a/Flipper/Packages/Core/Sources/Model/RemoteFileManager.swift b/Flipper/Packages/Core/Sources/FileManager/RemoteFileManager.swift similarity index 94% rename from Flipper/Packages/Core/Sources/Model/RemoteFileManager.swift rename to Flipper/Packages/Core/Sources/FileManager/RemoteFileManager.swift index e15b8b1b..586771c5 100644 --- a/Flipper/Packages/Core/Sources/Model/RemoteFileManager.swift +++ b/Flipper/Packages/Core/Sources/FileManager/RemoteFileManager.swift @@ -23,9 +23,11 @@ public class RemoteFileManager: ObservableObject { // MARK: Directory - public func list(at path: Path) async throws -> [Element] { + public func list(at path: Path) async throws -> [ExtendedElement] { do { - return try await storage.list(at: path) + return try await storage + .list(at: path) + .map { .init(element: $0, relativeTo: path) } } catch { logger.error("list directory: \(error)") throw Error.unknown(.init(describing: error)) diff --git a/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift b/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift index b9b05b4a..27652cb2 100644 --- a/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift +++ b/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift @@ -7,12 +7,10 @@ public struct StorageSpace: Equatable { public var used: Int { total - free } } -public enum Element: Equatable, Identifiable { +public enum Element: Equatable { case file(File) case directory(Directory) - public var id: String { name } - public var name: String { switch self { case .file(let file): return file.name diff --git a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift index 015da7a2..ea51cb37 100644 --- a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift +++ b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift @@ -1,11 +1,10 @@ import Core -import Peripheral import SwiftUI extension FileManagerView.FileManagerListing { struct ElementRow: View { - let element: Element + let element: ExtendedElement let type: DisplayType let onAction: () -> Void @@ -40,10 +39,10 @@ extension FileManagerView.FileManagerListing { fileprivate extension FileManagerView.FileManagerListing.ElementRow { struct Icon: View { - let element: Element + let element: ExtendedElement private var image: Image { - switch element { + switch element.type { case .directory: return .init("Folder") case .file: @@ -55,7 +54,7 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow { } } - init(for element: Element) { + init(for element: ExtendedElement) { self.element = element } @@ -69,9 +68,9 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow { } struct Title: View { - let element: Element + let element: ExtendedElement - init(for element: Element) { + init(for element: ExtendedElement) { self.element = element } @@ -82,7 +81,7 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow { .foregroundColor(.primary) .lineLimit(1) - if case let .file(file) = element { + if case let .file(file) = element.type { Text(file.size.hr) .font(.system(size: 10, weight: .medium)) .foregroundColor(.black30) diff --git a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift index 64efdadb..de80f888 100644 --- a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift +++ b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift @@ -1,15 +1,15 @@ -import Peripheral +import Core import SwiftUI extension FileManagerView.FileManagerListing { struct FileManagerElements: View { - let elements: [Element] + let elements: [ExtendedElement] let displayType: DisplayType - let onTap: (Element) -> Void - let onDelete: (Element) -> Void - let onAction: (Element) -> Void + let onTap: (ExtendedElement) -> Void + let onDelete: (ExtendedElement) -> Void + let onAction: (ExtendedElement) -> Void private let columns = [GridItem(.flexible()), GridItem(.flexible())] @@ -17,7 +17,7 @@ extension FileManagerView.FileManagerListing { Group { switch displayType { case .list: - ForEach(elements, id: \.description) { element in + ForEach(elements) { element in ElementRow( element: element, type: displayType, @@ -36,7 +36,7 @@ extension FileManagerView.FileManagerListing { } case .grid: LazyVGrid(columns: columns, spacing: 12) { - ForEach(elements, id: \.description) { element in + ForEach(elements) { element in ElementRow( element: element, type: displayType, diff --git a/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift b/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift index 952b8b6a..ac760cff 100644 --- a/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift +++ b/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift @@ -1,4 +1,4 @@ -import Peripheral +import Core import SwiftUI @@ -7,24 +7,24 @@ extension FileManagerView.FileManagerListing { @Environment(\.colorScheme) var colorScheme @Environment(\.dismiss) private var dismiss - let element: Element + let element: ExtendedElement - let onExport: (Element) -> Void - let onDelete: (Element) -> Void + let onExport: (ExtendedElement) -> Void + let onDelete: (ExtendedElement) -> Void private var backgroundColor: Color { colorScheme == .light ? .white : .black88 } private var type: String { - switch element { + switch element.type { case .directory: "Folder" case .file: "File" } } private var isDirectory: Bool { - return if case .directory = element { + return if case .directory = element.type { true } else { false diff --git a/Flipper/iOS/UI/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift index 5def34e2..99e3aff3 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerListing.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerListing.swift @@ -12,13 +12,13 @@ extension FileManagerView { @Environment(\.path) var navigationPath @Environment(\.dismiss) var dismiss - @State private var _elements: [Element] = [] + @State private var _elements: [ExtendedElement] = [] @State private var isLoading = true @State private var error: String? @State private var isFileImporterPresented = false @State private var showOptions = false - @State private var selectedElement: Element? + @State private var selectedElement: ExtendedElement? @AppStorage(.fileManagerShowHiddenFiles) private var isHiddenFilesShow: Bool = false @@ -37,7 +37,7 @@ extension FileManagerView { case grid } - var elements: [Element] { + var elements: [ExtendedElement] { isHiddenFilesShow ? _elements : _elements.filter { !$0.name.hasPrefix(".") } @@ -104,9 +104,9 @@ extension FileManagerView { isHiddenFilesShow: isHiddenFilesShow ) } - .sheet(item: $selectedElement) { element in + .sheet(item: $selectedElement) { SelectedElementSheet( - element: element, + element: $0, onExport: downloadFile, onDelete: deleteFile ) @@ -138,8 +138,8 @@ extension FileManagerView { } } - private func navigate(_ element: Element) { - switch element { + private func navigate(_ extended: ExtendedElement) { + switch extended.type { case .directory(let directory): let nextPath = path.appending(directory.name) navigationPath.append(Destination.listing(nextPath)) @@ -165,11 +165,11 @@ extension FileManagerView { } } - private func downloadFile(_ element: Element) { + private func downloadFile(_ extended: ExtendedElement) { isLoading = true defer { isLoading = false } - guard case let .file(file) = element else { return } + guard case let .file(file) = extended.type else { return } Task { do { @@ -187,13 +187,13 @@ extension FileManagerView { } } - private func deleteFile(_ element: Element) { + private func deleteFile(_ extended: ExtendedElement) { isLoading = true defer { isLoading = false } Task { do { - try await fileManager.delete(element, at: path) + try await fileManager.delete(extended.type, at: path) await load() } catch { self.error = String(describing: error) From 81fa78c83b0602bcb5fa438b2fb6f29a5ee817e5 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:03:33 +0200 Subject: [PATCH 04/16] [FileManager] Settings by one structure --- .../Storage/Patform/UserDefaultsStorage.swift | 3 +- .../FileManager/Components/ElementRow.swift | 2 +- .../Components/FileListingOptions.swift | 30 +++++------ .../Components/FileManagerElements.swift | 2 +- .../UI/FileManager/FileManagerListing.swift | 22 +++----- .../UI/FileManager/FileManagerSettings.swift | 51 +++++++++++++++++++ 6 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 Flipper/iOS/UI/FileManager/FileManagerSettings.swift diff --git a/Flipper/Packages/Core/Sources/Storage/Patform/UserDefaultsStorage.swift b/Flipper/Packages/Core/Sources/Storage/Patform/UserDefaultsStorage.swift index 790a56dc..11ae554b 100644 --- a/Flipper/Packages/Core/Sources/Storage/Patform/UserDefaultsStorage.swift +++ b/Flipper/Packages/Core/Sources/Storage/Patform/UserDefaultsStorage.swift @@ -110,7 +110,6 @@ public extension UserDefaults { case todayWidgetUpdated = "todayWidgetUpdated" - case fileManagerDisplayType = "fileManagerDisplayType" - case fileManagerShowHiddenFiles = "fileManagerShowHiddenFiles" + case fileManagerSettings = "fileManagerSettings" } } diff --git a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift index ea51cb37..4ca65eb0 100644 --- a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift +++ b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift @@ -5,7 +5,7 @@ import SwiftUI extension FileManagerView.FileManagerListing { struct ElementRow: View { let element: ExtendedElement - let type: DisplayType + let type: FileManagerSettings.DisplayType let onAction: () -> Void diff --git a/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift b/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift index beaee3c8..3ad41271 100644 --- a/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift +++ b/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift @@ -3,12 +3,9 @@ import SwiftUI extension FileManagerView.FileManagerListing { struct FileListingOptions: View { @Binding var isPresented: Bool + @Binding var settings: FileManagerSettings let upload: () -> Void - let selectDisplayType: (DisplayType) -> Void - let toggleHidenFiles: (Bool) -> Void - - let isHiddenFilesShow: Bool var body: some View { HStack { @@ -24,22 +21,20 @@ extension FileManagerView.FileManagerListing { Option(title: "List", image: "List") { isPresented = false - selectDisplayType(.list) + settings.displayType = .list } Option(title: "Grid", image: "Grid") { isPresented = false - selectDisplayType(.grid) + settings.displayType = .grid } Divider() ShowHiddenFilesOption( - isHiddenFilesShow: isHiddenFilesShow - ) { - isPresented = false - toggleHidenFiles(!isHiddenFilesShow) - } + isPresented: $isPresented, + settings: $settings + ) } } .frame(width: 200) @@ -75,17 +70,22 @@ fileprivate extension FileManagerView.FileManagerListing.FileListingOptions { } struct ShowHiddenFilesOption: View { - let isHiddenFilesShow: Bool - let onTap: () -> Void + @Binding var isPresented: Bool + @Binding var settings: FileManagerSettings var body: some View { - Button(action: onTap) { + Button( + action: { + settings.isHiddenFilesShow.toggle() + isPresented = false + } + ) { HStack(spacing: 8) { ZStack { Circle() .stroke(Color.black30, lineWidth: 2) - if isHiddenFilesShow { + if settings.isHiddenFilesShow { Circle() .fill(Color.a1) .padding(4) diff --git a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift index de80f888..48c2c7d2 100644 --- a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift +++ b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift @@ -5,7 +5,7 @@ import SwiftUI extension FileManagerView.FileManagerListing { struct FileManagerElements: View { let elements: [ExtendedElement] - let displayType: DisplayType + let displayType: FileManagerSettings.DisplayType let onTap: (ExtendedElement) -> Void let onDelete: (ExtendedElement) -> Void diff --git a/Flipper/iOS/UI/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift index 99e3aff3..196b892b 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerListing.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerListing.swift @@ -20,11 +20,8 @@ extension FileManagerView { @State private var showOptions = false @State private var selectedElement: ExtendedElement? - @AppStorage(.fileManagerShowHiddenFiles) - private var isHiddenFilesShow: Bool = false - - @AppStorage(.fileManagerDisplayType) - private var displayType: DisplayType = .list + @AppStorage(.fileManagerSettings) + private var settings: FileManagerSettings = .init() let path: Peripheral.Path @@ -32,13 +29,8 @@ extension FileManagerView { path.isRoot ? "File Manager" : path.lastComponent ?? "/" } - enum DisplayType: String { - case list - case grid - } - var elements: [ExtendedElement] { - isHiddenFilesShow + settings.isHiddenFilesShow ? _elements : _elements.filter { !$0.name.hasPrefix(".") } } @@ -62,7 +54,7 @@ extension FileManagerView { } else { FileManagerElements( elements: elements, - displayType: displayType, + displayType: settings.displayType, onTap: navigate, onDelete: deleteFile, onAction: { selectedElement = $0 } @@ -98,10 +90,8 @@ extension FileManagerView { .popup(isPresented: $showOptions) { FileListingOptions( isPresented: $showOptions, - upload: showUpload, - selectDisplayType: { displayType = $0 }, - toggleHidenFiles: { isHiddenFilesShow = $0 }, - isHiddenFilesShow: isHiddenFilesShow + settings: $settings, + upload: showUpload ) } .sheet(item: $selectedElement) { diff --git a/Flipper/iOS/UI/FileManager/FileManagerSettings.swift b/Flipper/iOS/UI/FileManager/FileManagerSettings.swift new file mode 100644 index 00000000..6068759d --- /dev/null +++ b/Flipper/iOS/UI/FileManager/FileManagerSettings.swift @@ -0,0 +1,51 @@ +import Foundation + +struct FileManagerSettings: Codable, RawRepresentable { + var isHiddenFilesShow: Bool = false + var displayType: DisplayType = .list + + enum DisplayType: String, Codable { + case list + case grid + } + + enum CodingKeys: String, CodingKey { + case isHiddenFilesShow + case displayType + } + + init() {} + + var rawValue: String { + guard let data = try? JSONEncoder().encode(self) else { return "" } + return String(decoding: data, as: UTF8.self) + } + + init?(rawValue: String) { + guard + let value = try? JSONDecoder().decode( + Self.self, + from: .init(rawValue.utf8) + ) + else { + return nil + } + self = value + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + isHiddenFilesShow = try container.decode( + Bool.self, + forKey: .isHiddenFilesShow) + displayType = try container.decode( + DisplayType.self, + forKey: .displayType) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(isHiddenFilesShow, forKey: .isHiddenFilesShow) + try container.encode(displayType, forKey: .displayType) + } +} From b27b65ad8d61e6751818909b28a66b4baa99eaae Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Mon, 13 Jan 2025 21:25:19 +0200 Subject: [PATCH 05/16] [FileManager] Fix display share activity --- .../iOS/UI/FileManager/Components/SelectedElementSheet.swift | 3 --- Flipper/iOS/UI/FileManager/FileManagerListing.swift | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift b/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift index ac760cff..85c5d2e4 100644 --- a/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift +++ b/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift @@ -5,7 +5,6 @@ import SwiftUI extension FileManagerView.FileManagerListing { struct SelectedElementSheet: View { @Environment(\.colorScheme) var colorScheme - @Environment(\.dismiss) private var dismiss let element: ExtendedElement @@ -45,7 +44,6 @@ extension FileManagerView.FileManagerListing { image: "Share", title: "Export" ) { - dismiss() onExport(element) } .disabled(isDirectory) @@ -55,7 +53,6 @@ extension FileManagerView.FileManagerListing { image: "Delete", title: "Delete" ) { - dismiss() onDelete(element) } .disabled(isDirectory) diff --git a/Flipper/iOS/UI/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift index 196b892b..732a2f66 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerListing.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerListing.swift @@ -170,6 +170,8 @@ extension FileManagerView { data: .init(bytes)) share(url) { try? FileManager.default.removeItem(at: url) + // Sheet close before present share activity + selectedElement = nil } } catch { self.error = String(describing: error) From 68c3b34421d8b8d7401b0e7a01c17dab1d7188a4 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:36:16 +0200 Subject: [PATCH 06/16] [FileManager] Allow delete folder --- .../Components/SelectedElementSheet.swift | 1 - .../UI/FileManager/FileManagerListing.swift | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift b/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift index 85c5d2e4..a29f1b67 100644 --- a/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift +++ b/Flipper/iOS/UI/FileManager/Components/SelectedElementSheet.swift @@ -55,7 +55,6 @@ extension FileManagerView.FileManagerListing { ) { onDelete(element) } - .disabled(isDirectory) .foregroundColor(.red) } .padding(.horizontal, 14) diff --git a/Flipper/iOS/UI/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift index 732a2f66..c5f019b1 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerListing.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerListing.swift @@ -16,9 +16,12 @@ extension FileManagerView { @State private var isLoading = true @State private var error: String? + @State private var isForceDeletePresented = false @State private var isFileImporterPresented = false @State private var showOptions = false + @State private var selectedElement: ExtendedElement? + @State private var deletedElement: ExtendedElement? @AppStorage(.fileManagerSettings) private var settings: FileManagerSettings = .init() @@ -56,7 +59,7 @@ extension FileManagerView { elements: elements, displayType: settings.displayType, onTap: navigate, - onDelete: deleteFile, + onDelete: { deleteFile($0) }, onAction: { selectedElement = $0 } ) } @@ -98,9 +101,18 @@ extension FileManagerView { SelectedElementSheet( element: $0, onExport: downloadFile, - onDelete: deleteFile + onDelete: { deleteFile($0) } ) } + .alert( + "Directory is not empty", + isPresented: $isForceDeletePresented, + presenting: deletedElement + ) { deletedElement in + Button("Force Delete", role: .destructive) { + deleteFile(deletedElement, force: true) + } + } .fileImporter( isPresented: $isFileImporterPresented, allowedContentTypes: [UTType.item], @@ -179,14 +191,26 @@ extension FileManagerView { } } - private func deleteFile(_ extended: ExtendedElement) { + private func deleteFile( + _ extended: ExtendedElement, + force: Bool = false + ) { + deletedElement = extended isLoading = true defer { isLoading = false } Task { + defer { selectedElement = nil } do { - try await fileManager.delete(extended.type, at: path) + try await fileManager.delete( + extended.type, + at: path, + force: force + ) await load() + } catch let error as RemoteFileManager.Error + where error == .directoryIsNotEmpty && !force { + self.isForceDeletePresented = true } catch { self.error = String(describing: error) } From 07e910ec62dd19bd01b019eb49a09a5ad331d1b7 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:12:48 +0200 Subject: [PATCH 07/16] [FileManager] Remove card from file editor --- Flipper/iOS/UI/FileManager/FileManagerEditor.swift | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Flipper/iOS/UI/FileManager/FileManagerEditor.swift b/Flipper/iOS/UI/FileManager/FileManagerEditor.swift index d388664b..bb1fcca6 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerEditor.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerEditor.swift @@ -30,17 +30,13 @@ extension FileManagerView { } else if isLoading { ProgressView() } else { - Card { - TextEditor(text: $current) - .focused($textFieldFocus) - .font(.system(size: 14, weight: .medium)) - .hideScrollBackground() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(4) - } - .padding(14) + TextEditor(text: $current) + .focused($textFieldFocus) + .font(.system(size: 14, weight: .medium)) + .hideScrollBackground() } } + .padding(14) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.background) .navigationBarBackground(Color.a1) From bb71c14aea27196f2a3570eea5cfb657c9793af6 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:14:06 +0200 Subject: [PATCH 08/16] [FileManager] SDCard with Preview --- .../Peripheral/Sources/RPC/Model/Storage.swift | 5 +++++ .../UI/FileManager/Components/SDCardInfo.swift | 16 +++++++++++----- .../iOS/UI/FileManager/FileManagerListing.swift | 6 +++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift b/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift index 27652cb2..a6bee263 100644 --- a/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift +++ b/Flipper/Packages/Peripheral/Sources/RPC/Model/Storage.swift @@ -5,6 +5,11 @@ public struct StorageSpace: Equatable { public let total: Int public var used: Int { total - free } + + public init(free: Int, total: Int) { + self.free = free + self.total = total + } } public enum Element: Equatable { diff --git a/Flipper/iOS/UI/FileManager/Components/SDCardInfo.swift b/Flipper/iOS/UI/FileManager/Components/SDCardInfo.swift index 15d1fc42..7a48ab30 100644 --- a/Flipper/iOS/UI/FileManager/Components/SDCardInfo.swift +++ b/Flipper/iOS/UI/FileManager/Components/SDCardInfo.swift @@ -5,10 +5,6 @@ extension FileManagerView { struct SDCardInfo: View { let storage: StorageSpace? - init(_ storage: StorageSpace?) { - self.storage = storage - } - var body: some View { HStack(spacing: 32) { VStack(alignment: .leading, spacing: 12) { @@ -32,7 +28,7 @@ extension FileManagerView { .frame(width: 84, height: 84) .foregroundColor(.primary) } - .padding(8) + .padding(12) .background(Color.groupedBackground) .cornerRadius(12) } @@ -97,3 +93,13 @@ fileprivate extension StorageSpace { return CGFloat(used) / CGFloat(total) } } + +#Preview { + VStack { + FileManagerView.SDCardInfo(storage: nil) + + FileManagerView.SDCardInfo(storage: .init(free: 10, total: 40)) + } + .padding(12) + .background(Color.background) +} diff --git a/Flipper/iOS/UI/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift index c5f019b1..7604529e 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerListing.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerListing.swift @@ -38,6 +38,10 @@ extension FileManagerView { : _elements.filter { !$0.name.hasPrefix(".") } } + var storage: StorageSpace? { + device.storageInfo?.external + } + var body: some View { VStack { if let error = error { @@ -47,7 +51,7 @@ extension FileManagerView { } else { List { if path.isRoot { - SDCardInfo(device.storageInfo?.external) + SDCardInfo(storage: storage) } else { NavigationPathView(path: path) } From 18a9cc19a08c0a8d3f14f760ab3bd59c15412939 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:14:36 +0200 Subject: [PATCH 09/16] [FileManager] Gradient for navigation path --- .../SDCardDummy.imageset/Contents.json | 21 +++++ .../SDCardDummy.imageset/SDCardDummy.svg | 3 + .../Components/NavigationPathView.swift | 90 +++++++++++++------ 3 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/SDCardDummy.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/SDCardDummy.imageset/SDCardDummy.svg diff --git a/Flipper/Shared/Assets.xcassets/FileManager/SDCardDummy.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/SDCardDummy.imageset/Contents.json new file mode 100644 index 00000000..a7854f7f --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/SDCardDummy.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "SDCardDummy.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/SDCardDummy.imageset/SDCardDummy.svg b/Flipper/Shared/Assets.xcassets/FileManager/SDCardDummy.imageset/SDCardDummy.svg new file mode 100644 index 00000000..04514fcf --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/SDCardDummy.imageset/SDCardDummy.svg @@ -0,0 +1,3 @@ + + + diff --git a/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift b/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift index 0e0dbba8..6823794a 100644 --- a/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift +++ b/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift @@ -15,36 +15,58 @@ extension FileManagerView { .filter { $0 != "ext" } } + var gradient: LinearGradient { + LinearGradient( + gradient: Gradient( + stops: [ + Gradient.Stop(color: .background, location: 0.2), + Gradient.Stop(color: .clear, location: 1), + ] + ), + startPoint: .leading, + endPoint: .trailing + ) + } + var body: some View { - HStack(spacing: 8) { - Image("SDCard") + HStack(spacing: 0) { + Image("SDCardDummy") .resizable() .renderingMode(.template) - .frame(width: 24, height: 24) + .frame(width: 20, height: 24) .foregroundColor(.primary) .onTapGesture { navigate(to: 0) } - Text("/") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.black30) + ZStack(alignment: .leading) { + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + Spacer() + .frame(width: 0, height: 0) - ScrollViewReader { proxy in - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(components.indices, id: \.self) { index in - Element( - component: components[index], - index: index, - onTap: navigate - ) + ForEach( + components.indices, + id: \.self + ) { index in + Element( + component: components[index], + index: index, + onTap: navigate + ) + .id(index) + } } } - } - .onAppear { - if let lastIndex = components.indices.last { - proxy.scrollTo(lastIndex, anchor: .trailing) + .onAppear { + if let lastIndex = components.indices.last { + proxy.scrollTo(lastIndex, anchor: .trailing) + } } } + + Rectangle() + .fill(gradient) + .frame(width: 8, height: 24) } } } @@ -64,18 +86,36 @@ fileprivate extension FileManagerView.NavigationPathView { var body: some View { HStack(spacing: 8) { - if index != 0 { - Text("/") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.black30) - } + Text("/") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black30) Text(component) .font(.system(size: 16, weight: .bold)) .foregroundColor(.primary) .onTapGesture { onTap(index + 1) } } - .id(index) } } } + +#Preview { + VStack { + FileManagerView.NavigationPathView( + path: Peripheral.Path(string: "/ext/apps") + ) + + FileManagerView.NavigationPathView( + path: Peripheral.Path(string: "/ext/Downloads/2021/08/17") + ) + + FileManagerView.NavigationPathView( + path: Peripheral.Path( + string: "/ext/Downloads/2021/08/17/dummy/test/file" + ) + ) + } + .environment(\.path, .constant(NavigationPath())) + .padding(12) + .background(Color.background) +} From 708b5bff4dc63f20930b20b83fb215552793e033 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:08:30 +0200 Subject: [PATCH 10/16] [FileManager] Remove List component --- .../FileManager/Components/EmptyFolder.swift | 5 +- .../Components/FileListingOptions.swift | 12 ++-- .../Components/FileManagerElements.swift | 49 +++++--------- .../Components/NavigationPathView.swift | 3 +- .../UI/FileManager/FileManagerListing.swift | 65 ++++++++++++++----- 5 files changed, 74 insertions(+), 60 deletions(-) diff --git a/Flipper/iOS/UI/FileManager/Components/EmptyFolder.swift b/Flipper/iOS/UI/FileManager/Components/EmptyFolder.swift index bccf59f1..1f82071f 100644 --- a/Flipper/iOS/UI/FileManager/Components/EmptyFolder.swift +++ b/Flipper/iOS/UI/FileManager/Components/EmptyFolder.swift @@ -25,7 +25,10 @@ extension FileManagerView.FileManagerListing { Spacer() } - .frame(maxWidth: .infinity) } } } + +#Preview { + FileManagerView.FileManagerListing.EmptyFolder(onUpload: {}) +} diff --git a/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift b/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift index 3ad41271..b8a275e3 100644 --- a/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift +++ b/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift @@ -74,12 +74,10 @@ fileprivate extension FileManagerView.FileManagerListing.FileListingOptions { @Binding var settings: FileManagerSettings var body: some View { - Button( - action: { - settings.isHiddenFilesShow.toggle() - isPresented = false - } - ) { + Button(action: { + settings.isHiddenFilesShow.toggle() + isPresented = false + }, label: { HStack(spacing: 8) { ZStack { Circle() @@ -99,7 +97,7 @@ fileprivate extension FileManagerView.FileManagerListing.FileListingOptions { .foregroundColor(.primary) Spacer() } - } + }) .padding(12) } } diff --git a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift index 48c2c7d2..cfcd118e 100644 --- a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift +++ b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift @@ -9,54 +9,35 @@ extension FileManagerView.FileManagerListing { let onTap: (ExtendedElement) -> Void let onDelete: (ExtendedElement) -> Void - let onAction: (ExtendedElement) -> Void + let onSelect: (ExtendedElement) -> Void private let columns = [GridItem(.flexible()), GridItem(.flexible())] var body: some View { - Group { - switch displayType { - case .list: + switch displayType { + case .list: + LazyVStack(spacing: 12) { ForEach(elements) { element in ElementRow( element: element, type: displayType, - onAction: { onAction(element) } + onAction: { onSelect(element) } ) .onTapGesture { onTap(element) } - .swipeActions { - Button(role: .destructive) { - onDelete(element) - } label: { - Image("Delete") - .foregroundColor(.red) - } - .tint(.red.opacity(0.1)) - } } - case .grid: - LazyVGrid(columns: columns, spacing: 12) { - ForEach(elements) { element in - ElementRow( - element: element, - type: displayType, - onAction: { onAction(element) } - ) - .onTapGesture { onTap(element) } - } + } + case .grid: + LazyVGrid(columns: columns, spacing: 12) { + ForEach(elements) { element in + ElementRow( + element: element, + type: displayType, + onAction: { onSelect(element) } + ) + .onTapGesture { onTap(element) } } } } - .listRowSeparator(.hidden) - .listRowInsets( - .init( - top: 0, - leading: 0, - bottom: 0, - trailing: 0 - ) - ) - .listRowBackground(Color.clear) } } } diff --git a/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift b/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift index 6823794a..6ec56717 100644 --- a/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift +++ b/Flipper/iOS/UI/FileManager/Components/NavigationPathView.swift @@ -20,7 +20,7 @@ extension FileManagerView { gradient: Gradient( stops: [ Gradient.Stop(color: .background, location: 0.2), - Gradient.Stop(color: .clear, location: 1), + Gradient.Stop(color: .clear, location: 1) ] ), startPoint: .leading, @@ -69,6 +69,7 @@ extension FileManagerView { .frame(width: 8, height: 24) } } + .padding(4) } private func navigate(to index: Int) { diff --git a/Flipper/iOS/UI/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift index 7604529e..a9b29bcc 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerListing.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerListing.swift @@ -43,32 +43,37 @@ extension FileManagerView { } var body: some View { - VStack { + Group { if let error = error { Text(error) } else if isLoading { ProgressView() } else { - List { - if path.isRoot { - SDCardInfo(storage: storage) - } else { - NavigationPathView(path: path) + FixedScrollView(showsIndicators: false) { + Group { + if path.isRoot { + SDCardInfo(storage: storage) + } else { + NavigationPathView(path: path) + } } + .padding([.horizontal, .top], 14) - if elements.isEmpty { - EmptyFolder(onUpload: showUpload) - } else { - FileManagerElements( - elements: elements, - displayType: settings.displayType, - onTap: navigate, - onDelete: { deleteFile($0) }, - onAction: { selectedElement = $0 } - ) + Group { + if elements.isEmpty { + EmptyFolder(onUpload: showUpload) + } else { + FileManagerElements( + elements: elements, + displayType: settings.displayType, + onTap: navigate, + onDelete: { deleteFile($0) }, + onSelect: { selectedElement = $0 } + ) + } } + .padding(14) } - .listRowSpacing(12) } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -232,3 +237,29 @@ fileprivate extension Peripheral.Path { self == "/ext" } } + +private struct FixedScrollView: View { + let showsIndicators: Bool + let content: () -> Content + + init( + showsIndicators: Bool, + @ViewBuilder content: @escaping () -> Content + ) { + self.showsIndicators = showsIndicators + self.content = content + } + + var body: some View { + GeometryReader { geometry in + ScrollView(showsIndicators: showsIndicators) { + VStack(spacing: 0) { + content() + Spacer() + } + .frame(minHeight: geometry.size.height) + } + .frame(width: geometry.size.width) + } + } +} From 0b0a1cd419ce4bbdbb7c14e969c7886cdf5aa82e Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:52:39 +0200 Subject: [PATCH 11/16] [FileManager] Create file and folder --- .../CreateFile.imageset/Contents.json | 21 ++++++++ .../CreateFile.imageset/CreateFile.svg | 4 ++ .../CreateFolder.imageset/Contents.json | 21 ++++++++ .../CreateFolder.imageset/CreateFolder.svg | 4 ++ .../Components/FileListingOptions.swift | 12 +++++ .../UI/FileManager/FileManagerListing.swift | 48 +++++++++++++++++++ ...Settings.swift => FileManagerModels.swift} | 14 ++++++ 7 files changed, 124 insertions(+) create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/CreateFile.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/CreateFile.imageset/CreateFile.svg create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/CreateFolder.imageset/Contents.json create mode 100644 Flipper/Shared/Assets.xcassets/FileManager/CreateFolder.imageset/CreateFolder.svg rename Flipper/iOS/UI/FileManager/{FileManagerSettings.swift => FileManagerModels.swift} (81%) diff --git a/Flipper/Shared/Assets.xcassets/FileManager/CreateFile.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/CreateFile.imageset/Contents.json new file mode 100644 index 00000000..a8e96ec9 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/CreateFile.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "CreateFile.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/CreateFile.imageset/CreateFile.svg b/Flipper/Shared/Assets.xcassets/FileManager/CreateFile.imageset/CreateFile.svg new file mode 100644 index 00000000..437831ba --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/CreateFile.imageset/CreateFile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Flipper/Shared/Assets.xcassets/FileManager/CreateFolder.imageset/Contents.json b/Flipper/Shared/Assets.xcassets/FileManager/CreateFolder.imageset/Contents.json new file mode 100644 index 00000000..2be9a19c --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/CreateFolder.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "CreateFolder.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Flipper/Shared/Assets.xcassets/FileManager/CreateFolder.imageset/CreateFolder.svg b/Flipper/Shared/Assets.xcassets/FileManager/CreateFolder.imageset/CreateFolder.svg new file mode 100644 index 00000000..5d43e160 --- /dev/null +++ b/Flipper/Shared/Assets.xcassets/FileManager/CreateFolder.imageset/CreateFolder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift b/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift index b8a275e3..f12ca053 100644 --- a/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift +++ b/Flipper/iOS/UI/FileManager/Components/FileListingOptions.swift @@ -5,6 +5,8 @@ extension FileManagerView.FileManagerListing { @Binding var isPresented: Bool @Binding var settings: FileManagerSettings + let createFolder: () -> Void + let createFile: () -> Void let upload: () -> Void var body: some View { @@ -12,6 +14,16 @@ extension FileManagerView.FileManagerListing { Spacer() Card { VStack(alignment: .leading, spacing: 0) { + Option(title: "Create Folder", image: "CreateFolder") { + isPresented = false + createFolder() + } + + Option(title: "Create File", image: "CreateFile") { + isPresented = false + createFile() + } + Option(title: "Upload", image: "Share") { isPresented = false upload() diff --git a/Flipper/iOS/UI/FileManager/FileManagerListing.swift b/Flipper/iOS/UI/FileManager/FileManagerListing.swift index a9b29bcc..ed1bda50 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerListing.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerListing.swift @@ -23,6 +23,10 @@ extension FileManagerView { @State private var selectedElement: ExtendedElement? @State private var deletedElement: ExtendedElement? + // MARK: Create File/Directory + @FocusState var isNameFocused: Bool + @State private var newElement: FileManagerNewElement? + @AppStorage(.fileManagerSettings) private var settings: FileManagerSettings = .init() @@ -59,6 +63,20 @@ extension FileManagerView { } .padding([.horizontal, .top], 14) + if let newElement { + TextField( + newElement.namePlaceholder, + text: Binding( + get: { self.newElement?.name ?? "" }, + set: { self.newElement?.name = $0 } + ) + ) + .textFieldStyle(.roundedBorder) + .onSubmit { submitNewElement() } + .focused($isNameFocused) + .padding([.horizontal, .top], 14) + } + Group { if elements.isEmpty { EmptyFolder(onUpload: showUpload) @@ -103,6 +121,8 @@ extension FileManagerView { FileListingOptions( isPresented: $showOptions, settings: $settings, + createFolder: { newElement(isDirectory: true) }, + createFile: { newElement(isDirectory: false) }, upload: showUpload ) } @@ -229,6 +249,34 @@ extension FileManagerView { private func showUpload() { isFileImporterPresented = true } + + // MARK: Create File/Directory + func newElement(isDirectory: Bool) { + newElement = .init(name: "", isNewDirectory: isDirectory) + isNameFocused = true + } + + func submitNewElement() { + guard let newElement = newElement else { return } + let name = newElement.name + let isNewDirectory = newElement.isNewDirectory + + if !name.isEmpty { + let path = path.appending(name) + let isDirectory = isNewDirectory + Task { + do { + try await fileManager.create( + path: path, + isDirectory: isDirectory) + await load() + } catch { + self.error = String(describing: error) + } + } + } + self.newElement = nil + } } } diff --git a/Flipper/iOS/UI/FileManager/FileManagerSettings.swift b/Flipper/iOS/UI/FileManager/FileManagerModels.swift similarity index 81% rename from Flipper/iOS/UI/FileManager/FileManagerSettings.swift rename to Flipper/iOS/UI/FileManager/FileManagerModels.swift index 6068759d..e40207b5 100644 --- a/Flipper/iOS/UI/FileManager/FileManagerSettings.swift +++ b/Flipper/iOS/UI/FileManager/FileManagerModels.swift @@ -49,3 +49,17 @@ struct FileManagerSettings: Codable, RawRepresentable { try container.encode(displayType, forKey: .displayType) } } + +struct FileManagerNewElement { + var name: String + let isNewDirectory: Bool + + init(name: String, isNewDirectory: Bool) { + self.name = name + self.isNewDirectory = isNewDirectory + } + + var namePlaceholder: String { + "\(isNewDirectory ? "directory" : "file") name" + } +} From cbb74baaece5fb049e0da4f1d79d68de5731d37b Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:48:34 +0200 Subject: [PATCH 12/16] [FileManager] Delete Swipe --- .../FileManager/Components/ElementRow.swift | 170 +++++++++++++++--- .../Components/FileManagerElements.swift | 15 +- 2 files changed, 152 insertions(+), 33 deletions(-) diff --git a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift index 4ca65eb0..d5b5d889 100644 --- a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift +++ b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift @@ -3,41 +3,50 @@ import Core import SwiftUI extension FileManagerView.FileManagerListing { - struct ElementRow: View { + struct ElementRowGrid: View { let element: ExtendedElement - let type: FileManagerSettings.DisplayType - let onAction: () -> Void + let onSelect: () -> Void + let onTap: () -> Void var body: some View { - Group { - switch type { - case .grid: - VStack(alignment: .leading, spacing: 12) { - HStack { - Icon(for: element) - Spacer() - Action(onTap: onAction) - } - Title(for: element) - } - case .list: - HStack(spacing: 12) { - Icon(for: element) - Title(for: element) - Spacer() - Action(onTap: onAction) - } + VStack(alignment: .leading, spacing: 12) { + HStack { + Icon(for: element) + Spacer() + Action(onTap: onSelect) } + Title(for: element) } .padding(12) .background(Color.groupedBackground) .cornerRadius(12) + .onTapGesture { onTap() } + } + } + + struct ElementRowList: View { + let element: ExtendedElement + + let onSelect: () -> Void + let onDelete: () -> Void + let onTap: () -> Void + + var body: some View { + HStack(spacing: 12) { + Icon(for: element) + Title(for: element) + Spacer() + Action(onTap: onSelect) + } + .padding(12) + .background(Color.groupedBackground) + .modifier(SwipeToDeleteModifier(onDelete: onDelete, onTap: onTap)) } } } -fileprivate extension FileManagerView.FileManagerListing.ElementRow { +fileprivate extension FileManagerView.FileManagerListing { struct Icon: View { let element: ExtendedElement @@ -46,10 +55,11 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow { case .directory: return .init("Folder") case .file: - if let item = try? ArchiveItem.Kind(filename: element.name) { + do { + let item = try ArchiveItem.Kind(filename: element.name) return item.icon - } else { - return .init("File") + } catch { + return Image("File") } } } @@ -107,3 +117,113 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow { } } } + +fileprivate extension FileManagerView.FileManagerListing { + struct SwipeToDeleteModifier: ViewModifier { + @State private var offset: CGFloat = 0 + @GestureState private var isDragging: Bool = false + + let onDelete: () -> Void + let onTap: () -> Void + + private var iconSize: Double { 24 } + private var iconPadding: Double { 16 } + + private var deleteThreshold: CGFloat { -(iconSize + iconPadding * 2) } + private var fullDeleteThreshold: CGFloat { -120 } + + private var delay: Double { 0.5 } + private var animation: Animation { .easeOut(duration: delay) } + + private var maxRadius: CGFloat { 12 } + private var radius: CGFloat { + let progress = min(1, abs(offset) / abs(deleteThreshold)) + return maxRadius * (1 - progress) + } + + func body(content: Content) -> some View { + ZStack { + Rectangle() + .foregroundColor(.red.opacity(0.1)) + .cornerRadius(12) + .overlay( + Image("Delete") + .resizable() + .renderingMode(.template) + .foregroundColor(.red) + .frame(width: iconSize, height: iconSize) + .padding(16) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(animation) { + offset = 0 + } + onDelete() + }, + alignment: .trailing + ) + + content + .clipShape( + .rect( + topLeadingRadius: maxRadius, + bottomLeadingRadius: maxRadius, + bottomTrailingRadius: radius, + topTrailingRadius: radius + ) + ) + .offset(x: offset) + .simultaneousGesture( + DragGesture( + minimumDistance: 50, + coordinateSpace: .local + ) + .updating($isDragging) { _, state, _ in + state = true + } + .onChanged { value in + let translation = value.translation.width + if translation <= 0 { + offset = translation + } + } + .onEnded { value in + let translation = value.translation.width + + if translation <= fullDeleteThreshold { + withAnimation(animation) { + offset = -UIScreen.main.bounds.width + } + + Task { @MainActor in + try await Task.sleep(seconds: delay) + onDelete() + + withAnimation(animation) { + offset = 0 + } + } + } else if translation <= deleteThreshold { + withAnimation(animation) { + offset = deleteThreshold + } + } else { + withAnimation(animation) { + offset = 0 + } + } + } + ) + .onTapGesture { + if offset != 0 { + withAnimation(animation) { + offset = 0 + } + } else { + onTap() + } + } + } + } + } +} diff --git a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift index cfcd118e..d05b89ff 100644 --- a/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift +++ b/Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift @@ -18,23 +18,22 @@ extension FileManagerView.FileManagerListing { case .list: LazyVStack(spacing: 12) { ForEach(elements) { element in - ElementRow( + ElementRowList( element: element, - type: displayType, - onAction: { onSelect(element) } + onSelect: { onSelect(element) }, + onDelete: { onDelete(element) }, + onTap: { onTap(element) } ) - .onTapGesture { onTap(element) } } } case .grid: LazyVGrid(columns: columns, spacing: 12) { ForEach(elements) { element in - ElementRow( + ElementRowGrid( element: element, - type: displayType, - onAction: { onSelect(element) } + onSelect: { onSelect(element) }, + onTap: { onTap(element) } ) - .onTapGesture { onTap(element) } } } } From 27960e2fe6dab8c44995c433299b01e3e39b4d16 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:13:32 +0200 Subject: [PATCH 13/16] [FIleManager] Disable action button on swipe --- .../iOS/UI/FileManager/Components/ElementRow.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift index d5b5d889..3130c7e5 100644 --- a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift +++ b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift @@ -26,6 +26,8 @@ extension FileManagerView.FileManagerListing { } struct ElementRowList: View { + @State private var offset: CGFloat = 0 + let element: ExtendedElement let onSelect: () -> Void @@ -38,10 +40,18 @@ extension FileManagerView.FileManagerListing { Title(for: element) Spacer() Action(onTap: onSelect) + .opacity(offset == 0 ? 1 : 0) + .animation(nil, value: offset) } .padding(12) .background(Color.groupedBackground) - .modifier(SwipeToDeleteModifier(onDelete: onDelete, onTap: onTap)) + .modifier( + SwipeToDeleteModifier( + offset: $offset, + onDelete: onDelete, + onTap: onTap + ) + ) } } } @@ -122,6 +132,7 @@ fileprivate extension FileManagerView.FileManagerListing { struct SwipeToDeleteModifier: ViewModifier { @State private var offset: CGFloat = 0 @GestureState private var isDragging: Bool = false + @Binding var offset: CGFloat let onDelete: () -> Void let onTap: () -> Void From 6e427e11be141b6fa483c94123e8797b3003a78c Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:15:00 +0200 Subject: [PATCH 14/16] [FileManager] Remove lag swipes --- .../FileManager/Components/ElementRow.swift | 109 ++++++++++-------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift index 3130c7e5..aede19ea 100644 --- a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift +++ b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift @@ -130,8 +130,6 @@ fileprivate extension FileManagerView.FileManagerListing { fileprivate extension FileManagerView.FileManagerListing { struct SwipeToDeleteModifier: ViewModifier { - @State private var offset: CGFloat = 0 - @GestureState private var isDragging: Bool = false @Binding var offset: CGFloat let onDelete: () -> Void @@ -141,7 +139,7 @@ fileprivate extension FileManagerView.FileManagerListing { private var iconPadding: Double { 16 } private var deleteThreshold: CGFloat { -(iconSize + iconPadding * 2) } - private var fullDeleteThreshold: CGFloat { -120 } + private var fullDeleteThreshold: CGFloat { -160 } private var delay: Double { 0.5 } private var animation: Animation { .easeOut(duration: delay) } @@ -153,26 +151,17 @@ fileprivate extension FileManagerView.FileManagerListing { } func body(content: Content) -> some View { - ZStack { - Rectangle() - .foregroundColor(.red.opacity(0.1)) - .cornerRadius(12) - .overlay( - Image("Delete") - .resizable() - .renderingMode(.template) - .foregroundColor(.red) - .frame(width: iconSize, height: iconSize) - .padding(16) - .contentShape(Rectangle()) - .onTapGesture { - withAnimation(animation) { - offset = 0 - } - onDelete() - }, - alignment: .trailing - ) + ZStack(alignment: .trailing) { + Image("Delete") + .resizable() + .renderingMode(.template) + .foregroundColor(.red) + .frame(width: iconSize, height: iconSize) + .padding(16) + .contentShape(Rectangle()) + .onTapGesture { + deleteAction() + } content .clipShape( @@ -183,43 +172,43 @@ fileprivate extension FileManagerView.FileManagerListing { topTrailingRadius: radius ) ) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.red.opacity(0.1)) + .offset(x: -offset) + ) .offset(x: offset) .simultaneousGesture( DragGesture( - minimumDistance: 50, + minimumDistance: 10, coordinateSpace: .local ) - .updating($isDragging) { _, state, _ in - state = true - } .onChanged { value in let translation = value.translation.width - if translation <= 0 { - offset = translation + + if value.isHorizontal { + if translation <= 0 { + withAnimation(.interactiveSpring()) { + offset = translation + } + } } } .onEnded { value in let translation = value.translation.width - if translation <= fullDeleteThreshold { - withAnimation(animation) { - offset = -UIScreen.main.bounds.width - } - - Task { @MainActor in - try await Task.sleep(seconds: delay) - onDelete() - + if value.isHorizontal { + if translation <= fullDeleteThreshold { + deleteAction() + } else if translation <= deleteThreshold { withAnimation(animation) { - offset = 0 + offset = deleteThreshold } - } - } else if translation <= deleteThreshold { - withAnimation(animation) { - offset = deleteThreshold + } else { + closeAction() } } else { - withAnimation(animation) { + withAnimation(.interactiveSpring()) { offset = 0 } } @@ -227,14 +216,40 @@ fileprivate extension FileManagerView.FileManagerListing { ) .onTapGesture { if offset != 0 { - withAnimation(animation) { - offset = 0 - } + closeAction() } else { onTap() } } } } + + private func closeAction() { + withAnimation(animation) { + offset = 0 + } + } + + private func deleteAction() { + Task { @MainActor in + withAnimation(animation) { + offset = -UIScreen.main.bounds.width + } + try await Task.sleep(seconds: delay) + onDelete() + + withAnimation(animation) { + offset = 0 + } + } + } + } +} + +fileprivate extension DragGesture.Value { + var isHorizontal: Bool { + let horizontalAmount = abs(translation.width) + let verticalAmount = abs(translation.height) + return horizontalAmount > verticalAmount } } From 47084aea5aaa884db1f2e6bc5bb1667281d0a214 Mon Sep 17 00:00:00 2001 From: Programistich <35292229+Programistich@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:41:30 +0200 Subject: [PATCH 15/16] [FileManager] Reset drag when start new swipe --- .../FileManager/Components/ElementRow.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift index aede19ea..9cfeed0a 100644 --- a/Flipper/iOS/UI/FileManager/Components/ElementRow.swift +++ b/Flipper/iOS/UI/FileManager/Components/ElementRow.swift @@ -47,6 +47,7 @@ extension FileManagerView.FileManagerListing { .background(Color.groupedBackground) .modifier( SwipeToDeleteModifier( + element: element, offset: $offset, onDelete: onDelete, onTap: onTap @@ -129,7 +130,16 @@ fileprivate extension FileManagerView.FileManagerListing { } fileprivate extension FileManagerView.FileManagerListing { + final class CurrentElementHolder: ObservableObject { + @MainActor static let shared = CurrentElementHolder() + + @Published var element: ExtendedElement? + } + struct SwipeToDeleteModifier: ViewModifier { + @ObservedObject private var currentHolder = CurrentElementHolder.shared + + let element: ExtendedElement @Binding var offset: CGFloat let onDelete: () -> Void @@ -184,6 +194,10 @@ fileprivate extension FileManagerView.FileManagerListing { coordinateSpace: .local ) .onChanged { value in + if currentHolder.element != element { + currentHolder.element = element + } + let translation = value.translation.width if value.isHorizontal { @@ -221,6 +235,11 @@ fileprivate extension FileManagerView.FileManagerListing { onTap() } } + .onChange(of: currentHolder.element) { new in + if new != element && offset != 0 { + closeAction() + } + } } } From 2577af50dfd4971c73f90052237fedcef897d996 Mon Sep 17 00:00:00 2001 From: Tony Freeman Date: Mon, 24 Feb 2025 11:25:06 +0300 Subject: [PATCH 16/16] Update packages --- .../xcshareddata/swiftpm/Package.resolved | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Flipper/Flipper.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Flipper/Flipper.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 55895e65..cc9c4c4e 100644 --- a/Flipper/Flipper.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Flipper/Flipper.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Countly/countly-sdk-ios.git", "state" : { - "revision" : "e12be8153743a17c1389f80f9f7401c9f7f7de61", - "version" : "24.4.0" + "revision" : "a19d604ae32ccdeafad0c30e60235e221af09e80", + "version" : "24.7.9" } }, { @@ -24,7 +24,7 @@ "location" : "https://github.com/tonyfreeman/firebase-ios-sdk", "state" : { "branch" : "master", - "revision" : "fccc5e45f145baf6c9ac73f9579180efd9334f8d" + "revision" : "f22ed7e37b87328cdd05a08b5a8e6b74a840ad10" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", - "version" : "9.4.0" + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "26c898aed8bed13b8a63057ee26500abbbcb8d55", - "version" : "7.13.1" + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/NetworkImage", "state" : { - "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", - "version" : "6.0.0" + "revision" : "2849f5323265386e200484b0d0f896e73c3411b9", + "version" : "6.0.1" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" + "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version" : "1.6.2" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "9f0c76544701845ad98716f3f6a774a892152bcb", - "version" : "1.26.0" + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" } }, {