Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions App/Extensions/Notification+NewIPAAdded.swift
Original file line number Diff line number Diff line change
@@ -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")
}
47 changes: 47 additions & 0 deletions App/File Manager/DocumnetPicker.swift
Original file line number Diff line number Diff line change
@@ -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<URL, Error>) -> 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<URL, Error>) -> Void

init(completion: @escaping (Result<URL, Error>) -> 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)))
}
}
}

131 changes: 131 additions & 0 deletions App/File Manager/File Details/FileDetailView.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
25 changes: 25 additions & 0 deletions App/File Manager/File Item/FileListItem.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
108 changes: 108 additions & 0 deletions App/File Manager/File List/FileManagerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// 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 {
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)
}
}
}
}
}
}
.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(statusMessage ?? "", 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()
}
}
}
}
}
Loading