mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-13 13:00:40 +00:00
anilist improvement, logic for upscaling images (#137)
This commit is contained in:
parent
967791878a
commit
f2d0e55e57
9 changed files with 358 additions and 61 deletions
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))
|
||||
|
|
@ -33,32 +33,41 @@ class AniListMutation {
|
|||
return String(data: tokenData, encoding: .utf8)
|
||||
}
|
||||
|
||||
func updateAnimeProgress(animeId: Int, episodeNumber: Int, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let sendPushUpdates = UserDefaults.standard.object(forKey: "sendPushUpdates") as? Bool,
|
||||
sendPushUpdates == false {
|
||||
return
|
||||
}
|
||||
|
||||
guard let userToken = getTokenFromKeychain() else {
|
||||
completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Access token not found"])))
|
||||
return
|
||||
}
|
||||
|
||||
let query = """
|
||||
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
|
||||
SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) {
|
||||
id
|
||||
progress
|
||||
status
|
||||
func updateAnimeProgress(
|
||||
animeId: Int,
|
||||
episodeNumber: Int,
|
||||
status: String = "CURRENT",
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
if let sendPushUpdates = UserDefaults.standard.object(forKey: "sendPushUpdates") as? Bool,
|
||||
sendPushUpdates == false {
|
||||
return
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
let variables: [String: Any] = [
|
||||
|
||||
guard let userToken = getTokenFromKeychain() else {
|
||||
completion(.failure(NSError(
|
||||
domain: "",
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Access token not found"]
|
||||
)))
|
||||
return
|
||||
}
|
||||
|
||||
let query = """
|
||||
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
|
||||
SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status) {
|
||||
id
|
||||
progress
|
||||
status
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
let variables: [String: Any] = [
|
||||
"mediaId": animeId,
|
||||
"progress": episodeNumber,
|
||||
"status": "CURRENT"
|
||||
]
|
||||
"status": status
|
||||
]
|
||||
|
||||
let requestBody: [String: Any] = [
|
||||
"query": query,
|
||||
|
|
@ -104,6 +113,52 @@ class AniListMutation {
|
|||
task.resume()
|
||||
}
|
||||
|
||||
func fetchMediaStatus(
|
||||
mediaId: Int,
|
||||
completion: @escaping (Result<String, Error>) -> Void
|
||||
) {
|
||||
guard let token = getTokenFromKeychain() else {
|
||||
completion(.failure(NSError(
|
||||
domain: "", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Access token not found"]
|
||||
)))
|
||||
return
|
||||
}
|
||||
|
||||
let query = """
|
||||
query ($mediaId: Int) {
|
||||
Media(id: $mediaId) {
|
||||
status
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
let vars = ["mediaId": mediaId]
|
||||
var req = URLRequest(url: URL(string: "https://graphql.anilist.co")!)
|
||||
req.httpMethod = "POST"
|
||||
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
req.httpBody = try? JSONSerialization.data(
|
||||
withJSONObject: ["query": query, "variables": vars]
|
||||
)
|
||||
|
||||
URLSession.shared.dataTask(with: req) { data, _, error in
|
||||
if let e = error { return completion(.failure(e)) }
|
||||
guard
|
||||
let d = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: d) as? [String: Any],
|
||||
let md = (json["data"] as? [String: Any])?["Media"] as? [String: Any],
|
||||
let status = md["status"] as? String
|
||||
else {
|
||||
return completion(.failure(NSError(
|
||||
domain: "", code: -2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Invalid response"]
|
||||
)))
|
||||
}
|
||||
completion(.success(status))
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func fetchMalID(animeId: Int, completion: @escaping (Result<Int, Error>) -> Void) {
|
||||
let query = """
|
||||
query ($id: Int) {
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ struct ContinueWatchingItem: Codable, Identifiable {
|
|||
let aniListID: Int?
|
||||
let module: ScrapingModule
|
||||
let headers: [String:String]?
|
||||
let totalEpisodes: Int
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
private var aniListUpdateImpossible: Bool = false
|
||||
private var aniListRetryCount = 0
|
||||
private let aniListMaxRetries = 6
|
||||
private let totalEpisodes: Int
|
||||
|
||||
var player: AVPlayer!
|
||||
var timeObserverToken: Any?
|
||||
|
|
@ -183,6 +184,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
onWatchNext: @escaping () -> Void,
|
||||
subtitlesURL: String?,
|
||||
aniListID: Int,
|
||||
totalEpisodes: Int,
|
||||
episodeImageUrl: String,headers:[String:String]?) {
|
||||
|
||||
self.module = module
|
||||
|
|
@ -195,6 +197,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
self.subtitlesURL = subtitlesURL
|
||||
self.aniListID = aniListID
|
||||
self.headers = headers
|
||||
self.totalEpisodes = totalEpisodes
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
|
|
@ -1424,7 +1427,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
subtitles: self.subtitlesURL,
|
||||
aniListID: self.aniListID,
|
||||
module: self.module,
|
||||
headers: self.headers
|
||||
headers: self.headers,
|
||||
totalEpisodes: self.totalEpisodes
|
||||
)
|
||||
ContinueWatchingManager.shared.save(item: item)
|
||||
}
|
||||
|
|
@ -1758,35 +1762,77 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
|
||||
private func tryAniListUpdate() {
|
||||
let aniListMutation = AniListMutation()
|
||||
aniListMutation.updateAnimeProgress(animeId: self.aniListID, episodeNumber: self.episodeNumber) { [weak self] result in
|
||||
guard !aniListUpdatedSuccessfully else { return }
|
||||
|
||||
guard aniListID > 0 else {
|
||||
Logger.shared.log("AniList ID is invalid, skipping update.", type: "Warning")
|
||||
return
|
||||
}
|
||||
|
||||
let client = AniListMutation()
|
||||
|
||||
client.fetchMediaStatus(mediaId: aniListID) { [weak self] statusResult in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.aniListUpdatedSuccessfully = true
|
||||
Logger.shared.log("Successfully updated AniList progress for episode \(self.episodeNumber)", type: "General")
|
||||
|
||||
case .failure(let error):
|
||||
let errorString = error.localizedDescription.lowercased()
|
||||
Logger.shared.log("AniList progress update failed: \(errorString)", type: "Error")
|
||||
|
||||
if errorString.contains("access token not found") {
|
||||
Logger.shared.log("AniList update will NOT retry due to missing token.", type: "Error")
|
||||
self.aniListUpdateImpossible = true
|
||||
|
||||
} else {
|
||||
if self.aniListRetryCount < self.aniListMaxRetries {
|
||||
self.aniListRetryCount += 1
|
||||
|
||||
let delaySeconds = 5.0
|
||||
Logger.shared.log("AniList update will retry in \(delaySeconds)s (attempt \(self.aniListRetryCount)).", type: "Debug")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) {
|
||||
self.tryAniListUpdate()
|
||||
}
|
||||
|
||||
let newStatus: String = {
|
||||
switch statusResult {
|
||||
case .success(let mediaStatus):
|
||||
if mediaStatus == "RELEASING" {
|
||||
return "CURRENT"
|
||||
}
|
||||
return (self.episodeNumber == self.totalEpisodes) ? "COMPLETED" : "CURRENT"
|
||||
|
||||
case .failure(let error):
|
||||
Logger.shared.log(
|
||||
"Failed to fetch AniList status: \(error.localizedDescription). " +
|
||||
"Using default CURRENT/COMPLETED logic.",
|
||||
type: "Warning"
|
||||
)
|
||||
return (self.episodeNumber == self.totalEpisodes) ? "COMPLETED" : "CURRENT"
|
||||
}
|
||||
}()
|
||||
|
||||
client.updateAnimeProgress(
|
||||
animeId: self.aniListID,
|
||||
episodeNumber: self.episodeNumber,
|
||||
status: newStatus
|
||||
) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.aniListUpdatedSuccessfully = true
|
||||
Logger.shared.log(
|
||||
"AniList progress updated to \(newStatus) for ep \(self.episodeNumber)",
|
||||
type: "General"
|
||||
)
|
||||
|
||||
case .failure(let error):
|
||||
let errorString = error.localizedDescription.lowercased()
|
||||
Logger.shared.log("AniList progress update failed: \(errorString)", type: "Error")
|
||||
|
||||
if errorString.contains("access token not found") {
|
||||
Logger.shared.log("AniList update will NOT retry due to missing token.", type: "Error")
|
||||
self.aniListUpdateImpossible = true
|
||||
|
||||
} else {
|
||||
Logger.shared.log("AniList update reached max retries. No more attempts.", type: "Error")
|
||||
if self.aniListRetryCount < self.aniListMaxRetries {
|
||||
self.aniListRetryCount += 1
|
||||
|
||||
let delaySeconds = 5.0
|
||||
Logger.shared.log(
|
||||
"AniList update will retry in \(delaySeconds)s " +
|
||||
"(attempt \(self.aniListRetryCount)).",
|
||||
type: "Debug"
|
||||
)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) {
|
||||
self.tryAniListUpdate()
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log(
|
||||
"Reached max retry count (\(self.aniListMaxRetries)). Giving up.",
|
||||
type: "Error"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class VideoPlayerViewController: UIViewController {
|
|||
var aniListID: Int = 0
|
||||
var headers: [String:String]? = nil
|
||||
|
||||
var totalEpisodes: Int = 0
|
||||
var episodeNumber: Int = 0
|
||||
var episodeImageUrl: String = ""
|
||||
var mediaTitle: String = ""
|
||||
|
|
@ -139,7 +140,8 @@ class VideoPlayerViewController: UIViewController {
|
|||
subtitles: self.subtitles,
|
||||
aniListID: self.aniListID,
|
||||
module: self.module,
|
||||
headers: self.headers
|
||||
headers: self.headers,
|
||||
totalEpisodes: self.totalEpisodes
|
||||
)
|
||||
ContinueWatchingManager.shared.save(item: item)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ struct DownloadView: View {
|
|||
onWatchNext: {},
|
||||
subtitlesURL: asset.localSubtitleURL?.absoluteString,
|
||||
aniListID: 0,
|
||||
totalEpisodes: asset.metadata?.episode ?? 0,
|
||||
episodeImageUrl: asset.metadata?.posterURL?.absoluteString ?? "",
|
||||
headers: nil
|
||||
)
|
||||
|
|
|
|||
|
|
@ -284,9 +284,9 @@ struct ContinueWatchingCell: View {
|
|||
onWatchNext: { },
|
||||
subtitlesURL: item.subtitles,
|
||||
aniListID: item.aniListID ?? 0,
|
||||
totalEpisodes: item.totalEpisodes,
|
||||
episodeImageUrl: item.imageUrl,
|
||||
headers: item.headers ?? nil
|
||||
|
||||
)
|
||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||
|
||||
|
|
|
|||
|
|
@ -568,18 +568,40 @@ struct MediaInfoView: View {
|
|||
var updates = [String: Double]()
|
||||
|
||||
for idx in 0..<index {
|
||||
if idx < episodeLinks.count {
|
||||
let href = episodeLinks[idx].href
|
||||
updates["lastPlayedTime_\(href)"] = 1000.0
|
||||
updates["totalTime_\(href)"] = 1000.0
|
||||
}
|
||||
let href = episodeLinks[idx].href
|
||||
updates["lastPlayedTime_\(href)"] = 1000.0
|
||||
updates["totalTime_\(href)"] = 1000.0
|
||||
}
|
||||
|
||||
for (key, value) in updates {
|
||||
userDefaults.set(value, forKey: key)
|
||||
}
|
||||
|
||||
Logger.shared.log("Marked \(ep.number - 1) episodes watched within series \"\(title)\".", type: "General")
|
||||
userDefaults.synchronize()
|
||||
Logger.shared.log(
|
||||
"Marked \(ep.number - 1) episodes watched within series \"\(title)\".",
|
||||
type: "General"
|
||||
)
|
||||
|
||||
guard let listID = itemID, listID > 0 else { return }
|
||||
let watchedCount = ep.number - 1
|
||||
let statusToSend = (watchedCount == episodeLinks.count) ? "COMPLETED" : "CURRENT"
|
||||
AniListMutation().updateAnimeProgress(
|
||||
animeId: listID,
|
||||
episodeNumber: watchedCount,
|
||||
status: statusToSend
|
||||
) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
Logger.shared.log(
|
||||
"AniList bulk‐sync: set progress to \(watchedCount) (\(statusToSend))",
|
||||
type: "General"
|
||||
)
|
||||
case .failure(let error):
|
||||
Logger.shared.log(
|
||||
"AniList bulk‐sync failed: \(error.localizedDescription)",
|
||||
type: "Error"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -997,6 +1019,7 @@ struct MediaInfoView: View {
|
|||
},
|
||||
subtitlesURL: subtitles,
|
||||
aniListID: itemID ?? 0,
|
||||
totalEpisodes: episodeLinks.count,
|
||||
episodeImageUrl: selectedEpisodeImage,
|
||||
headers: headers ?? nil
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; };
|
||||
1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E26E9E62DA9577900B9DC02 /* VolumeSlider.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 */; };
|
||||
1EF5C3A92DB988E40032BF07 /* CommunityLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */; };
|
||||
7205AEDB2DCCEF9500943F3F /* EpisodeMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205AED72DCCEF9500943F3F /* EpisodeMetadata.swift */; };
|
||||
|
|
@ -146,6 +147,7 @@
|
|||
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||
1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.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>"; };
|
||||
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>"; };
|
||||
|
|
@ -520,6 +522,7 @@
|
|||
72AC3A002DD4DAEA00C60B96 /* Managers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */,
|
||||
72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */,
|
||||
72AC39FE2DD4DAEA00C60B96 /* ImagePrefetchManager.swift */,
|
||||
72AC39FF2DD4DAEA00C60B96 /* PerformanceMonitor.swift */,
|
||||
|
|
@ -668,6 +671,7 @@
|
|||
727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */,
|
||||
727220722DD642B100C2A4A2 /* JSController+MP4Download.swift in Sources */,
|
||||
132FC5B32DE31DAE009A80F7 /* SettingsViewAlternateAppIconPicker.swift in Sources */,
|
||||
1EA64DCD2DE5030100AC14BC /* ImageUpscaler.swift in Sources */,
|
||||
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */,
|
||||
72AC3A012DD4DAEB00C60B96 /* ImagePrefetchManager.swift in Sources */,
|
||||
72AC3A022DD4DAEB00C60B96 /* EpisodeMetadataManager.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue