mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 08:32:00 +00:00
test backup system
This commit is contained in:
parent
0a6b3c83c9
commit
18075308dd
6 changed files with 344 additions and 16 deletions
115
Sora/Utils/Backups/BackupData.swift
Normal file
115
Sora/Utils/Backups/BackupData.swift
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
//
|
||||||
|
// BackupData.swift
|
||||||
|
// Sulfur
|
||||||
|
//
|
||||||
|
// Created by Francesco on 25/05/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct BackupData: Codable {
|
||||||
|
let version: String
|
||||||
|
let timestamp: Date
|
||||||
|
let userData: [String: Any]
|
||||||
|
|
||||||
|
init(userData: [String: Any]) {
|
||||||
|
self.version = "1.0"
|
||||||
|
self.timestamp = Date()
|
||||||
|
self.userData = userData
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version, timestamp, userData
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
version = try container.decode(String.self, forKey: .version)
|
||||||
|
timestamp = try container.decode(Date.self, forKey: .timestamp)
|
||||||
|
|
||||||
|
let userDataContainer = try container.nestedContainer(keyedBy: DynamicKey.self, forKey: .userData)
|
||||||
|
var userData: [String: Any] = [:]
|
||||||
|
|
||||||
|
for key in userDataContainer.allKeys {
|
||||||
|
userData[key.stringValue] = try userDataContainer.decode(AnyCodable.self, forKey: key).value
|
||||||
|
}
|
||||||
|
|
||||||
|
self.userData = userData
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(version, forKey: .version)
|
||||||
|
try container.encode(timestamp, forKey: .timestamp)
|
||||||
|
|
||||||
|
var userDataContainer = container.nestedContainer(keyedBy: DynamicKey.self, forKey: .userData)
|
||||||
|
for (key, value) in userData {
|
||||||
|
let dynamicKey = DynamicKey(stringValue: key)!
|
||||||
|
try userDataContainer.encode(AnyCodable(value), forKey: dynamicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DynamicKey: CodingKey {
|
||||||
|
var stringValue: String
|
||||||
|
var intValue: Int?
|
||||||
|
|
||||||
|
init?(stringValue: String) {
|
||||||
|
self.stringValue = stringValue
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(intValue: Int) {
|
||||||
|
self.intValue = intValue
|
||||||
|
self.stringValue = String(intValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AnyCodable: Codable {
|
||||||
|
let value: Any
|
||||||
|
|
||||||
|
init(_ value: Any) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
|
||||||
|
if let bool = try? container.decode(Bool.self) {
|
||||||
|
value = bool
|
||||||
|
} else if let int = try? container.decode(Int.self) {
|
||||||
|
value = int
|
||||||
|
} else if let double = try? container.decode(Double.self) {
|
||||||
|
value = double
|
||||||
|
} else if let string = try? container.decode(String.self) {
|
||||||
|
value = string
|
||||||
|
} else if let array = try? container.decode([AnyCodable].self) {
|
||||||
|
value = array.map { $0.value }
|
||||||
|
} else if let dictionary = try? container.decode([String: AnyCodable].self) {
|
||||||
|
value = dictionary.mapValues { $0.value }
|
||||||
|
} else {
|
||||||
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
|
||||||
|
switch value {
|
||||||
|
case let bool as Bool:
|
||||||
|
try container.encode(bool)
|
||||||
|
case let int as Int:
|
||||||
|
try container.encode(int)
|
||||||
|
case let double as Double:
|
||||||
|
try container.encode(double)
|
||||||
|
case let string as String:
|
||||||
|
try container.encode(string)
|
||||||
|
case let array as [Any]:
|
||||||
|
try container.encode(array.map { AnyCodable($0) })
|
||||||
|
case let dictionary as [String: Any]:
|
||||||
|
try container.encode(dictionary.mapValues { AnyCodable($0) })
|
||||||
|
default:
|
||||||
|
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: container.codingPath, debugDescription: "Unsupported type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
Sora/Utils/Backups/BackupManager.swift
Normal file
88
Sora/Utils/Backups/BackupManager.swift
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
//
|
||||||
|
// BackupManager.swift
|
||||||
|
// Sulfur
|
||||||
|
//
|
||||||
|
// Created by Francesco on 25/05/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class BackupManager: ObservableObject {
|
||||||
|
static let shared = BackupManager()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func createBackup() -> BackupData {
|
||||||
|
var userData: [String: Any] = [:]
|
||||||
|
|
||||||
|
let userDefaults = UserDefaults.standard
|
||||||
|
let defaultsDict = userDefaults.dictionaryRepresentation()
|
||||||
|
|
||||||
|
let appKeys = defaultsDict.keys.filter { key in
|
||||||
|
!key.hasPrefix("Apple") &&
|
||||||
|
!key.hasPrefix("NS") &&
|
||||||
|
!key.hasPrefix("com.apple") &&
|
||||||
|
!key.contains("Keyboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in appKeys {
|
||||||
|
userData[key] = defaultsDict[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return BackupData(userData: userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportBackup() -> URL? {
|
||||||
|
let backup = createBackup()
|
||||||
|
|
||||||
|
do {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.dateEncodingStrategy = .iso8601
|
||||||
|
encoder.outputFormatting = .prettyPrinted
|
||||||
|
|
||||||
|
let data = try encoder.encode(backup)
|
||||||
|
|
||||||
|
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
let fileName = "sora_backup_\(DateFormatter.backupFormatter.string(from: Date())).json"
|
||||||
|
let fileURL = documentsPath.appendingPathComponent(fileName)
|
||||||
|
|
||||||
|
try data.write(to: fileURL)
|
||||||
|
return fileURL
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("Failed to export backup: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func importBackup(from url: URL) -> Bool {
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
|
||||||
|
let backup = try decoder.decode(BackupData.self, from: data)
|
||||||
|
|
||||||
|
let userDefaults = UserDefaults.standard
|
||||||
|
for (key, value) in backup.userData {
|
||||||
|
userDefaults.set(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
|
||||||
|
NotificationCenter.default.post(name: .backupRestored, object: nil)
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("Failed to import backup: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shareBackup() -> URL? {
|
||||||
|
return exportBackup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
16
Sora/Utils/Extensions/DateFormatter.swift
Normal file
16
Sora/Utils/Extensions/DateFormatter.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// DateFormatter.swift
|
||||||
|
// Sulfur
|
||||||
|
//
|
||||||
|
// Created by Francesco on 25/05/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension DateFormatter {
|
||||||
|
static let backupFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
@ -13,4 +13,5 @@ extension Notification.Name {
|
||||||
static let ContinueWatchingDidUpdate = Notification.Name("ContinueWatchingDidUpdate")
|
static let ContinueWatchingDidUpdate = Notification.Name("ContinueWatchingDidUpdate")
|
||||||
static let DownloadManagerStatusUpdate = Notification.Name("DownloadManagerStatusUpdate")
|
static let DownloadManagerStatusUpdate = Notification.Name("DownloadManagerStatusUpdate")
|
||||||
static let modulesSyncDidComplete = Notification.Name("modulesSyncDidComplete")
|
static let modulesSyncDidComplete = Notification.Name("modulesSyncDidComplete")
|
||||||
|
static let backupRestored = Notification.Name("backupRestored")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct SettingsViewData: View {
|
struct SettingsViewData: View {
|
||||||
@State private var showEraseAppDataAlert = false
|
@State private var showEraseAppDataAlert = false
|
||||||
|
|
@ -18,14 +19,41 @@ struct SettingsViewData: View {
|
||||||
@State private var movPkgSize: Int64 = 0
|
@State private var movPkgSize: Int64 = 0
|
||||||
@State private var showRemoveMovPkgAlert = false
|
@State private var showRemoveMovPkgAlert = false
|
||||||
|
|
||||||
// State bindings for cache settings
|
|
||||||
@State private var isMetadataCachingEnabled: Bool = true
|
@State private var isMetadataCachingEnabled: Bool = true
|
||||||
@State private var isImageCachingEnabled: Bool = true
|
@State private var isImageCachingEnabled: Bool = true
|
||||||
@State private var isMemoryOnlyMode: Bool = false
|
@State private var isMemoryOnlyMode: Bool = false
|
||||||
|
|
||||||
|
@StateObject private var backupManager = BackupManager.shared
|
||||||
|
@State private var showingExportSuccess = false
|
||||||
|
@State private var showingImportSuccess = false
|
||||||
|
@State private var showingError = false
|
||||||
|
@State private var errorMessage = ""
|
||||||
|
@State private var showingFilePicker = false
|
||||||
|
@State private var showingShareSheet = false
|
||||||
|
@State private var backupURL: URL?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
// New section for cache settings
|
Section(header: Text("Backup & Restore"), footer: Text("Create backups to transfer your data to another device or restore from a previous backup.")) {
|
||||||
|
Button(action: exportBackup) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "square.and.arrow.up")
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
Text("Create Backup")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: { showingFilePicker = true }) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "square.and.arrow.down")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text("Restore from Backup")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section(header: Text("Cache Settings"), footer: Text("Caching helps reduce network usage and load content faster. You can disable it to save storage space.")) {
|
Section(header: Text("Cache Settings"), footer: Text("Caching helps reduce network usage and load content faster. You can disable it to save storage space.")) {
|
||||||
Toggle("Enable Metadata Caching", isOn: $isMetadataCachingEnabled)
|
Toggle("Enable Metadata Caching", isOn: $isMetadataCachingEnabled)
|
||||||
.onChange(of: isMetadataCachingEnabled) { newValue in
|
.onChange(of: isMetadataCachingEnabled) { newValue in
|
||||||
|
|
@ -48,7 +76,6 @@ struct SettingsViewData: View {
|
||||||
.onChange(of: isMemoryOnlyMode) { newValue in
|
.onChange(of: isMemoryOnlyMode) { newValue in
|
||||||
MetadataCacheManager.shared.isMemoryOnlyMode = newValue
|
MetadataCacheManager.shared.isMemoryOnlyMode = newValue
|
||||||
if newValue {
|
if newValue {
|
||||||
// Clear disk cache when switching to memory-only
|
|
||||||
MetadataCacheManager.shared.clearAllCache()
|
MetadataCacheManager.shared.clearAllCache()
|
||||||
calculateCacheSize()
|
calculateCacheSize()
|
||||||
}
|
}
|
||||||
|
|
@ -118,13 +145,42 @@ struct SettingsViewData: View {
|
||||||
.navigationTitle("App Data")
|
.navigationTitle("App Data")
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Initialize state with current values
|
|
||||||
isMetadataCachingEnabled = MetadataCacheManager.shared.isCachingEnabled
|
isMetadataCachingEnabled = MetadataCacheManager.shared.isCachingEnabled
|
||||||
isImageCachingEnabled = KingfisherCacheManager.shared.isCachingEnabled
|
isImageCachingEnabled = KingfisherCacheManager.shared.isCachingEnabled
|
||||||
isMemoryOnlyMode = MetadataCacheManager.shared.isMemoryOnlyMode
|
isMemoryOnlyMode = MetadataCacheManager.shared.isMemoryOnlyMode
|
||||||
calculateCacheSize()
|
calculateCacheSize()
|
||||||
updateSizes()
|
updateSizes()
|
||||||
}
|
}
|
||||||
|
.fileImporter(
|
||||||
|
isPresented: $showingFilePicker,
|
||||||
|
allowedContentTypes: [UTType.json],
|
||||||
|
allowsMultipleSelection: false
|
||||||
|
) { result in
|
||||||
|
handleFileImport(result)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingShareSheet) {
|
||||||
|
if let url = backupURL {
|
||||||
|
ShareSheet(items: [url])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Backup Created", isPresented: $showingExportSuccess) {
|
||||||
|
Button("Share") {
|
||||||
|
showingShareSheet = true
|
||||||
|
}
|
||||||
|
Button("OK") { }
|
||||||
|
} message: {
|
||||||
|
Text("Your backup has been created successfully. You can share it or find it in your Files app.")
|
||||||
|
}
|
||||||
|
.alert("Backup Restored", isPresented: $showingImportSuccess) {
|
||||||
|
Button("OK") { }
|
||||||
|
} message: {
|
||||||
|
Text("Your data has been restored successfully. The app will refresh with your restored settings.")
|
||||||
|
}
|
||||||
|
.alert("Error", isPresented: $showingError) {
|
||||||
|
Button("OK") { }
|
||||||
|
} message: {
|
||||||
|
Text(errorMessage)
|
||||||
|
}
|
||||||
.alert(isPresented: $showEraseAppDataAlert) {
|
.alert(isPresented: $showEraseAppDataAlert) {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Erase App Data"),
|
title: Text("Erase App Data"),
|
||||||
|
|
@ -157,24 +213,51 @@ struct SettingsViewData: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate and update the combined cache size
|
private func exportBackup() {
|
||||||
|
guard let url = backupManager.exportBackup() else {
|
||||||
|
errorMessage = "Failed to create backup file"
|
||||||
|
showingError = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backupURL = url
|
||||||
|
showingExportSuccess = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleFileImport(_ result: Result<[URL], Error>) {
|
||||||
|
switch result {
|
||||||
|
case .success(let urls):
|
||||||
|
guard let url = urls.first else { return }
|
||||||
|
|
||||||
|
if backupManager.importBackup(from: url) {
|
||||||
|
showingImportSuccess = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
isMetadataCachingEnabled = MetadataCacheManager.shared.isCachingEnabled
|
||||||
|
isImageCachingEnabled = KingfisherCacheManager.shared.isCachingEnabled
|
||||||
|
isMemoryOnlyMode = MetadataCacheManager.shared.isMemoryOnlyMode
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage = "Failed to restore backup. Please check if the file is a valid Sora backup."
|
||||||
|
showingError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
errorMessage = "Failed to read backup file: \(error.localizedDescription)"
|
||||||
|
showingError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
func calculateCacheSize() {
|
func calculateCacheSize() {
|
||||||
isCalculatingSize = true
|
isCalculatingSize = true
|
||||||
cacheSizeText = "Calculating..."
|
cacheSizeText = "Calculating..."
|
||||||
|
|
||||||
// Group all cache size calculations
|
|
||||||
DispatchQueue.global(qos: .background).async {
|
DispatchQueue.global(qos: .background).async {
|
||||||
var totalSize: Int64 = 0
|
var totalSize: Int64 = 0
|
||||||
|
|
||||||
// Get metadata cache size
|
|
||||||
let metadataSize = MetadataCacheManager.shared.getCacheSize()
|
let metadataSize = MetadataCacheManager.shared.getCacheSize()
|
||||||
totalSize += metadataSize
|
totalSize += metadataSize
|
||||||
|
|
||||||
// Get image cache size asynchronously
|
|
||||||
KingfisherCacheManager.shared.calculateCacheSize { imageSize in
|
KingfisherCacheManager.shared.calculateCacheSize { imageSize in
|
||||||
totalSize += Int64(imageSize)
|
totalSize += Int64(imageSize)
|
||||||
|
|
||||||
// Update the UI on the main thread
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.cacheSizeText = KingfisherCacheManager.formatCacheSize(UInt(totalSize))
|
self.cacheSizeText = KingfisherCacheManager.formatCacheSize(UInt(totalSize))
|
||||||
self.isCalculatingSize = false
|
self.isCalculatingSize = false
|
||||||
|
|
@ -183,14 +266,9 @@ struct SettingsViewData: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all caches (both metadata and images)
|
|
||||||
func clearAllCaches() {
|
func clearAllCaches() {
|
||||||
// Clear metadata cache
|
|
||||||
MetadataCacheManager.shared.clearAllCache()
|
MetadataCacheManager.shared.clearAllCache()
|
||||||
|
|
||||||
// Clear image cache
|
|
||||||
KingfisherCacheManager.shared.clearCache {
|
KingfisherCacheManager.shared.clearCache {
|
||||||
// Update cache size after clearing
|
|
||||||
calculateCacheSize()
|
calculateCacheSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,3 +392,13 @@ struct SettingsViewData: View {
|
||||||
return totalSize
|
return totalSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ShareSheet: UIViewControllerRepresentable {
|
||||||
|
let items: [Any]
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||||
|
UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@
|
||||||
132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1222D9995C300A0140B /* JSController-Details.swift */; };
|
132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1222D9995C300A0140B /* JSController-Details.swift */; };
|
||||||
132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; };
|
132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; };
|
||||||
132FC5B32DE31DAE009A80F7 /* SettingsViewAlternateAppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132FC5B22DE31DAD009A80F7 /* SettingsViewAlternateAppIconPicker.swift */; };
|
132FC5B32DE31DAE009A80F7 /* SettingsViewAlternateAppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132FC5B22DE31DAD009A80F7 /* SettingsViewAlternateAppIconPicker.swift */; };
|
||||||
|
132FC5BB2DE333D3009A80F7 /* BackupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132FC5BA2DE333D3009A80F7 /* BackupData.swift */; };
|
||||||
|
132FC5BD2DE333F4009A80F7 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132FC5BC2DE333F4009A80F7 /* DateFormatter.swift */; };
|
||||||
|
132FC5BF2DE33410009A80F7 /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132FC5BE2DE33410009A80F7 /* BackupManager.swift */; };
|
||||||
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; };
|
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; };
|
||||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; };
|
133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; };
|
||||||
133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; };
|
133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; };
|
||||||
|
|
@ -98,6 +101,9 @@
|
||||||
132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = "<group>"; };
|
132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = "<group>"; };
|
||||||
132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = "<group>"; };
|
132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = "<group>"; };
|
||||||
132FC5B22DE31DAD009A80F7 /* SettingsViewAlternateAppIconPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewAlternateAppIconPicker.swift; sourceTree = "<group>"; };
|
132FC5B22DE31DAD009A80F7 /* SettingsViewAlternateAppIconPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewAlternateAppIconPicker.swift; sourceTree = "<group>"; };
|
||||||
|
132FC5BA2DE333D3009A80F7 /* BackupData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupData.swift; sourceTree = "<group>"; };
|
||||||
|
132FC5BC2DE333F4009A80F7 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
|
||||||
|
132FC5BE2DE33410009A80F7 /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = "<group>"; };
|
||||||
133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = "<group>"; };
|
133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = "<group>"; };
|
||||||
133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -213,6 +219,15 @@
|
||||||
path = Analytics;
|
path = Analytics;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
132FC5B92DE333BD009A80F7 /* Backups */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
132FC5BA2DE333D3009A80F7 /* BackupData.swift */,
|
||||||
|
132FC5BE2DE33410009A80F7 /* BackupManager.swift */,
|
||||||
|
);
|
||||||
|
path = Backups;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
133D7C612D2BE2500075467E = {
|
133D7C612D2BE2500075467E = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -294,9 +309,10 @@
|
||||||
133D7C852D2BE2640075467E /* Utils */ = {
|
133D7C852D2BE2640075467E /* Utils */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
7205AEDA2DCCEF9500943F3F /* Cache */,
|
|
||||||
13D842532D45266900EBBFA6 /* Drops */,
|
13D842532D45266900EBBFA6 /* Drops */,
|
||||||
|
7205AEDA2DCCEF9500943F3F /* Cache */,
|
||||||
1399FAD12D3AB33D00E97C31 /* Logger */,
|
1399FAD12D3AB33D00E97C31 /* Logger */,
|
||||||
|
132FC5B92DE333BD009A80F7 /* Backups */,
|
||||||
133D7C882D2BE2640075467E /* Modules */,
|
133D7C882D2BE2640075467E /* Modules */,
|
||||||
133D7C8A2D2BE2640075467E /* JSLoader */,
|
133D7C8A2D2BE2640075467E /* JSLoader */,
|
||||||
1327FBA52D758CEA00FC6689 /* Analytics */,
|
1327FBA52D758CEA00FC6689 /* Analytics */,
|
||||||
|
|
@ -315,6 +331,7 @@
|
||||||
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */,
|
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */,
|
||||||
136BBE7F2DB1038000906B5E /* Notification+Name.swift */,
|
136BBE7F2DB1038000906B5E /* Notification+Name.swift */,
|
||||||
1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */,
|
1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */,
|
||||||
|
132FC5BC2DE333F4009A80F7 /* DateFormatter.swift */,
|
||||||
13637B892DE0EA1100BDA2FC /* UserDefaults.swift */,
|
13637B892DE0EA1100BDA2FC /* UserDefaults.swift */,
|
||||||
133D7C872D2BE2640075467E /* URLSession.swift */,
|
133D7C872D2BE2640075467E /* URLSession.swift */,
|
||||||
1359ED132D76F49900C13034 /* finTopView.swift */,
|
1359ED132D76F49900C13034 /* finTopView.swift */,
|
||||||
|
|
@ -620,6 +637,7 @@
|
||||||
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */,
|
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */,
|
||||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
||||||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
|
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
|
||||||
|
132FC5BF2DE33410009A80F7 /* BackupManager.swift in Sources */,
|
||||||
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */,
|
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */,
|
||||||
13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */,
|
13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */,
|
||||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
|
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
|
||||||
|
|
@ -662,6 +680,8 @@
|
||||||
72AC3A032DD4DAEB00C60B96 /* PerformanceMonitor.swift in Sources */,
|
72AC3A032DD4DAEB00C60B96 /* PerformanceMonitor.swift in Sources */,
|
||||||
72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */,
|
72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */,
|
||||||
13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */,
|
13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */,
|
||||||
|
132FC5BB2DE333D3009A80F7 /* BackupData.swift in Sources */,
|
||||||
|
132FC5BD2DE333F4009A80F7 /* DateFormatter.swift in Sources */,
|
||||||
7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */,
|
7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */,
|
||||||
722248602DCBAA2C00CABE2D /* M3U8StreamExtractor.swift in Sources */,
|
722248602DCBAA2C00CABE2D /* M3U8StreamExtractor.swift in Sources */,
|
||||||
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */,
|
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue