mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 08:32:00 +00:00
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:
parent
7d6e2e65d4
commit
fdc05a13ca
27 changed files with 987 additions and 742 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Kingfisher
|
|
||||||
|
|
||||||
struct SearchItem: Identifiable {
|
struct SearchItem: Identifiable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}) {
|
}) {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 */;
|
||||||
|
|
|
||||||
|
|
@ -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 |
Loading…
Reference in a new issue