episodes range + critical bug

the critical bug was an issue of the episodes response being cached in the userdefaults causing more than 120mb to be saved for 1k episodes
This commit is contained in:
cranci1 2025-01-27 15:11:59 +01:00
parent a75ca179ce
commit e2252e13d0
5 changed files with 124 additions and 57 deletions

View file

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
131845F92D47C62D00CA7A54 /* SettingsViewUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131845F82D47C62D00CA7A54 /* SettingsViewUI.swift */; };
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; };
133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; };
133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; };
@ -40,6 +41,7 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
131845F82D47C62D00CA7A54 /* SettingsViewUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewUI.swift; sourceTree = "<group>"; };
133D7C6A2D2BE2500075467E /* Sora.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sora.app; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
@ -152,6 +154,7 @@
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */,
1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */,
133D7C842D2BE2630075467E /* SettingsViewModule.swift */,
131845F82D47C62D00CA7A54 /* SettingsViewUI.swift */,
);
path = SettingsSubViews;
sourceTree = "<group>";
@ -368,6 +371,7 @@
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
131845F92D47C62D00CA7A54 /* SettingsViewUI.swift in Sources */,
133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */,
13EA2BDC2D32D9FF00C1EBD7 /* MiruDataStruct.swift in Sources */,
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */,

View file

@ -89,13 +89,6 @@ struct EpisodeCell: View {
}
func fetchEpisodeDetails() {
let cacheKey = "episodeDetails_\(itemID)_\(episodeID)"
if let cachedData = UserDefaults.standard.data(forKey: cacheKey) {
parseEpisodeDetails(data: cachedData)
return
}
guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(itemID)") else {
isLoading = false
return
@ -117,39 +110,30 @@ struct EpisodeCell: View {
return
}
DispatchQueue.main.async {
self.parseEpisodeDetails(data: data)
UserDefaults.standard.set(data, forKey: cacheKey)
self.isLoading = false
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
guard let json = jsonObject as? [String: Any],
let episodes = json["episodes"] as? [String: Any],
let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any],
let title = episodeDetails["title"] as? [String: String],
let image = episodeDetails["image"] as? String else {
Logger.shared.log("Invalid response format", type: "Error")
DispatchQueue.main.async {
self.isLoading = false
}
return
}
DispatchQueue.main.async {
self.episodeTitle = title["en"] ?? ""
self.episodeImageUrl = image
self.isLoading = false
}
} catch {
DispatchQueue.main.async {
self.isLoading = false
}
}
}.resume()
}
func parseEpisodeDetails(data: Data) {
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
guard let json = jsonObject as? [String: Any],
let episodes = json["episodes"] as? [String: Any],
let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any],
let title = episodeDetails["title"] as? [String: String],
let image = episodeDetails["image"] as? String else {
Logger.shared.log("Invalid response format", type: "Error")
DispatchQueue.main.async {
self.isLoading = false
}
return
}
DispatchQueue.main.async {
self.episodeTitle = title["en"] ?? ""
self.episodeImageUrl = image
self.isLoading = false
}
} catch {
Logger.shared.log("Failed to parse JSON: \(error)", type: "Error")
DispatchQueue.main.async {
self.isLoading = false
}
}
}
}

View file

@ -34,11 +34,14 @@ struct MediaInfoView: View {
@State var isRefetching: Bool = true
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
@AppStorage("episodeChunkSize") private var episodeChunkSize: Int = 100
@StateObject private var jsController = JSController()
@EnvironmentObject var moduleManager: ModuleManager
@EnvironmentObject private var libraryManager: LibraryManager
@State private var selectedRange: Range<Int> = 0..<100
var body: some View {
Group {
if isLoading {
@ -164,11 +167,31 @@ struct MediaInfoView: View {
if !episodeLinks.isEmpty {
VStack(alignment: .leading, spacing: 10) {
Text("Episodes")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("Episodes")
.font(.system(size: 18))
.fontWeight(.bold)
Spacer()
if episodeLinks.count > episodeChunkSize {
Menu {
ForEach(generateRanges(), id: \.self) { range in
Button(action: {
selectedRange = range
}) {
Text("\(range.lowerBound + 1)-\(range.upperBound)")
}
}
} label: {
Text("\(selectedRange.lowerBound + 1)-\(selectedRange.upperBound)")
.font(.system(size: 14))
.foregroundColor(.accentColor)
}
}
}
ForEach(episodeLinks.indices, id: \.self) { i in
ForEach(episodeLinks.indices.filter { selectedRange.contains($0) }, id: \.self) { i in
let ep = episodeLinks[i]
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
@ -233,9 +256,23 @@ struct MediaInfoView: View {
}
hasFetched = true
}
selectedRange = 0..<episodeChunkSize
}
}
private func generateRanges() -> [Range<Int>] {
let chunkSize = episodeChunkSize
let totalEpisodes = episodeLinks.count
var ranges: [Range<Int>] = []
for i in stride(from: 0, to: totalEpisodes, by: chunkSize) {
let end = min(i + chunkSize, totalEpisodes)
ranges.append(i..<end)
}
return ranges
}
func fetchDetails() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
Task {

View file

@ -0,0 +1,50 @@
//
// SettingsViewUI.swift
// Sora
//
// Created by Francesco on 27/01/25.
//
import SwiftUI
struct SettingsViewUI: View {
@AppStorage("episodeChunkSize") private var episodeChunkSize: Int = 100
@EnvironmentObject var settings: Settings
var body: some View {
Form {
Section(header: Text("Interface")) {
ColorPicker("Accent Color", selection: $settings.accentColor)
HStack() {
Text("Appearance")
Picker("Appearance", selection: $settings.selectedAppearance) {
Text("System").tag(Appearance.system)
Text("Light").tag(Appearance.light)
Text("Dark").tag(Appearance.dark)
}
.pickerStyle(SegmentedPickerStyle())
}
}
Section(header: Text("Episode Chunk Size")) {
Menu {
Button(action: { episodeChunkSize = 25 }) {
Text("25")
}
Button(action: { episodeChunkSize = 50 }) {
Text("50")
}
Button(action: { episodeChunkSize = 75 }) {
Text("75")
}
Button(action: { episodeChunkSize = 100 }) {
Text("100")
}
} label: {
Text("Chunk Size: \(episodeChunkSize)")
}
}
}
.navigationTitle("UI Settings")
}
}

View file

@ -8,28 +8,20 @@
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var settings: Settings
var body: some View {
NavigationView {
Form {
Section(header: Text("Interface")) {
ColorPicker("Accent Color", selection: $settings.accentColor)
HStack() {
Text("Appearance")
Picker("Appearance", selection: $settings.selectedAppearance) {
Text("System").tag(Appearance.system)
Text("Light").tag(Appearance.light)
Text("Dark").tag(Appearance.dark)
}
.pickerStyle(SegmentedPickerStyle())
Section(header: Text("Main Settings")) {
NavigationLink(destination: SettingsViewUI()) {
Text("UI Settings")
}
}
Section(header: Text("External Features")) {
NavigationLink(destination: SettingsViewModule()) {
Text("Modules")
}
NavigationLink(destination: SettingsViewModule()) {
Text("Media Player")
}
}
Section(header: Text("Debug")) {