diff --git a/Sora.xcodeproj/project.pbxproj b/Sora.xcodeproj/project.pbxproj index 76b81f2..658f976 100644 --- a/Sora.xcodeproj/project.pbxproj +++ b/Sora.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 132417D52D13240200B4F2D2 /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D42D13240200B4F2D2 /* EpisodeCell.swift */; }; 132417D72D13242400B4F2D2 /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D62D13242400B4F2D2 /* CircularProgressBar.swift */; }; 132417D92D1328B900B4F2D2 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */; }; + 13B3A4B22D1477F100BCC0D5 /* StorageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B3A4B12D1477F100BCC0D5 /* StorageSettingsView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -66,6 +67,7 @@ 132417D42D13240200B4F2D2 /* EpisodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = ""; }; 132417D62D13242400B4F2D2 /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; 132417D82D1328B900B4F2D2 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; + 13B3A4B12D1477F100BCC0D5 /* StorageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSettingsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -211,6 +213,7 @@ 132417AD2D131A0600B4F2D2 /* SettingsLogsView.swift */, 132417AE2D131A0600B4F2D2 /* SettingsModuleView.swift */, 132417AF2D131A0600B4F2D2 /* SettingsPlayerView.swift */, + 13B3A4B12D1477F100BCC0D5 /* StorageSettingsView.swift */, ); path = SubPages; sourceTree = ""; @@ -321,6 +324,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 13B3A4B22D1477F100BCC0D5 /* StorageSettingsView.swift in Sources */, 132417BB2D131A0600B4F2D2 /* SettingsIUView.swift in Sources */, 132417C42D131A0600B4F2D2 /* AnimeInfoExtraction.swift in Sources */, 132417B82D131A0600B4F2D2 /* SearchView.swift in Sources */, diff --git a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate index 37fdcf0..96cbbe0 100644 Binary files a/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate and b/Sora.xcodeproj/project.xcworkspace/xcuserdata/Francesco.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sora/Views/AnimeViews/AnimeInfoView.swift b/Sora/Views/AnimeViews/AnimeInfoView.swift index ba0f9cf..d0b8ebf 100644 --- a/Sora/Views/AnimeViews/AnimeInfoView.swift +++ b/Sora/Views/AnimeViews/AnimeInfoView.swift @@ -52,7 +52,7 @@ struct AnimeInfoView: View { Spacer() - HStack(alignment: .top, spacing: 12) { + HStack(alignment: .center, spacing: 12) { Text(module.name) .font(.system(size: 13)) .padding(4) diff --git a/Sora/Views/SettingsViews/SettingView.swift b/Sora/Views/SettingsViews/SettingView.swift index 7950eec..f09b18e 100644 --- a/Sora/Views/SettingsViews/SettingView.swift +++ b/Sora/Views/SettingsViews/SettingView.swift @@ -49,6 +49,12 @@ struct SettingsView: View { Text("Modules") } } + NavigationLink(destination: StorageSettingsView()) { + HStack { + Image(systemName: "externaldrive.fill") + Text("Storage") + } + } Button(action: { isDocumentPickerPresented = true }) { diff --git a/Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift b/Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift index 654bcce..151be451 100644 --- a/Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift +++ b/Sora/Views/SettingsViews/SubPages/SettingsAboutView.swift @@ -13,7 +13,7 @@ struct AboutView: View { Form { Section(footer: Text("Sora is a free open source app, under the GPLv3.0 License. You can find the entire Sora code in the github repo.")) { HStack(alignment: .center, spacing: 10) { - KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Sora/main/Sora/Assets.xcassets/AppIcon.appiconset/1024.jpg")) + KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Sora/main/Sora/Assets.xcassets/AppIcon.appiconset/180.png")) .resizable() .frame(width: 80, height: 80) .clipShape(Circle()) @@ -93,6 +93,21 @@ struct AboutView: View { .foregroundColor(.orange) } } + HStack { + KFImage(URL(string: "https://cdn.discordapp.com/avatars/1072985316916469870/6bc6c23a6f22f932d23f2c2452f755f7.png?size=4096")) + .resizable() + .frame(width: 40, height: 40) + .clipShape(Circle()) + + VStack(alignment: .leading) { + Text("50/50") + .font(.headline) + .foregroundColor(.orange) + Text("Discord Helper & Designer") + .font(.subheadline) + .foregroundColor(.secondary) + } + } } Section(header: Text("Acknowledgements"), footer: Text("Thanks to the creators of this frameworks, that made Sora creation much simplier.")) { diff --git a/Sora/Views/SettingsViews/SubPages/StorageSettingsView.swift b/Sora/Views/SettingsViews/SubPages/StorageSettingsView.swift new file mode 100644 index 0000000..b901cee --- /dev/null +++ b/Sora/Views/SettingsViews/SubPages/StorageSettingsView.swift @@ -0,0 +1,269 @@ +// +// StorageSettingsView.swift +// Sora +// +// Created by Francesco on 19/12/24. +// + +import SwiftUI + +struct StorageSettingsView: View { + @State private var appSize: String = "Calculating..." + @State private var storageDetails: [(String, Double, Color)] = [] + @State private var deviceStorage: (total: Int64, used: Int64) = (0, 0) + @State private var showingClearCacheAlert = false + @State private var showingClearDocumentsAlert = false + + var body: some View { + List { + Section { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .center) { + Text(appSize) + .font(.system(size: 28, weight: .bold)) + Text("of \(ByteCountFormatter.string(fromByteCount: deviceStorage.total, countStyle: .file))") + .foregroundColor(.secondary) + } + + VStack(alignment: .leading, spacing: 4) { + GeometryReader { geometry in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 8) + .fill(Color.gray.opacity(0.2)) + .frame(height: 24) + + RoundedRectangle(cornerRadius: 8) + .fill(Color.gray.opacity(0.3)) + .frame(width: geometry.size.width * CGFloat(deviceStorage.used) / CGFloat(deviceStorage.total)) + .frame(height: 24) + + HStack(spacing: 0) { + ForEach(storageDetails, id: \.0) { detail in + RoundedRectangle(cornerRadius: 0) + .fill(detail.2) + .frame(width: geometry.size.width * CGFloat(detail.1 * 1024 * 1024) / CGFloat(deviceStorage.total)) + } + } + .frame(height: 24) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + .frame(height: 24) + + HStack(spacing: 16) { + ForEach(storageDetails, id: \.0) { detail in + HStack(spacing: 4) { + Circle() + .fill(detail.2) + .frame(width: 8, height: 8) + Text(detail.0) + .font(.caption) + .foregroundColor(.secondary) + } + } + HStack(spacing: 4) { + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 8, height: 8) + Text("System") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + } + .padding(.vertical, 8) + } + + Section { + ForEach(storageDetails, id: \.0) { detail in + HStack { + Image(systemName: categoryIcon(for: detail.0)) + .foregroundColor(.white) + .frame(width: 28, height: 28) + .background(detail.2) + .clipShape(RoundedRectangle(cornerRadius: 6)) + + VStack(alignment: .leading, spacing: 2) { + Text(detail.0) + Text("\(detail.1, specifier: "%.2f") MB") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + } + + Section(header: Text("Actions")) { + Button(action: { showingClearCacheAlert = true }) { + actionRow( + icon: "clock.fill", + title: "Clear Cache", + subtitle: "Free up space used by cached items", + iconColor: .accentColor + ) + } + + Button(action: { showingClearDocumentsAlert = true }) { + actionRow( + icon: "doc.fill", + title: "Clear Documents", + subtitle: "Check and remove unnecessary files", + iconColor: .accentColor + ) + } + } + } + .navigationTitle("Storage") + .onAppear { + calculateAppSize() + getDeviceStorage() + } + .alert("Clear Cache", isPresented: $showingClearCacheAlert) { + Button("Cancel", role: .cancel) { } + Button("Clear", role: .destructive) { + clearCache() + } + } message: { + Text("Are you sure you want to clear the cache? This action cannot be undone.") + } + .alert("Clear Documents", isPresented: $showingClearDocumentsAlert) { + Button("Cancel", role: .cancel) { } + Button("Clear", role: .destructive) { + clearDocuments() + } + } message: { + Text("Are you sure you want to clear all documents? This action cannot be undone.") + } + } + + private func clearCache() { + if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { + do { + let fileURLs = try FileManager.default.contentsOfDirectory( + at: cacheURL, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles + ) + + for fileURL in fileURLs { + try FileManager.default.removeItem(at: fileURL) + } + + calculateAppSize() + getDeviceStorage() + Logger.shared.log("Cleared Cache") + } catch { + print("Error clearing cache: \(error)") + Logger.shared.log("Error clearing cache: \(error)") + } + } + } + + private func clearDocuments() { + if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + do { + let fileURLs = try FileManager.default.contentsOfDirectory( + at: documentsURL, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles + ) + + for fileURL in fileURLs { + try FileManager.default.removeItem(at: fileURL) + } + + calculateAppSize() + getDeviceStorage() + Logger.shared.log("Cleared Documents") + } catch { + print("Error clearing documents: \(error)") + Logger.shared.log("Error clearing documents: \(error)") + } + } + } + + private func getDeviceStorage() { + do { + let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String) + let values = try fileURL.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey]) + + if let total = values.volumeTotalCapacity, + let available = values.volumeAvailableCapacity { + deviceStorage.total = Int64(total) + deviceStorage.used = Int64(total - available) + } + } catch { + print("Error getting device storage: \(error)") + Logger.shared.log("Error getting device storage: \(error)") + } + } + + private func getTotalAppBytes() -> Int64 { + return Int64(totalSize() * 1024 * 1024) + } + + private func actionRow(icon: String, title: String, subtitle: String, iconColor: Color) -> some View { + HStack { + Image(systemName: icon) + .foregroundColor(.white) + .frame(width: 28, height: 28) + .background(iconColor) + .clipShape(RoundedRectangle(cornerRadius: 6)) + + VStack(alignment: .leading, spacing: 2) { + Text(title) + Text(subtitle) + .font(.caption) + .foregroundColor(.secondary) + } + } + } + + private func categoryIcon(for category: String) -> String { + switch category { + case "Documents": + return "doc.fill" + case "Cache": + return "clock.fill" + case "Temporary": + return "trash.fill" + default: + return "questionmark" + } + } + + private func calculateAppSize() { + let cacheSize = getDirectorySize(url: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!) + let documentsSize = getDirectorySize(url: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!) + let tmpSize = getDirectorySize(url: FileManager.default.temporaryDirectory) + + let totalSize = cacheSize + documentsSize + tmpSize + self.appSize = ByteCountFormatter.string(fromByteCount: Int64(totalSize), countStyle: .file) + self.storageDetails = [ + ("Documents", documentsSize / 1024 / 1024, .green), + ("Cache", cacheSize / 1024 / 1024, .orange), + ("Temporary", tmpSize / 1024 / 1024, .red) + ] + } + + private func getDirectorySize(url: URL) -> Double { + var size: Double = 0 + if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey], options: [], errorHandler: nil) { + for case let fileURL as URL in enumerator { + do { + let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) + size += Double(resourceValues.fileSize ?? 0) + } catch { + print("Error calculating size for file \(fileURL): \(error)") + Logger.shared.log("Error calculating size for file \(fileURL): \(error)") + } + } + } + return size + } + + private func totalSize() -> Double { + return storageDetails.reduce(0) { $0 + $1.1 } + } +}