diff --git a/Empusa.xcodeproj/project.pbxproj b/Empusa.xcodeproj/project.pbxproj index 8151c71..45a7ac1 100644 --- a/Empusa.xcodeproj/project.pbxproj +++ b/Empusa.xcodeproj/project.pbxproj @@ -8,13 +8,15 @@ /* Begin PBXBuildFile section */ 3F21D30B2ADADC7300B6A5D9 /* EmpusaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F21D30A2ADADC7300B6A5D9 /* EmpusaApp.swift */; }; - 3F21D30D2ADADC7300B6A5D9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F21D30C2ADADC7300B6A5D9 /* ContentView.swift */; }; + 3F21D30D2ADADC7300B6A5D9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F21D30C2ADADC7300B6A5D9 /* MainView.swift */; }; 3F21D30F2ADADC7400B6A5D9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3F21D30E2ADADC7400B6A5D9 /* Assets.xcassets */; }; - 3F21D3122ADADC7400B6A5D9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3F21D3112ADADC7400B6A5D9 /* Preview Assets.xcassets */; }; 3F21D31D2ADADC7400B6A5D9 /* EmpusaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F21D31C2ADADC7400B6A5D9 /* EmpusaTests.swift */; }; 3F21D33C2ADD442800B6A5D9 /* EmpusaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F21D33B2ADD442800B6A5D9 /* EmpusaModel.swift */; }; 3F21D33F2ADD504900B6A5D9 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F21D33E2ADD504900B6A5D9 /* HeaderView.swift */; }; 3F21D3412ADD77E700B6A5D9 /* EmpusaKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3F21D3402ADD77E700B6A5D9 /* EmpusaKit */; }; + 3F3B176E2ADDE117000283AE /* DestinationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3B176D2ADDE117000283AE /* DestinationView.swift */; }; + 3F3B17702ADDE16A000283AE /* ResourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3B176F2ADDE16A000283AE /* ResourcesView.swift */; }; + 3F3B17722ADDE25D000283AE /* DataProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3B17712ADDE25D000283AE /* DataProgressView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -30,17 +32,17 @@ /* Begin PBXFileReference section */ 3F21D3072ADADC7300B6A5D9 /* Empusa.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Empusa.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3F21D30A2ADADC7300B6A5D9 /* EmpusaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmpusaApp.swift; sourceTree = ""; }; - 3F21D30C2ADADC7300B6A5D9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 3F21D30C2ADADC7300B6A5D9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 3F21D30E2ADADC7400B6A5D9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 3F21D3112ADADC7400B6A5D9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 3F21D3132ADADC7400B6A5D9 /* Empusa.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Empusa.entitlements; sourceTree = ""; }; 3F21D3182ADADC7400B6A5D9 /* EmpusaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EmpusaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3F21D31C2ADADC7400B6A5D9 /* EmpusaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmpusaTests.swift; sourceTree = ""; }; - 3F21D3262ADADC7400B6A5D9 /* EmpusaUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmpusaUITests.swift; sourceTree = ""; }; - 3F21D3282ADADC7400B6A5D9 /* EmpusaUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmpusaUITestsLaunchTests.swift; sourceTree = ""; }; - 3F21D3372ADADEBE00B6A5D9 /* Services */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Services; sourceTree = ""; }; + 3F21D3372ADADEBE00B6A5D9 /* EmpusaKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = EmpusaKit; sourceTree = ""; }; 3F21D33B2ADD442800B6A5D9 /* EmpusaModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmpusaModel.swift; sourceTree = ""; }; 3F21D33E2ADD504900B6A5D9 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; + 3F3B176D2ADDE117000283AE /* DestinationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationView.swift; sourceTree = ""; }; + 3F3B176F2ADDE16A000283AE /* ResourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesView.swift; sourceTree = ""; }; + 3F3B17712ADDE25D000283AE /* DataProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProgressView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -68,7 +70,6 @@ 3F21D3362ADADE8B00B6A5D9 /* Packages */, 3F21D3092ADADC7300B6A5D9 /* Empusa */, 3F21D31B2ADADC7400B6A5D9 /* Empusa Tests */, - 3F21D3252ADADC7400B6A5D9 /* Empusa UITests */, 3F21D3082ADADC7300B6A5D9 /* Products */, 3F21D3382ADADED500B6A5D9 /* Frameworks */, ); @@ -86,25 +87,15 @@ 3F21D3092ADADC7300B6A5D9 /* Empusa */ = { isa = PBXGroup; children = ( + 3F21D3132ADADC7400B6A5D9 /* Empusa.entitlements */, 3F21D30A2ADADC7300B6A5D9 /* EmpusaApp.swift */, 3F21D33B2ADD442800B6A5D9 /* EmpusaModel.swift */, 3F21D33D2ADD503100B6A5D9 /* UI Components */, - 3F21D3132ADADC7400B6A5D9 /* Empusa.entitlements */, 3F21D3352ADADC8D00B6A5D9 /* Resources */, - 3F21D30C2ADADC7300B6A5D9 /* ContentView.swift */, - 3F21D3102ADADC7400B6A5D9 /* Preview Content */, ); path = Empusa; sourceTree = ""; }; - 3F21D3102ADADC7400B6A5D9 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 3F21D3112ADADC7400B6A5D9 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; 3F21D31B2ADADC7400B6A5D9 /* Empusa Tests */ = { isa = PBXGroup; children = ( @@ -113,15 +104,6 @@ path = "Empusa Tests"; sourceTree = ""; }; - 3F21D3252ADADC7400B6A5D9 /* Empusa UITests */ = { - isa = PBXGroup; - children = ( - 3F21D3262ADADC7400B6A5D9 /* EmpusaUITests.swift */, - 3F21D3282ADADC7400B6A5D9 /* EmpusaUITestsLaunchTests.swift */, - ); - path = "Empusa UITests"; - sourceTree = ""; - }; 3F21D3352ADADC8D00B6A5D9 /* Resources */ = { isa = PBXGroup; children = ( @@ -133,7 +115,7 @@ 3F21D3362ADADE8B00B6A5D9 /* Packages */ = { isa = PBXGroup; children = ( - 3F21D3372ADADEBE00B6A5D9 /* Services */, + 3F21D3372ADADEBE00B6A5D9 /* EmpusaKit */, ); path = Packages; sourceTree = ""; @@ -148,7 +130,11 @@ 3F21D33D2ADD503100B6A5D9 /* UI Components */ = { isa = PBXGroup; children = ( + 3F21D30C2ADADC7300B6A5D9 /* MainView.swift */, 3F21D33E2ADD504900B6A5D9 /* HeaderView.swift */, + 3F3B176D2ADDE117000283AE /* DestinationView.swift */, + 3F3B176F2ADDE16A000283AE /* ResourcesView.swift */, + 3F3B17712ADDE25D000283AE /* DataProgressView.swift */, ); path = "UI Components"; sourceTree = ""; @@ -237,7 +223,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3F21D3122ADADC7400B6A5D9 /* Preview Assets.xcassets in Resources */, 3F21D30F2ADADC7400B6A5D9 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -256,8 +241,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3F21D30D2ADADC7300B6A5D9 /* ContentView.swift in Sources */, + 3F3B17702ADDE16A000283AE /* ResourcesView.swift in Sources */, + 3F21D30D2ADADC7300B6A5D9 /* MainView.swift in Sources */, + 3F3B17722ADDE25D000283AE /* DataProgressView.swift in Sources */, 3F21D30B2ADADC7300B6A5D9 /* EmpusaApp.swift in Sources */, + 3F3B176E2ADDE117000283AE /* DestinationView.swift in Sources */, 3F21D33C2ADD442800B6A5D9 /* EmpusaModel.swift in Sources */, 3F21D33F2ADD504900B6A5D9 /* HeaderView.swift in Sources */, ); @@ -409,16 +397,18 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Empusa/Preview Content\""; DEVELOPMENT_TEAM = D97MJ3844Q; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "εmpusa"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = nl.trevisa.diego.Empusa; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -436,16 +426,18 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Empusa/Preview Content\""; DEVELOPMENT_TEAM = D97MJ3844Q; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "εmpusa"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = nl.trevisa.diego.Empusa; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Empusa/ContentView.swift b/Empusa/ContentView.swift deleted file mode 100644 index 0b08129..0000000 --- a/Empusa/ContentView.swift +++ /dev/null @@ -1,107 +0,0 @@ -import SwiftUI -import EmpusaKit - -struct ContentView: View { - @ObservedObject var model: EmpusaModel - - var body: some View { - VStack { - HeaderView() - - HStack { - GroupBox("Destination") { - VStack(alignment: .leading, spacing: 16) { - HStack { - Picker(selection: $model.selectedExternalStorage) { - ForEach(model.externalStorages, id: \.self) { storage in - Text(storage.name) - } - } label: { - EmptyView() - } - - Button(action: { - model.loadExternalStorages() - }) { - Image(systemName: "arrow.triangle.2.circlepath") - } - } - - if model.selectedExternalStorage != .none { - Text(model.selectedExternalStorage.formattedCapacity) - } - - Divider() - - VStack(alignment: .center) { - Button("Backup SD Card") {} - Button("Restore SD Card") {} - } - .frame(maxWidth: .infinity) - - Spacer() - } - .padding(4) - } - .disabled(model.isProcessing) - .frame(width: 240) - - GroupBox("Resources") { - VStack(alignment: .leading) { - ForEach(model.availableResources, id: \.self) { resource in - Toggle(isOn: .init(get: { - model.selectedResources.contains(resource) - }, set: { selected in - if selected { - model.selectedResources.append(resource) - } else { - guard let index = model.selectedResources.firstIndex(of: resource) else { - return - } - model.selectedResources.remove(at: index) - } - })) { - Text(resource.rawValue.capitalized) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - - Spacer() - } - .padding(8) - .frame(maxWidth: .infinity) - } - .disabled(model.isProcessing) - } - .padding() - - VStack { - if let progressData = model.progress { - ProgressView( - progressData.title, - value: progressData.progress, - total: progressData.total - ) - } - - HStack { - Spacer() - - Button("Download") { - model.execute() - } - .disabled(!model.canStartProcess) - } - } - .padding() - - Spacer() - } - } -} - -#Preview { - ContentView( - model: EmpusaModel() - ) -} diff --git a/Empusa/EmpusaApp.swift b/Empusa/EmpusaApp.swift index f66743f..06d283b 100644 --- a/Empusa/EmpusaApp.swift +++ b/Empusa/EmpusaApp.swift @@ -4,11 +4,11 @@ import SwiftUI struct EmpusaApp: App { var body: some Scene { WindowGroup { - ContentView( + MainView( model: EmpusaModel() ) -// .frame(width: 680, height: 500) + .frame(width: 680, height: 400) } - .windowResizability(.contentMinSize) + .windowResizability(.contentSize) } } diff --git a/Empusa/EmpusaModel.swift b/Empusa/EmpusaModel.swift index 73ec30e..ce64ac9 100644 --- a/Empusa/EmpusaModel.swift +++ b/Empusa/EmpusaModel.swift @@ -2,6 +2,16 @@ import Combine import EmpusaKit import OSLog +struct AlertData { + let title: String + let message: String + + init(error: Error) { + title = "Error" + message = error.localizedDescription + } +} + @MainActor final class EmpusaModel: ObservableObject { // MARK: - Public variables @@ -11,12 +21,26 @@ final class EmpusaModel: ObservableObject { @Published var availableResources: [SwitchResource] = SwitchResource.allCases @Published var selectedResources: [SwitchResource] = SwitchResource.allCases + @Published var isExporting: Bool = false + @Published var exportingFile: ZipFile? + @Published var isProcessing: Bool = false @Published var progress: ProgressData? - @Published var error: Error? + + @Published var isShowingAlert: Bool = false + @Published var alertData: AlertData? { + didSet { + isShowingAlert = alertData != nil + } + } var canStartProcess: Bool { - selectedExternalStorage != .none && !selectedResources.isEmpty && !isProcessing + selectedExternalStorage != .none && !selectedResources.isEmpty && !isProcessing && !isExporting + } + + lazy var backupCompletion: ((Result) -> Void) = { [weak self] result in + self?.exportingFile = nil + self?.isExporting = false } // MARK: - Dependencies @@ -42,7 +66,7 @@ final class EmpusaModel: ObservableObject { } catch { logger.error("Failed to load external storages: \(error.localizedDescription)") selectedExternalStorage = .none - self.error = error + alertData = .init(error: error) } } @@ -54,11 +78,9 @@ final class EmpusaModel: ObservableObject { isProcessing = true let progressSubject = CurrentValueSubject(nil) - let progressCancellable = progressSubject + progressSubject .receive(on: RunLoop.main) - .sink { progressData in - self.progress = progressData - } + .assign(to: &$progress) do { try await contentManager.download( @@ -67,12 +89,33 @@ final class EmpusaModel: ObservableObject { progressSubject: progressSubject ) } catch { - self.error = error + alertData = .init(error: error) } - self.progress = nil isProcessing = false - progressCancellable.cancel() + self.progress = nil + } + } + + func backup() { + guard selectedExternalStorage != .none else { return } + + Task(priority: .background) { [weak self] in + guard let self else { return } + + let progressSubject = CurrentValueSubject(nil) + progressSubject + .receive(on: RunLoop.main) + .assign(to: &$progress) + + let zipFile = try await contentManager.backupStorage( + at: selectedExternalStorage.url, + progressSubject: progressSubject + ) + + exportingFile = zipFile + isExporting = true + self.progress = nil } } diff --git a/Empusa/Preview Content/Preview Assets.xcassets/Contents.json b/Empusa/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Empusa/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Empusa/UI Components/DataProgressView.swift b/Empusa/UI Components/DataProgressView.swift new file mode 100644 index 0000000..d19f45a --- /dev/null +++ b/Empusa/UI Components/DataProgressView.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct DataProgressView: View { + @ObservedObject var model: EmpusaModel + + var body: some View { + if let progressData = model.progress { + ProgressView( + progressData.title, + value: progressData.progress, + total: progressData.total + ) + } + } +} + +#Preview { + DataProgressView( + model: EmpusaModel() + ) +} diff --git a/Empusa/UI Components/DestinationView.swift b/Empusa/UI Components/DestinationView.swift new file mode 100644 index 0000000..5517325 --- /dev/null +++ b/Empusa/UI Components/DestinationView.swift @@ -0,0 +1,60 @@ +import SwiftUI + +struct DestinationView: View { + @ObservedObject var model: EmpusaModel + + var body: some View { + GroupBox("Destination") { + VStack(alignment: .leading, spacing: 16) { + HStack { + Picker(selection: $model.selectedExternalStorage) { + ForEach(model.externalStorages, id: \.self) { storage in + if storage == .none { + Text(storage.name) + } else { + Text("\(storage.name) (\(storage.formattedCapacity))") + } + } + } label: { + EmptyView() + } + + Button(action: { + model.loadExternalStorages() + }) { + Image(systemName: "arrow.triangle.2.circlepath") + } + } + + Divider() + + VStack(alignment: .center) { + Button("Backup SD Card") { + model.backup() + } + + Button("Restore SD Card") {} + } + .frame(maxWidth: .infinity) + + Spacer() + } + .padding(4) + } + .disabled(model.isProcessing) + .frame(width: 240) + .fileExporter( + isPresented: $model.isExporting, + document: model.exportingFile, + contentType: .zip, + defaultFilename: "backup.zip", + onCompletion: model.backupCompletion + ) + } +} + +#Preview { + DestinationView( + model: EmpusaModel() + ) +} diff --git a/Empusa/UI Components/MainView.swift b/Empusa/UI Components/MainView.swift new file mode 100644 index 0000000..9a01b4a --- /dev/null +++ b/Empusa/UI Components/MainView.swift @@ -0,0 +1,46 @@ +import SwiftUI +import EmpusaKit + +struct MainView: View { + @ObservedObject var model: EmpusaModel + + var body: some View { + VStack { + HeaderView() + + HStack { + DestinationView(model: model) + ResourcesView(model: model) + } + .padding() + + VStack { + DataProgressView(model: model) + + HStack { + Spacer() + + Button("Run") { + model.execute() + } + .disabled(!model.canStartProcess) + } + } + .padding() + + Spacer() + } + .alert(isPresented: $model.isShowingAlert, content: { + .init( + title: Text(model.alertData?.title ?? ""), + message: Text(model.alertData?.message ?? "") + ) + }) + } +} + +#Preview { + MainView( + model: EmpusaModel() + ) +} diff --git a/Empusa/UI Components/ResourcesView.swift b/Empusa/UI Components/ResourcesView.swift new file mode 100644 index 0000000..7a5e95d --- /dev/null +++ b/Empusa/UI Components/ResourcesView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct ResourcesView: View { + @ObservedObject var model: EmpusaModel + + var body: some View { + GroupBox("Resources") { + VStack(alignment: .leading) { + ForEach(model.availableResources, id: \.self) { resource in + Toggle(isOn: .init(get: { + model.selectedResources.contains(resource) + }, set: { selected in + if selected { + model.selectedResources.append(resource) + } else { + guard let index = model.selectedResources.firstIndex(of: resource) else { return } + model.selectedResources.remove(at: index) + } + })) { + Text(resource.rawValue.capitalized) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + + Spacer() + } + .padding(8) + .frame(maxWidth: .infinity) + } + .disabled(model.isProcessing) + } +} + +#Preview { + ResourcesView( + model: EmpusaModel() + ) +} diff --git a/Packages/Services/.gitignore b/Packages/EmpusaKit/.gitignore similarity index 100% rename from Packages/Services/.gitignore rename to Packages/EmpusaKit/.gitignore diff --git a/Packages/Services/Package.swift b/Packages/EmpusaKit/Package.swift similarity index 96% rename from Packages/Services/Package.swift rename to Packages/EmpusaKit/Package.swift index e32f5f6..e4bbabe 100644 --- a/Packages/Services/Package.swift +++ b/Packages/EmpusaKit/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "EmpusaKit", platforms: [ - .macOS(.v14) + .macOS(.v13) ], products: [ .library( diff --git a/Packages/Services/Sources/EmpusaKit/Client/Client.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift similarity index 100% rename from Packages/Services/Sources/EmpusaKit/Client/Client.swift rename to Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift diff --git a/Packages/Services/Sources/EmpusaKit/ContentManager.swift b/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift similarity index 54% rename from Packages/Services/Sources/EmpusaKit/ContentManager.swift rename to Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift index 5f2e079..5d44f28 100644 --- a/Packages/Services/Sources/EmpusaKit/ContentManager.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift @@ -1,97 +1,17 @@ import Combine import Foundation -enum SwitchResouceSource { - case github(URL, assetPrefix: String) - case link(URL) -} - -public enum SwitchResource: String, CaseIterable { - case hekate - case atmosphere - case sigpatches - case tilfoil - - var source: SwitchResouceSource { - switch self { - case .hekate: - .github( - .init(string: "https://api.github.com/repos/CTCaer/hekate/releases/latest")!, - assetPrefix: "hekate_ctcaer_" - ) - case .atmosphere: - .github( - .init(string: "https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest")!, - assetPrefix: "atmosphere-" - ) - case .sigpatches: - .link(.init(string: "https://sigmapatches.coomer.party/sigpatches.zip")!) - case .tilfoil: - .link(.init(string: "https://tinfoil.media/repo/Tinfoil Self Installer [050000BADDAD0000][16.0][v2].zip")!) - } - } - - var assetFileName: String { - switch self { - case .hekate: - "hekate.zip" - case .atmosphere: - "atmosphere.zip" - case .sigpatches: - "sigpatches.zip" - case .tilfoil: - "tinfoil.zip" - } - } -} - -extension SwitchResource { - private var fileManager: FileManager { - .default - } - - func handleAsset( - at location: URL, - destination: URL, - progressSubject: CurrentValueSubject - ) throws { - switch self { - case .hekate: - let contentPaths = try fileManager.contentsOfDirectory(atPath: location.path()) - guard let bootloaderPath = contentPaths.first(where: { $0 == "bootloader" }) else { return } - - fileManager.merge( - atPath: location.appending(path: bootloaderPath).path(), - toPath: destination.appending(path: bootloaderPath).path(), - progressSubject: progressSubject - ) - - case .atmosphere, .sigpatches, .tilfoil: - fileManager.merge( - atPath: location.path(), - toPath: destination.path(), - progressSubject: progressSubject - ) - } - } -} - -public struct ProgressData { - public let title: String - public let progress: Double - public let total: Double -} - -public enum ContentManagerError: Error { - -} - public protocol ContentManagerProtocol { func download( resources: [SwitchResource], into destination: URL, progressSubject: CurrentValueSubject ) async throws + + func backupStorage( + at location: URL, + progressSubject: CurrentValueSubject + ) async throws -> ZipFile } public final class ContentManager: ContentManagerProtocol { @@ -119,13 +39,13 @@ public final class ContentManager: ContentManagerProtocol { downloadProgressSubject, unzipProgressSubject, mergeProgressSubject - ).sink { (title, downloadProgress, unzipProgress, mergeProgress) in - progressSubject.send(.init( + ).map { (title, downloadProgress, unzipProgress, mergeProgress) in + ProgressData( title: title, progress: accumulatedProgress + downloadProgress + unzipProgress + mergeProgress, total: totalProgress - )) - } + ) + }.assign(to: \.value, on: progressSubject) // Download asset progressTitleSubject.send("Downloading \(resource.assetFileName)...") @@ -163,4 +83,26 @@ public final class ContentManager: ContentManagerProtocol { cancellable.cancel() } } + + public func backupStorage( + at location: URL, + progressSubject: CurrentValueSubject + ) async throws -> ZipFile { + let zipProgressSubject = CurrentValueSubject(0) + + let cancellable = zipProgressSubject.map { zipProgress in + ProgressData( + title: "Zipping storage contents...", + progress: zipProgress, + total: 1 + ) + } + .assign(to: \.value, on: progressSubject) + + defer { + cancellable.cancel() + } + + return try storageService.zipDirectory(at: location, progressSubject: zipProgressSubject) + } } diff --git a/Packages/Services/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift similarity index 100% rename from Packages/Services/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift rename to Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift diff --git a/Packages/Services/Sources/EmpusaKit/Models/GitHubRelease.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/GitHubRelease.swift similarity index 100% rename from Packages/Services/Sources/EmpusaKit/Models/GitHubRelease.swift rename to Packages/EmpusaKit/Sources/EmpusaKit/Models/GitHubRelease.swift diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/ProgressData.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/ProgressData.swift new file mode 100644 index 0000000..c23f174 --- /dev/null +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/ProgressData.swift @@ -0,0 +1,5 @@ +public struct ProgressData { + public let title: String + public let progress: Double + public let total: Double +} diff --git a/Packages/Services/Sources/EmpusaKit/Models/Storage.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/Storage.swift similarity index 100% rename from Packages/Services/Sources/EmpusaKit/Models/Storage.swift rename to Packages/EmpusaKit/Sources/EmpusaKit/Models/Storage.swift diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift new file mode 100644 index 0000000..43ea1c4 --- /dev/null +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift @@ -0,0 +1,77 @@ +import Foundation +import Combine + +enum SwitchResouceSource { + case github(URL, assetPrefix: String) + case link(URL) +} + +public enum SwitchResource: String, CaseIterable { + case hekate + case atmosphere + case sigpatches + case tilfoil + + var source: SwitchResouceSource { + switch self { + case .hekate: + .github( + .init(string: "https://api.github.com/repos/CTCaer/hekate/releases/latest")!, + assetPrefix: "hekate_ctcaer_" + ) + case .atmosphere: + .github( + .init(string: "https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest")!, + assetPrefix: "atmosphere-" + ) + case .sigpatches: + .link(.init(string: "https://sigmapatches.coomer.party/sigpatches.zip")!) + case .tilfoil: + .link(.init(string: "https://tinfoil.media/repo/Tinfoil%20Self%20Installer%20%5B050000BADDAD0000%5D%5B16.0%5D%5Bv2%5D.zip")!) + } + } + + var assetFileName: String { + switch self { + case .hekate: + "hekate.zip" + case .atmosphere: + "atmosphere.zip" + case .sigpatches: + "sigpatches.zip" + case .tilfoil: + "tinfoil.zip" + } + } +} + +extension SwitchResource { + private var fileManager: FileManager { + .default + } + + func handleAsset( + at location: URL, + destination: URL, + progressSubject: CurrentValueSubject + ) throws { + switch self { + case .hekate: + let contentPaths = try fileManager.contentsOfDirectory(atPath: location.path()) + guard let bootloaderPath = contentPaths.first(where: { $0 == "bootloader" }) else { return } + + fileManager.merge( + atPath: location.appending(path: bootloaderPath).path(), + toPath: destination.appending(path: bootloaderPath).path(), + progressSubject: progressSubject + ) + + case .atmosphere, .sigpatches, .tilfoil: + fileManager.merge( + atPath: location.path(), + toPath: destination.path(), + progressSubject: progressSubject + ) + } + } +} diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/ZipFile.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/ZipFile.swift new file mode 100644 index 0000000..419d752 --- /dev/null +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/ZipFile.swift @@ -0,0 +1,20 @@ +import UniformTypeIdentifiers +import SwiftUI + +public struct ZipFile: FileDocument { + public static var readableContentTypes = [UTType.zip] + + public let url: URL + + public init(url: URL) { + self.url = url + } + + public init(configuration: ReadConfiguration) throws { + fatalError("Needs to be implemented") + } + + public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + try .init(url: url) + } +} diff --git a/Packages/Services/Sources/EmpusaKit/Services/AssetService.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift similarity index 100% rename from Packages/Services/Sources/EmpusaKit/Services/AssetService.swift rename to Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift diff --git a/Packages/Services/Sources/EmpusaKit/Services/StorageService.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift similarity index 78% rename from Packages/Services/Sources/EmpusaKit/Services/StorageService.swift rename to Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift index 0ffd0b9..51b45cb 100644 --- a/Packages/Services/Sources/EmpusaKit/Services/StorageService.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift @@ -8,6 +8,7 @@ public protocol StorageServiceProtocol { func saveFile(data: Data, fileName: String) async throws -> URL func unzipFile(at location: URL, progressSubject: CurrentValueSubject) throws -> URL func removeItem(at path: URL) + func zipDirectory(at location: URL, progressSubject: CurrentValueSubject) throws -> ZipFile } final public class StorageService: StorageServiceProtocol { @@ -89,4 +90,32 @@ final public class StorageService: StorageServiceProtocol { logger.error("StorageService: \(error.localizedDescription)") } } + + public func zipDirectory( + at location: URL, + progressSubject: CurrentValueSubject + ) throws -> ZipFile { + let paths = try fileManager + .contentsOfDirectory( + at: location, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles + ) + + let destinationPath = tempDirectoryPath + .appending(component: "backup.zip") + + try Zip.zipFiles( + paths: paths, + zipFilePath: destinationPath, + password: nil, + compression: .BestCompression + ) { progress in + progressSubject.send(progress) + } + + return ZipFile( + url: destinationPath + ) + } } diff --git a/Packages/Services/Tests/EmpusaKitTests/ServicesTests.swift b/Packages/EmpusaKit/Tests/EmpusaKitTests/ServicesTests.swift similarity index 100% rename from Packages/Services/Tests/EmpusaKitTests/ServicesTests.swift rename to Packages/EmpusaKit/Tests/EmpusaKitTests/ServicesTests.swift diff --git a/README.md b/README.md index ae4c620..460fb5c 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# Empusa \ No newline at end of file +# εmpusa +This is a macOS tool for downloading/updating/configuring latest resources into the SD card for a hacked Nintendo Switch. + +![εmpusa](Resources/main.png) + +## Work in progress +This is a pretty early version, mostly a P.O.C. Several resources and features are yet to be added very soon. This README shall also be improved. Feel free to contribute to the project! + +## Why εmpusa? +It's a reference to [Hekate](https://github.com/CTCaer/hekate). Empusa was the name of Hecate's daughter (or servant?). \ No newline at end of file diff --git a/Resources/main.png b/Resources/main.png new file mode 100644 index 0000000..132631e Binary files /dev/null and b/Resources/main.png differ