mirror of
https://github.com/cranci1/Sora.git
synced 2026-05-14 05:50:41 +00:00
parent
01d36394c6
commit
5d5365949e
8 changed files with 837 additions and 5 deletions
134
Sora/Managers/ImagePrefetchManager.swift
Normal file
134
Sora/Managers/ImagePrefetchManager.swift
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
//
|
||||||
|
// ImagePrefetchManager.swift
|
||||||
|
// Sora
|
||||||
|
//
|
||||||
|
// Created by doomsboygaming on 5/22/25
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Kingfisher
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Manager for image prefetching, caching, and optimization
|
||||||
|
class ImagePrefetchManager {
|
||||||
|
static let shared = ImagePrefetchManager()
|
||||||
|
|
||||||
|
// Prefetcher for batch prefetching images
|
||||||
|
private let prefetcher = ImagePrefetcher(
|
||||||
|
urls: [],
|
||||||
|
options: [
|
||||||
|
.processor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56))),
|
||||||
|
.scaleFactor(UIScreen.main.scale),
|
||||||
|
.cacheOriginalImage
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keep track of what's already prefetched to avoid duplication
|
||||||
|
private var prefetchedURLs = Set<URL>()
|
||||||
|
private let prefetchQueue = DispatchQueue(label: "com.sora.imagePrefetch", qos: .utility)
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Set up KingfisherManager for optimal image loading
|
||||||
|
ImageCache.default.memoryStorage.config.totalCostLimit = 300 * 1024 * 1024 // 300MB
|
||||||
|
ImageCache.default.diskStorage.config.sizeLimit = 1000 * 1024 * 1024 // 1GB
|
||||||
|
ImageDownloader.default.downloadTimeout = 15.0 // 15 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefetch a batch of images
|
||||||
|
func prefetchImages(_ urls: [String]) {
|
||||||
|
prefetchQueue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
// Filter out already prefetched URLs and invalid URLs
|
||||||
|
let urlObjects = urls.compactMap { URL(string: $0) }
|
||||||
|
.filter { !self.prefetchedURLs.contains($0) }
|
||||||
|
|
||||||
|
guard !urlObjects.isEmpty else { return }
|
||||||
|
|
||||||
|
// Create a new prefetcher with the URLs and start it
|
||||||
|
let newPrefetcher = ImagePrefetcher(
|
||||||
|
urls: urlObjects,
|
||||||
|
options: [
|
||||||
|
.processor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56))),
|
||||||
|
.scaleFactor(UIScreen.main.scale),
|
||||||
|
.cacheOriginalImage
|
||||||
|
]
|
||||||
|
)
|
||||||
|
newPrefetcher.start()
|
||||||
|
|
||||||
|
// Track prefetched URLs
|
||||||
|
urlObjects.forEach { self.prefetchedURLs.insert($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefetch a single image
|
||||||
|
func prefetchImage(_ url: String) {
|
||||||
|
guard let urlObject = URL(string: url),
|
||||||
|
!prefetchedURLs.contains(urlObject) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prefetchQueue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
// Create a new prefetcher with the URL and start it
|
||||||
|
let newPrefetcher = ImagePrefetcher(
|
||||||
|
urls: [urlObject],
|
||||||
|
options: [
|
||||||
|
.processor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56))),
|
||||||
|
.scaleFactor(UIScreen.main.scale),
|
||||||
|
.cacheOriginalImage
|
||||||
|
]
|
||||||
|
)
|
||||||
|
newPrefetcher.start()
|
||||||
|
|
||||||
|
// Track prefetched URL
|
||||||
|
self.prefetchedURLs.insert(urlObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefetch episode images for a batch of episodes
|
||||||
|
func prefetchEpisodeImages(anilistId: Int, startEpisode: Int, count: Int) {
|
||||||
|
prefetchQueue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
// Get metadata for episodes in the range
|
||||||
|
for episodeNumber in startEpisode...(startEpisode + count) where episodeNumber > 0 {
|
||||||
|
EpisodeMetadataManager.shared.fetchMetadata(anilistId: anilistId, episodeNumber: episodeNumber) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let metadata):
|
||||||
|
self.prefetchImage(metadata.imageUrl)
|
||||||
|
case .failure:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear prefetch queue and stop any ongoing prefetch operations
|
||||||
|
func cancelPrefetching() {
|
||||||
|
prefetcher.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - KFImage Extension
|
||||||
|
extension KFImage {
|
||||||
|
/// Load an image with optimal settings for episode thumbnails
|
||||||
|
static func optimizedEpisodeThumbnail(url: URL?) -> KFImage {
|
||||||
|
return KFImage(url)
|
||||||
|
.setProcessor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56)))
|
||||||
|
.memoryCacheExpiration(.seconds(300))
|
||||||
|
.cacheOriginalImage()
|
||||||
|
.fade(duration: 0.25)
|
||||||
|
.onProgress { _, _ in
|
||||||
|
// Track progress if needed
|
||||||
|
}
|
||||||
|
.onSuccess { _ in
|
||||||
|
// Success logger removed to reduce logs
|
||||||
|
}
|
||||||
|
.onFailure { error in
|
||||||
|
Logger.shared.log("Failed to load image: \(error)", type: "Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
165
Sora/Managers/ImageUpscaler.swift
Normal file
165
Sora/Managers/ImageUpscaler.swift
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
//
|
||||||
|
// ImageUpscaler.swift
|
||||||
|
// Sulfur
|
||||||
|
//
|
||||||
|
// Created by seiike on 26/05/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import CoreImage
|
||||||
|
import CoreImage.CIFilterBuiltins
|
||||||
|
import Vision
|
||||||
|
import CoreML
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
public enum ImageUpscaler {
|
||||||
|
/// Lanczos interpolation + unsharp mask for sharper upscaling.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - scale: The factor to upscale (e.g. 2.0 doubles width/height).
|
||||||
|
/// - sharpeningIntensity: The unsharp mask intensity (0...1).
|
||||||
|
/// - sharpeningRadius: The unsharp mask radius in pixels.
|
||||||
|
public static func lanczosProcessor(
|
||||||
|
scale: CGFloat,
|
||||||
|
sharpeningIntensity: Float = 0.7,
|
||||||
|
sharpeningRadius: Float = 2.0
|
||||||
|
) -> ImageProcessor {
|
||||||
|
return LanczosUpscaleProcessor(
|
||||||
|
scale: scale,
|
||||||
|
sharpeningIntensity: sharpeningIntensity,
|
||||||
|
sharpeningRadius: sharpeningRadius
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func superResolutionProcessor(modelURL: URL) -> ImageProcessor {
|
||||||
|
return MLScaleProcessor(modelURL: modelURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lanczos + Unsharp Mask Processor
|
||||||
|
public struct LanczosUpscaleProcessor: ImageProcessor {
|
||||||
|
public let scale: CGFloat
|
||||||
|
public let sharpeningIntensity: Float
|
||||||
|
public let sharpeningRadius: Float
|
||||||
|
public var identifier: String {
|
||||||
|
"com.yourapp.lanczos_\(scale)_sharp_\(sharpeningIntensity)_\(sharpeningRadius)"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(
|
||||||
|
scale: CGFloat,
|
||||||
|
sharpeningIntensity: Float = 0.7,
|
||||||
|
sharpeningRadius: Float = 2.0
|
||||||
|
) {
|
||||||
|
self.scale = scale
|
||||||
|
self.sharpeningIntensity = sharpeningIntensity
|
||||||
|
self.sharpeningRadius = sharpeningRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
public func process(
|
||||||
|
item: ImageProcessItem,
|
||||||
|
options: KingfisherParsedOptionsInfo
|
||||||
|
) -> KFCrossPlatformImage? {
|
||||||
|
|
||||||
|
let inputImage: KFCrossPlatformImage?
|
||||||
|
switch item {
|
||||||
|
case .image(let image):
|
||||||
|
inputImage = image
|
||||||
|
case .data(let data):
|
||||||
|
inputImage = KFCrossPlatformImage(data: data)
|
||||||
|
}
|
||||||
|
guard let uiImage = inputImage,
|
||||||
|
let cgImage = uiImage.cgImage else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let ciInput = CIImage(cgImage: cgImage)
|
||||||
|
|
||||||
|
let scaleFilter = CIFilter.lanczosScaleTransform()
|
||||||
|
scaleFilter.inputImage = ciInput
|
||||||
|
scaleFilter.scale = Float(scale)
|
||||||
|
scaleFilter.aspectRatio = 1.0
|
||||||
|
guard let scaledCI = scaleFilter.outputImage else {
|
||||||
|
return uiImage
|
||||||
|
}
|
||||||
|
|
||||||
|
let unsharp = CIFilter.unsharpMask()
|
||||||
|
unsharp.inputImage = scaledCI
|
||||||
|
unsharp.intensity = sharpeningIntensity
|
||||||
|
unsharp.radius = sharpeningRadius
|
||||||
|
guard let sharpCI = unsharp.outputImage else {
|
||||||
|
return UIImage(ciImage: scaledCI)
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = CIContext(options: nil)
|
||||||
|
guard let outputCG = context.createCGImage(sharpCI, from: sharpCI.extent) else {
|
||||||
|
return UIImage(ciImage: sharpCI)
|
||||||
|
}
|
||||||
|
return KFCrossPlatformImage(cgImage: outputCG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Core ML Super-Resolution Processor
|
||||||
|
public struct MLScaleProcessor: ImageProcessor {
|
||||||
|
private let request: VNCoreMLRequest
|
||||||
|
private let ciContext = CIContext()
|
||||||
|
public let identifier: String
|
||||||
|
|
||||||
|
public init(modelURL: URL) {
|
||||||
|
|
||||||
|
self.identifier = "com.yourapp.ml_sr_\(modelURL.lastPathComponent)"
|
||||||
|
guard let mlModel = try? MLModel(contentsOf: modelURL),
|
||||||
|
let visionModel = try? VNCoreMLModel(for: mlModel) else {
|
||||||
|
fatalError("Failed to load Core ML model at \(modelURL)")
|
||||||
|
}
|
||||||
|
let req = VNCoreMLRequest(model: visionModel)
|
||||||
|
req.imageCropAndScaleOption = .scaleFill
|
||||||
|
self.request = req
|
||||||
|
}
|
||||||
|
|
||||||
|
public func process(
|
||||||
|
item: ImageProcessItem,
|
||||||
|
options: KingfisherParsedOptionsInfo
|
||||||
|
) -> KFCrossPlatformImage? {
|
||||||
|
|
||||||
|
let inputImage: KFCrossPlatformImage?
|
||||||
|
switch item {
|
||||||
|
case .image(let image):
|
||||||
|
inputImage = image
|
||||||
|
case .data(let data):
|
||||||
|
inputImage = KFCrossPlatformImage(data: data)
|
||||||
|
}
|
||||||
|
guard let uiImage = inputImage,
|
||||||
|
let cgImage = uiImage.cgImage else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
|
||||||
|
do {
|
||||||
|
try handler.perform([request])
|
||||||
|
} catch {
|
||||||
|
print("[MLScaleProcessor] Vision error: \(error)")
|
||||||
|
return uiImage
|
||||||
|
}
|
||||||
|
guard let obs = request.results?.first as? VNPixelBufferObservation else {
|
||||||
|
return uiImage
|
||||||
|
}
|
||||||
|
|
||||||
|
let ciOutput = CIImage(cvPixelBuffer: obs.pixelBuffer)
|
||||||
|
let rect = CGRect(
|
||||||
|
origin: .zero,
|
||||||
|
size: CGSize(
|
||||||
|
width: CVPixelBufferGetWidth(obs.pixelBuffer),
|
||||||
|
height: CVPixelBufferGetHeight(obs.pixelBuffer)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
guard let finalCG = ciContext.createCGImage(ciOutput, from: rect) else {
|
||||||
|
return uiImage
|
||||||
|
}
|
||||||
|
return KFCrossPlatformImage(cgImage: finalCG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the sweet spot (for mediainfoview poster)
|
||||||
|
// .setProcessor(ImageUpscaler.lanczosProcessor(scale: 3.2,
|
||||||
|
// sharpeningIntensity: 0.75,
|
||||||
|
// sharpeningRadius: 2.25))
|
||||||
510
Sora/Managers/PerformanceMonitor.swift
Normal file
510
Sora/Managers/PerformanceMonitor.swift
Normal file
|
|
@ -0,0 +1,510 @@
|
||||||
|
//
|
||||||
|
// PerformanceMonitor.swift
|
||||||
|
// Sora
|
||||||
|
//
|
||||||
|
// Created by doomsboygaming on 5/22/25
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
import QuartzCore
|
||||||
|
|
||||||
|
/// Performance metrics tracking system with advanced jitter detection
|
||||||
|
class PerformanceMonitor: ObservableObject {
|
||||||
|
static let shared = PerformanceMonitor()
|
||||||
|
|
||||||
|
// Published properties to allow UI observation
|
||||||
|
@Published private(set) var networkRequestCount: Int = 0
|
||||||
|
@Published private(set) var cacheHitCount: Int = 0
|
||||||
|
@Published private(set) var cacheMissCount: Int = 0
|
||||||
|
@Published private(set) var averageLoadTime: TimeInterval = 0
|
||||||
|
@Published private(set) var memoryUsage: UInt64 = 0
|
||||||
|
@Published private(set) var diskUsage: UInt64 = 0
|
||||||
|
@Published private(set) var isEnabled: Bool = false
|
||||||
|
|
||||||
|
// Advanced performance metrics for jitter detection
|
||||||
|
@Published private(set) var currentFPS: Double = 60.0
|
||||||
|
@Published private(set) var mainThreadBlocks: Int = 0
|
||||||
|
@Published private(set) var memorySpikes: Int = 0
|
||||||
|
@Published private(set) var cpuUsage: Double = 0.0
|
||||||
|
@Published private(set) var jitterEvents: Int = 0
|
||||||
|
|
||||||
|
// Internal tracking properties
|
||||||
|
private var loadTimes: [TimeInterval] = []
|
||||||
|
private var startTimes: [String: Date] = [:]
|
||||||
|
private var memoryTimer: Timer?
|
||||||
|
private var logTimer: Timer?
|
||||||
|
|
||||||
|
// Advanced monitoring properties
|
||||||
|
private var displayLink: CADisplayLink?
|
||||||
|
private var frameCount: Int = 0
|
||||||
|
private var lastFrameTime: CFTimeInterval = 0
|
||||||
|
private var frameTimes: [CFTimeInterval] = []
|
||||||
|
private var lastMemoryUsage: UInt64 = 0
|
||||||
|
private var mainThreadOperations: [String: CFTimeInterval] = [:]
|
||||||
|
private var cpuTimer: Timer?
|
||||||
|
|
||||||
|
// Thresholds for performance issues
|
||||||
|
private let mainThreadBlockingThreshold: TimeInterval = 0.016 // 16ms for 60fps
|
||||||
|
private let memorySpikeTreshold: UInt64 = 50 * 1024 * 1024 // 50MB spike
|
||||||
|
private let fpsThreshold: Double = 50.0 // Below 50fps is considered poor
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
// Default is off unless explicitly enabled
|
||||||
|
isEnabled = UserDefaults.standard.bool(forKey: "enablePerformanceMonitoring")
|
||||||
|
|
||||||
|
// Setup memory monitoring if enabled
|
||||||
|
if isEnabled {
|
||||||
|
startMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
|
||||||
|
/// Enable or disable the performance monitoring
|
||||||
|
func setEnabled(_ enabled: Bool) {
|
||||||
|
isEnabled = enabled
|
||||||
|
UserDefaults.standard.set(enabled, forKey: "enablePerformanceMonitoring")
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
startMonitoring()
|
||||||
|
} else {
|
||||||
|
stopMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset all tracked metrics
|
||||||
|
func resetMetrics() {
|
||||||
|
networkRequestCount = 0
|
||||||
|
cacheHitCount = 0
|
||||||
|
cacheMissCount = 0
|
||||||
|
averageLoadTime = 0
|
||||||
|
loadTimes = []
|
||||||
|
startTimes = [:]
|
||||||
|
|
||||||
|
// Reset advanced metrics
|
||||||
|
mainThreadBlocks = 0
|
||||||
|
memorySpikes = 0
|
||||||
|
jitterEvents = 0
|
||||||
|
frameTimes = []
|
||||||
|
frameCount = 0
|
||||||
|
mainThreadOperations = [:]
|
||||||
|
|
||||||
|
updateMemoryUsage()
|
||||||
|
|
||||||
|
Logger.shared.log("Performance metrics reset", type: "Debug")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track a network request starting
|
||||||
|
func trackRequestStart(identifier: String) {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
|
||||||
|
networkRequestCount += 1
|
||||||
|
startTimes[identifier] = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track a network request completing
|
||||||
|
func trackRequestEnd(identifier: String) {
|
||||||
|
guard isEnabled, let startTime = startTimes[identifier] else { return }
|
||||||
|
|
||||||
|
let endTime = Date()
|
||||||
|
let duration = endTime.timeIntervalSince(startTime)
|
||||||
|
loadTimes.append(duration)
|
||||||
|
|
||||||
|
// Update average load time
|
||||||
|
if !loadTimes.isEmpty {
|
||||||
|
averageLoadTime = loadTimes.reduce(0, +) / Double(loadTimes.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove start time to avoid memory leaks
|
||||||
|
startTimes.removeValue(forKey: identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track a cache hit
|
||||||
|
func trackCacheHit() {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
cacheHitCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track a cache miss
|
||||||
|
func trackCacheMiss() {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
cacheMissCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Advanced Performance Monitoring
|
||||||
|
|
||||||
|
/// Track the start of a main thread operation
|
||||||
|
func trackMainThreadOperationStart(operation: String) {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
mainThreadOperations[operation] = CACurrentMediaTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track the end of a main thread operation and detect blocking
|
||||||
|
func trackMainThreadOperationEnd(operation: String) {
|
||||||
|
guard isEnabled, let startTime = mainThreadOperations[operation] else { return }
|
||||||
|
|
||||||
|
let endTime = CACurrentMediaTime()
|
||||||
|
let duration = endTime - startTime
|
||||||
|
|
||||||
|
if duration > mainThreadBlockingThreshold {
|
||||||
|
mainThreadBlocks += 1
|
||||||
|
jitterEvents += 1
|
||||||
|
|
||||||
|
let durationMs = Int(duration * 1000)
|
||||||
|
Logger.shared.log("🚨 Main thread blocked for \(durationMs)ms during: \(operation)", type: "Performance")
|
||||||
|
}
|
||||||
|
|
||||||
|
mainThreadOperations.removeValue(forKey: operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track memory spikes during downloads
|
||||||
|
func checkMemorySpike() {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
|
||||||
|
let currentMemory = getAppMemoryUsage()
|
||||||
|
|
||||||
|
if lastMemoryUsage > 0 {
|
||||||
|
let spike = currentMemory > lastMemoryUsage ? currentMemory - lastMemoryUsage : 0
|
||||||
|
|
||||||
|
if spike > memorySpikeTreshold {
|
||||||
|
memorySpikes += 1
|
||||||
|
jitterEvents += 1
|
||||||
|
|
||||||
|
let spikeSize = Double(spike) / (1024 * 1024)
|
||||||
|
Logger.shared.log("🚨 Memory spike detected: +\(String(format: "%.1f", spikeSize))MB", type: "Performance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMemoryUsage = currentMemory
|
||||||
|
memoryUsage = currentMemory
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start frame rate monitoring
|
||||||
|
private func startFrameRateMonitoring() {
|
||||||
|
guard displayLink == nil else { return }
|
||||||
|
|
||||||
|
displayLink = CADisplayLink(target: self, selector: #selector(frameCallback))
|
||||||
|
displayLink?.add(to: .main, forMode: .common)
|
||||||
|
|
||||||
|
frameCount = 0
|
||||||
|
lastFrameTime = CACurrentMediaTime()
|
||||||
|
frameTimes = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop frame rate monitoring
|
||||||
|
private func stopFrameRateMonitoring() {
|
||||||
|
displayLink?.invalidate()
|
||||||
|
displayLink = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame callback for FPS monitoring
|
||||||
|
@objc private func frameCallback() {
|
||||||
|
let currentTime = CACurrentMediaTime()
|
||||||
|
|
||||||
|
if lastFrameTime > 0 {
|
||||||
|
let frameDuration = currentTime - lastFrameTime
|
||||||
|
frameTimes.append(frameDuration)
|
||||||
|
|
||||||
|
// Keep only last 60 frames for rolling average
|
||||||
|
if frameTimes.count > 60 {
|
||||||
|
frameTimes.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate current FPS
|
||||||
|
if !frameTimes.isEmpty {
|
||||||
|
let averageFrameTime = frameTimes.reduce(0, +) / Double(frameTimes.count)
|
||||||
|
currentFPS = 1.0 / averageFrameTime
|
||||||
|
|
||||||
|
// Detect FPS drops
|
||||||
|
if currentFPS < fpsThreshold {
|
||||||
|
jitterEvents += 1
|
||||||
|
Logger.shared.log("🚨 FPS drop detected: \(String(format: "%.1f", currentFPS))fps", type: "Performance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastFrameTime = currentTime
|
||||||
|
frameCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current CPU usage
|
||||||
|
private func getCPUUsage() -> Double {
|
||||||
|
var info = mach_task_basic_info()
|
||||||
|
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
|
||||||
|
|
||||||
|
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
|
||||||
|
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
||||||
|
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if kerr == KERN_SUCCESS {
|
||||||
|
// This is a simplified CPU usage calculation
|
||||||
|
// For more accurate results, we'd need to track over time
|
||||||
|
return Double(info.user_time.seconds + info.system_time.seconds)
|
||||||
|
} else {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current cache hit rate
|
||||||
|
var cacheHitRate: Double {
|
||||||
|
let total = cacheHitCount + cacheMissCount
|
||||||
|
guard total > 0 else { return 0 }
|
||||||
|
return Double(cacheHitCount) / Double(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log current performance metrics
|
||||||
|
func logMetrics() {
|
||||||
|
guard isEnabled else { return }
|
||||||
|
|
||||||
|
checkMemorySpike()
|
||||||
|
|
||||||
|
let hitRate = String(format: "%.1f%%", cacheHitRate * 100)
|
||||||
|
let avgLoad = String(format: "%.2f", averageLoadTime)
|
||||||
|
let memory = String(format: "%.1f MB", Double(memoryUsage) / (1024 * 1024))
|
||||||
|
let disk = String(format: "%.1f MB", Double(diskUsage) / (1024 * 1024))
|
||||||
|
let fps = String(format: "%.1f", currentFPS)
|
||||||
|
let cpu = String(format: "%.1f%%", cpuUsage)
|
||||||
|
|
||||||
|
let metrics = """
|
||||||
|
📊 Performance Metrics Report:
|
||||||
|
═══════════════════════════════
|
||||||
|
Network & Cache:
|
||||||
|
- Network Requests: \(networkRequestCount)
|
||||||
|
- Cache Hit Rate: \(hitRate) (\(cacheHitCount)/\(cacheHitCount + cacheMissCount))
|
||||||
|
- Average Load Time: \(avgLoad)s
|
||||||
|
|
||||||
|
System Resources:
|
||||||
|
- Memory Usage: \(memory)
|
||||||
|
- Disk Usage: \(disk)
|
||||||
|
- CPU Usage: \(cpu)
|
||||||
|
|
||||||
|
Performance Issues:
|
||||||
|
- Current FPS: \(fps)
|
||||||
|
- Main Thread Blocks: \(mainThreadBlocks)
|
||||||
|
- Memory Spikes: \(memorySpikes)
|
||||||
|
- Total Jitter Events: \(jitterEvents)
|
||||||
|
═══════════════════════════════
|
||||||
|
"""
|
||||||
|
|
||||||
|
Logger.shared.log(metrics, type: "Performance")
|
||||||
|
|
||||||
|
// Alert if performance is poor
|
||||||
|
if jitterEvents > 0 {
|
||||||
|
Logger.shared.log("⚠️ Performance issues detected! Check logs above for details.", type: "Warning")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Methods
|
||||||
|
|
||||||
|
private func startMonitoring() {
|
||||||
|
// Setup timer to update memory usage periodically and check for spikes
|
||||||
|
memoryTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
|
||||||
|
self?.checkMemorySpike()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup timer to log metrics periodically
|
||||||
|
logTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in
|
||||||
|
self?.logMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup CPU monitoring timer
|
||||||
|
cpuTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
|
||||||
|
self?.cpuUsage = self?.getCPUUsage() ?? 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure timers run even when scrolling
|
||||||
|
RunLoop.current.add(memoryTimer!, forMode: .common)
|
||||||
|
RunLoop.current.add(logTimer!, forMode: .common)
|
||||||
|
RunLoop.current.add(cpuTimer!, forMode: .common)
|
||||||
|
|
||||||
|
// Start frame rate monitoring
|
||||||
|
startFrameRateMonitoring()
|
||||||
|
|
||||||
|
Logger.shared.log("Advanced performance monitoring started - tracking FPS, main thread blocks, memory spikes", type: "Debug")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopMonitoring() {
|
||||||
|
memoryTimer?.invalidate()
|
||||||
|
memoryTimer = nil
|
||||||
|
|
||||||
|
logTimer?.invalidate()
|
||||||
|
logTimer = nil
|
||||||
|
|
||||||
|
cpuTimer?.invalidate()
|
||||||
|
cpuTimer = nil
|
||||||
|
|
||||||
|
stopFrameRateMonitoring()
|
||||||
|
|
||||||
|
Logger.shared.log("Performance monitoring stopped", type: "Debug")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateMemoryUsage() {
|
||||||
|
memoryUsage = getAppMemoryUsage()
|
||||||
|
diskUsage = getCacheDiskUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getAppMemoryUsage() -> UInt64 {
|
||||||
|
var info = mach_task_basic_info()
|
||||||
|
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
|
||||||
|
|
||||||
|
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
|
||||||
|
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
||||||
|
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if kerr == KERN_SUCCESS {
|
||||||
|
return info.resident_size
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getCacheDiskUsage() -> UInt64 {
|
||||||
|
// Try to get Kingfisher's disk cache size
|
||||||
|
let diskCache = ImageCache.default.diskStorage
|
||||||
|
|
||||||
|
do {
|
||||||
|
let size = try diskCache.totalSize()
|
||||||
|
return UInt64(size)
|
||||||
|
} catch {
|
||||||
|
Logger.shared.log("Failed to get disk cache size: \(error)", type: "Error")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Extensions to integrate with managers
|
||||||
|
|
||||||
|
extension EpisodeMetadataManager {
|
||||||
|
/// Integrate performance tracking
|
||||||
|
func trackFetchStart(anilistId: Int, episodeNumber: Int) {
|
||||||
|
let identifier = "metadata_\(anilistId)_\(episodeNumber)"
|
||||||
|
PerformanceMonitor.shared.trackRequestStart(identifier: identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackFetchEnd(anilistId: Int, episodeNumber: Int) {
|
||||||
|
let identifier = "metadata_\(anilistId)_\(episodeNumber)"
|
||||||
|
PerformanceMonitor.shared.trackRequestEnd(identifier: identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackCacheHit() {
|
||||||
|
PerformanceMonitor.shared.trackCacheHit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackCacheMiss() {
|
||||||
|
PerformanceMonitor.shared.trackCacheMiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImagePrefetchManager {
|
||||||
|
/// Integrate performance tracking
|
||||||
|
func trackImageLoadStart(url: String) {
|
||||||
|
let identifier = "image_\(url.hashValue)"
|
||||||
|
PerformanceMonitor.shared.trackRequestStart(identifier: identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackImageLoadEnd(url: String) {
|
||||||
|
let identifier = "image_\(url.hashValue)"
|
||||||
|
PerformanceMonitor.shared.trackRequestEnd(identifier: identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackImageCacheHit() {
|
||||||
|
PerformanceMonitor.shared.trackCacheHit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackImageCacheMiss() {
|
||||||
|
PerformanceMonitor.shared.trackCacheMiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Debug View
|
||||||
|
struct PerformanceMetricsView: View {
|
||||||
|
@ObservedObject private var monitor = PerformanceMonitor.shared
|
||||||
|
@State private var isExpanded = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("Performance Metrics")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
isExpanded.toggle()
|
||||||
|
}) {
|
||||||
|
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
if isExpanded {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Network Requests: \(monitor.networkRequestCount)")
|
||||||
|
Text("Cache Hit Rate: \(Int(monitor.cacheHitRate * 100))%")
|
||||||
|
Text("Avg Load Time: \(String(format: "%.2f", monitor.averageLoadTime))s")
|
||||||
|
Text("Memory: \(String(format: "%.1f MB", Double(monitor.memoryUsage) / (1024 * 1024)))")
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// Advanced metrics
|
||||||
|
Text("FPS: \(String(format: "%.1f", monitor.currentFPS))")
|
||||||
|
.foregroundColor(monitor.currentFPS < 50 ? .red : .primary)
|
||||||
|
Text("Main Thread Blocks: \(monitor.mainThreadBlocks)")
|
||||||
|
.foregroundColor(monitor.mainThreadBlocks > 0 ? .red : .primary)
|
||||||
|
Text("Memory Spikes: \(monitor.memorySpikes)")
|
||||||
|
.foregroundColor(monitor.memorySpikes > 0 ? .orange : .primary)
|
||||||
|
Text("Jitter Events: \(monitor.jitterEvents)")
|
||||||
|
.foregroundColor(monitor.jitterEvents > 0 ? .red : .primary)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button(action: {
|
||||||
|
monitor.resetMetrics()
|
||||||
|
}) {
|
||||||
|
Text("Reset")
|
||||||
|
.font(.caption)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color.blue)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
monitor.logMetrics()
|
||||||
|
}) {
|
||||||
|
Text("Log")
|
||||||
|
.font(.caption)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color.green)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle("", isOn: Binding(
|
||||||
|
get: { monitor.isEnabled },
|
||||||
|
set: { monitor.setEnabled($0) }
|
||||||
|
))
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.bottom, 8)
|
||||||
|
.background(Color.secondary.opacity(0.1))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color.secondary.opacity(0.05))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.padding(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,9 @@ class TMDBFetcher {
|
||||||
let results: [TMDBResult]
|
let results: [TMDBResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let apiKey = "738b4edd0a156cc126dc4a4b8aea4aca"
|
||||||
|
private let session = URLSession.custom
|
||||||
|
|
||||||
func fetchBestMatchID(for title: String, completion: @escaping (Int?, MediaType?) -> Void) {
|
func fetchBestMatchID(for title: String, completion: @escaping (Int?, MediaType?) -> Void) {
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
var bestResults: [(id: Int, score: Double, type: MediaType)] = []
|
var bestResults: [(id: Int, score: Double, type: MediaType)] = []
|
||||||
|
|
@ -45,13 +48,13 @@ class TMDBFetcher {
|
||||||
|
|
||||||
private func fetchBestMatchID(for title: String, type: MediaType, completion: @escaping (Int?, Double?) -> Void) {
|
private func fetchBestMatchID(for title: String, type: MediaType, completion: @escaping (Int?, Double?) -> Void) {
|
||||||
let query = title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
let query = title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||||
let urlString = "https://api.themoviedb.org/3/search/\(type.rawValue)?api_key=738b4edd0a156cc126dc4a4b8aea4aca&query=\(query)"
|
let urlString = "https://api.themoviedb.org/3/search/\(type.rawValue)?api_key=\(apiKey)&query=\(query)"
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
completion(nil, nil)
|
completion(nil, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
session.dataTask(with: url) { data, _, error in
|
||||||
guard let data = data, error == nil else {
|
guard let data = data, error == nil else {
|
||||||
completion(nil, nil)
|
completion(nil, nil)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -895,6 +895,8 @@ struct EpisodeCell: View {
|
||||||
let urlString = "https://api.themoviedb.org/3/tv/\(tmdbID)/season/\(season)/episode/\(episodeNum)?api_key=738b4edd0a156cc126dc4a4b8aea4aca"
|
let urlString = "https://api.themoviedb.org/3/tv/\(tmdbID)/season/\(season)/episode/\(episodeNum)?api_key=738b4edd0a156cc126dc4a4b8aea4aca"
|
||||||
guard let url = URL(string: urlString) else { return }
|
guard let url = URL(string: urlString) else { return }
|
||||||
|
|
||||||
|
let tmdbImageWidth = UserDefaults.standard.string(forKey: "tmdbImageWidth") ?? "original"
|
||||||
|
|
||||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||||
guard let data = data, error == nil else { return }
|
guard let data = data, error == nil else { return }
|
||||||
do {
|
do {
|
||||||
|
|
@ -903,7 +905,11 @@ struct EpisodeCell: View {
|
||||||
let stillPath = json["still_path"] as? String
|
let stillPath = json["still_path"] as? String
|
||||||
let imageUrl: String
|
let imageUrl: String
|
||||||
if let stillPath = stillPath {
|
if let stillPath = stillPath {
|
||||||
imageUrl = "https://image.tmdb.org/t/p/w300\(stillPath)"
|
if tmdbImageWidth == "original" {
|
||||||
|
imageUrl = "https://image.tmdb.org/t/p/original\(stillPath)"
|
||||||
|
} else {
|
||||||
|
imageUrl = "https://image.tmdb.org/t/p/w\(tmdbImageWidth)\(stillPath)"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
imageUrl = ""
|
imageUrl = ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,12 +212,14 @@ struct MediaInfoView: View {
|
||||||
.fill(Color.gray.opacity(0.3))
|
.fill(Color.gray.opacity(0.3))
|
||||||
.shimmering()
|
.shimmering()
|
||||||
}
|
}
|
||||||
|
.setProcessor(ImageUpscaler.lanczosProcessor(scale: 3, sharpeningIntensity: 1.5, sharpeningRadius: 0.8))
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: UIScreen.main.bounds.width, height: 700)
|
.frame(width: UIScreen.main.bounds.width, height: 700)
|
||||||
.clipped()
|
.clipped()
|
||||||
KFImage(URL(string: imageUrl))
|
KFImage(URL(string: imageUrl))
|
||||||
.placeholder { EmptyView() }
|
.placeholder { EmptyView() }
|
||||||
|
.setProcessor(ImageUpscaler.lanczosProcessor(scale: 3, sharpeningIntensity: 1.5, sharpeningRadius: 0.8))
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: UIScreen.main.bounds.width, height: 700)
|
.frame(width: UIScreen.main.bounds.width, height: 700)
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ struct SettingsViewGeneral: View {
|
||||||
|
|
||||||
SettingsPickerRow(
|
SettingsPickerRow(
|
||||||
icon: "square.stack.3d.down.right",
|
icon: "square.stack.3d.down.right",
|
||||||
title: "Poster Quality",
|
title: "Thumbnails Width",
|
||||||
options: TMDBimageWidhtList,
|
options: TMDBimageWidhtList,
|
||||||
optionToString: { $0 },
|
optionToString: { $0 },
|
||||||
selection: $TMDBimageWidht,
|
selection: $TMDBimageWidht,
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@
|
||||||
1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */; };
|
1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */; };
|
||||||
1E47859B2DEBC5960095BF2F /* AnilistMatchPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */; };
|
1E47859B2DEBC5960095BF2F /* AnilistMatchPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */; };
|
||||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
|
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
|
||||||
|
1EA64DCD2DE5030100AC14BC /* ImageUpscaler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */; };
|
||||||
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; };
|
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; };
|
||||||
1EF5C3A92DB988E40032BF07 /* CommunityLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */; };
|
1EF5C3A92DB988E40032BF07 /* CommunityLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */; };
|
||||||
7205AEDB2DCCEF9500943F3F /* EpisodeMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205AED72DCCEF9500943F3F /* EpisodeMetadata.swift */; };
|
7205AEDB2DCCEF9500943F3F /* EpisodeMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205AED72DCCEF9500943F3F /* EpisodeMetadata.swift */; };
|
||||||
|
|
@ -97,7 +98,9 @@
|
||||||
72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */; };
|
72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */; };
|
||||||
727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */; };
|
727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */; };
|
||||||
727220722DD642B100C2A4A2 /* JSController+MP4Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = 727220702DD642B100C2A4A2 /* JSController+MP4Download.swift */; };
|
727220722DD642B100C2A4A2 /* JSController+MP4Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = 727220702DD642B100C2A4A2 /* JSController+MP4Download.swift */; };
|
||||||
|
72AC3A012DD4DAEB00C60B96 /* ImagePrefetchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72AC39FE2DD4DAEA00C60B96 /* ImagePrefetchManager.swift */; };
|
||||||
72AC3A022DD4DAEB00C60B96 /* EpisodeMetadataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */; };
|
72AC3A022DD4DAEB00C60B96 /* EpisodeMetadataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */; };
|
||||||
|
72AC3A032DD4DAEB00C60B96 /* PerformanceMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72AC39FF2DD4DAEA00C60B96 /* PerformanceMonitor.swift */; };
|
||||||
73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; };
|
73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
|
@ -178,6 +181,7 @@
|
||||||
1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = "<group>"; };
|
1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = "<group>"; };
|
||||||
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchPopupView.swift; sourceTree = "<group>"; };
|
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchPopupView.swift; sourceTree = "<group>"; };
|
||||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
|
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
|
||||||
|
1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUpscaler.swift; sourceTree = "<group>"; };
|
||||||
1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||||
1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLib.swift; sourceTree = "<group>"; };
|
1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLib.swift; sourceTree = "<group>"; };
|
||||||
7205AED72DCCEF9500943F3F /* EpisodeMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeMetadata.swift; sourceTree = "<group>"; };
|
7205AED72DCCEF9500943F3F /* EpisodeMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeMetadata.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -193,6 +197,8 @@
|
||||||
7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-StreamTypeDownload.swift"; sourceTree = "<group>"; };
|
7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-StreamTypeDownload.swift"; sourceTree = "<group>"; };
|
||||||
727220702DD642B100C2A4A2 /* JSController+MP4Download.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController+MP4Download.swift"; sourceTree = "<group>"; };
|
727220702DD642B100C2A4A2 /* JSController+MP4Download.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController+MP4Download.swift"; sourceTree = "<group>"; };
|
||||||
72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeMetadataManager.swift; sourceTree = "<group>"; };
|
72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeMetadataManager.swift; sourceTree = "<group>"; };
|
||||||
|
72AC39FE2DD4DAEA00C60B96 /* ImagePrefetchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePrefetchManager.swift; sourceTree = "<group>"; };
|
||||||
|
72AC39FF2DD4DAEA00C60B96 /* PerformanceMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMonitor.swift; sourceTree = "<group>"; };
|
||||||
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = "<group>"; };
|
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
|
@ -321,9 +327,9 @@
|
||||||
133D7C6C2D2BE2500075467E /* Sora */ = {
|
133D7C6C2D2BE2500075467E /* Sora */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
72AC3A002DD4DAEA00C60B96 /* Managers */,
|
||||||
130C6BF82D53A4C200DC1432 /* Sora.entitlements */,
|
130C6BF82D53A4C200DC1432 /* Sora.entitlements */,
|
||||||
13DC0C412D2EC9BA00D0F966 /* Info.plist */,
|
13DC0C412D2EC9BA00D0F966 /* Info.plist */,
|
||||||
72AC3A002DD4DAEA00C60B96 /* Managers */,
|
|
||||||
13103E802D589D6C000F0673 /* Tracking Services */,
|
13103E802D589D6C000F0673 /* Tracking Services */,
|
||||||
133D7C852D2BE2640075467E /* Utils */,
|
133D7C852D2BE2640075467E /* Utils */,
|
||||||
133D7C7B2D2BE2630075467E /* Views */,
|
133D7C7B2D2BE2630075467E /* Views */,
|
||||||
|
|
@ -623,7 +629,10 @@
|
||||||
72AC3A002DD4DAEA00C60B96 /* Managers */ = {
|
72AC3A002DD4DAEA00C60B96 /* Managers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */,
|
||||||
72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */,
|
72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */,
|
||||||
|
72AC39FE2DD4DAEA00C60B96 /* ImagePrefetchManager.swift */,
|
||||||
|
72AC39FF2DD4DAEA00C60B96 /* PerformanceMonitor.swift */,
|
||||||
);
|
);
|
||||||
path = Managers;
|
path = Managers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -779,12 +788,15 @@
|
||||||
727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */,
|
727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */,
|
||||||
727220722DD642B100C2A4A2 /* JSController+MP4Download.swift in Sources */,
|
727220722DD642B100C2A4A2 /* JSController+MP4Download.swift in Sources */,
|
||||||
132FC5B32DE31DAE009A80F7 /* SettingsViewAlternateAppIconPicker.swift in Sources */,
|
132FC5B32DE31DAE009A80F7 /* SettingsViewAlternateAppIconPicker.swift in Sources */,
|
||||||
|
1EA64DCD2DE5030100AC14BC /* ImageUpscaler.swift in Sources */,
|
||||||
0402DA132DE7B5EC003BB42C /* SearchStateView.swift in Sources */,
|
0402DA132DE7B5EC003BB42C /* SearchStateView.swift in Sources */,
|
||||||
0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */,
|
0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */,
|
||||||
0402DA152DE7B5EC003BB42C /* SearchComponents.swift in Sources */,
|
0402DA152DE7B5EC003BB42C /* SearchComponents.swift in Sources */,
|
||||||
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */,
|
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */,
|
||||||
04F08EDF2DE10C1D006B29D9 /* TabBar.swift in Sources */,
|
04F08EDF2DE10C1D006B29D9 /* TabBar.swift in Sources */,
|
||||||
|
72AC3A012DD4DAEB00C60B96 /* ImagePrefetchManager.swift in Sources */,
|
||||||
72AC3A022DD4DAEB00C60B96 /* EpisodeMetadataManager.swift in Sources */,
|
72AC3A022DD4DAEB00C60B96 /* EpisodeMetadataManager.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 */,
|
||||||
7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */,
|
7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue