mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-11 07:43:28 +01:00
Reorganize AltStore project into UIKit and SwiftUI folders
This commit is contained in:
81
AltStore/SwiftUI/View Components/AppIconView.swift
Normal file
81
AltStore/SwiftUI/View Components/AppIconView.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// SwiftUIView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AsyncImage
|
||||
|
||||
struct AppIconView: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
@ObservedObject private var sideStoreIconData = AppIconsData.shared
|
||||
|
||||
let iconUrl: URL?
|
||||
var isSideStore: Bool
|
||||
var size: CGFloat = 64
|
||||
var cornerRadius: CGFloat {
|
||||
size * 0.234
|
||||
}
|
||||
|
||||
var image: some View {
|
||||
if isSideStore {
|
||||
return AnyView(
|
||||
Image(uiImage: UIImage(named: sideStoreIconData.selectedIconName! + "-image") ?? UIImage())
|
||||
.resizable()
|
||||
.renderingMode(.original)
|
||||
)
|
||||
}
|
||||
if let iconUrl {
|
||||
return AnyView(
|
||||
AsyncImage(url: iconUrl) { image in
|
||||
image
|
||||
.resizable()
|
||||
} placeholder: {
|
||||
Color(UIColor.secondarySystemBackground)
|
||||
}
|
||||
)
|
||||
}
|
||||
return AnyView(Color(UIColor.secondarySystemBackground))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
image
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
||||
.enableInjection()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppIconView: Equatable {
|
||||
/// Prevent re-rendering of the view if the parameters didn't change
|
||||
static func == (lhs: AppIconView, rhs: AppIconView) -> Bool {
|
||||
lhs.iconUrl == rhs.iconUrl && lhs.cornerRadius == rhs.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
struct AppIconView_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
HStack {
|
||||
AppIconView(iconUrl: app.iconURL, isSideStore: true)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(app.name)
|
||||
.bold()
|
||||
Text(app.developerName)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
AltStore/SwiftUI/View Components/AppPillButton.swift
Normal file
144
AltStore/SwiftUI/View Components/AppPillButton.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// AppPillButton.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
struct AppPillButton: View {
|
||||
|
||||
@ObservedObject
|
||||
var appManager = AppManager.shared.publisher
|
||||
|
||||
let app: AppProtocol
|
||||
var showRemainingDays = false
|
||||
|
||||
var storeApp: StoreApp? {
|
||||
(app as? StoreApp) ?? (app as? InstalledApp)?.storeApp
|
||||
}
|
||||
|
||||
var installedApp: InstalledApp? {
|
||||
(app as? InstalledApp) ?? (app as? StoreApp)?.installedApp
|
||||
}
|
||||
|
||||
var progress: Progress? {
|
||||
appManager.refreshProgress[app.bundleIdentifier] ?? appManager.installationProgress[app.bundleIdentifier]
|
||||
}
|
||||
// let progress = {
|
||||
// let progress = Progress(totalUnitCount: 100)
|
||||
// progress.completedUnitCount = 20
|
||||
// return progress
|
||||
// }()
|
||||
|
||||
var buttonText: String {
|
||||
// guard progress == nil else {
|
||||
// return ""
|
||||
// }
|
||||
|
||||
if let installedApp {
|
||||
if self.showRemainingDays {
|
||||
return DateFormatterHelper.string(forExpirationDate: installedApp.expirationDate)
|
||||
}
|
||||
|
||||
return L10n.AppPillButton.open
|
||||
}
|
||||
|
||||
return L10n.AppPillButton.free
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SwiftUI.Button(action: handleButton) {
|
||||
Text(buttonText.uppercased())
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(PillButtonStyle(tintColor: storeApp?.tintColor ?? .black, progress: progress))
|
||||
}
|
||||
|
||||
func handleButton() {
|
||||
if let installedApp {
|
||||
if showRemainingDays {
|
||||
self.refreshApp(installedApp)
|
||||
} else {
|
||||
self.openApp(installedApp)
|
||||
}
|
||||
} else if let storeApp {
|
||||
self.installApp(storeApp)
|
||||
}
|
||||
}
|
||||
|
||||
func openApp(_ installedApp: InstalledApp) {
|
||||
UIApplication.shared.open(installedApp.openAppURL)
|
||||
}
|
||||
|
||||
func refreshApp(_ installedApp: InstalledApp) {
|
||||
AppManager.shared.refresh([installedApp], presentingViewController: nil)
|
||||
}
|
||||
|
||||
func installApp(_ storeApp: StoreApp) {
|
||||
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
|
||||
guard previousProgress == nil else {
|
||||
previousProgress?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
let _ = AppManager.shared.install(storeApp, presentingViewController: UIApplication.shared.keyWindow?.rootViewController) { result in
|
||||
|
||||
switch result {
|
||||
case let .success(installedApp):
|
||||
print("Installed app: \(installedApp.bundleIdentifier)")
|
||||
|
||||
case let .failure(error):
|
||||
print("Failed to install app: \(error.localizedDescription)")
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
AppManager.shared.installationProgress(for: storeApp)?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppPillButton_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
static let installedApp = InstalledApp.fetchAltStore(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
self.preview(for: app)
|
||||
|
||||
self.preview(for: installedApp!)
|
||||
|
||||
self.preview(for: installedApp!, showRemainingDays: true)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static func preview(for app: AppProtocol, showRemainingDays: Bool = false) -> some View {
|
||||
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
|
||||
HStack {
|
||||
AppIconView(iconUrl: self.app.iconURL, isSideStore: true)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(app is StoreApp ? "Store App" : "Installed App")
|
||||
.bold()
|
||||
Text(
|
||||
app is StoreApp ?
|
||||
"Can be installed" :
|
||||
showRemainingDays ? "Can be refreshed" : "Can be opened"
|
||||
)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
AppPillButton(app: app, showRemainingDays: showRemainingDays)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
AltStore/SwiftUI/View Components/AppRowView.swift
Normal file
55
AltStore/SwiftUI/View Components/AppRowView.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// AppRowView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
struct AppRowView: View {
|
||||
let app: AppProtocol
|
||||
|
||||
var storeApp: StoreApp? {
|
||||
(app as? StoreApp) ?? (app as? InstalledApp)?.storeApp
|
||||
}
|
||||
|
||||
var showRemainingDays: Bool = false
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
AppIconView(iconUrl: storeApp?.iconURL, isSideStore: storeApp?.isSideStore ?? false)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(app.name)
|
||||
.bold()
|
||||
|
||||
Text(storeApp?.developerName ?? L10n.AppRowView.sideloaded)
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if false {
|
||||
RatingStars(rating: 4)
|
||||
.frame(height: 12)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
AppPillButton(app: app, showRemainingDays: showRemainingDays)
|
||||
}
|
||||
.padding()
|
||||
.tintedBackground(Color(storeApp?.tintColor ?? UIColor(Color.accentColor)))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
|
||||
}
|
||||
}
|
||||
|
||||
//struct AppRowView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// AppRowView()
|
||||
// }
|
||||
//}
|
||||
55
AltStore/SwiftUI/View Components/AppScreenshot.swift
Normal file
55
AltStore/SwiftUI/View Components/AppScreenshot.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// AppScreenshot.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.12.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import AsyncImage
|
||||
|
||||
struct AppScreenshot: View {
|
||||
let url: URL
|
||||
var aspectRatio: CGFloat = 9/16
|
||||
|
||||
static let processor = Self.ScreenshotProcessor()
|
||||
|
||||
var body: some View {
|
||||
AsyncImage(url: self.url, processor: Self.processor) { image in
|
||||
image
|
||||
.resizable()
|
||||
} placeholder: {
|
||||
Rectangle()
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.aspectRatio(self.aspectRatio, contentMode: .fit)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppScreenshot {
|
||||
class ScreenshotProcessor: ImageProcessor {
|
||||
func process(image: UIImage) -> UIImage {
|
||||
guard let cgImage = image.cgImage, image.size.width > image.size.height else { return image }
|
||||
|
||||
let rotatedImage = UIImage(cgImage: cgImage, scale: image.scale, orientation: .right)
|
||||
return rotatedImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
struct AppScreenshot_Previews: PreviewProvider {
|
||||
|
||||
static let context = DatabaseManager.shared.viewContext
|
||||
static let app = StoreApp.makeAltStoreApp(in: context)
|
||||
|
||||
static var previews: some View {
|
||||
AppScreenshot(url: app.screenshotURLs[0])
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
118
AltStore/SwiftUI/View Components/AsyncFallibleButton.swift
Normal file
118
AltStore/SwiftUI/View Components/AsyncFallibleButton.swift
Normal file
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// AsyncFallibleButton.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 2/18/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
private enum AsyncFallibleButtonState {
|
||||
case none
|
||||
case loading
|
||||
case success
|
||||
case error
|
||||
}
|
||||
|
||||
struct AsyncFallibleButton<Label: View>: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
let action: () throws -> Void
|
||||
let label: (_ execute: @escaping () -> Void) -> Label
|
||||
|
||||
var afterFinish: (_ success: Bool) -> Void = { success in } // runs after the checkmark/X has disappeared
|
||||
var wrapInButton = true
|
||||
var secondsToDisplayResultIcon: Double = 3
|
||||
|
||||
@State private var state: AsyncFallibleButtonState = .none
|
||||
@State private var showErrorAlert = false
|
||||
@State private var errorAlertMessage = ""
|
||||
|
||||
private var inside: some View {
|
||||
HStack {
|
||||
label(execute)
|
||||
if state != .none {
|
||||
if wrapInButton {
|
||||
Spacer()
|
||||
}
|
||||
switch (state) {
|
||||
case .loading:
|
||||
ProgressView()
|
||||
case .success:
|
||||
Image(systemSymbol: .checkmark)
|
||||
.foregroundColor(Color.green)
|
||||
case .error:
|
||||
Image(systemSymbol: .xmark)
|
||||
.foregroundColor(Color.red)
|
||||
default:
|
||||
Image(systemSymbol: .questionmark)
|
||||
.foregroundColor(Color.yellow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var wrapped: some View {
|
||||
if wrapInButton {
|
||||
return AnyView(SwiftUI.Button(action: {
|
||||
execute()
|
||||
}) {
|
||||
inside
|
||||
})
|
||||
} else {
|
||||
return AnyView(inside)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
wrapped
|
||||
.alert(isPresented: $showErrorAlert) {
|
||||
Alert(
|
||||
title: Text(L10n.AsyncFallibleButton.error),
|
||||
message: Text(errorAlertMessage)
|
||||
)
|
||||
}
|
||||
.disabled(state != .none)
|
||||
.animation(.default, value: state)
|
||||
.enableInjection()
|
||||
}
|
||||
|
||||
func execute() {
|
||||
if state != .none { return }
|
||||
state = .loading
|
||||
DispatchQueue.global().async {
|
||||
do {
|
||||
try action()
|
||||
DispatchQueue.main.async { state = .success }
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
state = .error
|
||||
errorAlertMessage = (error as? LocalizedError)?.failureReason ?? error.localizedDescription
|
||||
showErrorAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + secondsToDisplayResultIcon) {
|
||||
let lastState = state
|
||||
state = .none
|
||||
afterFinish(lastState == .success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AsyncFallibleButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AsyncFallibleButton(action: {
|
||||
print("Start")
|
||||
for index in 0...5000000 {
|
||||
_ = index + index
|
||||
}
|
||||
throw NSError(domain: "TestError", code: -1)
|
||||
//print("Finish")
|
||||
}) { execute in
|
||||
Text("Hello World")
|
||||
}
|
||||
}
|
||||
}
|
||||
457
AltStore/SwiftUI/View Components/FileExplorer.swift
Normal file
457
AltStore/SwiftUI/View Components/FileExplorer.swift
Normal file
@@ -0,0 +1,457 @@
|
||||
//
|
||||
// FileExplorer.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 2/16/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ZIPFoundation
|
||||
import UniformTypeIdentifiers
|
||||
import minimuxer
|
||||
|
||||
extension Binding<URL?>: Equatable {
|
||||
public static func == (lhs: Binding<URL?>, rhs: Binding<URL?>) -> Bool {
|
||||
return lhs.wrappedValue == rhs.wrappedValue
|
||||
}
|
||||
}
|
||||
|
||||
private protocol FileExplorerBackend {
|
||||
func delete(_ path: URL) throws
|
||||
func zip(_ path: URL) throws
|
||||
func insert(file: URL, to: URL) throws
|
||||
func iterate(_ directory: URL) -> DirectoryEntry
|
||||
func getQuickLookURL(_ path: URL) throws -> URL
|
||||
}
|
||||
|
||||
private class NormalFileExplorerBackend: FileExplorerBackend {
|
||||
func delete(_ path: URL) throws {
|
||||
try FileManager.default.removeItem(at: path)
|
||||
}
|
||||
|
||||
func zip(_ path: URL) throws {
|
||||
let dest = FileManager.default.documentsDirectory.appendingPathComponent(path.pathComponents.last! + ".zip")
|
||||
do {
|
||||
try FileManager.default.removeItem(at: dest)
|
||||
} catch {}
|
||||
|
||||
try FileManager.default.zipItem(at: path, to: dest)
|
||||
}
|
||||
|
||||
func insert(file: URL, to: URL) throws {
|
||||
try FileManager.default.copyItem(at: file, to: to.appendingPathComponent(file.pathComponents.last!), shouldReplace: true)
|
||||
}
|
||||
|
||||
private func _iterate(directory: URL, parent: URL) -> DirectoryEntry {
|
||||
var directoryEntry = DirectoryEntry(path: directory, parent: parent, isFile: false)
|
||||
if let contents = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) {
|
||||
for entry in contents {
|
||||
if entry.hasDirectoryPath {
|
||||
directoryEntry.children!.append(_iterate(directory: entry, parent: directory))
|
||||
} else {
|
||||
directoryEntry.children!.append(DirectoryEntry(path: entry, parent: directory, isFile: true, size: {
|
||||
guard let attributes = try? FileManager.default.attributesOfItem(atPath: entry.description.replacingOccurrences(of: "file://", with: "")) else { return nil }
|
||||
return attributes[FileAttributeKey.size] as? Double
|
||||
}()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return directoryEntry
|
||||
}
|
||||
|
||||
func iterate(_ directory: URL) -> DirectoryEntry {
|
||||
return _iterate(directory: directory, parent: directory)
|
||||
}
|
||||
|
||||
func getQuickLookURL(_ path: URL) throws -> URL {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
private class AfcFileExplorerBackend: FileExplorerBackend {
|
||||
func delete(_ path: URL) throws {
|
||||
try AfcFileManager.remove(path.description.replacingOccurrences(of: "file://", with: "").removingPercentEncoding!)
|
||||
}
|
||||
|
||||
func zip(_ path: URL) throws {
|
||||
throw NSError(domain: "AFC currently doesn't support zipping a directory/file. however, it is possible (we should be able to copy the files outside of AFC and then zip the copied directory/file), it just hasn't been implemented", code: -1)
|
||||
}
|
||||
|
||||
func insert(file: URL, to: URL) throws {
|
||||
let data = try Data(contentsOf: file)
|
||||
let rustByteSlice = data.toRustByteSlice()
|
||||
let to = to.appendingPathComponent(file.lastPathComponent).description.replacingOccurrences(of: "file://", with: "").removingPercentEncoding!
|
||||
print("writing to \(to)")
|
||||
try AfcFileManager.writeFile(to, rustByteSlice.forRust())
|
||||
}
|
||||
|
||||
private func _addChildren(_ rustEntry: RustDirectoryEntryRef) -> DirectoryEntry {
|
||||
var entry = DirectoryEntry(
|
||||
path: URL(string: rustEntry.path().toString())!,
|
||||
parent: URL(string: rustEntry.parent().toString())!,
|
||||
isFile: rustEntry.isFile(),
|
||||
size: rustEntry.size() != nil ? Double(rustEntry.size()!) : nil
|
||||
)
|
||||
for child in rustEntry.children() {
|
||||
entry.children!.append(_addChildren(child))
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func iterate(_ directory: URL) -> DirectoryEntry {
|
||||
var directoryEntry = DirectoryEntry(path: directory, parent: directory, isFile: false)
|
||||
for child in AfcFileManager.contents() {
|
||||
directoryEntry.children!.append(_addChildren(child))
|
||||
}
|
||||
return directoryEntry
|
||||
}
|
||||
|
||||
func getQuickLookURL(_ path: URL) throws -> URL {
|
||||
throw NSError(domain: "AFC currently doesn't support viewing a file. however, it is possible (we should be able to copy the file outside of AFC and then view the copied file), it just hasn't been implemented", code: -1)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DirectoryEntry: Identifiable {
|
||||
var id = UUID()
|
||||
|
||||
var path: URL
|
||||
var parent: URL
|
||||
|
||||
var isFile: Bool
|
||||
var size: Double?
|
||||
var children: [DirectoryEntry]? = []
|
||||
|
||||
var asString: String {
|
||||
let str = path.description.replacingOccurrences(of: parent.description, with: "").removingPercentEncoding!
|
||||
if str.count <= 0 {
|
||||
return "/"
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
private enum FileExplorerAction {
|
||||
case delete
|
||||
case zip
|
||||
case insert
|
||||
case quickLook
|
||||
}
|
||||
|
||||
private struct File: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
var item: DirectoryEntry
|
||||
var backend: FileExplorerBackend
|
||||
@Binding var explorerHidden: Bool
|
||||
|
||||
@State var quickLookURL: URL?
|
||||
@State var fileExplorerAction: FileExplorerAction?
|
||||
@State var hidden = false
|
||||
@State var isShowingFilePicker = false
|
||||
@State var selectedFile: URL?
|
||||
|
||||
var body: some View {
|
||||
AsyncFallibleButton(action: {
|
||||
switch (fileExplorerAction) {
|
||||
case .delete:
|
||||
print("deleting \(item.path.description)")
|
||||
try backend.delete(item.path)
|
||||
|
||||
case .zip:
|
||||
print("zipping \(item.path.description)")
|
||||
try backend.zip(item.path)
|
||||
|
||||
case .insert:
|
||||
print("inserting \(selectedFile!.description) to \(item.path.description)")
|
||||
|
||||
try backend.insert(file: selectedFile!, to: item.path)
|
||||
explorerHidden = true
|
||||
explorerHidden = false
|
||||
|
||||
case .quickLook:
|
||||
print("viewing \(item.path.description)")
|
||||
quickLookURL = try backend.getQuickLookURL(item.path)
|
||||
|
||||
default:
|
||||
print("unknown action for \(item.path.description): \(String(describing: fileExplorerAction))")
|
||||
}
|
||||
}, label: { execute in
|
||||
HStack {
|
||||
Text(item.asString)
|
||||
if item.isFile {
|
||||
Text(getFileSize(item.size)).foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Menu {
|
||||
if item.isFile {
|
||||
SwiftUI.Button(action: {
|
||||
fileExplorerAction = .quickLook
|
||||
execute()
|
||||
}) {
|
||||
Label("View/Share", systemSymbol: .eye)
|
||||
}
|
||||
} else {
|
||||
SwiftUI.Button(action: {
|
||||
fileExplorerAction = .zip
|
||||
execute()
|
||||
}) {
|
||||
Label("Save to ZIP file", systemSymbol: .squareAndArrowDown)
|
||||
}
|
||||
|
||||
SwiftUI.Button {
|
||||
isShowingFilePicker = true
|
||||
} label: {
|
||||
Label("Insert file", systemSymbol: .plus)
|
||||
}
|
||||
}
|
||||
|
||||
if item.asString != "/" {
|
||||
SwiftUI.Button(action: {
|
||||
fileExplorerAction = .delete
|
||||
execute()
|
||||
}) {
|
||||
Label("Delete", systemSymbol: .trash)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemSymbol: .ellipsis)
|
||||
.frame(width: 20, height: 20) // Make it easier to tap
|
||||
}
|
||||
}
|
||||
.onChange(of: $selectedFile) { file in
|
||||
guard file.wrappedValue != nil else { return }
|
||||
|
||||
fileExplorerAction = .insert
|
||||
execute()
|
||||
}
|
||||
}, afterFinish: { success in
|
||||
switch (fileExplorerAction) {
|
||||
case .delete:
|
||||
if success { hidden = true }
|
||||
|
||||
case .zip:
|
||||
UIApplication.shared.open(URL(string: "shareddocuments://" + FileManager.default.documentsDirectory.description.replacingOccurrences(of: "file://", with: ""))!, options: [:], completionHandler: nil)
|
||||
|
||||
default: break
|
||||
}
|
||||
}, wrapInButton: false)
|
||||
.quickLookPreview($quickLookURL)
|
||||
.sheet(isPresented: $isShowingFilePicker) {
|
||||
DocumentPicker(selectedUrl: $selectedFile, supportedTypes: allUTITypes().map({ $0.identifier }))
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.isHidden($hidden)
|
||||
.enableInjection()
|
||||
}
|
||||
|
||||
func getFileSize(_ bytes: Double?) -> String {
|
||||
guard var bytes = bytes else { return "Unknown file size" }
|
||||
|
||||
// https://stackoverflow.com/a/14919494 (ported to swift)
|
||||
let thresh = 1024.0;
|
||||
|
||||
if (bytes < thresh) {
|
||||
return String(describing: bytes) + " B";
|
||||
}
|
||||
|
||||
let units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
var u = -1;
|
||||
|
||||
while (bytes >= thresh && u < units.count - 1) {
|
||||
bytes /= thresh;
|
||||
u += 1;
|
||||
}
|
||||
|
||||
return String(format: "%.2f", bytes) + " " + units[u];
|
||||
}
|
||||
}
|
||||
|
||||
struct FileExplorer: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
private var url: URL?
|
||||
private var backend: FileExplorerBackend
|
||||
|
||||
private init(_ url: URL?, _ backend: FileExplorerBackend) {
|
||||
self.url = url
|
||||
self.backend = backend
|
||||
}
|
||||
|
||||
static func normal(url: URL?) -> FileExplorer {
|
||||
FileExplorer(url, NormalFileExplorerBackend())
|
||||
}
|
||||
|
||||
static func afc() -> FileExplorer {
|
||||
FileExplorer(URL(string: "/")!, AfcFileExplorerBackend())
|
||||
}
|
||||
|
||||
@State var hidden = false
|
||||
|
||||
var body: some View {
|
||||
List([backend.iterate(url!)], children: \.children) { item in
|
||||
File(item: item, backend: backend, explorerHidden: $hidden)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
SwiftUI.Button {
|
||||
hidden = true
|
||||
hidden = false
|
||||
} label: {
|
||||
Image(systemSymbol: .arrowClockwise)
|
||||
}
|
||||
}
|
||||
}
|
||||
.isHidden($hidden)
|
||||
.enableInjection()
|
||||
}
|
||||
}
|
||||
|
||||
struct FileExplorer_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FileExplorer.normal(url: FileManager.default.altstoreSharedDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/72165424
|
||||
func allUTITypes() -> [UTType] {
|
||||
let types: [UTType] =
|
||||
[.item,
|
||||
.content,
|
||||
.compositeContent,
|
||||
.diskImage,
|
||||
.data,
|
||||
.directory,
|
||||
.resolvable,
|
||||
.symbolicLink,
|
||||
.executable,
|
||||
.mountPoint,
|
||||
.aliasFile,
|
||||
.urlBookmarkData,
|
||||
.url,
|
||||
.fileURL,
|
||||
.text,
|
||||
.plainText,
|
||||
.utf8PlainText,
|
||||
.utf16ExternalPlainText,
|
||||
.utf16PlainText,
|
||||
.delimitedText,
|
||||
.commaSeparatedText,
|
||||
.tabSeparatedText,
|
||||
.utf8TabSeparatedText,
|
||||
.rtf,
|
||||
.html,
|
||||
.xml,
|
||||
.yaml,
|
||||
.sourceCode,
|
||||
.assemblyLanguageSource,
|
||||
.cSource,
|
||||
.objectiveCSource,
|
||||
.swiftSource,
|
||||
.cPlusPlusSource,
|
||||
.objectiveCPlusPlusSource,
|
||||
.cHeader,
|
||||
.cPlusPlusHeader]
|
||||
|
||||
let types_1: [UTType] =
|
||||
[.script,
|
||||
.appleScript,
|
||||
.osaScript,
|
||||
.osaScriptBundle,
|
||||
.javaScript,
|
||||
.shellScript,
|
||||
.perlScript,
|
||||
.pythonScript,
|
||||
.rubyScript,
|
||||
.phpScript,
|
||||
.json,
|
||||
.propertyList,
|
||||
.xmlPropertyList,
|
||||
.binaryPropertyList,
|
||||
.pdf,
|
||||
.rtfd,
|
||||
.flatRTFD,
|
||||
.webArchive,
|
||||
.image,
|
||||
.jpeg,
|
||||
.tiff,
|
||||
.gif,
|
||||
.png,
|
||||
.icns,
|
||||
.bmp,
|
||||
.ico,
|
||||
.rawImage,
|
||||
.svg,
|
||||
.livePhoto,
|
||||
.heif,
|
||||
.heic,
|
||||
.webP,
|
||||
.threeDContent,
|
||||
.usd,
|
||||
.usdz,
|
||||
.realityFile,
|
||||
.sceneKitScene,
|
||||
.arReferenceObject,
|
||||
.audiovisualContent]
|
||||
|
||||
let types_2: [UTType] =
|
||||
[.movie,
|
||||
.video,
|
||||
.audio,
|
||||
.quickTimeMovie,
|
||||
UTType("com.apple.quicktime-image"),
|
||||
.mpeg,
|
||||
.mpeg2Video,
|
||||
.mpeg2TransportStream,
|
||||
.mp3,
|
||||
.mpeg4Movie,
|
||||
.mpeg4Audio,
|
||||
.appleProtectedMPEG4Audio,
|
||||
.appleProtectedMPEG4Video,
|
||||
.avi,
|
||||
.aiff,
|
||||
.wav,
|
||||
.midi,
|
||||
.playlist,
|
||||
.m3uPlaylist,
|
||||
.folder,
|
||||
.volume,
|
||||
.package,
|
||||
.bundle,
|
||||
.pluginBundle,
|
||||
.spotlightImporter,
|
||||
.quickLookGenerator,
|
||||
.xpcService,
|
||||
.framework,
|
||||
.application,
|
||||
.applicationBundle,
|
||||
.applicationExtension,
|
||||
.unixExecutable,
|
||||
.exe,
|
||||
.systemPreferencesPane,
|
||||
.archive,
|
||||
.gzip,
|
||||
.bz2,
|
||||
.zip,
|
||||
.appleArchive,
|
||||
.spreadsheet,
|
||||
.presentation,
|
||||
.database,
|
||||
.message,
|
||||
.contact,
|
||||
.vCard,
|
||||
.toDoItem,
|
||||
.calendarEvent,
|
||||
.emailMessage,
|
||||
.internetLocation,
|
||||
.internetShortcut,
|
||||
.font,
|
||||
.bookmark,
|
||||
.pkcs12,
|
||||
.x509Certificate,
|
||||
.epub,
|
||||
.log]
|
||||
.compactMap({ $0 })
|
||||
|
||||
return types + types_1 + types_2
|
||||
}
|
||||
42
AltStore/SwiftUI/View Components/HintView.swift
Normal file
42
AltStore/SwiftUI/View Components/HintView.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// HintView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 15.01.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HintView<Content: View>: View {
|
||||
|
||||
var backgroundColor: Color = Color(.tertiarySystemBackground)
|
||||
|
||||
@ViewBuilder
|
||||
let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
self.content()
|
||||
}
|
||||
.padding()
|
||||
.background(self.backgroundColor)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
struct HintView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ZStack {
|
||||
Color(.secondarySystemBackground).edgesIgnoringSafeArea(.all)
|
||||
|
||||
HintView {
|
||||
Text("Hint Title")
|
||||
.bold()
|
||||
|
||||
Text("This hint view can be used to tell the user something about how SideStore works.")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
AltStore/SwiftUI/View Components/ModalNavigationLink.swift
Normal file
45
AltStore/SwiftUI/View Components/ModalNavigationLink.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// ModalNavigationLink.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 03.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ModalNavigationLink<Label: View, Modal: View>: View {
|
||||
let modal: () -> Modal
|
||||
let label: () -> Label
|
||||
|
||||
@State var isPresentingModal: Bool = false
|
||||
|
||||
init(@ViewBuilder modal: @escaping () -> Modal, @ViewBuilder label: @escaping () -> Label) {
|
||||
self.modal = modal
|
||||
self.label = label
|
||||
}
|
||||
|
||||
init(_ title: String, @ViewBuilder modal: @escaping () -> Modal) where Label == Text {
|
||||
self.modal = modal
|
||||
self.label = { Text(title) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
SwiftUI.Button {
|
||||
self.isPresentingModal = true
|
||||
} label: {
|
||||
self.label()
|
||||
}
|
||||
.sheet(isPresented: self.$isPresentingModal) {
|
||||
self.modal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ModalNavigationLink_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ModalNavigationLink("Present Modal") {
|
||||
Text("Modal")
|
||||
}
|
||||
}
|
||||
}
|
||||
47
AltStore/SwiftUI/View Components/ObservableScrollView.swift
Normal file
47
AltStore/SwiftUI/View Components/ObservableScrollView.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// ObservableScrollView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 20.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ObservableScrollView<Content: View>: View {
|
||||
@Namespace var scrollViewNamespace
|
||||
|
||||
@Binding var scrollOffset: CGFloat
|
||||
|
||||
let content: (ScrollViewProxy) -> Content
|
||||
|
||||
init(scrollOffset: Binding<CGFloat>, @ViewBuilder content: @escaping (ScrollViewProxy) -> Content) {
|
||||
self._scrollOffset = scrollOffset
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
ScrollViewReader { proxy in
|
||||
content(proxy)
|
||||
.background(GeometryReader { geoReader in
|
||||
let offset = -geoReader.frame(in: .named(scrollViewNamespace)).minY
|
||||
Color.clear
|
||||
.preference(key: ScrollViewOffsetPreferenceKey.self, value: offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
.coordinateSpace(name: scrollViewNamespace)
|
||||
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
|
||||
scrollOffset = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
|
||||
static var defaultValue = CGFloat.zero
|
||||
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
31
AltStore/SwiftUI/View Components/RatingStars.swift
Normal file
31
AltStore/SwiftUI/View Components/RatingStars.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// RatingStars.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct RatingStars: View {
|
||||
|
||||
let rating: Int
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<5) { i in
|
||||
Image(systemSymbol: i < rating ? .starFill : .star)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RatingStars_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RatingStars(rating: 4)
|
||||
}
|
||||
}
|
||||
52
AltStore/SwiftUI/View Components/RoundedTextField.swift
Normal file
52
AltStore/SwiftUI/View Components/RoundedTextField.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// RoundedTextField.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 29.11.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RoundedTextField: View {
|
||||
|
||||
let title: String?
|
||||
let placeholder: String
|
||||
@Binding var text: String
|
||||
let isSecure: Bool
|
||||
|
||||
init(title: String?, placeholder: String, text: Binding<String>, isSecure: Bool = false) {
|
||||
self.title = title
|
||||
self.placeholder = placeholder
|
||||
self._text = text
|
||||
self.isSecure = isSecure
|
||||
}
|
||||
|
||||
init(_ placeholder: String, text: Binding<String>, isSecure: Bool = false) {
|
||||
self.init(title: nil, placeholder: placeholder, text: text, isSecure: isSecure)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let title {
|
||||
Text(title.uppercased())
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
HStack(alignment: .center) {
|
||||
if isSecure {
|
||||
SecureField(placeholder, text: $text)
|
||||
} else {
|
||||
TextField(placeholder, text: $text)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(Color(.secondarySystemBackground))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user