testflgith (#162)

* Main (#158)

* dwo (#132)

* bug fixes  (#127)

* yeah @realdoomsboygaming fault

* fixes

* freaky ahh update

---------

Co-authored-by: Seiike <122684677+Seeike@users.noreply.github.com>

* opds

* bug fixes around the downloads (#133) (#134)

* yeah nice xcode

* yeah fuck ts

* yeah

* ok this shit is really fucked then

---------

Co-authored-by: Seiike <122684677+Seeike@users.noreply.github.com>

* Yeah idk what i am even doing at this point

* opsi

* would this wrk?

* test UI mode

* should be better now

* my bad sorru

* ok should be good now + better quality parsesr

* ohhhh i forgot my bad

* ok now its fixed

* who know if this works 😭

* ohhh yeah my bad

* ok should work now

* brooo come one 😭

* who tf does this work 😭

* ok yeah im done bro 😭

* oh yeah my bad ok

* audio track please work holy moly

* ok yeah no audio for now

* ok please this time audio should work

* Revert "ok please this time audio should work"

This reverts commit a14d7db5ea.

* d

* Updated Dark and Light mode thumbnails  (#159)

* Update CustomPlayer.swift

* Update README.md

* boom shakalaka (#160)

* now if the user leaves mediainfoview on chunk 51-100 it will remember it and put him back the next time

same with seasons

* fixed memory leak from urldelegate

---------

Co-authored-by: cranci <100066266+cranci1@users.noreply.github.com>

* yeah idk tf is ts 😭

* migrated to NukeUI from KingFisher

* why was it even imported 😭

* Update README.md

* POP THE CHAMPAGNE 🍾 (#161)

* this is a test i guess. macOS ventura on top

* updated Nuke to branch + adjusted some stuffs

* yeah this is not needed

* Update README.md

---------

Co-authored-by: Seiike <122684677+Seeike@users.noreply.github.com>
Co-authored-by: CiroHoodLove <3issawii667@gmail.com>
This commit is contained in:
cranci 2025-06-09 11:34:43 +02:00 committed by GitHub
parent 7d6e2e65d4
commit fdc05a13ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 987 additions and 742 deletions

View file

@ -64,13 +64,13 @@ Sora does not include any modules by default. You will need to find and add the
## Acknowledgements ## Acknowledgements
Frameworks: Frameworks:
- [KingFisher](https://github.com/onevcat/Kingfisher) - MIT License - [Nuke](https://github.com/kean/Nuke) - MIT License
- [Drops](https://github.com/omaralbeik/Drops) - MIT License - [Drops](https://github.com/omaralbeik/Drops) - MIT License
- [MarqueeLabel](https://github.com/cbpowell/MarqueeLabel) - MIT License - [MarqueeLabel](https://github.com/cbpowell/MarqueeLabel) - MIT License
Misc: Misc:
- [50/50](https://github.com/50n50) for the app icon - [50/50](https://github.com/50n50) for the app icon
- Ciro for the episode banner images - [Ciro](https://github.com/CiroHoodLove) for the episodes banners
## License ## License

View file

@ -111,7 +111,11 @@ extension JSContext {
} }
} }
Logger.shared.log("Redirect value is \(redirect.boolValue)", type: "Error") Logger.shared.log("Redirect value is \(redirect.boolValue)", type: "Error")
let task = URLSession.fetchData(allowRedirects: redirect.boolValue).downloadTask(with: request) { tempFileURL, response, error in let session = URLSession.fetchData(allowRedirects: redirect.boolValue)
let task = session.downloadTask(with: request) { tempFileURL, response, error in
defer { session.finishTasksAndInvalidate() }
let callReject: (String) -> Void = { message in let callReject: (String) -> Void = { message in
DispatchQueue.main.async { DispatchQueue.main.async {
reject.call(withArguments: [message]) reject.call(withArguments: [message])

View file

@ -9,9 +9,12 @@ import Foundation
class FetchDelegate: NSObject, URLSessionTaskDelegate { class FetchDelegate: NSObject, URLSessionTaskDelegate {
private let allowRedirects: Bool private let allowRedirects: Bool
init(allowRedirects: Bool) { init(allowRedirects: Bool) {
self.allowRedirects = allowRedirects self.allowRedirects = allowRedirects
} }
deinit { Logger.shared.log("FetchDelegate deallocated", type: "Debug")
}
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
if(allowRedirects) { if(allowRedirects) {

View file

@ -13,6 +13,7 @@ import AVFoundation
import MarqueeLabel import MarqueeLabel
class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate { class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate {
private var airplayButton: AVRoutePickerView!
let module: ScrapingModule let module: ScrapingModule
let streamURL: String let streamURL: String
let fullUrl: String let fullUrl: String
@ -41,7 +42,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
var currentTimeVal: Double = 0.0 var currentTimeVal: Double = 0.0
var duration: Double = 0.0 var duration: Double = 0.0
var isVideoLoaded = false var isVideoLoaded = false
var detachedWindow: UIWindow?
private var isHoldPauseEnabled: Bool { private var isHoldPauseEnabled: Bool {
UserDefaults.standard.bool(forKey: "holdForPauseEnabled") UserDefaults.standard.bool(forKey: "holdForPauseEnabled")
@ -73,7 +73,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} }
private var pipController: AVPictureInPictureController? private var pipController: AVPictureInPictureController?
private var pipButton: UIButton! private var pipButton: UIButton!
var portraitButtonVisibleConstraints: [NSLayoutConstraint] = [] var portraitButtonVisibleConstraints: [NSLayoutConstraint] = []
var portraitButtonHiddenConstraints: [NSLayoutConstraint] = [] var portraitButtonHiddenConstraints: [NSLayoutConstraint] = []
@ -82,7 +81,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
var currentMarqueeConstraints: [NSLayoutConstraint] = [] var currentMarqueeConstraints: [NSLayoutConstraint] = []
private var currentMenuButtonTrailing: NSLayoutConstraint! private var currentMenuButtonTrailing: NSLayoutConstraint!
var subtitleForegroundColor: String = "white" var subtitleForegroundColor: String = "white"
var subtitleBackgroundEnabled: Bool = true var subtitleBackgroundEnabled: Bool = true
var subtitleFontSize: Double = 20.0 var subtitleFontSize: Double = 20.0
@ -177,7 +175,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
qualityButton, qualityButton,
speedButton, speedButton,
watchNextButton, watchNextButton,
volumeSliderHostingView volumeSliderHostingView,
pipButton,
airplayButton
].compactMap { $0 } ].compactMap { $0 }
private var originalHiddenStates: [UIView: Bool] = [:] private var originalHiddenStates: [UIView: Bool] = [:]
@ -422,23 +422,73 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} }
deinit { deinit {
playerRateObserver?.invalidate()
inactivityTimer?.invalidate() inactivityTimer?.invalidate()
inactivityTimer = nil
updateTimer?.invalidate() updateTimer?.invalidate()
updateTimer = nil
lockButtonTimer?.invalidate() lockButtonTimer?.invalidate()
lockButtonTimer = nil
dimButtonTimer?.invalidate() dimButtonTimer?.invalidate()
loadedTimeRangesObservation?.invalidate() dimButtonTimer = nil
playerTimeControlStatusObserver?.invalidate()
volumeObserver?.invalidate()
player.replaceCurrentItem(with: nil) playerRateObserver?.invalidate()
player.pause() playerRateObserver = nil
loadedTimeRangesObservation?.invalidate()
loadedTimeRangesObservation = nil
playerTimeControlStatusObserver?.invalidate()
playerTimeControlStatusObserver = nil
volumeObserver?.invalidate()
volumeObserver = nil
NotificationCenter.default.removeObserver(self)
if let token = timeObserverToken {
player?.removeTimeObserver(token)
timeObserverToken = nil
}
player?.replaceCurrentItem(with: nil)
player?.pause()
player = nil
if let playerVC = playerViewController {
playerVC.willMove(toParent: nil)
playerVC.view.removeFromSuperview()
playerVC.removeFromParent()
}
if let sliderHost = sliderHostingController {
sliderHost.willMove(toParent: nil)
sliderHost.view.removeFromSuperview()
sliderHost.removeFromParent()
}
playerViewController = nil playerViewController = nil
sliderHostingController = nil sliderHostingController = nil
volumeSliderHostingView = nil
volumeSliderHostingView?.removeFromSuperview()
hiddenVolumeView.removeFromSuperview()
subtitleStackView?.removeFromSuperview()
marqueeLabel?.removeFromSuperview()
controlsContainerView?.removeFromSuperview()
blackCoverView?.removeFromSuperview()
skipIntroButton?.removeFromSuperview()
skipOutroButton?.removeFromSuperview()
skip85Button?.removeFromSuperview()
pipButton?.removeFromSuperview()
airplayButton?.removeFromSuperview()
menuButton?.removeFromSuperview()
speedButton?.removeFromSuperview()
qualityButton?.removeFromSuperview()
holdSpeedIndicator?.removeFromSuperview()
lockButton?.removeFromSuperview()
dimButton?.removeFromSuperview()
dismissButton?.removeFromSuperview()
watchNextButton?.removeFromSuperview()
try? AVAudioSession.sharedInstance().setActive(false) try? AVAudioSession.sharedInstance().setActive(false)
} }
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard context == &playerItemKVOContext else { guard context == &playerItemKVOContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
@ -449,7 +499,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} }
} }
@objc private func playerItemDidChange() { @objc private func playerItemDidChange() {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@ -1240,51 +1289,64 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} }
private func setupPipIfSupported() { private func setupPipIfSupported() {
airplayButton = AVRoutePickerView(frame: .zero)
airplayButton.translatesAutoresizingMaskIntoConstraints = false
airplayButton.activeTintColor = .white
airplayButton.tintColor = .white
airplayButton.backgroundColor = .clear
airplayButton.prioritizesVideoDevices = true
airplayButton.setContentHuggingPriority(.required, for: .horizontal)
airplayButton.setContentCompressionResistancePriority(.required, for: .horizontal)
controlsContainerView.addSubview(airplayButton)
guard AVPictureInPictureController.isPictureInPictureSupported() else { guard AVPictureInPictureController.isPictureInPictureSupported() else {
return return
} }
let pipPlayerLayer = AVPlayerLayer(player: playerViewController.player) let pipPlayerLayer = AVPlayerLayer(player: playerViewController.player)
pipPlayerLayer.frame = playerViewController.view.layer.bounds pipPlayerLayer.frame = playerViewController.view.layer.bounds
pipPlayerLayer.videoGravity = .resizeAspect pipPlayerLayer.videoGravity = .resizeAspect
playerViewController.view.layer.insertSublayer(pipPlayerLayer, at: 0) playerViewController.view.layer.insertSublayer(pipPlayerLayer, at: 0)
pipController = AVPictureInPictureController(playerLayer: pipPlayerLayer) pipController = AVPictureInPictureController(playerLayer: pipPlayerLayer)
pipController?.delegate = self pipController?.delegate = self
let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
let Image = UIImage(systemName: "pip", withConfiguration: config) let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
pipButton = UIButton(type: .system) let Image = UIImage(systemName: "pip", withConfiguration: config)
pipButton.setImage(Image, for: .normal) pipButton = UIButton(type: .system)
pipButton.tintColor = .white pipButton.setImage(Image, for: .normal)
pipButton.addTarget(self, action: #selector(pipButtonTapped(_:)), for: .touchUpInside) pipButton.tintColor = .white
pipButton.addTarget(self, action: #selector(pipButtonTapped(_:)), for: .touchUpInside)
pipButton.layer.shadowColor = UIColor.black.cgColor
pipButton.layer.shadowOffset = CGSize(width: 0, height: 2) pipButton.layer.shadowColor = UIColor.black.cgColor
pipButton.layer.shadowOpacity = 0.6 pipButton.layer.shadowOffset = CGSize(width: 0, height: 2)
pipButton.layer.shadowRadius = 4 pipButton.layer.shadowOpacity = 0.6
pipButton.layer.masksToBounds = false pipButton.layer.shadowRadius = 4
pipButton.layer.masksToBounds = false
controlsContainerView.addSubview(pipButton)
pipButton.translatesAutoresizingMaskIntoConstraints = false controlsContainerView.addSubview(pipButton)
pipButton.translatesAutoresizingMaskIntoConstraints = false
// NEW: pin pipButton to the left of lockButton:
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
pipButton.centerYAnchor.constraint(equalTo: dimButton.centerYAnchor), pipButton.centerYAnchor.constraint(equalTo: dimButton.centerYAnchor),
pipButton.trailingAnchor.constraint(equalTo: dimButton.leadingAnchor, constant: -8), pipButton.trailingAnchor.constraint(equalTo: dimButton.leadingAnchor, constant: -8),
pipButton.widthAnchor.constraint(equalToConstant: 44), pipButton.widthAnchor.constraint(equalToConstant: 44),
pipButton.heightAnchor.constraint(equalToConstant: 44) pipButton.heightAnchor.constraint(equalToConstant: 44),
]) airplayButton.centerYAnchor.constraint(equalTo: pipButton.centerYAnchor),
airplayButton.trailingAnchor.constraint(equalTo: pipButton.leadingAnchor, constant: -8),
pipButton.isHidden = !isPipButtonVisible airplayButton.widthAnchor.constraint(equalToConstant: 44),
airplayButton.heightAnchor.constraint(equalToConstant: 44)
NotificationCenter.default.addObserver( ])
self,
selector: #selector(startPipIfNeeded), pipButton.isHidden = !isPipButtonVisible
name: UIApplication.willResignActiveNotification,
object: nil NotificationCenter.default.addObserver(
) self,
} selector: #selector(startPipIfNeeded),
name: UIApplication.willResignActiveNotification,
object: nil
)
}
func setupMenuButton() { func setupMenuButton() {
let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold) let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
@ -1356,7 +1418,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
watchNextButton.tintColor = .white watchNextButton.tintColor = .white
watchNextButton.setTitleColor(.white, for: .normal) watchNextButton.setTitleColor(.white, for: .normal)
// The shadow:
watchNextButton.layer.shadowColor = UIColor.black.cgColor watchNextButton.layer.shadowColor = UIColor.black.cgColor
watchNextButton.layer.shadowOffset = CGSize(width: 0, height: 2) watchNextButton.layer.shadowOffset = CGSize(width: 0, height: 2)
watchNextButton.layer.shadowOpacity = 0.6 watchNextButton.layer.shadowOpacity = 0.6
@ -1460,9 +1521,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
func addTimeObserver() { func addTimeObserver() {
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
queue: .main)
{ [weak self] time in
guard let self = self, guard let self = self,
let currentItem = self.player.currentItem, let currentItem = self.player.currentItem,
currentItem.duration.seconds.isFinite else { return } currentItem.duration.seconds.isFinite else { return }
@ -1509,7 +1568,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
let segmentsColor = self.getSegmentsColor() let segmentsColor = self.getSegmentsColor()
DispatchQueue.main.async { DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if let currentItem = self.player.currentItem, currentItem.duration.seconds > 0 { if let currentItem = self.player.currentItem, currentItem.duration.seconds > 0 {
let progress = min(max(self.currentTimeVal / self.duration, 0), 1.0) let progress = min(max(self.currentTimeVal / self.duration, 0), 1.0)
@ -1530,7 +1590,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
ContinueWatchingManager.shared.save(item: item) ContinueWatchingManager.shared.save(item: item)
} }
let remainingPercentage = (self.duration - self.currentTimeVal) / self.duration let remainingPercentage = (self.duration - self.currentTimeVal) / self.duration
if remainingPercentage < 0.1 && if remainingPercentage < 0.1 &&
@ -1592,7 +1651,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} }
} }
func startUpdateTimer() { func startUpdateTimer() {
updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
@ -1750,13 +1808,13 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
pip.startPictureInPicture() pip.startPictureInPicture()
} }
} }
@objc private func startPipIfNeeded() { @objc private func startPipIfNeeded() {
guard isPipAutoEnabled, guard isPipAutoEnabled,
let pip = pipController, let pip = pipController,
!pip.isPictureInPictureActive else { !pip.isPictureInPictureActive else {
return return
} }
pip.startPictureInPicture() pip.startPictureInPicture()
} }
@ -1800,7 +1858,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
updateSkipButtonsVisibility() updateSkipButtonsVisibility()
} }
} }
@objc private func skipIntro() { @objc private func skipIntro() {
if let range = skipIntervals.op { if let range = skipIntervals.op {
player.seek(to: range.end) player.seek(to: range.end)
@ -1816,15 +1874,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} }
@objc func dismissTapped() { @objc func dismissTapped() {
dismiss(animated: true) { [weak self] in dismiss(animated: true, completion: nil)
self?.detachedWindow = nil
}
} }
@objc func watchNextTapped() { @objc func watchNextTapped() {
player.pause() player.pause()
dismiss(animated: true) { [weak self] in dismiss(animated: true) { [weak self] in
self?.detachedWindow = nil
self?.onWatchNext() self?.onWatchNext()
} }
} }
@ -1849,19 +1904,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
UIView.animate(withDuration: 0.25) { UIView.animate(withDuration: 0.25) {
self.blackCoverView.alpha = self.isDimmed ? 1.0 : 0.4 self.blackCoverView.alpha = self.isDimmed ? 1.0 : 0.4
// fade all controls (and lock button) in or out for v in self.controlsToHide { v.alpha = self.isDimmed ? 0 : 1 }
for v in self.controlsToHide { v.alpha = self.isDimmed ? 0 : 1 }
self.dimButton.alpha = self.isDimmed ? 0 : 1 self.dimButton.alpha = self.isDimmed ? 0 : 1
self.lockButton.alpha = self.isDimmed ? 0 : 1 self.lockButton.alpha = self.isDimmed ? 0 : 1
// switch subtitle constraints just like toggleControls()
self.subtitleBottomToSafeAreaConstraint?.isActive = !self.isControlsVisible self.subtitleBottomToSafeAreaConstraint?.isActive = !self.isControlsVisible
self.subtitleBottomToSliderConstraint?.isActive = self.isControlsVisible self.subtitleBottomToSliderConstraint?.isActive = self.isControlsVisible
self.view.layoutIfNeeded() self.view.layoutIfNeeded()
} }
// slide the dim-icon over
dimButtonToSlider.isActive = !isDimmed dimButtonToSlider.isActive = !isDimmed
dimButtonToRight.isActive = isDimmed dimButtonToRight.isActive = isDimmed
} }
@ -1881,17 +1933,17 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private func tryAniListUpdate() { private func tryAniListUpdate() {
guard !aniListUpdatedSuccessfully else { return } guard !aniListUpdatedSuccessfully else { return }
guard aniListID > 0 else { guard aniListID > 0 else {
Logger.shared.log("AniList ID is invalid, skipping update.", type: "Warning") Logger.shared.log("AniList ID is invalid, skipping update.", type: "Warning")
return return
} }
let client = AniListMutation() let client = AniListMutation()
client.fetchMediaStatus(mediaId: aniListID) { [weak self] statusResult in client.fetchMediaStatus(mediaId: aniListID) { [weak self] statusResult in
guard let self = self else { return } guard let self = self else { return }
let newStatus: String = { let newStatus: String = {
switch statusResult { switch statusResult {
case .success(let mediaStatus): case .success(let mediaStatus):
@ -1899,7 +1951,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
return "CURRENT" return "CURRENT"
} }
return (self.episodeNumber == self.totalEpisodes) ? "COMPLETED" : "CURRENT" return (self.episodeNumber == self.totalEpisodes) ? "COMPLETED" : "CURRENT"
case .failure(let error): case .failure(let error):
Logger.shared.log( Logger.shared.log(
"Failed to fetch AniList status: \(error.localizedDescription). " + "Failed to fetch AniList status: \(error.localizedDescription). " +
@ -1922,26 +1974,26 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
"AniList progress updated to \(newStatus) for ep \(self.episodeNumber)", "AniList progress updated to \(newStatus) for ep \(self.episodeNumber)",
type: "General" type: "General"
) )
case .failure(let error): case .failure(let error):
let errorString = error.localizedDescription.lowercased() let errorString = error.localizedDescription.lowercased()
Logger.shared.log("AniList progress update failed: \(errorString)", type: "Error") Logger.shared.log("AniList progress update failed: \(errorString)", type: "Error")
if errorString.contains("access token not found") { if errorString.contains("access token not found") {
Logger.shared.log("AniList update will NOT retry due to missing token.", type: "Error") Logger.shared.log("AniList update will NOT retry due to missing token.", type: "Error")
self.aniListUpdateImpossible = true self.aniListUpdateImpossible = true
} else { } else {
if self.aniListRetryCount < self.aniListMaxRetries { if self.aniListRetryCount < self.aniListMaxRetries {
self.aniListRetryCount += 1 self.aniListRetryCount += 1
let delaySeconds = 5.0 let delaySeconds = 5.0
Logger.shared.log( Logger.shared.log(
"AniList update will retry in \(delaySeconds)s " + "AniList update will retry in \(delaySeconds)s " +
"(attempt \(self.aniListRetryCount)).", "(attempt \(self.aniListRetryCount)).",
type: "Debug" type: "Debug"
) )
DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) {
self.tryAniListUpdate() self.tryAniListUpdate()
} }
@ -1983,20 +2035,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private func parseM3U8(url: URL, completion: @escaping () -> Void) { private func parseM3U8(url: URL, completion: @escaping () -> Void) {
var request = URLRequest(url: url) var request = URLRequest(url: url)
if let mydict = headers, !mydict.isEmpty if let mydict = headers, !mydict.isEmpty {
{ for (key,value) in mydict {
for (key,value) in mydict
{
request.addValue(value, forHTTPHeaderField: key) request.addValue(value, forHTTPHeaderField: key)
} }
} } else {
else
{
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin") request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
} }
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
forHTTPHeaderField: "User-Agent")
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let self = self, guard let self = self,
@ -2080,20 +2127,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
let wasPlaying = player.rate > 0 let wasPlaying = player.rate > 0
var request = URLRequest(url: url) var request = URLRequest(url: url)
if let mydict = headers, !mydict.isEmpty if let mydict = headers, !mydict.isEmpty {
{ for (key,value) in mydict {
for (key,value) in mydict
{
request.addValue(value, forHTTPHeaderField: key) request.addValue(value, forHTTPHeaderField: key)
} }
} } else {
else
{
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin") request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
} }
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
forHTTPHeaderField: "User-Agent")
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]]) let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
let playerItem = AVPlayerItem(asset: asset) let playerItem = AVPlayerItem(asset: asset)
@ -2110,10 +2152,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
qualityButton.menu = qualitySelectionMenu() qualityButton.menu = qualitySelectionMenu()
if let selectedQuality = qualities.first(where: { $0.1 == urlString })?.0 { if let selectedQuality = qualities.first(where: { $0.1 == urlString })?.0 {
DropManager.shared.showDrop(title: "Quality: \(selectedQuality)", DropManager.shared.showDrop(title: "Quality: \(selectedQuality)", subtitle: "", duration: 0.5, icon: UIImage(systemName: "eye"))
subtitle: "",
duration: 0.5,
icon: UIImage(systemName: "eye"))
} }
} }
@ -2156,8 +2195,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private func checkForHLSStream() { private func checkForHLSStream() {
guard let url = URL(string: streamURL) else { return } guard let url = URL(string: streamURL) else { return }
let streamType = module.metadata.streamType.lowercased()
if url.absoluteString.contains(".m3u8") { if url.absoluteString.contains(".m3u8") || url.absoluteString.contains(".m3u") {
isHLSStream = true isHLSStream = true
baseM3U8URL = url baseM3U8URL = url
currentQualityURL = url currentQualityURL = url
@ -2487,9 +2527,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
switch gesture.state { switch gesture.state {
case .ended: case .ended:
if translation.y > 100 { if translation.y > 100 {
dismiss(animated: true) { [weak self] in dismiss(animated: true, completion: nil)
self?.detachedWindow = nil
}
} }
default: default:
break break
@ -2619,9 +2657,7 @@ extension CustomMediaPlayerViewController: AVPictureInPictureControllerDelegate
pipButton.alpha = 1.0 pipButton.alpha = 1.0
} }
func pictureInPictureController(_ pipController: AVPictureInPictureController, func pictureInPictureController(_ pipController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
failedToStartPictureInPictureWithError error: Error) {
Logger.shared.log("PiP failed to start: \(error.localizedDescription)", type: "Error") Logger.shared.log("PiP failed to start: \(error.localizedDescription)", type: "Error")
} }
} }
@ -2631,4 +2667,4 @@ extension CustomMediaPlayerViewController: AVPictureInPictureControllerDelegate
// The mind is the source of good and evil, only you yourself can decide which you will bring yourself. -seiike // The mind is the source of good and evil, only you yourself can decide which you will bring yourself. -seiike
// guys watch Clannad already - ibro // guys watch Clannad already - ibro
// May the Divine Providence bestow its infinite mercy upon your soul, and may eternal grace find you beyond the shadows of this mortal realm. - paul, 15/11/2005 - 13/05/2023 // May the Divine Providence bestow its infinite mercy upon your soul, and may eternal grace find you beyond the shadows of this mortal realm. - paul, 15/11/2005 - 13/05/2023
// this dumbass defo used gpt // this dumbass defo used gpt, ong he did bro

View file

@ -24,7 +24,6 @@ class VideoPlayerViewController: UIViewController {
var episodeNumber: Int = 0 var episodeNumber: Int = 0
var episodeImageUrl: String = "" var episodeImageUrl: String = ""
var mediaTitle: String = "" var mediaTitle: String = ""
var detachedWindow: UIWindow?
init(module: ScrapingModule) { init(module: ScrapingModule) {
self.module = module self.module = module

View file

@ -5,8 +5,8 @@
// Created by Francesco on 01/02/25. // Created by Francesco on 01/02/25.
// //
import NukeUI
import SwiftUI import SwiftUI
import Kingfisher
struct ModuleAdditionSettingsView: View { struct ModuleAdditionSettingsView: View {
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@ -19,127 +19,197 @@ struct ModuleAdditionSettingsView: View {
var moduleUrl: String var moduleUrl: String
var body: some View { var body: some View {
VStack { ZStack {
ScrollView { LinearGradient(
VStack { gradient: Gradient(colors: [
if let metadata = moduleMetadata { colorScheme == .light ? Color.black : Color.white,
VStack(spacing: 25) { Color.accentColor.opacity(0.08)
VStack(spacing: 15) { ]),
KFImage(URL(string: metadata.iconUrl)) startPoint: .top,
.resizable() endPoint: .bottom
.aspectRatio(contentMode: .fit) )
.frame(width: 120, height: 120) .ignoresSafeArea()
.clipShape(Circle())
.shadow(radius: 5) VStack(spacing: 0) {
.transition(.scale) HStack {
Spacer()
Capsule()
.frame(width: 40, height: 5)
.foregroundColor(Color(.systemGray4))
.padding(.top, 10)
Spacer()
}
.padding(.bottom, 8)
ScrollView {
VStack(spacing: 24) {
if let metadata = moduleMetadata {
VStack(spacing: 0) {
LazyImage(url: URL(string: metadata.iconUrl)) { state in
if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Rectangle()
.fill(Color(.systemGray5))
}
}
.frame(width: 90, height: 90)
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
.shadow(color: Color.accentColor.opacity(0.18), radius: 10, x: 0, y: 6)
.overlay(
RoundedRectangle(cornerRadius: 22)
.stroke(Color.accentColor, lineWidth: 2)
)
.padding(.top, 10)
Text(metadata.sourceName) VStack(spacing: 6) {
.font(.system(size: 28, weight: .bold)) Text(metadata.sourceName)
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundColor(.primary)
.multilineTextAlignment(.center)
.padding(.top, 6)
HStack(spacing: 10) {
LazyImage(url: URL(string: metadata.author.icon)) { state in
if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Circle()
.fill(Color(.systemGray5))
}
}
.frame(width: 32, height: 32)
.clipShape(Circle())
.shadow(radius: 2)
VStack(alignment: .leading, spacing: 0) {
Text(metadata.author.name)
.font(.headline)
.foregroundColor(.primary)
Text("Author")
.font(.caption2)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal, 18)
.padding(.vertical, 8)
.background(
Capsule()
.fill(Color.accentColor.opacity(colorScheme == .dark ? 0.13 : 0.08))
)
.padding(.top, 2)
}
VStack(spacing: 0) {
HStack(spacing: 0) {
FancyInfoTile(icon: "globe", label: "Language", value: metadata.language)
Divider().frame(height: 44)
FancyInfoTile(icon: "film", label: "Type", value: metadata.type ?? "-")
}
Divider()
HStack(spacing: 0) {
FancyInfoTile(icon: "arrow.down.circle", label: "Quality", value: metadata.quality)
Divider().frame(height: 44)
FancyInfoTile(icon: "waveform", label: "Stream", value: metadata.streamType)
}
Divider()
HStack(spacing: 0) {
FancyInfoTile(icon: "number", label: "Version", value: metadata.version)
Divider().frame(height: 44)
FancyInfoTile(icon: "bolt.horizontal", label: "Async JS", value: metadata.asyncJS == true ? "Yes" : "No")
}
}
.background(
RoundedRectangle(cornerRadius: 22)
.fill(Color(.systemGray6).opacity(colorScheme == .dark ? 0.18 : 0.8))
)
.padding(.top, 18)
.padding(.horizontal, 2)
VStack(spacing: 0) {
FancyUrlRow(title: "Base URL", value: metadata.baseUrl)
Divider().padding(.horizontal, 8)
if !metadata.searchBaseUrl.isEmpty {
FancyUrlRow(title: "Search URL", value: metadata.searchBaseUrl)
Divider().padding(.horizontal, 8)
}
FancyUrlRow(title: "Script URL", value: metadata.scriptUrl)
}
.padding(16)
.background(
RoundedRectangle(cornerRadius: 18)
.fill(Color(.systemGray6).opacity(colorScheme == .dark ? 0.13 : 0.85))
)
.padding(.top, 18)
}
.padding(.horizontal, 18)
.padding(.top, 8)
} else if isLoading {
VStack(spacing: 20) {
ProgressView()
.scaleEffect(1.5)
Text("Loading module information...")
.foregroundColor(.secondary)
}
.frame(maxHeight: .infinity)
.padding(.top, 100)
} else if let errorMessage = errorMessage {
VStack(spacing: 20) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 50))
.foregroundColor(.red)
Text(errorMessage)
.foregroundColor(.red)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
.padding(.top) .frame(maxHeight: .infinity)
.padding(.top, 100)
Divider()
HStack(spacing: 15) {
KFImage(URL(string: metadata.author.icon))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.clipShape(Circle())
.shadow(radius: 3)
VStack(alignment: .leading, spacing: 4) {
Text(metadata.author.name)
.font(.headline)
Text("Author")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal)
Divider()
VStack(alignment: .leading, spacing: 12) {
InfoRow(title: "Version", value: metadata.version)
InfoRow(title: "Language", value: metadata.language)
InfoRow(title: "Quality", value: metadata.quality)
InfoRow(title: "Stream Typed", value: metadata.streamType)
InfoRow(title: "Base URL", value: metadata.baseUrl)
.onLongPressGesture {
UIPasteboard.general.string = metadata.baseUrl
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
}
InfoRow(title: "Script URL", value: metadata.scriptUrl)
.onLongPressGesture {
UIPasteboard.general.string = metadata.scriptUrl
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
}
}
.padding(.horizontal)
} }
Divider()
} else if isLoading {
VStack(spacing: 20) {
ProgressView()
.scaleEffect(1.5)
Text("Loading module information...")
.foregroundColor(.secondary)
}
.frame(maxHeight: .infinity)
.padding(.top, 100)
} else if let errorMessage = errorMessage {
VStack(spacing: 20) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 50))
.foregroundColor(.red)
Text(errorMessage)
.foregroundColor(.red)
.multilineTextAlignment(.center)
}
.frame(maxHeight: .infinity)
.padding(.top, 100)
} }
.padding(.bottom, 30)
} }
}
Spacer()
VStack {
Button(action: addModule) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add Module")
}
.font(.headline)
.foregroundColor(colorScheme == .dark ? .black : .white)
.padding()
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 15)
.foregroundColor(colorScheme == .dark ? .white : .black)
)
.padding(.horizontal)
}
.disabled(isLoading)
.opacity(isLoading ? 0.6 : 1)
Button(action: { VStack(spacing: 10) {
self.presentationMode.wrappedValue.dismiss() Button(action: addModule) {
}) { HStack {
Text("Cancel") Image(systemName: "plus.circle.fill")
.foregroundColor(colorScheme == .dark ? Color.white : Color.black) Text("Add Module")
.padding(.top, 10) }
.font(.headline)
.foregroundColor(colorScheme == .light ? .black : .white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(
LinearGradient(
gradient: Gradient(colors: [
Color.accentColor.opacity(0.95),
Color.accentColor.opacity(0.7)
]),
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 18))
)
.shadow(color: Color.accentColor.opacity(0.18), radius: 8, x: 0, y: 4)
.padding(.horizontal, 20)
}
.disabled(isLoading || moduleMetadata == nil)
.opacity(isLoading ? 0.6 : 1)
Button(action: { presentationMode.wrappedValue.dismiss() }) {
Text("Cancel")
.font(.body)
.foregroundColor(.secondary)
.padding(.vertical, 8)
}
} }
.padding(.bottom, 24)
} }
.padding(.bottom, 20)
} }
.navigationTitle("Add Module")
.onAppear(perform: fetchModuleMetadata) .onAppear(perform: fetchModuleMetadata)
} }
@ -197,18 +267,58 @@ struct ModuleAdditionSettingsView: View {
} }
} }
struct InfoRow: View { struct FancyInfoTile: View {
let icon: String
let label: String
let value: String
var body: some View {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.accentColor)
Text(label)
.font(.caption2)
.foregroundColor(.secondary)
Text(value)
.font(.system(size: 15, weight: .semibold, design: .rounded))
.foregroundColor(.primary)
.lineLimit(1)
.minimumScaleFactor(0.7)
}
.frame(maxWidth: .infinity, minHeight: 54)
.padding(.vertical, 6)
}
}
struct FancyUrlRow: View {
let title: String let title: String
let value: String let value: String
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) {
Text(title) Text(title)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Spacer()
Text(value) Text(value)
.font(.body) .font(.footnote.monospaced())
.foregroundColor(.accentColor)
.lineLimit(1) .lineLimit(1)
.truncationMode(.middle)
.onLongPressGesture {
UIPasteboard.general.string = value
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
}
Image(systemName: "doc.on.clipboard")
.foregroundColor(.accentColor)
.font(.system(size: 14))
.onTapGesture {
UIPasteboard.general.string = value
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
}
} }
.padding(.vertical, 7)
.padding(.horizontal, 2)
} }
} }

View file

@ -203,12 +203,6 @@ class ModuleManager: ObservableObject {
return try String(contentsOf: localUrl, encoding: .utf8) return try String(contentsOf: localUrl, encoding: .utf8)
} }
func getModule(for episodeUrl: String) -> ScrapingModule {
// For now, return the first active module
// In the future, we might want to add logic to determine which module to use based on the URL
return modules.first(where: { $0.isActive }) ?? modules.first!
}
func refreshModules() async { func refreshModules() async {
for (index, module) in modules.enumerated() { for (index, module) in modules.enumerated() {
do { do {
@ -236,10 +230,8 @@ class ModuleManager: ObservableObject {
isActive: module.isActive isActive: module.isActive
) )
await MainActor.run { self.modules[index] = updatedModule
self.modules[index] = updatedModule self.saveModules()
self.saveModules()
}
Logger.shared.log("Updated module: \(module.metadata.sourceName) to version \(newMetadata.version)") Logger.shared.log("Updated module: \(module.metadata.sourceName) to version \(newMetadata.version)")
} }

View file

@ -5,9 +5,9 @@
// Created by doomsboygaming on 5/22/25 // Created by doomsboygaming on 5/22/25
// //
import SwiftUI
import AVKit import AVKit
import Kingfisher import NukeUI
import SwiftUI
struct DownloadView: View { struct DownloadView: View {
@EnvironmentObject var jsController: JSController @EnvironmentObject var jsController: JSController
@ -741,13 +741,16 @@ struct EnhancedActiveDownloadCard: View {
HStack(spacing: 16) { HStack(spacing: 16) {
Group { Group {
if let imageURL = download.imageURL { if let imageURL = download.imageURL {
KFImage(imageURL) LazyImage(url: imageURL) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Rectangle() Rectangle()
.fill(.tertiary) .fill(.tertiary)
} }
.resizable() }
.aspectRatio(contentMode: .fill)
} else { } else {
Rectangle() Rectangle()
.fill(.tertiary) .fill(.tertiary)
@ -899,16 +902,18 @@ struct EnhancedDownloadGroupCard: View {
NavigationLink(destination: EnhancedShowEpisodesView(group: group, onDelete: onDelete, onPlay: onPlay)) { NavigationLink(destination: EnhancedShowEpisodesView(group: group, onDelete: onDelete, onPlay: onPlay)) {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 16) { HStack(spacing: 16) {
// Poster
Group { Group {
if let posterURL = group.posterURL { if let posterURL = group.posterURL {
KFImage(posterURL) LazyImage(url: posterURL) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Rectangle() Rectangle()
.fill(.tertiary) .fill(.tertiary)
} }
.resizable() }
.aspectRatio(contentMode: .fill)
} else { } else {
Rectangle() Rectangle()
.fill(.tertiary) .fill(.tertiary)
@ -921,7 +926,6 @@ struct EnhancedDownloadGroupCard: View {
.frame(width: 56, height: 84) .frame(width: 56, height: 84)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
// Content
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text(group.title) Text(group.title)
.font(.headline) .font(.headline)
@ -1000,18 +1004,20 @@ struct EnhancedShowEpisodesView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 24) { VStack(spacing: 24) {
// Header Section
VStack(spacing: 20) { VStack(spacing: 20) {
HStack(alignment: .top, spacing: 20) { HStack(alignment: .top, spacing: 20) {
Group { Group {
if let posterURL = group.posterURL { if let posterURL = group.posterURL {
KFImage(posterURL) LazyImage(url: posterURL) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Rectangle() Rectangle()
.fill(.tertiary) .fill(.tertiary)
} }
.resizable() }
.aspectRatio(contentMode: .fill)
} else { } else {
Rectangle() Rectangle()
.fill(.tertiary) .fill(.tertiary)
@ -1192,16 +1198,18 @@ struct EnhancedEpisodeRow: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 16) { HStack(spacing: 16) {
// Thumbnail
Group { Group {
if let backdropURL = asset.metadata?.backdropURL ?? asset.metadata?.posterURL { if let backdropURL = asset.metadata?.backdropURL ?? asset.metadata?.posterURL {
KFImage(backdropURL) LazyImage(url: backdropURL) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Rectangle() Rectangle()
.fill(.tertiary) .fill(.tertiary)
} }
.resizable() }
.aspectRatio(contentMode: .fill)
} else { } else {
Rectangle() Rectangle()
.fill(.tertiary) .fill(.tertiary)
@ -1214,7 +1222,6 @@ struct EnhancedEpisodeRow: View {
.frame(width: 100, height: 60) .frame(width: 100, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
// Content
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(asset.episodeDisplayName) Text(asset.episodeDisplayName)
.font(.headline) .font(.headline)

View file

@ -5,9 +5,9 @@
// Created by paul on 29/04/2025. // Created by paul on 29/04/2025.
// //
import SwiftUI
import Kingfisher
import UIKit import UIKit
import NukeUI
import SwiftUI
extension View { extension View {
func circularGradientOutlineTwo() -> some View { func circularGradientOutlineTwo() -> some View {
@ -59,28 +59,44 @@ struct BookmarkCell: View {
var body: some View { var body: some View {
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) { if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
ZStack { ZStack {
KFImage(URL(string: bookmark.imageUrl)) LazyImage(url: URL(string: bookmark.imageUrl)) { state in
.resizable() if let uiImage = state.imageContainer?.image {
.aspectRatio(0.72, contentMode: .fill) Image(uiImage: uiImage)
.frame(width: 162, height: 243) .resizable()
.cornerRadius(12) .aspectRatio(0.72, contentMode: .fill)
.clipped() .frame(width: 162, height: 243)
.overlay( .cornerRadius(12)
ZStack { .clipped()
Circle() } else {
.fill(Color.black.opacity(0.5)) RoundedRectangle(cornerRadius: 12)
.frame(width: 28, height: 28) .fill(Color.gray.opacity(0.3))
.overlay( .frame(width: 162, height: 243)
KFImage(URL(string: module.metadata.iconUrl)) }
.resizable() }
.scaledToFill() .overlay(
.frame(width: 32, height: 32) ZStack {
.clipShape(Circle()) Circle()
) .fill(Color.black.opacity(0.5))
} .frame(width: 28, height: 28)
.padding(8), .overlay(
alignment: .topLeading LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
) if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: 32, height: 32)
.clipShape(Circle())
} else {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 32, height: 32)
}
}
)
}
.padding(8),
alignment: .topLeading
)
VStack { VStack {
Spacer() Spacer()

View file

@ -5,9 +5,9 @@
// Created by paul on 24/05/2025. // Created by paul on 24/05/2025.
// //
import SwiftUI
import Kingfisher
import UIKit import UIKit
import NukeUI
import SwiftUI
extension View { extension View {
func circularGradientOutline() -> some View { func circularGradientOutline() -> some View {
@ -206,75 +206,86 @@ struct FullWidthContinueWatchingCell: View {
}) { }) {
GeometryReader { geometry in GeometryReader { geometry in
ZStack(alignment: .bottomLeading) { ZStack(alignment: .bottomLeading) {
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) LazyImage(url: URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: 157.03)
.cornerRadius(10)
.clipped()
} else {
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3)) .fill(Color.gray.opacity(0.3))
.frame(height: 157.03) .frame(height: 157.03)
.shimmering() .shimmering()
} }
.resizable() }
.aspectRatio(contentMode: .fill) .overlay(
.frame(width: geometry.size.width, height: 157.03) ZStack {
.cornerRadius(10) ProgressiveBlurView()
.clipped() .cornerRadius(10, corners: [.bottomLeft, .bottomRight])
.overlay(
ZStack { VStack(alignment: .leading, spacing: 4) {
ProgressiveBlurView() Spacer()
.cornerRadius(10, corners: [.bottomLeft, .bottomRight]) Text(item.mediaTitle)
.font(.headline)
.foregroundColor(.white)
.lineLimit(1)
VStack(alignment: .leading, spacing: 4) { HStack {
Spacer() Text("Episode \(item.episodeNumber)")
Text(item.mediaTitle) .font(.subheadline)
.font(.headline) .foregroundColor(.white.opacity(0.9))
.foregroundColor(.white)
.lineLimit(1)
HStack { Spacer()
Text("Episode \(item.episodeNumber)")
.font(.subheadline) Text("\(Int(item.progress * 100))% seen")
.foregroundColor(.white.opacity(0.9)) .font(.caption)
.foregroundColor(.white.opacity(0.9))
Spacer()
Text("\(Int(item.progress * 100))% seen")
.font(.caption)
.foregroundColor(.white.opacity(0.9))
}
} }
.padding(10) }
.background( .padding(10)
LinearGradient( .background(
colors: [ LinearGradient(
.black.opacity(0.7), colors: [
.black.opacity(0.0) .black.opacity(0.7),
], .black.opacity(0.0)
startPoint: .bottom, ],
endPoint: .top startPoint: .bottom,
) endPoint: .top
)
.clipped() .clipped()
.cornerRadius(10, corners: [.bottomLeft, .bottomRight]) .cornerRadius(10, corners: [.bottomLeft, .bottomRight])
.shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1) .shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1)
)
},
alignment: .bottom
)
.overlay(
ZStack {
Circle()
.fill(Color.black.opacity(0.5))
.frame(width: 28, height: 28)
.overlay(
LazyImage(url: URL(string: item.module.metadata.iconUrl)) { state in
if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: 32, height: 32)
.clipShape(Circle())
} else {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 32, height: 32)
}
}
) )
}, }
alignment: .bottom
)
.overlay(
ZStack {
Circle()
.fill(Color.black.opacity(0.5))
.frame(width: 28, height: 28)
.overlay(
KFImage(URL(string: item.module.metadata.iconUrl))
.resizable()
.scaledToFill()
.frame(width: 32, height: 32)
.clipShape(Circle())
)
}
.padding(8), .padding(8),
alignment: .topLeading alignment: .topLeading
) )
} }
} }
.frame(height: 157.03) .frame(height: 157.03)

View file

@ -5,9 +5,8 @@
// Created by paul on 28/05/25. // Created by paul on 28/05/25.
// //
import SwiftUI
import Kingfisher
import UIKit import UIKit
import SwiftUI
struct BookmarksDetailView: View { struct BookmarksDetailView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss

View file

@ -5,13 +5,14 @@
// Created by Francesco on 05/01/25. // Created by Francesco on 05/01/25.
// //
import SwiftUI
import Kingfisher
import UIKit import UIKit
import NukeUI
import SwiftUI
struct LibraryView: View { struct LibraryView: View {
@EnvironmentObject private var libraryManager: LibraryManager @EnvironmentObject private var libraryManager: LibraryManager
@EnvironmentObject private var moduleManager: ModuleManager @EnvironmentObject private var moduleManager: ModuleManager
@Environment(\.scenePhase) private var scenePhase
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
@ -165,6 +166,11 @@ struct LibraryView: View {
.onAppear { .onAppear {
fetchContinueWatching() fetchContinueWatching()
} }
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
fetchContinueWatching()
}
}
} }
} }
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(StackNavigationViewStyle())
@ -237,8 +243,8 @@ struct ContinueWatchingCell: View {
var markAsWatched: () -> Void var markAsWatched: () -> Void
var removeItem: () -> Void var removeItem: () -> Void
@State private @State private var currentProgress: Double = 0.0
var currentProgress: Double = 0.0 @Environment(\.scenePhase) private var scenePhase
var body: some View { var body: some View {
Button(action: { Button(action: {
@ -280,85 +286,96 @@ struct ContinueWatchingCell: View {
} }
}) { }) {
ZStack(alignment: .bottomLeading) { ZStack(alignment: .bottomLeading) {
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) LazyImage(url: URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(16/9, contentMode: .fill)
.frame(width: 280, height: 157.03)
.cornerRadius(10)
.clipped()
} else {
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3)) .fill(Color.gray.opacity(0.3))
.frame(width: 280, height: 157.03) .frame(width: 280, height: 157.03)
.shimmering() .redacted(reason: .placeholder)
} }
.resizable() }
.aspectRatio(16/9, contentMode: .fill) .overlay(
.frame(width: 280, height: 157.03) ZStack {
.cornerRadius(10) ProgressiveBlurView()
.clipped() .cornerRadius(10, corners: [.bottomLeft, .bottomRight])
.overlay(
ZStack { VStack(alignment: .leading, spacing: 4) {
ProgressiveBlurView() Spacer()
.cornerRadius(10, corners: [.bottomLeft, .bottomRight]) Text(item.mediaTitle)
.font(.headline)
.foregroundColor(.white)
.lineLimit(1)
VStack(alignment: .leading, spacing: 4) { HStack {
Spacer() Text("Episode \(item.episodeNumber)")
Text(item.mediaTitle) .font(.subheadline)
.font(.headline) .foregroundColor(.white.opacity(0.9))
.foregroundColor(.white)
.lineLimit(1)
HStack { Spacer()
Text("Episode \(item.episodeNumber)")
.font(.subheadline) Text("\(Int(item.progress * 100))% seen")
.foregroundColor(.white.opacity(0.9)) .font(.caption)
.foregroundColor(.white.opacity(0.9))
Spacer()
Text("\(Int(item.progress * 100))% seen")
.font(.caption)
.foregroundColor(.white.opacity(0.9))
}
} }
.padding(10) }
.background( .padding(10)
LinearGradient( .background(
colors: [ LinearGradient(
.black.opacity(0.7), colors: [
.black.opacity(0.0) .black.opacity(0.7),
], .black.opacity(0.0)
startPoint: .bottom, ],
endPoint: .top startPoint: .bottom,
) endPoint: .top
.clipped()
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
.shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1)
) )
}, .clipped()
alignment: .bottom .cornerRadius(10, corners: [.bottomLeft, .bottomRight])
) .shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1)
.overlay( )
ZStack { },
if item.streamUrl.hasPrefix("file://") { alignment: .bottom
Image(systemName: "arrow.down.app.fill") )
.resizable() .overlay(
.scaledToFit() ZStack {
.frame(width: 24, height: 24) if item.streamUrl.hasPrefix("file://") {
.foregroundColor(.white) Image(systemName: "arrow.down.app.fill")
.background(Color.black.cornerRadius(6)) .resizable()
.padding(8) .scaledToFit()
} else { .frame(width: 24, height: 24)
Circle() .foregroundColor(.white)
.fill(Color.black.opacity(0.5)) .background(Color.black.cornerRadius(6))
.frame(width: 28, height: 28) .padding(8)
.overlay( } else {
KFImage(URL(string: item.module.metadata.iconUrl)) Circle()
.resizable() .fill(Color.black.opacity(0.5))
.scaledToFill() .frame(width: 28, height: 28)
.frame(width: 32, height: 32) .overlay(
.clipShape(Circle()) LazyImage(url: URL(string: item.module.metadata.iconUrl)) { state in
) if let uiImage = state.imageContainer?.image {
.padding(8) Image(uiImage: uiImage)
} .resizable()
}, .scaledToFill()
alignment: .topLeading .frame(width: 32, height: 32)
) .clipShape(Circle())
} else {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 32, height: 32)
}
}
)
.padding(8)
}
},
alignment: .topLeading
)
} }
.frame(width: 280, height: 157.03) .frame(width: 280, height: 157.03)
} }
@ -377,11 +394,11 @@ struct ContinueWatchingCell: View {
.onAppear { .onAppear {
updateProgress() updateProgress()
} }
.onReceive(NotificationCenter.default.publisher( .onChange(of: scenePhase) { newPhase in
for: UIApplication.didBecomeActiveNotification)) { if newPhase == .active {
_ in
updateProgress() updateProgress()
} }
}
} }
private func updateProgress() { private func updateProgress() {
@ -525,34 +542,45 @@ struct BookmarkItemView: View {
isDetailActive = true isDetailActive = true
}) { }) {
ZStack { ZStack {
KFImage(URL(string: item.imageUrl)) LazyImage(url: URL(string: item.imageUrl)) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(0.72, contentMode: .fill)
.frame(width: 162, height: 243)
.cornerRadius(12)
.clipped()
} else {
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3)) .fill(Color.gray.opacity(0.3))
.aspectRatio(2 / 3, contentMode: .fit) .aspectRatio(2/3, contentMode: .fit)
.shimmering() .redacted(reason: .placeholder)
} }
.resizable() }
.aspectRatio(0.72, contentMode: .fill) .overlay(
.frame(width: 162, height: 243) ZStack {
.cornerRadius(12) Circle()
.clipped() .fill(Color.black.opacity(0.5))
.overlay( .frame(width: 28, height: 28)
ZStack { .overlay(
Circle() LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
.fill(Color.black.opacity(0.5)) if let uiImage = state.imageContainer?.image {
.frame(width: 28, height: 28) Image(uiImage: uiImage)
.overlay( .resizable()
KFImage(URL(string: module.metadata.iconUrl)) .scaledToFill()
.resizable() .frame(width: 32, height: 32)
.scaledToFill() .clipShape(Circle())
.frame(width: 32, height: 32) } else {
.clipShape(Circle()) Circle()
) .fill(Color.gray.opacity(0.3))
} .frame(width: 32, height: 32)
.padding(8), }
alignment: .topLeading }
) )
}
.padding(8),
alignment: .topLeading
)
VStack { VStack {
Spacer() Spacer()

View file

@ -5,8 +5,8 @@
// Created by seiike on 01/06/2025. // Created by seiike on 01/06/2025.
// //
import NukeUI
import SwiftUI import SwiftUI
import Kingfisher
struct AnilistMatchPopupView: View { struct AnilistMatchPopupView: View {
let seriesTitle: String let seriesTitle: String
@ -32,7 +32,6 @@ struct AnilistMatchPopupView: View {
NavigationView { NavigationView {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
// (Optional) A hidden header; can be omitted if empty
Text("".uppercased()) Text("".uppercased())
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
@ -62,11 +61,20 @@ struct AnilistMatchPopupView: View {
HStack(spacing: 12) { HStack(spacing: 12) {
if let cover = result["cover"] as? String, if let cover = result["cover"] as? String,
let url = URL(string: cover) { let url = URL(string: cover) {
KFImage(url) LazyImage(url: url) { state in
.resizable() if let uiImage = state.imageContainer?.image {
.aspectRatio(contentMode: .fill) Image(uiImage: uiImage)
.frame(width: 50, height: 70) .resizable()
.cornerRadius(6) .aspectRatio(contentMode: .fill)
.frame(width: 50, height: 70)
.cornerRadius(6)
} else {
Rectangle()
.fill(.tertiary)
.frame(width: 50, height: 70)
.cornerRadius(6)
}
}
} }
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {

View file

@ -5,8 +5,8 @@
// Created by Francesco on 18/12/24. // Created by Francesco on 18/12/24.
// //
import NukeUI
import SwiftUI import SwiftUI
import Kingfisher
import AVFoundation import AVFoundation
struct EpisodeCell: View { struct EpisodeCell: View {
@ -264,14 +264,28 @@ struct EpisodeCell: View {
private var episodeThumbnail: some View { private var episodeThumbnail: some View {
ZStack { ZStack {
if let url = URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl) { if let url = URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl) {
KFImage(url) LazyImage(url: url) { state in
.onFailure { error in if let image = state.imageContainer?.image {
Logger.shared.log("Failed to load episode image: \(error)", type: "Error") Image(uiImage: image)
.resizable()
.aspectRatio(16/9, contentMode: .fill)
.frame(width: 100, height: 56)
.cornerRadius(8)
} else if state.error != nil {
Rectangle()
.fill(.tertiary)
.frame(width: 100, height: 56)
.cornerRadius(8)
.onAppear {
Logger.shared.log("Failed to load episode image: \(state.error?.localizedDescription ?? "Unknown error")", type: "Error")
}
} else {
Rectangle()
.fill(.tertiary)
.frame(width: 100, height: 56)
.cornerRadius(8)
} }
.resizable() }
.aspectRatio(16/9, contentMode: .fill)
.frame(width: 100, height: 56)
.cornerRadius(8)
} else { } else {
Rectangle() Rectangle()
.fill(Color.gray.opacity(0.3)) .fill(Color.gray.opacity(0.3))

View file

@ -5,8 +5,8 @@
// Created by Francesco on 05/01/25. // Created by Francesco on 05/01/25.
// //
import NukeUI
import SwiftUI import SwiftUI
import Kingfisher
import SafariServices import SafariServices
private let tmdbFetcher = TMDBFetcher() private let tmdbFetcher = TMDBFetcher()
@ -60,7 +60,7 @@ struct MediaInfoView: View {
@State private var isMatchingPresented = false @State private var isMatchingPresented = false
@State private var matchedTitle: String? = nil @State private var matchedTitle: String? = nil
@StateObject private var jsController = JSController.shared @ObservedObject private var jsController = JSController.shared
@EnvironmentObject var moduleManager: ModuleManager @EnvironmentObject var moduleManager: ModuleManager
@EnvironmentObject private var libraryManager: LibraryManager @EnvironmentObject private var libraryManager: LibraryManager
@EnvironmentObject var tabBarController: TabBarController @EnvironmentObject var tabBarController: TabBarController
@ -85,7 +85,6 @@ struct MediaInfoView: View {
@State private var isBulkDownloading: Bool = false @State private var isBulkDownloading: Bool = false
@State private var bulkDownloadProgress: String = "" @State private var bulkDownloadProgress: String = ""
@State private var tmdbType: TMDBFetcher.MediaType? = nil @State private var tmdbType: TMDBFetcher.MediaType? = nil
@State private var latestProgress: Double = 0.0
private var isGroupedBySeasons: Bool { private var isGroupedBySeasons: Bool {
return groupedEpisodes().count > 1 return groupedEpisodes().count > 1
@ -123,7 +122,6 @@ struct MediaInfoView: View {
.navigationBarHidden(true) .navigationBarHidden(true)
.ignoresSafeArea(.container, edges: .top) .ignoresSafeArea(.container, edges: .top)
.onAppear { .onAppear {
updateLatestProgress()
buttonRefreshTrigger.toggle() buttonRefreshTrigger.toggle()
let savedID = UserDefaults.standard.integer(forKey: "custom_anilist_id_\(href)") let savedID = UserDefaults.standard.integer(forKey: "custom_anilist_id_\(href)")
@ -168,6 +166,34 @@ struct MediaInfoView: View {
.onDisappear(){ .onDisappear(){
tabBarController.showTabBar() tabBarController.showTabBar()
} }
.task {
guard !hasFetched else { return }
let savedCustomID = UserDefaults.standard.integer(forKey: "custom_anilist_id_\(href)")
if savedCustomID != 0 { customAniListID = savedCustomID }
if let savedPoster = UserDefaults.standard.string(forKey: "tmdbPosterURL_\(href)") {
imageUrl = savedPoster
}
DropManager.shared.showDrop(
title: "Fetching Data",
subtitle: "Please wait while fetching.",
duration: 0.5,
icon: UIImage(systemName: "arrow.triangle.2.circlepath")
)
fetchDetails()
if savedCustomID != 0 {
itemID = savedCustomID
} else {
fetchMetadataIDIfNeeded()
}
hasFetched = true
AnalyticsManager.shared.sendEvent(
event: "MediaInfoView",
additionalData: ["title": title]
)
}
.alert("Loading Stream", isPresented: $showLoadingAlert) { .alert("Loading Stream", isPresented: $showLoadingAlert) {
Button("Cancel", role: .cancel) { Button("Cancel", role: .cancel) {
activeFetchID = nil activeFetchID = nil
@ -214,54 +240,25 @@ struct MediaInfoView: View {
private var mainScrollView: some View { private var mainScrollView: some View {
ScrollView { ScrollView {
ZStack(alignment: .top) { ZStack(alignment: .top) {
KFImage(URL(string: imageUrl)) LazyImage(url: URL(string: imageUrl)) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width, height: 700)
.clipped()
} else {
Rectangle() Rectangle()
.fill(Color.gray.opacity(0.3)) .fill(Color.gray.opacity(0.3))
.shimmering() .shimmering()
.frame(width: UIScreen.main.bounds.width, height: 700)
.clipped()
} }
.resizable() }
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width, height: 700)
.clipped()
KFImage(URL(string: imageUrl))
.placeholder { EmptyView() }
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width, height: 700)
.clipped()
.blur(radius: 30)
.mask(
LinearGradient(
gradient: Gradient(stops: [
.init(color: .clear, location: 0.0),
.init(color: .clear, location: 0.6),
.init(color: .black, location: 0.8),
.init(color: .black, location: 1.0)
]),
startPoint: .top,
endPoint: .bottom
)
)
.overlay(
VStack(spacing: 0) {
Spacer()
LinearGradient(
gradient: Gradient(stops: [
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.0), location: 0.0),
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.5), location: 0.5),
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(1.0), location: 1.0)
]),
startPoint: .top,
endPoint: .bottom
)
.frame(height: 150)
}
)
VStack(spacing: 0) { VStack(spacing: 0) {
Rectangle() Rectangle()
.fill(Color.clear) .fill(Color.clear)
.frame(height: 450) .frame(height: 400)
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
headerSection headerSection
if !episodeLinks.isEmpty { if !episodeLinks.isEmpty {
@ -275,15 +272,15 @@ struct MediaInfoView: View {
LinearGradient( LinearGradient(
gradient: Gradient(stops: [ gradient: Gradient(stops: [
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.0), location: 0.0), .init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.0), location: 0.0),
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.3), location: 0.1), .init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.5), location: 0.2),
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.6), location: 0.3), .init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.8), location: 0.5),
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.9), location: 0.7), .init(color: (colorScheme == .dark ? Color.black : Color.white), location: 1.0)
]), ]),
startPoint: .top, startPoint: .top,
endPoint: .bottom endPoint: .bottom
) )
.clipShape(RoundedRectangle(cornerRadius: 0)) .clipShape(RoundedRectangle(cornerRadius: 0))
.shadow(color: (colorScheme == .dark ? Color.black : Color.white).opacity(1), radius: 15, x: 0, y: 15) .shadow(color: (colorScheme == .dark ? Color.black : Color.white).opacity(1), radius: 10, x: 0, y: 10)
) )
} }
.deviceScaled() .deviceScaled()
@ -359,12 +356,10 @@ struct MediaInfoView: View {
UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(ep.href)") UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(ep.href)")
UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(ep.href)") UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(ep.href)")
DropManager.shared.showDrop(title: "Marked as Watched", subtitle: "", duration: 1.0, icon: UIImage(systemName: "checkmark.circle.fill")) DropManager.shared.showDrop(title: "Marked as Watched", subtitle: "", duration: 1.0, icon: UIImage(systemName: "checkmark.circle.fill"))
updateLatestProgress()
} else { } else {
UserDefaults.standard.set(0.0, forKey: "lastPlayedTime_\(ep.href)") UserDefaults.standard.set(0.0, forKey: "lastPlayedTime_\(ep.href)")
UserDefaults.standard.set(0.0, forKey: "totalTime_\(ep.href)") UserDefaults.standard.set(0.0, forKey: "totalTime_\(ep.href)")
DropManager.shared.showDrop(title: "Progress Reset", subtitle: "", duration: 1.0, icon: UIImage(systemName: "arrow.counterclockwise")) DropManager.shared.showDrop(title: "Progress Reset", subtitle: "", duration: 1.0, icon: UIImage(systemName: "arrow.counterclockwise"))
updateLatestProgress()
} }
} }
}) { }) {
@ -587,34 +582,25 @@ struct MediaInfoView: View {
@ViewBuilder @ViewBuilder
private var playAndBookmarkSection: some View { private var playAndBookmarkSection: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
ZStack(alignment: .leading) { Button(action: {
RoundedRectangle(cornerRadius: 25) playFirstUnwatchedEpisode()
.fill(Color.accentColor) }) {
.frame(height: 48) HStack(spacing: 8) {
Image(systemName: "play.fill")
Button(action: { .foregroundColor(colorScheme == .dark ? .black : .white)
playFirstUnwatchedEpisode() Text(startWatchingText)
}) { .font(.system(size: 16, weight: .medium))
HStack(spacing: 8) { .foregroundColor(colorScheme == .dark ? .black : .white)
Image(systemName: "play.fill")
.foregroundColor(colorScheme == .dark ? .black : .white)
Text(continueWatchingText)
.font(.system(size: 16, weight: .medium))
.foregroundColor(colorScheme == .dark ? .black : .white)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.padding(.horizontal, 20)
.background(Color.clear)
.contentShape(RoundedRectangle(cornerRadius: 25))
} }
.disabled(isFetchingEpisode) .frame(maxWidth: .infinity)
.padding(.vertical, 12)
.padding(.horizontal, 20)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.accentColor)
)
} }
.clipShape(RoundedRectangle(cornerRadius: 25)) .disabled(isFetchingEpisode)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.accentColor, lineWidth: 0)
)
Button(action: { Button(action: {
libraryManager.toggleBookmark( libraryManager.toggleBookmark(
@ -966,18 +952,6 @@ struct MediaInfoView: View {
} }
} }
private func updateLatestProgress() {
for ep in episodeLinks.reversed() {
let last = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
let total = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
if total > 0 {
latestProgress = last / total
return
}
}
latestProgress = 0.0
}
@ViewBuilder @ViewBuilder
private var noEpisodesSection: some View { private var noEpisodesSection: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
@ -1000,46 +974,53 @@ struct MediaInfoView: View {
.padding(.vertical, 50) .padding(.vertical, 50)
} }
private var continueWatchingText: String { private var startWatchingText: String {
for ep in episodeLinks { let indices = finishedAndUnfinishedIndices()
let last = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") let finished = indices.finished
let total = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") let unfinished = indices.unfinished
let progress = total > 0 ? last / total : 0
if episodeLinks.count == 1 {
if progress > 0 && progress < 0.9 { if let unfinishedIndex = unfinished {
return "Continue Watching Episode \(ep.number)" return "Continue Watching"
} }
return "Start Watching"
} }
for ep in episodeLinks { if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 {
let last = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") let nextEp = episodeLinks[finishedIndex + 1]
let total = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") return "Start Watching Episode \(nextEp.number)"
let progress = total > 0 ? last / total : 0 }
if progress < 0.9 { if let unfinishedIndex = unfinished {
return "Start Watching Episode \(ep.number)" let currentEp = episodeLinks[unfinishedIndex]
} return "Continue Watching Episode \(currentEp.number)"
} }
return "Start Watching" return "Start Watching"
} }
private func playFirstUnwatchedEpisode() { private func playFirstUnwatchedEpisode() {
for ep in episodeLinks { let indices = finishedAndUnfinishedIndices()
let last = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") let finished = indices.finished
let total = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") let unfinished = indices.unfinished
let progress = total > 0 ? last / total : 0
if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 {
if progress < 0.9 { let nextEp = episodeLinks[finishedIndex + 1]
selectedEpisodeNumber = ep.number selectedEpisodeNumber = nextEp.number
fetchStream(href: ep.href) fetchStream(href: nextEp.href)
return return
}
} }
if let first = episodeLinks.first { if let unfinishedIndex = unfinished {
selectedEpisodeNumber = first.number let ep = episodeLinks[unfinishedIndex]
fetchStream(href: first.href) selectedEpisodeNumber = ep.number
fetchStream(href: ep.href)
return
}
if let firstEpisode = episodeLinks.first {
selectedEpisodeNumber = firstEpisode.number
fetchStream(href: firstEpisode.href)
} }
} }
@ -1347,9 +1328,12 @@ struct MediaInfoView: View {
videoPlayerViewController.mediaTitle = title videoPlayerViewController.mediaTitle = title
videoPlayerViewController.subtitles = subtitles ?? "" videoPlayerViewController.subtitles = subtitles ?? ""
videoPlayerViewController.aniListID = itemID ?? 0 videoPlayerViewController.aniListID = itemID ?? 0
videoPlayerViewController.modalPresentationStyle = .fullScreen videoPlayerViewController.modalPresentationStyle = .overFullScreen
presentPlayerWithDetachedContext(videoPlayerViewController: videoPlayerViewController) if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
}
return return
default: default:
break break
@ -1384,10 +1368,16 @@ struct MediaInfoView: View {
episodeImageUrl: selectedEpisodeImage, episodeImageUrl: selectedEpisodeImage,
headers: headers ?? nil headers: headers ?? nil
) )
customMediaPlayer.modalPresentationStyle = .fullScreen customMediaPlayer.modalPresentationStyle = .overFullScreen
Logger.shared.log("Opening custom media player with stream URL: \(url), and subtitles URL: \(String(describing: subtitles))", type: "Stream") Logger.shared.log("Opening custom media player with url: \(url)")
presentPlayerWithDetachedContext(customMediaPlayer: customMediaPlayer) if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
} else {
Logger.shared.log("Failed to find root view controller", type: "Error")
DropManager.shared.showDrop(title: "Error", subtitle: "Failed to present player", duration: 2.0, icon: UIImage(systemName: "xmark.circle"))
}
} }
} }
} }
@ -1936,34 +1926,4 @@ struct MediaInfoView: View {
} }
}.resume() }.resume()
} }
private func presentPlayerWithDetachedContext(videoPlayerViewController: VideoPlayerViewController) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
let detachedWindow = UIWindow(windowScene: windowScene)
let hostingController = UIViewController()
hostingController.view.backgroundColor = .clear
detachedWindow.rootViewController = hostingController
detachedWindow.backgroundColor = .clear
detachedWindow.windowLevel = .normal + 1
detachedWindow.makeKeyAndVisible()
videoPlayerViewController.detachedWindow = detachedWindow
hostingController.present(videoPlayerViewController, animated: true, completion: nil)
}
private func presentPlayerWithDetachedContext(customMediaPlayer: CustomMediaPlayerViewController) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
let detachedWindow = UIWindow(windowScene: windowScene)
let hostingController = UIViewController()
hostingController.view.backgroundColor = .clear
detachedWindow.rootViewController = hostingController
detachedWindow.backgroundColor = .clear
detachedWindow.windowLevel = .normal + 1
detachedWindow.makeKeyAndVisible()
customMediaPlayer.detachedWindow = detachedWindow
hostingController.present(customMediaPlayer, animated: true, completion: nil)
}
} }

View file

@ -6,7 +6,6 @@
// //
import SwiftUI import SwiftUI
import Kingfisher
struct SearchItem: Identifiable { struct SearchItem: Identifiable {
let id = UUID() let id = UUID()

View file

@ -5,8 +5,8 @@
// Created by paul on 28/05/25. // Created by paul on 28/05/25.
// //
import NukeUI
import SwiftUI import SwiftUI
import Kingfisher
struct SearchResultsGrid: View { struct SearchResultsGrid: View {
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
@ -32,12 +32,22 @@ struct SearchResultsGrid: View {
ForEach(items) { item in ForEach(items) { item in
NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule)) { NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule)) {
ZStack { ZStack {
KFImage(URL(string: item.imageUrl)) LazyImage(url: URL(string: item.imageUrl)) { state in
.resizable() if let uiImage = state.imageContainer?.image {
.aspectRatio(0.72, contentMode: .fill) Image(uiImage: uiImage)
.frame(width: cellWidth, height: cellWidth * 1.5) .resizable()
.cornerRadius(12) .aspectRatio(0.72, contentMode: .fill)
.clipped() .frame(width: cellWidth, height: cellWidth * 1.5)
.cornerRadius(12)
.clipped()
} else {
Rectangle()
.fill(.tertiary)
.frame(width: cellWidth, height: cellWidth * 1.5)
.cornerRadius(12)
.clipped()
}
}
VStack { VStack {
Spacer() Spacer()

View file

@ -6,7 +6,6 @@
// //
import SwiftUI import SwiftUI
import Kingfisher
struct ModuleButtonModifier: ViewModifier { struct ModuleButtonModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {

View file

@ -5,8 +5,8 @@
// Created by Francesco on 27/01/25. // Created by Francesco on 27/01/25.
// //
import NukeUI
import SwiftUI import SwiftUI
import Kingfisher
struct ModuleSelectorMenu: View { struct ModuleSelectorMenu: View {
let selectedModule: ScrapingModule? let selectedModule: ScrapingModule?
@ -27,11 +27,19 @@ struct ModuleSelectorMenu: View {
onModuleSelected(module.id.uuidString) onModuleSelected(module.id.uuidString)
} label: { } label: {
HStack { HStack {
KFImage(URL(string: module.metadata.iconUrl)) LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
.resizable() if let uiImage = state.imageContainer?.image {
.aspectRatio(contentMode: .fit) Image(uiImage: uiImage)
.frame(width: 20, height: 20) .resizable()
.cornerRadius(4) .aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.cornerRadius(4)
} else {
Circle()
.fill(Color(.systemGray5))
}
}
Text(module.metadata.sourceName) Text(module.metadata.sourceName)
if module.id.uuidString == selectedModuleId { if module.id.uuidString == selectedModuleId {
Image(systemName: "checkmark") Image(systemName: "checkmark")
@ -48,29 +56,37 @@ struct ModuleSelectorMenu: View {
Text(selectedModule.metadata.sourceName) Text(selectedModule.metadata.sourceName)
.font(.headline) .font(.headline)
.foregroundColor(.primary) .foregroundColor(.primary)
KFImage(URL(string: selectedModule.metadata.iconUrl)) LazyImage(url: URL(string: selectedModule.metadata.iconUrl)) { state in
.resizable() if let uiImage = state.imageContainer?.image {
.frame(width: 36, height: 36) Image(uiImage: uiImage)
.clipShape(Circle()) .resizable()
.background( .frame(width: 36, height: 36)
.clipShape(Circle())
} else {
Circle() Circle()
.fill(.ultraThinMaterial) .fill(.ultraThinMaterial)
.overlay( .frame(width: 36, height: 36)
Circle() }
.stroke( }
LinearGradient( .background(
gradient: Gradient(stops: [ Circle()
.init(color: Color.accentColor.opacity(gradientOpacity), location: 0), .fill(.ultraThinMaterial)
.init(color: Color.accentColor.opacity(0), location: 1) .overlay(
]), Circle()
startPoint: .top, .stroke(
endPoint: .bottom LinearGradient(
), gradient: Gradient(stops: [
lineWidth: 0.5 .init(color: Color.accentColor.opacity(gradientOpacity), location: 0),
) .init(color: Color.accentColor.opacity(0), location: 1)
) ]),
.matchedGeometryEffect(id: "background_circle", in: animation) startPoint: .top,
) endPoint: .bottom
),
lineWidth: 0.5
)
)
.matchedGeometryEffect(id: "background_circle", in: animation)
)
} else { } else {
Text("Select Module") Text("Select Module")
.font(.headline) .font(.headline)

View file

@ -5,8 +5,8 @@
// Created by Francesco on 26/05/25. // Created by Francesco on 26/05/25.
// //
import NukeUI
import SwiftUI import SwiftUI
import Kingfisher
fileprivate struct SettingsSection<Content: View>: View { fileprivate struct SettingsSection<Content: View>: View {
let title: String let title: String
@ -66,14 +66,18 @@ struct SettingsViewAbout: View {
VStack(spacing: 24) { VStack(spacing: 24) {
SettingsSection(title: "App Info", footer: "Sora/Sulfur will always remain free with no ADs!") { SettingsSection(title: "App Info", footer: "Sora/Sulfur will always remain free with no ADs!") {
HStack(alignment: .center, spacing: 16) { HStack(alignment: .center, spacing: 16) {
KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/Sora/Assets.xcassets/AppIcons/AppIcon_Default.appiconset/darkmode.png")) LazyImage(url: URL(string: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/Sora/Assets.xcassets/AppIcons/AppIcon_Default.appiconset/darkmode.png")) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.frame(width: 100, height: 100)
.cornerRadius(20)
.shadow(radius: 5)
} else {
ProgressView() ProgressView()
.frame(width: 40, height: 40)
} }
.resizable() }
.frame(width: 100, height: 100)
.cornerRadius(20)
.shadow(radius: 5)
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("Sora") Text("Sora")
@ -96,13 +100,17 @@ struct SettingsViewAbout: View {
} }
}) { }) {
HStack { HStack {
KFImage(URL(string: "https://avatars.githubusercontent.com/u/100066266?v=4")) LazyImage(url: URL(string: "https://avatars.githubusercontent.com/u/100066266?v=4")) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.frame(width: 40, height: 40)
.clipShape(Circle())
} else {
ProgressView() ProgressView()
.frame(width: 40, height: 40)
} }
.resizable() }
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("cranci1") Text("cranci1")
@ -205,13 +213,17 @@ struct ContributorView: View {
} }
}) { }) {
HStack { HStack {
KFImage(URL(string: contributor.avatarUrl)) LazyImage(url: URL(string: contributor.avatarUrl)) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.frame(width: 40, height: 40)
.clipShape(Circle())
} else {
ProgressView() ProgressView()
.frame(width: 40, height: 40)
} }
.resizable() }
.frame(width: 40, height: 40)
.clipShape(Circle())
Text(contributor.login) Text(contributor.login)
.font(.headline) .font(.headline)

View file

@ -6,7 +6,6 @@
// //
import SwiftUI import SwiftUI
import Kingfisher
fileprivate struct SettingsSection<Content: View>: View { fileprivate struct SettingsSection<Content: View>: View {
let title: String let title: String

View file

@ -5,8 +5,8 @@
// Created by Francesco on 05/01/25. // Created by Francesco on 05/01/25.
// //
import NukeUI
import SwiftUI import SwiftUI
import Kingfisher
fileprivate struct SettingsSection<Content: View>: View { fileprivate struct SettingsSection<Content: View>: View {
let title: String let title: String
@ -67,11 +67,19 @@ fileprivate struct ModuleListItemView: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack { HStack {
KFImage(URL(string: module.metadata.iconUrl)) LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
.resizable() if let uiImage = state.imageContainer?.image {
.frame(width: 40, height: 40) Image(uiImage: uiImage)
.clipShape(Circle()) .resizable()
.padding(.trailing, 10) .frame(width: 40, height: 40)
.clipShape(Circle())
.padding(.trailing, 10)
} else {
Circle()
.frame(width: 40, height: 40)
.padding(.trailing, 10)
}
}
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .bottom, spacing: 4) { HStack(alignment: .bottom, spacing: 4) {
@ -211,7 +219,7 @@ struct SettingsViewModule: View {
.navigationTitle("Modules") .navigationTitle("Modules")
.navigationBarItems(trailing: .navigationBarItems(trailing:
HStack(spacing: 16) { HStack(spacing: 16) {
if didReceiveDefaultPageLink && !moduleManager.modules.isEmpty { if didReceiveDefaultPageLink {
Button(action: { Button(action: {
showLibrary = true showLibrary = true
}) { }) {

View file

@ -5,9 +5,9 @@
// Created by Francesco on 23/03/25. // Created by Francesco on 23/03/25.
// //
import NukeUI
import SwiftUI import SwiftUI
import Security import Security
import Kingfisher
fileprivate struct SettingsSection<Content: View>: View { fileprivate struct SettingsSection<Content: View>: View {
let title: String let title: String
@ -120,18 +120,21 @@ struct SettingsViewTrackers: View {
SettingsSection(title: "AniList") { SettingsSection(title: "AniList") {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(alignment: .center, spacing: 10) { HStack(alignment: .center, spacing: 10) {
KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) LazyImage(url: URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.frame(width: 60, height: 60)
.clipShape(Rectangle())
.cornerRadius(10)
.padding(.trailing, 10)
} else {
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3)) .fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
.shimmering() .shimmering()
} }
.resizable() }
.frame(width: 60, height: 60)
.clipShape(Rectangle())
.cornerRadius(10)
.padding(.trailing, 10)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("AniList.co") Text("AniList.co")
@ -212,18 +215,21 @@ struct SettingsViewTrackers: View {
SettingsSection(title: "Trakt") { SettingsSection(title: "Trakt") {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(alignment: .center, spacing: 10) { HStack(alignment: .center, spacing: 10) {
KFImage(URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png")) LazyImage(url: URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png")) { state in
.placeholder { if let uiImage = state.imageContainer?.image {
Image(uiImage: uiImage)
.resizable()
.frame(width: 60, height: 60)
.clipShape(Rectangle())
.cornerRadius(10)
.padding(.trailing, 10)
} else {
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3)) .fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
.shimmering() .shimmering()
} }
.resizable() }
.frame(width: 60, height: 60)
.clipShape(Rectangle())
.cornerRadius(10)
.padding(.trailing, 10)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Trakt.tv") Text("Trakt.tv")

View file

@ -48,7 +48,6 @@
1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; }; 1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; };
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; }; 135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; };
13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13637B892DE0EA1100BDA2FC /* UserDefaults.swift */; }; 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13637B892DE0EA1100BDA2FC /* UserDefaults.swift */; };
13637B8D2DE0ECCC00BDA2FC /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B8C2DE0ECCC00BDA2FC /* Kingfisher */; };
13637B902DE0ECD200BDA2FC /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B8F2DE0ECD200BDA2FC /* Drops */; }; 13637B902DE0ECD200BDA2FC /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B8F2DE0ECD200BDA2FC /* Drops */; };
13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */; }; 13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */; };
136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136BBE7F2DB1038000906B5E /* Notification+Name.swift */; }; 136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136BBE7F2DB1038000906B5E /* Notification+Name.swift */; };
@ -59,6 +58,8 @@
139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; }; 139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; };
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */; }; 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */; };
1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD52D3AB3DB00E97C31 /* Logger.swift */; }; 1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD52D3AB3DB00E97C31 /* Logger.swift */; };
13AF34B42DF6CB5900C77880 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 13AF34B32DF6CB5900C77880 /* Nuke */; };
13AF34B62DF6CB5900C77880 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 13AF34B52DF6CB5900C77880 /* NukeUI */; };
13B77E202DA457AA00126FDF /* AniListPushUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B77E1F2DA457AA00126FDF /* AniListPushUpdates.swift */; }; 13B77E202DA457AA00126FDF /* AniListPushUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B77E1F2DA457AA00126FDF /* AniListPushUpdates.swift */; };
13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; }; 13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; };
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; }; 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; };
@ -191,9 +192,10 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
13637B8D2DE0ECCC00BDA2FC /* Kingfisher in Frameworks */, 13AF34B62DF6CB5900C77880 /* NukeUI in Frameworks */,
13637B902DE0ECD200BDA2FC /* Drops in Frameworks */, 13637B902DE0ECD200BDA2FC /* Drops in Frameworks */,
13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */, 13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */,
13AF34B42DF6CB5900C77880 /* Nuke in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -614,9 +616,10 @@
); );
name = Sulfur; name = Sulfur;
packageProductDependencies = ( packageProductDependencies = (
13637B8C2DE0ECCC00BDA2FC /* Kingfisher */,
13637B8F2DE0ECD200BDA2FC /* Drops */, 13637B8F2DE0ECD200BDA2FC /* Drops */,
13637B922DE0ECDB00BDA2FC /* MarqueeLabel */, 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */,
13AF34B32DF6CB5900C77880 /* Nuke */,
13AF34B52DF6CB5900C77880 /* NukeUI */,
); );
productName = Sora; productName = Sora;
productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */; productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */;
@ -643,12 +646,13 @@
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
en, en,
Base,
); );
mainGroup = 133D7C612D2BE2500075467E; mainGroup = 133D7C612D2BE2500075467E;
packageReferences = ( packageReferences = (
13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */,
13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */, 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */,
13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */, 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */,
13AF34B22DF6CB5900C77880 /* XCRemoteSwiftPackageReference "Nuke" */,
); );
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */; productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -920,8 +924,9 @@
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES; SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6"; TARGETED_DEVICE_FAMILY = "1,2,6";
@ -962,8 +967,9 @@
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES; SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6"; TARGETED_DEVICE_FAMILY = "1,2,6";
@ -994,14 +1000,6 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
requirement = {
kind = exactVersion;
version = 7.9.1;
};
};
13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */ = { 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/omaralbeik/Drops.git"; repositoryURL = "https://github.com/omaralbeik/Drops.git";
@ -1014,18 +1012,21 @@
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/cbpowell/MarqueeLabel"; repositoryURL = "https://github.com/cbpowell/MarqueeLabel";
requirement = { requirement = {
kind = exactVersion; branch = master;
version = 4.2.1; kind = branch;
};
};
13AF34B22DF6CB5900C77880 /* XCRemoteSwiftPackageReference "Nuke" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Nuke.git";
requirement = {
branch = main;
kind = branch;
}; };
}; };
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
13637B8C2DE0ECCC00BDA2FC /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
13637B8F2DE0ECD200BDA2FC /* Drops */ = { 13637B8F2DE0ECD200BDA2FC /* Drops */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */; package = 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */;
@ -1036,6 +1037,16 @@
package = 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */; package = 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */;
productName = MarqueeLabel; productName = MarqueeLabel;
}; };
13AF34B32DF6CB5900C77880 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = 13AF34B22DF6CB5900C77880 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
13AF34B52DF6CB5900C77880 /* NukeUI */ = {
isa = XCSwiftPackageProductDependency;
package = 13AF34B22DF6CB5900C77880 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = NukeUI;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 133D7C622D2BE2500075467E /* Project object */; rootObject = 133D7C622D2BE2500075467E /* Project object */;

View file

@ -1,34 +1,32 @@
{ {
"object": { "pins" : [
"pins": [ {
{ "identity" : "drops",
"package": "Drops", "kind" : "remoteSourceControl",
"repositoryURL": "https://github.com/omaralbeik/Drops.git", "location" : "https://github.com/omaralbeik/Drops.git",
"state": { "state" : {
"branch": "main", "branch" : "main",
"revision": "5824681795286c36bdc4a493081a63e64e2a064e", "revision" : "5824681795286c36bdc4a493081a63e64e2a064e"
"version": null
}
},
{
"package": "Kingfisher",
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": {
"branch": null,
"revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e",
"version": "7.9.1"
}
},
{
"package": "MarqueeLabel",
"repositoryURL": "https://github.com/cbpowell/MarqueeLabel",
"state": {
"branch": null,
"revision": "cffb6938940d3242882e6a2f9170b7890a4729ea",
"version": "4.2.1"
}
} }
] },
}, {
"version": 1 "identity" : "marqueelabel",
"kind" : "remoteSourceControl",
"location" : "https://github.com/cbpowell/MarqueeLabel",
"state" : {
"branch" : "master",
"revision" : "18e4787f4dc1c26d2d581c4bc9aeae34686eeeae"
}
},
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke.git",
"state" : {
"branch" : "main",
"revision" : "c7ba4833b1b38f09e9708858aeaf91babc69f65c"
}
}
],
"version" : 2
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 203 KiB