From 6fa171de6370d094f5c1eeaf380ff3985b741fc9 Mon Sep 17 00:00:00 2001 From: Amin Rahmani Date: Sun, 22 Dec 2024 01:00:31 +0330 Subject: [PATCH 1/2] added file manager to show shared and imported ipa files into app --- App/Extensions/Notification+NewIPAAdded.swift | 10 ++ App/File Manager/DocumnetPicker.swift | 47 ++++++ .../File Details/FileDetailView.swift | 131 ++++++++++++++++ App/File Manager/File Item/FileListItem.swift | 25 ++++ .../File List/FileManagerView.swift | 102 +++++++++++++ .../File List/FileManagerViewModel.swift | 76 ++++++++++ App/File Manager/IPAFileManager.swift | 94 ++++++++++++ App/File Manager/IpaFile.swift | 21 +++ App/Navigation/Panel.swift | 7 + .../Products List/ProductsListViewModel.swift | 1 + App/SibaroApp.swift | 5 + App/SibaroAppServices.swift | 15 ++ Applications/Utilities/IPAUtilities.swift | 141 ++++++++++++++++++ Sibaro-Info.plist | 46 +++++- Sibaro.xcodeproj/project.pbxproj | 85 +++++++++++ .../xcshareddata/swiftpm/Package.resolved | 41 +++-- 16 files changed, 830 insertions(+), 17 deletions(-) create mode 100644 App/Extensions/Notification+NewIPAAdded.swift create mode 100644 App/File Manager/DocumnetPicker.swift create mode 100644 App/File Manager/File Details/FileDetailView.swift create mode 100644 App/File Manager/File Item/FileListItem.swift create mode 100644 App/File Manager/File List/FileManagerView.swift create mode 100644 App/File Manager/File List/FileManagerViewModel.swift create mode 100644 App/File Manager/IPAFileManager.swift create mode 100644 App/File Manager/IpaFile.swift create mode 100644 Applications/Utilities/IPAUtilities.swift diff --git a/App/Extensions/Notification+NewIPAAdded.swift b/App/Extensions/Notification+NewIPAAdded.swift new file mode 100644 index 0000000..12024b0 --- /dev/null +++ b/App/Extensions/Notification+NewIPAAdded.swift @@ -0,0 +1,10 @@ +// +// Notification+NewIPAAdded.swift +// Sibaro +// +// Created by AminRa on 9/24/1403 AP. +// + +extension Notification.Name { + static let onNewIpaAdded = Notification.Name("on-new-ipa-added") +} diff --git a/App/File Manager/DocumnetPicker.swift b/App/File Manager/DocumnetPicker.swift new file mode 100644 index 0000000..c975950 --- /dev/null +++ b/App/File Manager/DocumnetPicker.swift @@ -0,0 +1,47 @@ +// +// DocumnetPicker.swift +// Sibaro +// +// Created by AminRa on 9/15/1403 AP. +// + +import SwiftUI +import UniformTypeIdentifiers + +struct DocumentPicker: UIViewControllerRepresentable { + let completion: (Result) -> Void + let fileTypes: [UTType] = [.init(filenameExtension: "ipa")!] + + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { + let picker = UIDocumentPickerViewController(forOpeningContentTypes: fileTypes) + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(completion: completion) + } + + class Coordinator: NSObject, UIDocumentPickerDelegate { + let completion: (Result) -> Void + + init(completion: @escaping (Result) -> Void) { + self.completion = completion + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url = urls.first else { + completion(.failure(NSError(domain: "No file selected.", code: 0, userInfo: nil))) + return + } + completion(.success(url)) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + completion(.failure(NSError(domain: "User cancelled file selection.", code: 0, userInfo: nil))) + } + } +} + diff --git a/App/File Manager/File Details/FileDetailView.swift b/App/File Manager/File Details/FileDetailView.swift new file mode 100644 index 0000000..edae63b --- /dev/null +++ b/App/File Manager/File Details/FileDetailView.swift @@ -0,0 +1,131 @@ +import SwiftUI + +struct FileDetailView: View { + let ipa: IPAFile + let ipaInfo: IPAInformation + @Environment(\.openURL) var openURL + + var body: some View { + VStack { + + VStack(spacing: 16) { + if let appIcon = ipa.appIcon { + appIcon + .resizable() + .frame(width: 120, height: 120) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .shadow(radius: 10) + } else { + Image(systemName: "app.fill") + .resizable() + .frame(width: 120, height: 120) + .foregroundColor(.gray) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .shadow(radius: 10) + } + + Text(ipaInfo.name) + .font(.title) + .fontWeight(.bold) + .multilineTextAlignment(.center) + } + .padding(.top, 20) + + + HStack(spacing: 20) { + Button(action: signIPA) { + Label("Sign", systemImage: "pencil") + } + + Button(action: installIPA) { + Label("Install", systemImage: "arrow.down.circle") + } + } + .buttonStyle(.bordered) + .padding() + + Divider() + .padding(.horizontal) + + + List { + Section(header: Text("App Details").font(.headline)) { + DetailRow(label: "Bundle Identifier", value: ipaInfo.bundleIdentifier) + DetailRow(label: "Version", value: ipaInfo.version) + DetailRow(label: "Build", value: ipaInfo.build) + DetailRow(label: "Minimum OS Version", value: ipaInfo.minimumOSVersion) + DetailRow(label: "Device Family", value: ipaInfo.deviceFamily.joined(separator: ", ")) + } + + if let entitlements = ipaInfo.entitlements, !entitlements.isEmpty { + Section(header: Text("Entitlements").font(.headline)) { + ForEach(entitlements.keys.sorted(), id: \.self) { key in + DetailRow(label: key, value: "\(entitlements[key] ?? "N/A")") + } + } + } + + if !ipaInfo.supportedLanguages.isEmpty { + Section(header: Text("Supported Languages").font(.headline)) { + ForEach(ipaInfo.supportedLanguages, id: \.self) { language in + Text(language.capitalized) + .foregroundColor(.primary) + .padding(.vertical, 2) + } + } + } + } + .listStyle(InsetGroupedListStyle()) + } + .navigationTitle("IPA Information") + .navigationBarTitleDisplayMode(.inline) + .background(Color(.systemGroupedBackground).edgesIgnoringSafeArea(.all)) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + shareIPA() + } label: { + Image(systemName: "square.and.arrow.up") + } + } + } + } + + private func shareIPA() { + let ipaURL = ipa.fileUrl + let activityVC = UIActivityViewController(activityItems: [ipaURL], applicationActivities: nil) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + windowScene.windows.first?.rootViewController?.present(activityVC, animated: true) + } + } + + private func signIPA() { + print("Sign button tapped") + } + + private func installIPA() { + print("Install button tapped") + } +} + +extension FileDetailView { + struct DetailRow: View { + let label: String + let value: String + + var body: some View { + HStack { + Text(label) + .font(.body) + .foregroundColor(.secondary) + Spacer() + Text(value) + .font(.body) + .multilineTextAlignment(.trailing) + .foregroundColor(.primary) + } + .padding(.vertical, 4) + } + } +} diff --git a/App/File Manager/File Item/FileListItem.swift b/App/File Manager/File Item/FileListItem.swift new file mode 100644 index 0000000..4eebe31 --- /dev/null +++ b/App/File Manager/File Item/FileListItem.swift @@ -0,0 +1,25 @@ +// +// FileListItem.swift +// Sibaro +// +// Created by AminRa on 9/15/1403 AP. +// + +import SwiftUI + +struct FileListItem: View { + let ipaFile: IPAFile + + var body: some View { + HStack { + (ipaFile.appIcon ?? Image(systemName: "doc.circle")) + .resizable() + .frame(width: 40, height: 40) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Text(ipaFile.name) + .font(.headline) + .padding(.leading, 10) + } + } +} diff --git a/App/File Manager/File List/FileManagerView.swift b/App/File Manager/File List/FileManagerView.swift new file mode 100644 index 0000000..57aaf9a --- /dev/null +++ b/App/File Manager/File List/FileManagerView.swift @@ -0,0 +1,102 @@ +// +// FileManagerView.swift +// Sibaro +// +// Created by AminRa on 9/4/1403 AP. +// + +import SwiftUI + +struct FileManagerView: View { + + @StateObject var viewModel: ViewModel +#if os(iOS) + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + var layout: [GridItem] { + if horizontalSizeClass == .compact { + [GridItem(.flexible(), alignment: .top)] + } else { + [GridItem(.adaptive(minimum: 360), alignment: .top)] + } + } +#else + var layout = [GridItem(.adaptive(minimum: 360), alignment: .top)] +#endif + + init() { + self._viewModel = StateObject(wrappedValue: ViewModel()) + } + + @State private var showAlert = false + @State private var showingDocumentPicker = false + @State private var statusMessage: String? + + var body: some View { + NavigationView { + VStack { + List { + ForEach(viewModel.files) { ipa in + NavigationLink( + destination: FileDetailView( + ipa: ipa, + ipaInfo: viewModel.getIPAInformation(ipa) + ) + ) { + FileListItem(ipaFile: ipa) + .swipeActions(allowsFullSwipe: false) { + Button(role: .destructive) { + viewModel.deleteIPA(ipa) + } label: { + Label("Delete", systemImage: "trash") + } + + Button { + viewModel.signIPA(ipa) + } label: { + Label("Sign", systemImage: "pencil") + } + + Button { + viewModel.installIPA(ipa) + } label: { + Label("Install", systemImage: "arrow.down.circle") + } + .tint(.green) + } + } + } + } + } + .navigationTitle("Files") + .sheet(isPresented: $showingDocumentPicker) { + DocumentPicker { result in + switch result { + case .success(let url): + viewModel.saveFileToCurrentDirectory(fileURL: url) + case .failure(let error): + statusMessage = "Error: \(error.localizedDescription)" + } + } + }.alert("", isPresented: $showAlert) { + Button("Dismiss", role: .cancel, action: { + showAlert.toggle() + statusMessage = nil + }) + }.onReceive(viewModel.alertSubject.eraseToAnyPublisher()) { message in + statusMessage = message + showAlert.toggle() + } + }.toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + showingDocumentPicker = true + }) { + Image(systemName: "plus") + .font(.system(size: 12, weight: .bold)) + .padding() + } + } + } + } +} diff --git a/App/File Manager/File List/FileManagerViewModel.swift b/App/File Manager/File List/FileManagerViewModel.swift new file mode 100644 index 0000000..0849061 --- /dev/null +++ b/App/File Manager/File List/FileManagerViewModel.swift @@ -0,0 +1,76 @@ +// +// FileManagerViewModel.swift +// Sibaro +// +// Created by AminRa on 9/4/1403 AP. +// + +import Foundation +import Combine + +extension FileManagerView { + class ViewModel: BaseViewModel { + + private var currentDirectory: URL + @Published var files: [IPAFile] = [] + @Injected(\.ipaFileManager) var ipaFileManager + @Injected(\.notificationCenter) var notificationCenter + + var alertSubject: PassthroughSubject = PassthroughSubject() + + init(currentDirectory: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!) { + self.currentDirectory = currentDirectory + super.init() + loadFiles() + notificationCenter.addObserver(forName: .onNewIpaAdded, object: nil, queue: nil) { [weak self] notif in + if let url = notif.object as? URL { + self?.saveFileToCurrentDirectory(fileURL: url) + } + } + } + + deinit { + notificationCenter.removeObserver(self, name: .onNewIpaAdded, object: nil) + } + + + private func loadFiles() { + self.files = ipaFileManager.loadIPAFiles() + } + + func saveFileToCurrentDirectory(fileURL: URL) { + do { + let ipaFile = try ipaFileManager.saveIPA(fromSource: fileURL) + self.files.append(ipaFile) + } catch let err { + alertSubject.send(err.rawValue) + } + + } + + private func filterAndSort() { + + } + + func deleteIPA(_ ipa: IPAFile) { + do { + try ipaFileManager.deleteIPA(ipa) + files.removeAll { $0.id == ipa.id } + } catch { + alertSubject.send("Error deleting IPA file: \(error)") + } + } + + func signIPA(_ ipa: IPAFile) { + //TODO: Sign IPA + } + + func installIPA(_ ipa: IPAFile) { + //TODO: Install IPA + } + + func getIPAInformation(_ ipaFile: IPAFile) -> IPAInformation { + IPAUtilities.extractInformation(from: ipaFile.fileUrl, with: ipaFileManager.fileManager) ?? .unknown + } + } +} diff --git a/App/File Manager/IPAFileManager.swift b/App/File Manager/IPAFileManager.swift new file mode 100644 index 0000000..a6846f1 --- /dev/null +++ b/App/File Manager/IPAFileManager.swift @@ -0,0 +1,94 @@ +// +// IPAFileManager.swift +// Sibaro +// +// Created by AminRa on 9/22/1403 AP. +// + +import Foundation + +protocol IPAFileManager { + var fileManager: FileManager { set get } + func handleIncomingFile(url: URL) + func loadIPAFiles() -> [IPAFile] + func deleteIPA(_ ipa: IPAFile) throws + func saveIPA(fromSource fileURL: URL) throws(IPAFileError) -> IPAFile +} + +enum IPAFileError: String, Error { + case fileExists = "File already exists" + case destinationDirectoryNotAvailable = "Destination directory not available" + case ipaFileNotAvailable = "IPA file not available" + case unknownError = "Unknown error" +} + +class SBRIPAFileManager: IPAFileManager, ObservableObject { + + @Injected(\.fileManager) var fileManager + @Injected(\.notificationCenter) var notificationCenter + + private var documentsDirectory: URL? { + fileManager.urls(for: .documentDirectory, in: .userDomainMask).first + } + + func handleIncomingFile(url: URL) { + guard let documentsDirectory else { + print("Failed to locate documents directory.") + return + } + + let destinationURL = documentsDirectory.appendingPathComponent(url.lastPathComponent) + do { + try fileManager.copyItem(at: url, to: destinationURL) + notificationCenter.post(name: .onNewIpaAdded, object: destinationURL) + } catch { + print("Error handling incoming file: \(error)") + } + } + + func loadIPAFiles() -> [IPAFile] { + let fileManager = FileManager.default + if let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { + do { + let files = try fileManager.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil) + return files + .filter { $0.pathExtension == "ipa" } + .map { IPAFile(name: $0.deletingPathExtension().lastPathComponent, fileUrl: $0, appIcon: IPAUtilities.extractAppIcon(from: $0, with: fileManager)) } + } catch { + print("Error loading IPA files: \(error)") + return [] + } + } + return [] + } + + func deleteIPA(_ ipa: IPAFile) throws { + do { + try fileManager.removeItem(at: ipa.fileUrl) + } catch { + throw error + } + } + + func saveIPA(fromSource fileURL: URL) throws(IPAFileError) -> IPAFile { + guard let documentsDirectory else { + throw .destinationDirectoryNotAvailable + } + let destinationURL = documentsDirectory.appendingPathComponent(fileURL.lastPathComponent) + if fileManager.fileExists(atPath: destinationURL.path) { + throw .fileExists + } + + do { + try fileManager.copyItem(at: fileURL, to: destinationURL) + if let ipaFile = IPAUtilities.ipa(from: destinationURL, with: fileManager) { + return ipaFile + } else { + throw IPAFileError.ipaFileNotAvailable + } + } catch let err { + print("Error while trying to load file: \(err.localizedDescription)") + throw .unknownError + } + } +} diff --git a/App/File Manager/IpaFile.swift b/App/File Manager/IpaFile.swift new file mode 100644 index 0000000..94a67fc --- /dev/null +++ b/App/File Manager/IpaFile.swift @@ -0,0 +1,21 @@ +// +// IpaFile.swift +// Sibaro +// +// Created by AminRa on 9/16/1403 AP. +// + +import SwiftUI + +struct IPAFile: Identifiable { + let id = UUID() + let name: String + let fileUrl: URL + let appIcon: Image? + + init (name: String, fileUrl: URL, appIcon: Image? = nil) { + self.name = name + self.fileUrl = fileUrl + self.appIcon = appIcon + } +} diff --git a/App/Navigation/Panel.swift b/App/Navigation/Panel.swift index 40d64a3..3910b82 100644 --- a/App/Navigation/Panel.swift +++ b/App/Navigation/Panel.swift @@ -11,6 +11,7 @@ enum Panel: CaseIterable { case apps case games case profile + case files var title: String { switch self { @@ -20,6 +21,8 @@ enum Panel: CaseIterable { return "Games" case .profile: return "Profile" + case .files: + return "Files" } } @@ -31,6 +34,8 @@ enum Panel: CaseIterable { return "gamecontroller" case .profile: return "person.crop.circle" + case .files: + return "folder" } } @@ -42,6 +47,8 @@ enum Panel: CaseIterable { ProductsListView(type: .game) case .profile: ProfileView() + case .files: + FileManagerView() } } } diff --git a/App/Products/Products List/ProductsListViewModel.swift b/App/Products/Products List/ProductsListViewModel.swift index 3b53852..8e9c7f1 100644 --- a/App/Products/Products List/ProductsListViewModel.swift +++ b/App/Products/Products List/ProductsListViewModel.swift @@ -6,6 +6,7 @@ // import Foundation +import ZIPFoundation extension ProductsListView { @MainActor diff --git a/App/SibaroApp.swift b/App/SibaroApp.swift index 4a78c03..a03b9ba 100644 --- a/App/SibaroApp.swift +++ b/App/SibaroApp.swift @@ -12,11 +12,16 @@ struct SibaroApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject var i18n: I18nService = I18nService() + @StateObject var ipaFileManager: SBRIPAFileManager = SBRIPAFileManager() var body: some Scene { WindowGroup { MainView() .environmentObject(i18n) + .environmentObject(ipaFileManager) + .onOpenURL { url in + ipaFileManager.handleIncomingFile(url: url) + } } #if os(macOS) .defaultSize(width: 1000, height: 650) diff --git a/App/SibaroAppServices.swift b/App/SibaroAppServices.swift index be5a355..3aaae2a 100644 --- a/App/SibaroAppServices.swift +++ b/App/SibaroAppServices.swift @@ -28,6 +28,10 @@ extension Container { var update: Factory { Factory(self) { UpdateService() } } + + var ipaFileManager: Factory { + Factory(self) { SBRIPAFileManager() } + } } @@ -46,6 +50,17 @@ extension Container { } } +//MARK: - System intances +extension Container { + var fileManager: Factory { + Factory(self) { FileManager.default } + } + + var notificationCenter: Factory { + Factory(self) { NotificationCenter.default } + } +} + // MARK: - Functions extension Container { var openURL: Factory<(_ url: URL) -> ()> { diff --git a/Applications/Utilities/IPAUtilities.swift b/Applications/Utilities/IPAUtilities.swift new file mode 100644 index 0000000..b290602 --- /dev/null +++ b/Applications/Utilities/IPAUtilities.swift @@ -0,0 +1,141 @@ +// +// IPAUtilities.swift +// Sibaro +// +// Created by AminRa on 9/16/1403 AP. +// + +import SwiftUI + +struct IPAUtilities { + static func extractAppIcon(from ipaUrl: URL, with fileManager: FileManager) -> Image? { + let fileManager = FileManager.default + let tempDir = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + do { + try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true) + try fileManager.unzipItem(at: ipaUrl, to: tempDir) + + guard let appBundle = try fileManager.contentsOfDirectory(at: tempDir.appendingPathComponent("Payload"), includingPropertiesForKeys: nil).first(where: { $0.pathExtension == "app" }) else { + return nil + } + + let appIconFiles = try fileManager.contentsOfDirectory(at: appBundle, includingPropertiesForKeys: nil) + .filter { $0.lastPathComponent.contains("AppIcon") && $0.pathExtension == "png" } + + if let bestIcon = appIconFiles.sorted(by: { $0.lastPathComponent.count > $1.lastPathComponent.count }).first { + return Image(uiImage: UIImage(contentsOfFile: bestIcon.path) ?? UIImage()) + } + } catch { + print("Error extracting app icon: \(error)") + } + + try? fileManager.removeItem(at: tempDir) + return nil + } + + static func ipa(from url: URL, with fileManager: FileManager) -> IPAFile? { + let appIcon = IPAUtilities.extractAppIcon(from: url, with: fileManager) + let ipaFile = IPAFile(name: url.deletingPathExtension().lastPathComponent, fileUrl: url, appIcon: appIcon) + return ipaFile + } + + static func extractInformation(from url: URL, with fileManager: FileManager) -> IPAInformation? { + let tempDir = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + do { + try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true) + try fileManager.unzipItem(at: url, to: tempDir) + + guard let appBundle = try fileManager.contentsOfDirectory(at: tempDir.appendingPathComponent("Payload"), includingPropertiesForKeys: nil).first(where: { $0.pathExtension == "app" }) else { + print("App bundle not found.") + return nil + } + + let infoPlistURL = appBundle.appendingPathComponent("Info.plist") + let plistData = try Data(contentsOf: infoPlistURL) + guard let plist = try PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any] else { + print("Failed to parse Info.plist.") + return nil + } + + let name = plist["CFBundleName"] as? String ?? "Unknown" + let bundleIdentifier = plist["CFBundleIdentifier"] as? String ?? "Unknown" + let version = plist["CFBundleShortVersionString"] as? String ?? "Unknown" + let build = plist["CFBundleVersion"] as? String ?? "Unknown" + let minimumOSVersion = plist["MinimumOSVersion"] as? String ?? "Unknown" + let deviceFamily = (plist["UIDeviceFamily"] as? [Int])?.map { + $0 == 1 ? "iPhone" : "iPad" + } ?? [] + + let supportedLanguages = try fileManager.contentsOfDirectory(at: appBundle, includingPropertiesForKeys: nil) + .filter { $0.pathExtension == "lproj" } + .map { $0.deletingPathExtension().lastPathComponent } + + let entitlements = extractEntitlements(from: appBundle.appendingPathComponent("embedded.mobileprovision")) + + return IPAInformation( + name: name, + bundleIdentifier: bundleIdentifier, + version: version, + build: build, + minimumOSVersion: minimumOSVersion, + deviceFamily: deviceFamily, + entitlements: entitlements, + supportedLanguages: supportedLanguages + ) + } catch { + print("Error extracting IPA: \(error)") + } + + try? fileManager.removeItem(at: tempDir) + return nil + } + + static func extractEntitlements(from provisioningURL: URL) -> [String: Any]? { + guard let provisioningData = try? Data(contentsOf: provisioningURL), + let provisioningContent = String(data: provisioningData, encoding: .utf8) else { + return nil + } + + let plistStart = provisioningContent.range(of: "")?.upperBound + guard let start = plistStart, let end = plistEnd else { return nil } + + let plistString = provisioningContent[start.. - CFBundleName - $(APP_NAME) - CFBundleShortVersionString - $(MARKETING_VERSION) + CFBundleDocumentTypes + + + CFBundleTypeName + IPA File + LSHandlerRank + Owner + LSItemContentTypes + + com.apple.itunes.ipa + + + IsDebug $(RUNTIME_IS_DEBUG) + NSUserNotificationsUsageDescription + If you want to be informed about your app's update please accept push notification permission. OneSignalAppID $(ONESIGNAL_APP_ID) SentryDSN $(SENTRY_DSN) - NSUserNotificationsUsageDescription - If you want to be informed about your app's update please accept push notification permission. UIBackgroundModes remote-notification + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + public.archive + + UTTypeDescription + IPA File + UTTypeIdentifier + com.apple.itunes.ipa + UTTypeTagSpecification + + public.filename-extension + + ipa + + public.mime-type + + application/octet-stream + + + + diff --git a/Sibaro.xcodeproj/project.pbxproj b/Sibaro.xcodeproj/project.pbxproj index 0918d13..3be344a 100644 --- a/Sibaro.xcodeproj/project.pbxproj +++ b/Sibaro.xcodeproj/project.pbxproj @@ -46,6 +46,16 @@ 363A1BDF2A8C8DF500148CBB /* ProductsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363A1BDE2A8C8DF500148CBB /* ProductsListViewModel.swift */; }; 363A1BE32A8CA8E700148CBB /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363A1BE22A8CA8E700148CBB /* ProfileViewModel.swift */; }; 363A1BE62A8D25A300148CBB /* ProductDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363A1BE52A8D25A300148CBB /* ProductDetailsViewModel.swift */; }; + 367AF55A2D01DAA50065A3B9 /* FileManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF5572D01DAA50065A3B9 /* FileManagerView.swift */; }; + 367AF55B2D01DAA50065A3B9 /* FileManagerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF5582D01DAA50065A3B9 /* FileManagerViewModel.swift */; }; + 367AF55D2D01DAF40065A3B9 /* DocumnetPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF55C2D01DAF40065A3B9 /* DocumnetPicker.swift */; }; + 367AF5602D01DB800065A3B9 /* FileDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF55F2D01DB800065A3B9 /* FileDetailView.swift */; }; + 367AF5642D0203210065A3B9 /* FileListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF5632D0203210065A3B9 /* FileListItem.swift */; }; + 367AF5692D02616B0065A3B9 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 367AF5682D02616B0065A3B9 /* ZIPFoundation */; }; + 367AF56B2D0261DC0065A3B9 /* IpaFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF56A2D0261DC0065A3B9 /* IpaFile.swift */; }; + 367AF56D2D026D340065A3B9 /* IPAUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF56C2D026D340065A3B9 /* IPAUtilities.swift */; }; + 367AF56F2D0B3DFE0065A3B9 /* IPAFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF56E2D0B3DFE0065A3B9 /* IPAFileManager.swift */; }; + 367AF5712D0DFAFB0065A3B9 /* Notification+NewIPAAdded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 367AF5702D0DFAEC0065A3B9 /* Notification+NewIPAAdded.swift */; }; 36B7CE962A8FCBE1000ABD7A /* Password.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B7CE952A8FCBE1000ABD7A /* Password.swift */; }; 36FE9D052A8E4BC300A71B12 /* ChangePasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FE9D042A8E4BC300A71B12 /* ChangePasswordView.swift */; }; 36FE9D072A8E4BD000A71B12 /* ChangePasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FE9D062A8E4BD000A71B12 /* ChangePasswordViewModel.swift */; }; @@ -172,6 +182,15 @@ 363A1BDE2A8C8DF500148CBB /* ProductsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsListViewModel.swift; sourceTree = ""; }; 363A1BE22A8CA8E700148CBB /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; 363A1BE52A8D25A300148CBB /* ProductDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailsViewModel.swift; sourceTree = ""; }; + 367AF5572D01DAA50065A3B9 /* FileManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerView.swift; sourceTree = ""; }; + 367AF5582D01DAA50065A3B9 /* FileManagerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerViewModel.swift; sourceTree = ""; }; + 367AF55C2D01DAF40065A3B9 /* DocumnetPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumnetPicker.swift; sourceTree = ""; }; + 367AF55F2D01DB800065A3B9 /* FileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDetailView.swift; sourceTree = ""; }; + 367AF5632D0203210065A3B9 /* FileListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListItem.swift; sourceTree = ""; }; + 367AF56A2D0261DC0065A3B9 /* IpaFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IpaFile.swift; sourceTree = ""; }; + 367AF56C2D026D340065A3B9 /* IPAUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAUtilities.swift; sourceTree = ""; }; + 367AF56E2D0B3DFE0065A3B9 /* IPAFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAFileManager.swift; sourceTree = ""; }; + 367AF5702D0DFAEC0065A3B9 /* Notification+NewIPAAdded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+NewIPAAdded.swift"; sourceTree = ""; }; 36B7CE952A8FCBE1000ABD7A /* Password.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Password.swift; sourceTree = ""; }; 36FE9D042A8E4BC300A71B12 /* ChangePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordView.swift; sourceTree = ""; }; 36FE9D062A8E4BD000A71B12 /* ChangePasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordViewModel.swift; sourceTree = ""; }; @@ -245,6 +264,7 @@ 11E619552AE3CF16001F353B /* OneSignalFramework in Frameworks */, C62BF8012A8A3059002CFFF9 /* MarkdownUI in Frameworks */, 11EA59242AF6DA7A004E40AE /* (null) in Frameworks */, + 367AF5692D02616B0065A3B9 /* ZIPFoundation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -370,6 +390,7 @@ 117ED6E42A8B743400775CEC /* Private Headers */, 117ED6EA2A8B743400775CEC /* DirectoryReader */, 117ED6F02A8B743400775CEC /* Invocator */, + 367AF56C2D026D340065A3B9 /* IPAUtilities.swift */, ); path = Utilities; sourceTree = ""; @@ -487,6 +508,44 @@ path = "Product Details"; sourceTree = ""; }; + 367AF5592D01DAA50065A3B9 /* File Manager */ = { + isa = PBXGroup; + children = ( + 367AF5612D01DB990065A3B9 /* File List */, + 367AF5662D0203370065A3B9 /* File Item */, + 367AF55E2D01DB690065A3B9 /* File Details */, + 367AF55C2D01DAF40065A3B9 /* DocumnetPicker.swift */, + 367AF56A2D0261DC0065A3B9 /* IpaFile.swift */, + 367AF56E2D0B3DFE0065A3B9 /* IPAFileManager.swift */, + ); + path = "File Manager"; + sourceTree = ""; + }; + 367AF55E2D01DB690065A3B9 /* File Details */ = { + isa = PBXGroup; + children = ( + 367AF55F2D01DB800065A3B9 /* FileDetailView.swift */, + ); + path = "File Details"; + sourceTree = ""; + }; + 367AF5612D01DB990065A3B9 /* File List */ = { + isa = PBXGroup; + children = ( + 367AF5572D01DAA50065A3B9 /* FileManagerView.swift */, + 367AF5582D01DAA50065A3B9 /* FileManagerViewModel.swift */, + ); + path = "File List"; + sourceTree = ""; + }; + 367AF5662D0203370065A3B9 /* File Item */ = { + isa = PBXGroup; + children = ( + 367AF5632D0203210065A3B9 /* FileListItem.swift */, + ); + path = "File Item"; + sourceTree = ""; + }; 36FE9D032A8E4B9D00A71B12 /* Change Password */ = { isa = PBXGroup; children = ( @@ -532,6 +591,7 @@ C604EB202A8932E900B18E0E /* User */, C604EB1A2A89314900B18E0E /* Products */, C604EB102A89309700B18E0E /* Resources */, + 367AF5592D01DAA50065A3B9 /* File Manager */, 11C24C002AF652A60060020A /* Config.swift */, 03FDA2F62A8C110A003D192D /* SibaroAppServices.swift */, C604EB012A892FDA00B18E0E /* SibaroApp.swift */, @@ -657,6 +717,7 @@ C667B4D42A89872800B2427D /* Extensions */ = { isa = PBXGroup; children = ( + 367AF5702D0DFAEC0065A3B9 /* Notification+NewIPAAdded.swift */, C61B1ADB2A8E805800F3AD27 /* NSTextView+Extensions.swift */, C667B4D52A89873400B2427D /* String+Extensions.swift */, C60FFD732AA8ECD400A51D38 /* View+Extensions.swift */, @@ -720,6 +781,7 @@ C67979782A8AA6E00091BE25 /* NukeUI */, 11E619542AE3CF16001F353B /* OneSignalFramework */, CF776C1F205844B1BEF00C35 /* Sentry */, + 367AF5682D02616B0065A3B9 /* ZIPFoundation */, ); productName = Sibaro; productReference = C604EAFE2A892FDA00B18E0E /* Seebaro Dev.app */; @@ -759,6 +821,7 @@ C67979732A8AA6E00091BE25 /* XCRemoteSwiftPackageReference "Nuke" */, 11E619512AE3CF16001F353B /* XCRemoteSwiftPackageReference "OneSignal-iOS-SDK" */, 2FF9CD89667E4E78A5DD055A /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + 367AF5672D02616B0065A3B9 /* XCRemoteSwiftPackageReference "ZIPFoundation" */, ); productRefGroup = C604EAFF2A892FDA00B18E0E /* Products */; projectDirPath = ""; @@ -824,6 +887,7 @@ C604EB372A8939BB00B18E0E /* RequestError.swift in Sources */, C6CF976A2A8BDB090035210F /* HapticFeedback.swift in Sources */, C604EB502A89546F00B18E0E /* EmptyStateView.swift in Sources */, + 367AF56F2D0B3DFE0065A3B9 /* IPAFileManager.swift in Sources */, 03FDA30A2A8C169A003D192D /* Locking.swift in Sources */, 03FDA3242A8DD4D3003D192D /* SingleAxisGeometryReader.swift in Sources */, C604EB522A895EC000B18E0E /* ProductItemView.swift in Sources */, @@ -836,6 +900,7 @@ C67E59A12ABB5964009FCC86 /* Shimmer.swift in Sources */, 117ED6F42A8B743400775CEC /* ApplicationService.swift in Sources */, C604EB442A893D3500B18E0E /* ProductsEndpoint.swift in Sources */, + 367AF56B2D0261DC0065A3B9 /* IpaFile.swift in Sources */, 03FDA31F2A8D470F003D192D /* Container.swift in Sources */, C604EB3F2A893ADB00B18E0E /* Product.swift in Sources */, C604EB192A8930C400B18E0E /* MainView.swift in Sources */, @@ -849,6 +914,7 @@ C604EB382A8939BB00B18E0E /* Endpoint.swift in Sources */, 03E248D52A8A75FE00DDE525 /* Date+DecodeString.swift in Sources */, C60FFD742AA8ECD400A51D38 /* View+Extensions.swift in Sources */, + 367AF5602D01DB800065A3B9 /* FileDetailView.swift in Sources */, 03FDA2F72A8C110A003D192D /* SibaroAppServices.swift in Sources */, 03FDA3112A8C1A48003D192D /* ViewModel.swift in Sources */, C61B1ADC2A8E805800F3AD27 /* NSTextView+Extensions.swift in Sources */, @@ -860,19 +926,24 @@ C604EB422A893B6600B18E0E /* Errors.swift in Sources */, 11D5D8E52A8BBC6300A3C22B /* Dictionary+Apps.swift in Sources */, C662A5562AB30130001A8FEE /* ScreenshotPortraitView.swift in Sources */, + 367AF55A2D01DAA50065A3B9 /* FileManagerView.swift in Sources */, + 367AF55B2D01DAA50065A3B9 /* FileManagerViewModel.swift in Sources */, 03FDA3182A8D0C1C003D192D /* ProductItemViewModel.swift in Sources */, C667B4D62A89873400B2427D /* String+Extensions.swift in Sources */, 11EA59272AF6DB8B004E40AE /* UpdateService.swift in Sources */, C64E692B2AB257BD0057A8F7 /* SettingsItemView.swift in Sources */, C667B4D32A897F1300B2427D /* ProductDetailsView.swift in Sources */, C60FFD762AA8ED6900A51D38 /* ScreenshotView.swift in Sources */, + 367AF5712D0DFAFB0065A3B9 /* Notification+NewIPAAdded.swift in Sources */, C662A5542AB3008F001A8FEE /* ScreenshotLandscapeView.swift in Sources */, 363A1BDF2A8C8DF500148CBB /* ProductsListViewModel.swift in Sources */, 117ED6F52A8B743400775CEC /* SystemApplication.swift in Sources */, + 367AF55D2D01DAF40065A3B9 /* DocumnetPicker.swift in Sources */, C604EB1C2A89316C00B18E0E /* ProductsListView.swift in Sources */, 474D63722AAC4FD60003AF6D /* ObservableScrollView.swift in Sources */, C604EB182A8930C400B18E0E /* TabNavigation.swift in Sources */, 363A1BE32A8CA8E700148CBB /* ProfileViewModel.swift in Sources */, + 367AF56D2D026D340065A3B9 /* IPAUtilities.swift in Sources */, 03FDA31E2A8D470F003D192D /* Injections.swift in Sources */, C604EB342A8939BB00B18E0E /* HTTPContentType.swift in Sources */, 117ED7012A8B87D000775CEC /* String+VersionComparison.swift in Sources */, @@ -880,6 +951,7 @@ C604EB172A8930C400B18E0E /* Sidebar.swift in Sources */, C604EB272A89337400B18E0E /* VisualEffectBlur.swift in Sources */, C604EB352A8939BB00B18E0E /* HTTPMethod.swift in Sources */, + 367AF5642D0203210065A3B9 /* FileListItem.swift in Sources */, C60FFD782AA8EDBC00A51D38 /* ImagePreviewerView.swift in Sources */, C604EB242A89333800B18E0E /* LoginView.swift in Sources */, 03E248DD2A8A9C4F00DDE525 /* I18nService.swift in Sources */, @@ -1229,6 +1301,14 @@ minimumVersion = 8.0.0; }; }; + 367AF5672D02616B0065A3B9 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/weichsel/ZIPFoundation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.19; + }; + }; C604EB4C2A894AE400B18E0E /* XCRemoteSwiftPackageReference "SimpleKeychain" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/auth0/SimpleKeychain"; @@ -1266,6 +1346,11 @@ package = 11E619512AE3CF16001F353B /* XCRemoteSwiftPackageReference "OneSignal-iOS-SDK" */; productName = OneSignalFramework; }; + 367AF5682D02616B0065A3B9 /* ZIPFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = 367AF5672D02616B0065A3B9 /* XCRemoteSwiftPackageReference "ZIPFoundation" */; + productName = ZIPFoundation; + }; C604EB4D2A894AE400B18E0E /* SimpleKeychain */ = { isa = XCSwiftPackageProductDependency; package = C604EB4C2A894AE400B18E0E /* XCRemoteSwiftPackageReference "SimpleKeychain" */; diff --git a/Sibaro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sibaro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fc7c22e..00ddfed 100644 --- a/Sibaro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sibaro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "14a57a9480f3e93a6ed8b6f8eb7efdfd11afbbb4f150d4111fa0cfc7e879a487", "pins" : [ { "identity" : "networkimage", "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/NetworkImage", "state" : { - "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", - "version" : "6.0.0" + "revision" : "2849f5323265386e200484b0d0f896e73c3411b9", + "version" : "6.0.1" } }, { @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke", "state" : { - "revision" : "3f666f120b63ea7de57d42e9a7c9b47f8e7a290b", - "version" : "12.1.6" + "revision" : "0ead44350d2737db384908569c012fe67c421e4d", + "version" : "12.8.0" } }, { @@ -32,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa/", "state" : { - "revision" : "008325304ada69fa32aa7aeba967b65984f30569", - "version" : "8.14.2" + "revision" : "56bfb7e723c76614be4c0861ee820ccbaed14c6d", + "version" : "8.41.0" } }, { @@ -41,8 +42,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/auth0/SimpleKeychain", "state" : { - "revision" : "f082494aab8056139a4fe234c920a9f92f681aff", - "version" : "1.1.0" + "revision" : "b694f155907b189bc82e93586695a26f558c742f", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark", + "state" : { + "revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53", + "version" : "0.5.0" } }, { @@ -50,10 +60,19 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/swift-markdown-ui", "state" : { - "revision" : "5df8a4adedd6ae4eb2455ef60ff75183984daeb8", - "version" : "2.2.0" + "revision" : "5f613358148239d0292c0cef674a3c2314737f9e", + "version" : "2.4.1" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation.git", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" } } ], - "version" : 2 + "version" : 3 } From 72c5fb357173745bf63bd64151537dbebcde1c21 Mon Sep 17 00:00:00 2001 From: Amin Rahmani Date: Tue, 24 Dec 2024 23:42:45 +0330 Subject: [PATCH 2/2] added empty files list message --- .../File List/FileManagerView.swift | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/App/File Manager/File List/FileManagerView.swift b/App/File Manager/File List/FileManagerView.swift index 57aaf9a..8d207b2 100644 --- a/App/File Manager/File List/FileManagerView.swift +++ b/App/File Manager/File List/FileManagerView.swift @@ -35,35 +35,41 @@ struct FileManagerView: View { var body: some View { NavigationView { VStack { - List { - ForEach(viewModel.files) { ipa in - NavigationLink( - destination: FileDetailView( - ipa: ipa, - ipaInfo: viewModel.getIPAInformation(ipa) - ) - ) { - FileListItem(ipaFile: ipa) - .swipeActions(allowsFullSwipe: false) { - Button(role: .destructive) { - viewModel.deleteIPA(ipa) - } label: { - Label("Delete", systemImage: "trash") + if viewModel.files.isEmpty { + Text("The are no files yet\nuse **+** to add files") + .multilineTextAlignment(.center) + .font(.system(size: 16, design: .rounded)) + } else { + List { + ForEach(viewModel.files) { ipa in + NavigationLink( + destination: FileDetailView( + ipa: ipa, + ipaInfo: viewModel.getIPAInformation(ipa) + ) + ) { + FileListItem(ipaFile: ipa) + .swipeActions(allowsFullSwipe: false) { + Button(role: .destructive) { + viewModel.deleteIPA(ipa) + } label: { + Label("Delete", systemImage: "trash") + } + + Button { + viewModel.signIPA(ipa) + } label: { + Label("Sign", systemImage: "pencil") + } + + Button { + viewModel.installIPA(ipa) + } label: { + Label("Install", systemImage: "arrow.down.circle") + } + .tint(.green) } - - Button { - viewModel.signIPA(ipa) - } label: { - Label("Sign", systemImage: "pencil") - } - - Button { - viewModel.installIPA(ipa) - } label: { - Label("Install", systemImage: "arrow.down.circle") - } - .tint(.green) - } + } } } } @@ -78,7 +84,7 @@ struct FileManagerView: View { statusMessage = "Error: \(error.localizedDescription)" } } - }.alert("", isPresented: $showAlert) { + }.alert(statusMessage ?? "", isPresented: $showAlert) { Button("Dismiss", role: .cancel, action: { showAlert.toggle() statusMessage = nil