mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-12 16:23:32 +01:00
Reorganize AltStore project into UIKit and SwiftUI folders
This commit is contained in:
76
AltStore/SwiftUI/Views/Settings/AdvancedSettingsView.swift
Normal file
76
AltStore/SwiftUI/Views/Settings/AdvancedSettingsView.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// AdvancedSettingsView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 2/19/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
private struct Server: Identifiable {
|
||||
var id: String { value }
|
||||
var display: String
|
||||
var value: String
|
||||
}
|
||||
|
||||
struct AdvancedSettingsView: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
private let anisetteServers = [
|
||||
Server(display: "SideStore", value: "http://ani.sidestore.io"),
|
||||
Server(display: "Macley (US)", value: "http://us1.sternserv.tech"),
|
||||
Server(display: "Macley (DE)", value: "http://de1.sternserv.tech"),
|
||||
Server(display: "DrPudding", value: "https://sign.rheaa.xyz"),
|
||||
Server(display: "jkcoxson (AltServer)", value: "http://jkcoxson.com:2095"),
|
||||
Server(display: "jkcoxson (Provision)", value: "http://jkcoxson.com:2052"),
|
||||
Server(display: "Sideloadly", value: "https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx"),
|
||||
Server(display: "Nick", value: "http://45.33.29.114"),
|
||||
Server(display: "Jawshoeadan", value: "https://anisette.jawshoeadan.me"),
|
||||
Server(display: "crystall1nedev", value: "https://anisette.crystall1ne.software/"),
|
||||
]
|
||||
|
||||
@AppStorage("textServer")
|
||||
var usePreferred: Bool = true
|
||||
|
||||
@AppStorage("textInputAnisetteURL")
|
||||
var anisetteURL: String = ""
|
||||
|
||||
@AppStorage("customAnisetteURL")
|
||||
var selectedAnisetteServer: String = ""
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
Picker(L10n.AdvancedSettingsView.anisette, selection: $selectedAnisetteServer) {
|
||||
ForEach(anisetteServers) { server in
|
||||
Text(server.display)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(L10n.AdvancedSettingsView.DangerZone.usePreferred, isOn: $usePreferred)
|
||||
|
||||
HStack {
|
||||
Text(L10n.AdvancedSettingsView.DangerZone.anisetteURL)
|
||||
TextField("", text: $anisetteURL)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled(true)
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.AdvancedSettingsView.dangerZone)
|
||||
} footer: {
|
||||
Text(L10n.AdvancedSettingsView.dangerZoneInfo)
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.AdvancedSettingsView.title)
|
||||
.enableInjection()
|
||||
}
|
||||
}
|
||||
|
||||
struct AdvancedSettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AdvancedSettingsView()
|
||||
}
|
||||
}
|
||||
135
AltStore/SwiftUI/Views/Settings/AppIconsView.swift
Normal file
135
AltStore/SwiftUI/Views/Settings/AppIconsView.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// AppIconsView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 2/14/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct Icon: Identifiable {
|
||||
var id: String { assetName }
|
||||
var displayName: String
|
||||
let assetName: String
|
||||
}
|
||||
|
||||
private struct SpecialIcon {
|
||||
let assetName: String
|
||||
let suffix: String?
|
||||
let forceIndex: Int?
|
||||
}
|
||||
|
||||
class AppIconsData: ObservableObject {
|
||||
static let shared = AppIconsData()
|
||||
|
||||
private static let specialIcons = [
|
||||
SpecialIcon(assetName: "Neon", suffix: "(Stable)", forceIndex: 0),
|
||||
SpecialIcon(assetName: "Starburst", suffix: "(Beta)", forceIndex: 1),
|
||||
SpecialIcon(assetName: "Steel", suffix: "(Nightly)", forceIndex: 2),
|
||||
]
|
||||
|
||||
@Published var icons: [Icon] = []
|
||||
@Published var primaryIcon: Icon?
|
||||
@Published var selectedIconName: String?
|
||||
|
||||
private init() {
|
||||
let bundleIcons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as! [String: Any]
|
||||
|
||||
let primaryIconData = bundleIcons["CFBundlePrimaryIcon"] as! [String: Any]
|
||||
let primaryIconName = primaryIconData["CFBundleIconName"] as! String
|
||||
primaryIcon = Icon(displayName: primaryIconName, assetName: primaryIconName)
|
||||
icons.append(primaryIcon!)
|
||||
|
||||
for (key, _) in bundleIcons["CFBundleAlternateIcons"] as! [String: Any] {
|
||||
icons.append(Icon(displayName: key, assetName: key))
|
||||
}
|
||||
|
||||
// sort alphabetically
|
||||
icons.sort { $0.assetName < $1.assetName }
|
||||
|
||||
for specialIcon in AppIconsData.specialIcons {
|
||||
guard let icon = icons.enumerated().first(where: { $0.element.assetName == specialIcon.assetName }) else { continue }
|
||||
|
||||
if let suffix = specialIcon.suffix {
|
||||
icons[icon.offset].displayName += " " + suffix
|
||||
}
|
||||
|
||||
if let forceIndex = specialIcon.forceIndex {
|
||||
let e = icons.remove(at: icon.offset)
|
||||
icons.insert(e, at: forceIndex)
|
||||
}
|
||||
}
|
||||
|
||||
if let alternateIconName = UIApplication.shared.alternateIconName {
|
||||
selectedIconName = icons.first { $0.assetName == alternateIconName }?.assetName ?? primaryIcon!.assetName
|
||||
} else {
|
||||
selectedIconName = primaryIcon!.assetName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppIconsView: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
@ObservedObject private var data = AppIconsData.shared
|
||||
|
||||
private let artists = [
|
||||
"Chris (LitRitt)": ["Neon", "Starburst", "Steel", "Storm"],
|
||||
"naturecodevoid": ["Honeydew", "Midnight", "Sky"],
|
||||
"Swifticul": ["Vista"],
|
||||
]
|
||||
|
||||
@State private var selectedIcon: String? = "" // this is just so the list row background changes when selecting a value, I couldn't get it to keep the selected icon name (for some reason it was always "", even when I set it to the selected icon asset name)
|
||||
|
||||
private let size: CGFloat = 72
|
||||
private var cornerRadius: CGFloat {
|
||||
size * 0.234
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List(data.icons, selection: $selectedIcon) { icon in
|
||||
SwiftUI.Button(action: {
|
||||
data.selectedIconName = icon.assetName
|
||||
// Pass nil for primary icon
|
||||
UIApplication.shared.setAlternateIconName(icon.assetName == data.primaryIcon!.assetName ? nil : icon.assetName, completionHandler: { error in
|
||||
if let error = error {
|
||||
print("error when setting alternate app icon to \(icon.assetName): \(error.localizedDescription)")
|
||||
} else {
|
||||
print("successfully changed app icon to \(icon.assetName)")
|
||||
}
|
||||
})
|
||||
}) {
|
||||
HStack(spacing: 20) {
|
||||
// if we don't have an additional image asset for each icon, it will have low resolution
|
||||
Image(uiImage: UIImage(named: icon.assetName + "-image") ?? UIImage())
|
||||
.resizable()
|
||||
.renderingMode(.original)
|
||||
.cornerRadius(cornerRadius)
|
||||
.frame(width: size, height: size)
|
||||
VStack(alignment: .leading) {
|
||||
Text(icon.displayName)
|
||||
if let artist = artists.first(where: { $0.value.contains(icon.assetName) }) {
|
||||
Text("By " + artist.key)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if data.selectedIconName == icon.assetName {
|
||||
Image(systemSymbol: .checkmark)
|
||||
.foregroundColor(Color.blue)
|
||||
}
|
||||
}
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
.navigationTitle(L10n.AppIconsView.title)
|
||||
.enableInjection()
|
||||
}
|
||||
}
|
||||
|
||||
struct AppIconsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AppIconsView()
|
||||
}
|
||||
}
|
||||
109
AltStore/SwiftUI/Views/Settings/ConnectAppleIDView.swift
Normal file
109
AltStore/SwiftUI/Views/Settings/ConnectAppleIDView.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// ConnectAppleIDView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 29.11.22.
|
||||
// Copyright © 2022 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltSign
|
||||
|
||||
struct ConnectAppleIDView: View {
|
||||
typealias AuthenticationHandler = (String, String, @escaping (Result<(ALTAccount, ALTAppleAPISession), Error>) -> Void) -> Void
|
||||
typealias CompletionHandler = ((ALTAccount, ALTAppleAPISession, String)?) -> Void
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
var authenticationHandler: AuthenticationHandler?
|
||||
var completionHandler: CompletionHandler?
|
||||
|
||||
@State var email: String = ""
|
||||
@State var password: String = ""
|
||||
@State var isLoading: Bool = false
|
||||
|
||||
var isFormValid: Bool {
|
||||
!email.isEmpty && !password.isEmpty
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 32) {
|
||||
Text(L10n.ConnectAppleIDView.startWithSignIn)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
RoundedTextField(title: L10n.ConnectAppleIDView.appleID, placeholder: "user@sidestore.io", text: $email)
|
||||
|
||||
RoundedTextField(title: L10n.ConnectAppleIDView.password, placeholder: "••••••", text: $password, isSecure: true)
|
||||
}
|
||||
|
||||
SwiftUI.Button(action: signIn) {
|
||||
Text(L10n.ConnectAppleIDView.signIn)
|
||||
.bold()
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle(isLoading: isLoading))
|
||||
.disabled(!isFormValid)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(L10n.ConnectAppleIDView.whyDoWeNeedThis)
|
||||
.bold()
|
||||
|
||||
Text(L10n.ConnectAppleIDView.footer)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(Color(.secondarySystemBackground))
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.navigationTitle(L10n.ConnectAppleIDView.connectYourAppleID)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
SwiftUI.Button(action: self.cancel) {
|
||||
Text(L10n.ConnectAppleIDView.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func signIn() {
|
||||
self.isLoading = true
|
||||
self.authenticationHandler?(email, password) { (result) in
|
||||
defer {
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(ALTAppleAPIError.requiresTwoFactorAuthentication):
|
||||
// Ignore
|
||||
break
|
||||
|
||||
case .failure(let error as NSError):
|
||||
let error = error.withLocalizedFailure(NSLocalizedString(L10n.ConnectAppleIDView.failedToSignIn, comment: ""))
|
||||
print(error)
|
||||
|
||||
case .success((let account, let session)):
|
||||
self.completionHandler?((account, session, password))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
self.completionHandler?(nil)
|
||||
// self.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectAppleIDView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ConnectAppleIDView()
|
||||
}
|
||||
}
|
||||
190
AltStore/SwiftUI/Views/Settings/DevModeView.swift
Normal file
190
AltStore/SwiftUI/Views/Settings/DevModeView.swift
Normal file
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// DevModeView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by naturecodevoid on 2/16/23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import LocalConsole
|
||||
import minimuxer
|
||||
|
||||
// Yes, we know the password is right here. It's not supposed to be a secret, just something to hopefully prevent people breaking SideStore with dev mode and then complaining to us.
|
||||
let DEV_MODE_PASSWORD = "devmode"
|
||||
|
||||
struct DevModePrompt: View {
|
||||
@Binding var isShowingDevModePrompt: Bool
|
||||
@Binding var isShowingDevModeMenu: Bool
|
||||
|
||||
@State var countdown = 0
|
||||
@State var isShowingPasswordAlert = false
|
||||
@State var isShowingIncorrectPasswordAlert = false
|
||||
@State var password = ""
|
||||
|
||||
var button: some View {
|
||||
SwiftUI.Button(action: {
|
||||
if #available(iOS 16.0, *) {
|
||||
isShowingPasswordAlert = true
|
||||
} else {
|
||||
// iOS 14 doesn't support .alert, so just go straight to dev mode without asking for a password
|
||||
// iOS 15 also doesn't seem to support TextField in an alert (the text field was nonexistent)
|
||||
enableDevMode()
|
||||
}
|
||||
}) {
|
||||
Text(countdown <= 0 ? L10n.Action.enable + " " + L10n.DevModeView.title : L10n.DevModeView.read + " (\(countdown))")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(FilledButtonStyle()) // TODO: set tintColor so text is more readable
|
||||
.disabled(countdown > 0)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var text: some View {
|
||||
if #available(iOS 15.0, *),
|
||||
let string = try? AttributedString(markdown: L10n.DevModeView.prompt, options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
|
||||
Text(string)
|
||||
} else {
|
||||
Text(L10n.DevModeView.prompt)
|
||||
}
|
||||
}
|
||||
|
||||
var view: some View {
|
||||
ScrollView {
|
||||
VStack {
|
||||
text
|
||||
.foregroundColor(.primary)
|
||||
.padding(.bottom)
|
||||
|
||||
button
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.navigationTitle(L10n.DevModeView.title)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
SwiftUI.Button(action: { isShowingDevModePrompt = false }) {
|
||||
Text(L10n.Action.close)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
countdown = 20
|
||||
tickCountdown()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
if #available(iOS 15.0, *) {
|
||||
view
|
||||
.alert(L10n.DevModeView.password, isPresented: $isShowingPasswordAlert) {
|
||||
TextField(L10n.DevModeView.password, text: $password)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled(true)
|
||||
SwiftUI.Button(L10n.Action.submit, action: {
|
||||
if password == DEV_MODE_PASSWORD {
|
||||
enableDevMode()
|
||||
} else {
|
||||
isShowingIncorrectPasswordAlert = true
|
||||
}
|
||||
})
|
||||
}
|
||||
.alert(L10n.DevModeView.incorrectPassword, isPresented: $isShowingIncorrectPasswordAlert) {
|
||||
SwiftUI.Button(L10n.Action.tryAgain, action: {
|
||||
isShowingIncorrectPasswordAlert = false
|
||||
isShowingPasswordAlert = true
|
||||
})
|
||||
SwiftUI.Button(L10n.Action.cancel, action: {
|
||||
isShowingIncorrectPasswordAlert = false
|
||||
isShowingDevModePrompt = false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableDevMode() {
|
||||
UserDefaults.standard.isDevModeEnabled = true
|
||||
isShowingDevModePrompt = false
|
||||
isShowingDevModeMenu = true
|
||||
}
|
||||
|
||||
func tickCountdown() {
|
||||
if countdown <= 0 { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
countdown -= 1
|
||||
tickCountdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DevModeMenu: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
@AppStorage("isConsoleEnabled")
|
||||
var isConsoleEnabled: Bool = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
Toggle(L10n.DevModeView.console, isOn: self.$isConsoleEnabled)
|
||||
.onChange(of: self.isConsoleEnabled) { value in
|
||||
LCManager.shared.isVisible = value
|
||||
}
|
||||
|
||||
NavigationLink(L10n.DevModeView.dataExplorer) {
|
||||
FileExplorer.normal(url: FileManager.default.altstoreSharedDirectory)
|
||||
.navigationTitle(L10n.DevModeView.dataExplorer)
|
||||
}.foregroundColor(.red)
|
||||
|
||||
NavigationLink(L10n.DevModeView.tmpExplorer) {
|
||||
FileExplorer.normal(url: FileManager.default.temporaryDirectory)
|
||||
.navigationTitle(L10n.DevModeView.tmpExplorer)
|
||||
}.foregroundColor(.red)
|
||||
|
||||
Toggle(L10n.DevModeView.skipResign, isOn: ResignAppOperation.skipResignBinding)
|
||||
.foregroundColor(.red)
|
||||
} footer: {
|
||||
Text(L10n.DevModeView.footer)
|
||||
}
|
||||
|
||||
Section {
|
||||
AsyncFallibleButton(action: {
|
||||
let dir = try dump_profiles(FileManager.default.documentsDirectory.absoluteString)
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(URL(string: "shareddocuments://" + dir.toString())!, options: [:], completionHandler: nil)
|
||||
}
|
||||
}) { execute in
|
||||
Text(L10n.DevModeView.Minimuxer.dumpProfiles)
|
||||
}
|
||||
|
||||
NavigationLink(L10n.DevModeView.Minimuxer.afcExplorer) {
|
||||
FileExplorer.afc()
|
||||
.navigationTitle(L10n.DevModeView.Minimuxer.afcExplorer)
|
||||
}.foregroundColor(.red)
|
||||
} header: {
|
||||
Text(L10n.DevModeView.minimuxer)
|
||||
} footer: {
|
||||
Text(L10n.DevModeView.Minimuxer.footer)
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.DevModeView.title)
|
||||
.enableInjection()
|
||||
}
|
||||
}
|
||||
|
||||
struct DevModeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
NavigationLink("DevModeMenu") {
|
||||
DevModeMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
169
AltStore/SwiftUI/Views/Settings/ErrorLogView.swift
Normal file
169
AltStore/SwiftUI/Views/Settings/ErrorLogView.swift
Normal file
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// ErrorLogView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 03.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
import ExpandableText
|
||||
|
||||
struct ErrorLogView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \LoggedError.date, ascending: false)
|
||||
])
|
||||
var loggedErrors: FetchedResults<LoggedError>
|
||||
|
||||
var groupedLoggedErrors: [Date: [LoggedError]] {
|
||||
Dictionary(grouping: loggedErrors, by: { Calendar.current.startOfDay(for: $0.date) })
|
||||
}
|
||||
|
||||
@State var currentFaqUrl: URL?
|
||||
@State var isShowingMinimuxerLog: Bool = false
|
||||
@State var isShowingDeleteConfirmation: Bool = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(groupedLoggedErrors.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
|
||||
Section {
|
||||
let errors = groupedLoggedErrors[date] ?? []
|
||||
ForEach(errors, id: \.date) { error in
|
||||
VStack(spacing: 8) {
|
||||
HStack(alignment: .top) {
|
||||
Group {
|
||||
if let storeApp = error.storeApp {
|
||||
AppIconView(iconUrl: storeApp.iconURL, isSideStore: storeApp.isSideStore, size: 50)
|
||||
} else {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 50*0.234, style: .continuous)
|
||||
.foregroundColor(Color(UIColor.secondarySystemFill))
|
||||
|
||||
Image(systemSymbol: .exclamationmarkCircle)
|
||||
.imageScale(.large)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(error.localizedFailure ?? "Operation Failed")
|
||||
.bold()
|
||||
|
||||
Group {
|
||||
switch error.domain {
|
||||
case AltServerErrorDomain: Text("SideServer Error \(error.code)")
|
||||
case OperationError.domain: Text("SideStore Error \(error.code)")
|
||||
default: Text(error.error.localizedErrorCode)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(DateFormatterHelper.timeString(for: error.date))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
let nsError = error.error as NSError
|
||||
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
||||
|
||||
Menu {
|
||||
SwiftUI.Button {
|
||||
UIPasteboard.general.string = errorDescription
|
||||
} label: {
|
||||
Label("Copy Error Message", systemSymbol: .docOnDoc)
|
||||
}
|
||||
|
||||
SwiftUI.Button {
|
||||
UIPasteboard.general.string = error.error.localizedErrorCode
|
||||
} label: {
|
||||
Label("Copy Error Code", systemSymbol: .docOnDoc)
|
||||
}
|
||||
|
||||
SwiftUI.Button {
|
||||
self.searchFAQ(for: error)
|
||||
} label: {
|
||||
Label("Search FAQ", systemSymbol: .magnifyingglass)
|
||||
}
|
||||
|
||||
} label: {
|
||||
Text(errorDescription)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(DateFormatterHelper.string(for: date))
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Error Log")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
ModalNavigationLink {
|
||||
FilePreviewView(urls: [
|
||||
FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
|
||||
])
|
||||
.ignoresSafeArea()
|
||||
} label: {
|
||||
Image(systemSymbol: .ladybug)
|
||||
}
|
||||
|
||||
|
||||
SwiftUI.Button {
|
||||
self.isShowingDeleteConfirmation = true
|
||||
} label: {
|
||||
Image(systemSymbol: .trash)
|
||||
}
|
||||
.actionSheet(isPresented: self.$isShowingDeleteConfirmation) {
|
||||
ActionSheet(
|
||||
title: Text("Are you sure you want to clear the error log?"),
|
||||
buttons: [
|
||||
.destructive(Text("Clear Error Log"), action: self.clearLoggedErrors),
|
||||
.cancel()
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: self.$currentFaqUrl) { url in
|
||||
SafariView(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
func searchFAQ(for error: LoggedError) {
|
||||
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
|
||||
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
||||
|
||||
let query = [error.domain, "\(error.code)"].joined(separator: "+")
|
||||
components.queryItems = [URLQueryItem(name: "q", value: query)]
|
||||
|
||||
self.currentFaqUrl = components.url ?? baseURL
|
||||
}
|
||||
|
||||
func clearLoggedErrors() {
|
||||
DatabaseManager.shared.purgeLoggedErrors { result in
|
||||
if case let .failure(error) = result {
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ErrorLogView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
ErrorLogView()
|
||||
}
|
||||
}
|
||||
}
|
||||
112
AltStore/SwiftUI/Views/Settings/LicensesView.swift
Normal file
112
AltStore/SwiftUI/Views/Settings/LicensesView.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// LicensesView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 21.01.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LicensesView: View {
|
||||
|
||||
let licenses = """
|
||||
Jay Freeman (ldid)
|
||||
Copyright (C) 2007-2012 Jay Freeman (saurik)
|
||||
|
||||
libimobiledevice
|
||||
© 2007-2015 by the contributors of libimobiledevice - All rights reserved.
|
||||
|
||||
Gilles Vollant (minizip)
|
||||
Copyright (C) 1998-2005 Gilles Vollant
|
||||
|
||||
Kishikawa Katsumi (KeychainAccess)
|
||||
Copyright (c) 2014 kishikawa katsumi
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Alexander Grebenyuk (Nuke)
|
||||
Copyright (c) 2015-2019 Alexander Grebenyuk
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Craig Hockenberry (MarkdownAttributedString)
|
||||
Copyright (c) 2020 The Iconfactory, Inc. https://iconfactory.com
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
The OpenSSL Project (OpenSSL)
|
||||
Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved.
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. All advertising materials mentioning features or use of this software must display the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
|
||||
4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact openssl-core@openssl.org.
|
||||
5. Products derived from this software may not be called "OpenSSL" nor may "OpenSSL" appear in their names without prior written permission of the OpenSSL Project.
|
||||
6. Redistributions of any form whatsoever must retain the following acknowledgment:
|
||||
"This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/)"
|
||||
THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com).
|
||||
|
||||
Eric Young (SSLeay)
|
||||
/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
|
||||
All rights reserved.
|
||||
This package is an SSL implementation written by Eric Young (eay@cryptsoft.com).
|
||||
The implementation was written so as to conform with Netscapes SSL. This library is free for commercial and non-commercial use as long as the following conditions are aheared to. The following conditions apply to all code found in this distribution, be it the RC4, RSA, lhash, DES, etc., code; not just the SSL code. The SSL documentation included with this distribution is covered by the same copyright terms except that the holder is Tim Hudson (tjh@cryptsoft.com).
|
||||
Copyright remains Eric Young's, and as such any Copyright notices in the code are not to be removed. If this package is used in a product, Eric Young should be given attribution as the author of the parts of the library used. This can be in the form of a textual message at program startup or in documentation (online or textual) provided with the package.
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
|
||||
"This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)"
|
||||
The word 'cryptographic' can be left out if the rouines from the library being used are not cryptographic related :-).
|
||||
4. If you include any Windows specific code (or a derivative thereof) from the apps directory (application code) you must include an acknowledgement:
|
||||
"This product includes software written by Tim Hudson (tjh@cryptsoft.com)" THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
The licence and distribution terms for any publically available version or derivative of this code cannot be changed. i.e. this code cannot simply be copied and put under another distribution licence [including the GNU Public Licence.]
|
||||
|
||||
Toni Ronkko (dirent)
|
||||
Copyright (c) 1998-2019 Toni Ronkko
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Microsoft Corporation (C++ REST SDK)
|
||||
Copyright (c) Microsoft Corporation
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Kutuzov Viktor (mman-win32)
|
||||
Copyright (c) Kutuzov Viktor
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
ICONS
|
||||
|
||||
Settings by i cons from the Noun Project
|
||||
"""
|
||||
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
Text(licenses)
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Software Licenses")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct LicensesView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
LicensesView()
|
||||
}
|
||||
}
|
||||
}
|
||||
90
AltStore/SwiftUI/Views/Settings/RefreshAttemptsView.swift
Normal file
90
AltStore/SwiftUI/Views/Settings/RefreshAttemptsView.swift
Normal file
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// RefreshAttemptsView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 04.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
|
||||
struct RefreshAttemptsView: View {
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \RefreshAttempt.date, ascending: false)
|
||||
])
|
||||
var refreshAttempts: FetchedResults<RefreshAttempt>
|
||||
|
||||
var groupedRefreshAttempts: [Date: [RefreshAttempt]] {
|
||||
Dictionary(grouping: refreshAttempts, by: { Calendar.current.startOfDay(for: $0.date) })
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(groupedRefreshAttempts.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
|
||||
Section {
|
||||
let attempts = groupedRefreshAttempts[date] ?? []
|
||||
ForEach(attempts, id: \.date) { attempt in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
if attempt.isSuccess {
|
||||
Text("Success")
|
||||
.bold()
|
||||
.foregroundColor(.green)
|
||||
} else {
|
||||
Text("Failure")
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(DateFormatterHelper.timeString(for: attempt.date))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if let description = attempt.errorDescription {
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(DateFormatterHelper.string(for: date))
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(self.listBackground)
|
||||
.navigationTitle("Refresh Attempts")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var listBackground: some View {
|
||||
if self.refreshAttempts.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Spacer()
|
||||
Text("No Refresh Attempts")
|
||||
.font(.title)
|
||||
|
||||
Text("The more you use SideStore, the more often iOS will allow it to refresh apps in the background.")
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
.padding()
|
||||
} else {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct RefreshAttemptsView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
RefreshAttemptsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
374
AltStore/SwiftUI/Views/Settings/SettingsView.swift
Normal file
374
AltStore/SwiftUI/Views/Settings/SettingsView.swift
Normal file
@@ -0,0 +1,374 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// SideStoreUI
|
||||
//
|
||||
// Created by Fabian Thies on 18.11.22.
|
||||
// Copyright © 2022 Fabian Thies. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AsyncImage
|
||||
import SFSafeSymbols
|
||||
import LocalConsole
|
||||
import AltStoreCore
|
||||
import Intents
|
||||
import minimuxer
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject private var iO = Inject.observer
|
||||
|
||||
var connectedAppleID: Team? {
|
||||
DatabaseManager.shared.activeTeam()
|
||||
}
|
||||
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam)))
|
||||
var connectedTeams: FetchedResults<Team>
|
||||
|
||||
|
||||
@AppStorage("isBackgroundRefreshEnabled")
|
||||
var isBackgroundRefreshEnabled: Bool = true
|
||||
|
||||
@AppStorage("isDevModeEnabled")
|
||||
var isDevModeEnabled: Bool = false
|
||||
|
||||
@AppStorage("isDebugLoggingEnabled")
|
||||
var isDebugLoggingEnabled: Bool = false
|
||||
|
||||
@State var isShowingConnectAppleIDView = false
|
||||
@State var isShowingResetPairingFileConfirmation = false
|
||||
@State var isShowingDevModePrompt = false
|
||||
@State var isShowingDevModeMenu = false
|
||||
|
||||
@State var externalURLToShow: URL?
|
||||
@State var quickLookURL: URL?
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown Version"
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
if let connectedAppleID = connectedTeams.first {
|
||||
HStack {
|
||||
Text(L10n.SettingsView.ConnectedAppleID.name)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(connectedAppleID.name)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(L10n.SettingsView.ConnectedAppleID.eMail)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(connectedAppleID.account.appleID)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(L10n.SettingsView.ConnectedAppleID.type)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(connectedAppleID.type.localizedDescription)
|
||||
}
|
||||
} else {
|
||||
SwiftUI.Button {
|
||||
self.connectAppleID()
|
||||
} label: {
|
||||
Text(L10n.SettingsView.connectAppleID)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
if !connectedTeams.isEmpty {
|
||||
HStack {
|
||||
Text(L10n.SettingsView.ConnectedAppleID.text)
|
||||
Spacer()
|
||||
SwiftUI.Button {
|
||||
self.disconnectAppleID()
|
||||
} label: {
|
||||
Text(L10n.SettingsView.ConnectedAppleID.signOut)
|
||||
.font(.callout)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(L10n.SettingsView.ConnectedAppleID.Footer.p1)
|
||||
|
||||
Text(L10n.SettingsView.ConnectedAppleID.Footer.p2)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
NavigationLink(L10n.AppIconsView.title) {
|
||||
AppIconsView()
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: self.$isBackgroundRefreshEnabled, label: {
|
||||
Text(L10n.SettingsView.backgroundRefresh)
|
||||
})
|
||||
|
||||
ModalNavigationLink(L10n.SettingsView.addToSiri) {
|
||||
if let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) {
|
||||
SiriShortcutSetupView(shortcut: shortcut)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.SettingsView.refreshingApps)
|
||||
} footer: {
|
||||
Text(L10n.SettingsView.refreshingAppsFooter)
|
||||
}
|
||||
|
||||
Section {
|
||||
SwiftUI.Button {
|
||||
self.externalURLToShow = URL(string: "https://sidestore.io")!
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Developers")
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text("SideStore Team")
|
||||
Image(systemSymbol: .chevronRight)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
SwiftUI.Button {
|
||||
self.externalURLToShow = URL(string: "https://fabian-thies.de")!
|
||||
} label: {
|
||||
HStack {
|
||||
Text(L10n.SettingsView.swiftUIRedesign)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text("fabianthdev")
|
||||
Image(systemSymbol: .chevronRight)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
NavigationLink {
|
||||
LicensesView()
|
||||
} label: {
|
||||
Text("Licenses")
|
||||
}
|
||||
|
||||
} header: {
|
||||
Text(L10n.SettingsView.credits)
|
||||
}
|
||||
|
||||
Section {
|
||||
NavigationLink("Show Error Log") {
|
||||
ErrorLogView()
|
||||
}
|
||||
|
||||
NavigationLink("Show Refresh Attempts") {
|
||||
RefreshAttemptsView()
|
||||
}
|
||||
|
||||
NavigationLink(L10n.AdvancedSettingsView.title) {
|
||||
AdvancedSettingsView()
|
||||
}
|
||||
|
||||
Toggle(L10n.SettingsView.debugLogging, isOn: self.$isDebugLoggingEnabled)
|
||||
.onChange(of: self.isDebugLoggingEnabled) { value in
|
||||
UserDefaults.shared.isDebugLoggingEnabled = value
|
||||
set_debug(value)
|
||||
}
|
||||
|
||||
AsyncFallibleButton(action: self.exportLogs, label: { execute in Text(L10n.SettingsView.exportLogs) })
|
||||
|
||||
if MailComposeView.canSendMail {
|
||||
ModalNavigationLink("Send Feedback") {
|
||||
MailComposeView(recipients: ["support@sidestore.io"],
|
||||
subject: "SideStore Beta \(appVersion) Feedback") {
|
||||
NotificationManager.shared.showNotification(title: "Thank you for your feedback!")
|
||||
} onError: { error in
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
SwiftUI.Button(L10n.SettingsView.switchToUIKit, action: self.switchToUIKit)
|
||||
|
||||
SwiftUI.Button(L10n.SettingsView.resetImageCache, action: self.resetImageCache)
|
||||
.foregroundColor(.red)
|
||||
|
||||
SwiftUI.Button("Reset Pairing File") {
|
||||
self.isShowingResetPairingFileConfirmation = true
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
.actionSheet(isPresented: self.$isShowingResetPairingFileConfirmation) {
|
||||
ActionSheet(title: Text("Are you sure to reset the pairing file?"), message: Text("You can reset the pairing file when you cannot sideload apps or enable JIT. SideStore will close when the file has been deleted."), buttons: [
|
||||
.destructive(Text("Delete and Reset"), action: self.resetPairingFile),
|
||||
.cancel()
|
||||
])
|
||||
}
|
||||
|
||||
if isDevModeEnabled {
|
||||
NavigationLink(L10n.DevModeView.title, isActive: self.$isShowingDevModeMenu) {
|
||||
DevModeMenu()
|
||||
}.foregroundColor(.red)
|
||||
} else {
|
||||
SwiftUI.Button(L10n.DevModeView.title) {
|
||||
self.isShowingDevModePrompt = true
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
.sheet(isPresented: self.$isShowingDevModePrompt) {
|
||||
DevModePrompt(isShowingDevModePrompt: self.$isShowingDevModePrompt, isShowingDevModeMenu: self.$isShowingDevModeMenu)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.SettingsView.debug)
|
||||
}
|
||||
|
||||
|
||||
Section {
|
||||
|
||||
} footer: {
|
||||
Text("SideStore \(appVersion)")
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationTitle(L10n.SettingsView.title)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
SwiftUI.Button {
|
||||
|
||||
} label: {
|
||||
Image(systemSymbol: .personCropCircle)
|
||||
.imageScale(.large)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.sheet(item: $externalURLToShow) { url in
|
||||
SafariView(url: url)
|
||||
}
|
||||
.quickLookPreview($quickLookURL)
|
||||
.enableInjection()
|
||||
}
|
||||
|
||||
|
||||
// var appleIDSection: some View {
|
||||
//
|
||||
// }
|
||||
|
||||
|
||||
|
||||
func connectAppleID() {
|
||||
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
AppManager.shared.authenticate(presentingViewController: rootViewController) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(OperationError.cancelled):
|
||||
// Ignore
|
||||
break
|
||||
|
||||
case .failure(let error):
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
|
||||
case .success: break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectAppleID() {
|
||||
DatabaseManager.shared.signOut { (error) in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error
|
||||
{
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func switchToUIKit() {
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: .main)
|
||||
let rootVC = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController
|
||||
|
||||
UIApplication.shared.keyWindow?.rootViewController = rootVC
|
||||
}
|
||||
|
||||
func resetImageCache() {
|
||||
do {
|
||||
let url = try FileManager.default.url(
|
||||
for: .cachesDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil,
|
||||
create: true)
|
||||
try FileManager.default.removeItem(at: url.appendingPathComponent("com.zeu.cache", isDirectory: true))
|
||||
} catch let error {
|
||||
fatalError("\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func resetPairingFile() {
|
||||
let filename = "ALTPairingFile.mobiledevicepairing"
|
||||
let fileURL = FileManager.default.documentsDirectory.appendingPathComponent(filename)
|
||||
|
||||
// Delete the pairing file if it exists
|
||||
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
print("Pairing file deleted successfully.")
|
||||
} catch {
|
||||
print("Failed to delete pairing file:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Close and exit SideStore
|
||||
UIApplication.shared.perform(#selector(URLSessionTask.suspend))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .milliseconds(500))) {
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func exportLogs() throws {
|
||||
let path = FileManager.default.documentsDirectory.appendingPathComponent("sidestore.log")
|
||||
var text = LCManager.shared.currentText
|
||||
|
||||
// TODO: add more potentially sensitive info to this array
|
||||
var remove = [String]()
|
||||
if let connectedAppleID = connectedTeams.first {
|
||||
remove.append(connectedAppleID.name)
|
||||
remove.append(connectedAppleID.account.appleID)
|
||||
remove.append(connectedAppleID.account.firstName)
|
||||
remove.append(connectedAppleID.account.lastName)
|
||||
remove.append(connectedAppleID.account.localizedName)
|
||||
remove.append(connectedAppleID.account.identifier)
|
||||
remove.append(connectedAppleID.identifier)
|
||||
}
|
||||
if let udid = fetch_udid() {
|
||||
remove.append(udid.toString())
|
||||
}
|
||||
|
||||
for toRemove in remove {
|
||||
text = text.replacingOccurrences(of: toRemove, with: "[removed]")
|
||||
}
|
||||
|
||||
guard let data = text.data(using: .utf8) else { throw NSError(domain: "Failed to get data.", code: 2) }
|
||||
try data.write(to: path)
|
||||
quickLookURL = path
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
SettingsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user