mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +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
|
||||
|
||||
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
|
||||
- [MarqueeLabel](https://github.com/cbpowell/MarqueeLabel) - MIT License
|
||||
|
||||
Misc:
|
||||
- [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
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,11 @@ extension JSContext {
|
|||
}
|
||||
}
|
||||
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
|
||||
DispatchQueue.main.async {
|
||||
reject.call(withArguments: [message])
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ import Foundation
|
|||
|
||||
class FetchDelegate: NSObject, URLSessionTaskDelegate {
|
||||
private let allowRedirects: Bool
|
||||
|
||||
init(allowRedirects: Bool) {
|
||||
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) {
|
||||
if(allowRedirects) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import AVFoundation
|
|||
import MarqueeLabel
|
||||
|
||||
class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate {
|
||||
private var airplayButton: AVRoutePickerView!
|
||||
let module: ScrapingModule
|
||||
let streamURL: String
|
||||
let fullUrl: String
|
||||
|
|
@ -41,7 +42,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
var currentTimeVal: Double = 0.0
|
||||
var duration: Double = 0.0
|
||||
var isVideoLoaded = false
|
||||
var detachedWindow: UIWindow?
|
||||
|
||||
private var isHoldPauseEnabled: Bool {
|
||||
UserDefaults.standard.bool(forKey: "holdForPauseEnabled")
|
||||
|
|
@ -73,7 +73,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
private var pipController: AVPictureInPictureController?
|
||||
private var pipButton: UIButton!
|
||||
|
||||
|
||||
var portraitButtonVisibleConstraints: [NSLayoutConstraint] = []
|
||||
var portraitButtonHiddenConstraints: [NSLayoutConstraint] = []
|
||||
|
|
@ -82,7 +81,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
var currentMarqueeConstraints: [NSLayoutConstraint] = []
|
||||
private var currentMenuButtonTrailing: NSLayoutConstraint!
|
||||
|
||||
|
||||
var subtitleForegroundColor: String = "white"
|
||||
var subtitleBackgroundEnabled: Bool = true
|
||||
var subtitleFontSize: Double = 20.0
|
||||
|
|
@ -177,7 +175,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
qualityButton,
|
||||
speedButton,
|
||||
watchNextButton,
|
||||
volumeSliderHostingView
|
||||
volumeSliderHostingView,
|
||||
pipButton,
|
||||
airplayButton
|
||||
].compactMap { $0 }
|
||||
|
||||
private var originalHiddenStates: [UIView: Bool] = [:]
|
||||
|
|
@ -422,23 +422,73 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
|
||||
deinit {
|
||||
playerRateObserver?.invalidate()
|
||||
inactivityTimer?.invalidate()
|
||||
inactivityTimer = nil
|
||||
updateTimer?.invalidate()
|
||||
updateTimer = nil
|
||||
lockButtonTimer?.invalidate()
|
||||
lockButtonTimer = nil
|
||||
dimButtonTimer?.invalidate()
|
||||
loadedTimeRangesObservation?.invalidate()
|
||||
playerTimeControlStatusObserver?.invalidate()
|
||||
volumeObserver?.invalidate()
|
||||
dimButtonTimer = nil
|
||||
|
||||
player.replaceCurrentItem(with: nil)
|
||||
player.pause()
|
||||
playerRateObserver?.invalidate()
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard context == &playerItemKVOContext else {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
|
|
@ -449,7 +499,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func playerItemDidChange() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
|
@ -1240,51 +1289,64 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
let pipPlayerLayer = AVPlayerLayer(player: playerViewController.player)
|
||||
pipPlayerLayer.frame = playerViewController.view.layer.bounds
|
||||
pipPlayerLayer.videoGravity = .resizeAspect
|
||||
|
||||
|
||||
playerViewController.view.layer.insertSublayer(pipPlayerLayer, at: 0)
|
||||
pipController = AVPictureInPictureController(playerLayer: pipPlayerLayer)
|
||||
pipController?.delegate = self
|
||||
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
|
||||
let Image = UIImage(systemName: "pip", withConfiguration: config)
|
||||
pipButton = UIButton(type: .system)
|
||||
pipButton.setImage(Image, for: .normal)
|
||||
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.shadowOpacity = 0.6
|
||||
pipButton.layer.shadowRadius = 4
|
||||
pipButton.layer.masksToBounds = false
|
||||
|
||||
controlsContainerView.addSubview(pipButton)
|
||||
pipButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// NEW: pin pipButton to the left of lockButton:
|
||||
NSLayoutConstraint.activate([
|
||||
pipButton.centerYAnchor.constraint(equalTo: dimButton.centerYAnchor),
|
||||
pipButton.trailingAnchor.constraint(equalTo: dimButton.leadingAnchor, constant: -8),
|
||||
pipButton.widthAnchor.constraint(equalToConstant: 44),
|
||||
pipButton.heightAnchor.constraint(equalToConstant: 44)
|
||||
])
|
||||
|
||||
pipButton.isHidden = !isPipButtonVisible
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(startPipIfNeeded),
|
||||
name: UIApplication.willResignActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
|
||||
let Image = UIImage(systemName: "pip", withConfiguration: config)
|
||||
pipButton = UIButton(type: .system)
|
||||
pipButton.setImage(Image, for: .normal)
|
||||
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.shadowOpacity = 0.6
|
||||
pipButton.layer.shadowRadius = 4
|
||||
pipButton.layer.masksToBounds = false
|
||||
|
||||
controlsContainerView.addSubview(pipButton)
|
||||
pipButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
pipButton.centerYAnchor.constraint(equalTo: dimButton.centerYAnchor),
|
||||
pipButton.trailingAnchor.constraint(equalTo: dimButton.leadingAnchor, constant: -8),
|
||||
pipButton.widthAnchor.constraint(equalToConstant: 44),
|
||||
pipButton.heightAnchor.constraint(equalToConstant: 44),
|
||||
airplayButton.centerYAnchor.constraint(equalTo: pipButton.centerYAnchor),
|
||||
airplayButton.trailingAnchor.constraint(equalTo: pipButton.leadingAnchor, constant: -8),
|
||||
airplayButton.widthAnchor.constraint(equalToConstant: 44),
|
||||
airplayButton.heightAnchor.constraint(equalToConstant: 44)
|
||||
])
|
||||
|
||||
pipButton.isHidden = !isPipButtonVisible
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(startPipIfNeeded),
|
||||
name: UIApplication.willResignActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
func setupMenuButton() {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
|
||||
|
|
@ -1356,7 +1418,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
watchNextButton.tintColor = .white
|
||||
watchNextButton.setTitleColor(.white, for: .normal)
|
||||
|
||||
// The shadow:
|
||||
watchNextButton.layer.shadowColor = UIColor.black.cgColor
|
||||
watchNextButton.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
watchNextButton.layer.shadowOpacity = 0.6
|
||||
|
|
@ -1460,9 +1521,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
func addTimeObserver() {
|
||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval,
|
||||
queue: .main)
|
||||
{ [weak self] time in
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
||||
guard let self = self,
|
||||
let currentItem = self.player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else { return }
|
||||
|
|
@ -1509,7 +1568,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
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 {
|
||||
let progress = min(max(self.currentTimeVal / self.duration, 0), 1.0)
|
||||
|
||||
|
|
@ -1530,7 +1590,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
ContinueWatchingManager.shared.save(item: item)
|
||||
}
|
||||
|
||||
|
||||
let remainingPercentage = (self.duration - self.currentTimeVal) / self.duration
|
||||
|
||||
if remainingPercentage < 0.1 &&
|
||||
|
|
@ -1592,7 +1651,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func startUpdateTimer() {
|
||||
updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
|
@ -1750,13 +1808,13 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
pip.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func startPipIfNeeded() {
|
||||
guard isPipAutoEnabled,
|
||||
let pip = pipController,
|
||||
!pip.isPictureInPictureActive else {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
pip.startPictureInPicture()
|
||||
}
|
||||
|
||||
|
|
@ -1800,7 +1858,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
updateSkipButtonsVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func skipIntro() {
|
||||
if let range = skipIntervals.op {
|
||||
player.seek(to: range.end)
|
||||
|
|
@ -1816,15 +1874,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
|
||||
@objc func dismissTapped() {
|
||||
dismiss(animated: true) { [weak self] in
|
||||
self?.detachedWindow = nil
|
||||
}
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func watchNextTapped() {
|
||||
player.pause()
|
||||
dismiss(animated: true) { [weak self] in
|
||||
self?.detachedWindow = nil
|
||||
self?.onWatchNext()
|
||||
}
|
||||
}
|
||||
|
|
@ -1849,19 +1904,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
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.lockButton.alpha = self.isDimmed ? 0 : 1
|
||||
|
||||
// switch subtitle constraints just like toggleControls()
|
||||
self.subtitleBottomToSafeAreaConstraint?.isActive = !self.isControlsVisible
|
||||
self.subtitleBottomToSliderConstraint?.isActive = self.isControlsVisible
|
||||
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
// slide the dim-icon over
|
||||
dimButtonToSlider.isActive = !isDimmed
|
||||
dimButtonToRight.isActive = isDimmed
|
||||
}
|
||||
|
|
@ -1881,17 +1933,17 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
private func tryAniListUpdate() {
|
||||
guard !aniListUpdatedSuccessfully else { return }
|
||||
|
||||
|
||||
guard aniListID > 0 else {
|
||||
Logger.shared.log("AniList ID is invalid, skipping update.", type: "Warning")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let client = AniListMutation()
|
||||
|
||||
|
||||
client.fetchMediaStatus(mediaId: aniListID) { [weak self] statusResult in
|
||||
guard let self = self else { return }
|
||||
|
||||
|
||||
let newStatus: String = {
|
||||
switch statusResult {
|
||||
case .success(let mediaStatus):
|
||||
|
|
@ -1899,7 +1951,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
return "CURRENT"
|
||||
}
|
||||
return (self.episodeNumber == self.totalEpisodes) ? "COMPLETED" : "CURRENT"
|
||||
|
||||
|
||||
case .failure(let error):
|
||||
Logger.shared.log(
|
||||
"Failed to fetch AniList status: \(error.localizedDescription). " +
|
||||
|
|
@ -1922,26 +1974,26 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
"AniList progress updated to \(newStatus) for ep \(self.episodeNumber)",
|
||||
type: "General"
|
||||
)
|
||||
|
||||
|
||||
case .failure(let error):
|
||||
let errorString = error.localizedDescription.lowercased()
|
||||
Logger.shared.log("AniList progress update failed: \(errorString)", type: "Error")
|
||||
|
||||
|
||||
if errorString.contains("access token not found") {
|
||||
Logger.shared.log("AniList update will NOT retry due to missing token.", type: "Error")
|
||||
self.aniListUpdateImpossible = true
|
||||
|
||||
|
||||
} else {
|
||||
if self.aniListRetryCount < self.aniListMaxRetries {
|
||||
self.aniListRetryCount += 1
|
||||
|
||||
|
||||
let delaySeconds = 5.0
|
||||
Logger.shared.log(
|
||||
"AniList update will retry in \(delaySeconds)s " +
|
||||
"(attempt \(self.aniListRetryCount)).",
|
||||
type: "Debug"
|
||||
)
|
||||
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) {
|
||||
self.tryAniListUpdate()
|
||||
}
|
||||
|
|
@ -1983,20 +2035,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
private func parseM3U8(url: URL, completion: @escaping () -> Void) {
|
||||
var request = URLRequest(url: url)
|
||||
if let mydict = headers, !mydict.isEmpty
|
||||
{
|
||||
for (key,value) in mydict
|
||||
{
|
||||
if let mydict = headers, !mydict.isEmpty {
|
||||
for (key,value) in mydict {
|
||||
request.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
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",
|
||||
forHTTPHeaderField: "User-Agent")
|
||||
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")
|
||||
|
||||
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
||||
guard let self = self,
|
||||
|
|
@ -2080,20 +2127,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
let wasPlaying = player.rate > 0
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if let mydict = headers, !mydict.isEmpty
|
||||
{
|
||||
for (key,value) in mydict
|
||||
{
|
||||
if let mydict = headers, !mydict.isEmpty {
|
||||
for (key,value) in mydict {
|
||||
request.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
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",
|
||||
forHTTPHeaderField: "User-Agent")
|
||||
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")
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
|
@ -2110,10 +2152,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
qualityButton.menu = qualitySelectionMenu()
|
||||
|
||||
if let selectedQuality = qualities.first(where: { $0.1 == urlString })?.0 {
|
||||
DropManager.shared.showDrop(title: "Quality: \(selectedQuality)",
|
||||
subtitle: "",
|
||||
duration: 0.5,
|
||||
icon: UIImage(systemName: "eye"))
|
||||
DropManager.shared.showDrop(title: "Quality: \(selectedQuality)", subtitle: "", duration: 0.5, icon: UIImage(systemName: "eye"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2156,8 +2195,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
private func checkForHLSStream() {
|
||||
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
|
||||
baseM3U8URL = url
|
||||
currentQualityURL = url
|
||||
|
|
@ -2487,9 +2527,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
switch gesture.state {
|
||||
case .ended:
|
||||
if translation.y > 100 {
|
||||
dismiss(animated: true) { [weak self] in
|
||||
self?.detachedWindow = nil
|
||||
}
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
|
@ -2619,9 +2657,7 @@ extension CustomMediaPlayerViewController: AVPictureInPictureControllerDelegate
|
|||
pipButton.alpha = 1.0
|
||||
}
|
||||
|
||||
func pictureInPictureController(_ pipController: AVPictureInPictureController,
|
||||
failedToStartPictureInPictureWithError error: Error) {
|
||||
|
||||
func pictureInPictureController(_ pipController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: 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
|
||||
// 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
|
||||
// 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 episodeImageUrl: String = ""
|
||||
var mediaTitle: String = ""
|
||||
var detachedWindow: UIWindow?
|
||||
|
||||
init(module: ScrapingModule) {
|
||||
self.module = module
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 01/02/25.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ModuleAdditionSettingsView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
|
@ -19,127 +19,197 @@ struct ModuleAdditionSettingsView: View {
|
|||
var moduleUrl: String
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollView {
|
||||
VStack {
|
||||
if let metadata = moduleMetadata {
|
||||
VStack(spacing: 25) {
|
||||
VStack(spacing: 15) {
|
||||
KFImage(URL(string: metadata.iconUrl))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(Circle())
|
||||
.shadow(radius: 5)
|
||||
.transition(.scale)
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
colorScheme == .light ? Color.black : Color.white,
|
||||
Color.accentColor.opacity(0.08)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
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)
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
VStack(spacing: 6) {
|
||||
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)
|
||||
}
|
||||
.padding(.top)
|
||||
|
||||
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)
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding(.top, 100)
|
||||
}
|
||||
|
||||
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: {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}) {
|
||||
Text("Cancel")
|
||||
.foregroundColor(colorScheme == .dark ? Color.white : Color.black)
|
||||
.padding(.top, 10)
|
||||
VStack(spacing: 10) {
|
||||
Button(action: addModule) {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
Text("Add Module")
|
||||
}
|
||||
.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)
|
||||
}
|
||||
|
||||
|
|
@ -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 value: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 8) {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.body)
|
||||
.font(.footnote.monospaced())
|
||||
.foregroundColor(.accentColor)
|
||||
.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)
|
||||
}
|
||||
|
||||
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 {
|
||||
for (index, module) in modules.enumerated() {
|
||||
do {
|
||||
|
|
@ -236,10 +230,8 @@ class ModuleManager: ObservableObject {
|
|||
isActive: module.isActive
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
self.modules[index] = updatedModule
|
||||
self.saveModules()
|
||||
}
|
||||
self.modules[index] = updatedModule
|
||||
self.saveModules()
|
||||
|
||||
Logger.shared.log("Updated module: \(module.metadata.sourceName) to version \(newMetadata.version)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
// Created by doomsboygaming on 5/22/25
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import Kingfisher
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
struct DownloadView: View {
|
||||
@EnvironmentObject var jsController: JSController
|
||||
|
|
@ -741,13 +741,16 @@ struct EnhancedActiveDownloadCard: View {
|
|||
HStack(spacing: 16) {
|
||||
Group {
|
||||
if let imageURL = download.imageURL {
|
||||
KFImage(imageURL)
|
||||
.placeholder {
|
||||
LazyImage(url: imageURL) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
}
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
|
|
@ -899,16 +902,18 @@ struct EnhancedDownloadGroupCard: View {
|
|||
NavigationLink(destination: EnhancedShowEpisodesView(group: group, onDelete: onDelete, onPlay: onPlay)) {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 16) {
|
||||
// Poster
|
||||
Group {
|
||||
if let posterURL = group.posterURL {
|
||||
KFImage(posterURL)
|
||||
.placeholder {
|
||||
LazyImage(url: posterURL) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
}
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
|
|
@ -921,7 +926,6 @@ struct EnhancedDownloadGroupCard: View {
|
|||
.frame(width: 56, height: 84)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
// Content
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(group.title)
|
||||
.font(.headline)
|
||||
|
|
@ -1000,18 +1004,20 @@ struct EnhancedShowEpisodesView: View {
|
|||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// Header Section
|
||||
VStack(spacing: 20) {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
Group {
|
||||
if let posterURL = group.posterURL {
|
||||
KFImage(posterURL)
|
||||
.placeholder {
|
||||
LazyImage(url: posterURL) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
}
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
|
|
@ -1192,16 +1198,18 @@ struct EnhancedEpisodeRow: View {
|
|||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 16) {
|
||||
// Thumbnail
|
||||
Group {
|
||||
if let backdropURL = asset.metadata?.backdropURL ?? asset.metadata?.posterURL {
|
||||
KFImage(backdropURL)
|
||||
.placeholder {
|
||||
LazyImage(url: backdropURL) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
}
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
|
|
@ -1214,7 +1222,6 @@ struct EnhancedEpisodeRow: View {
|
|||
.frame(width: 100, height: 60)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
// Content
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(asset.episodeDisplayName)
|
||||
.font(.headline)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
// Created by paul on 29/04/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func circularGradientOutlineTwo() -> some View {
|
||||
|
|
@ -59,28 +59,44 @@ struct BookmarkCell: View {
|
|||
var body: some View {
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
ZStack {
|
||||
KFImage(URL(string: bookmark.imageUrl))
|
||||
.resizable()
|
||||
.aspectRatio(0.72, contentMode: .fill)
|
||||
.frame(width: 162, height: 243)
|
||||
.cornerRadius(12)
|
||||
.clipped()
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
KFImage(URL(string: module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
)
|
||||
LazyImage(url: URL(string: bookmark.imageUrl)) { state in
|
||||
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)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 162, height: 243)
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
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 {
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
// Created by paul on 24/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func circularGradientOutline() -> some View {
|
||||
|
|
@ -206,75 +206,86 @@ struct FullWidthContinueWatchingCell: View {
|
|||
}) {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) { state in
|
||||
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)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(height: 157.03)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geometry.size.width, height: 157.03)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
.overlay(
|
||||
ZStack {
|
||||
ProgressiveBlurView()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
ProgressiveBlurView()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Spacer()
|
||||
Text(item.mediaTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Spacer()
|
||||
Text(item.mediaTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
HStack {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
HStack {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(Int(item.progress * 100))% seen")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Text("\(Int(item.progress * 100))% seen")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
.padding(10)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
}
|
||||
.padding(10)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.clipped()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
.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),
|
||||
alignment: .topLeading
|
||||
)
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(height: 157.03)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@
|
|||
// Created by paul on 28/05/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct BookmarksDetailView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@
|
|||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryView: View {
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
|
|
@ -165,6 +166,11 @@ struct LibraryView: View {
|
|||
.onAppear {
|
||||
fetchContinueWatching()
|
||||
}
|
||||
.onChange(of: scenePhase) { newPhase in
|
||||
if newPhase == .active {
|
||||
fetchContinueWatching()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
|
|
@ -237,8 +243,8 @@ struct ContinueWatchingCell: View {
|
|||
var markAsWatched: () -> Void
|
||||
var removeItem: () -> Void
|
||||
|
||||
@State private
|
||||
var currentProgress: Double = 0.0
|
||||
@State private var currentProgress: Double = 0.0
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
|
|
@ -280,85 +286,96 @@ struct ContinueWatchingCell: View {
|
|||
}
|
||||
}) {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) { state in
|
||||
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)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 280, height: 157.03)
|
||||
.shimmering()
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(16/9, contentMode: .fill)
|
||||
.frame(width: 280, height: 157.03)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
.overlay(
|
||||
ZStack {
|
||||
ProgressiveBlurView()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
ProgressiveBlurView()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Spacer()
|
||||
Text(item.mediaTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Spacer()
|
||||
Text(item.mediaTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
HStack {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
HStack {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(Int(item.progress * 100))% seen")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Text("\(Int(item.progress * 100))% seen")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
.padding(10)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.clipped()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
.shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1)
|
||||
}
|
||||
.padding(10)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
.overlay(
|
||||
ZStack {
|
||||
if item.streamUrl.hasPrefix("file://") {
|
||||
Image(systemName: "arrow.down.app.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.cornerRadius(6))
|
||||
.padding(8)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
},
|
||||
alignment: .topLeading
|
||||
)
|
||||
.clipped()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
.shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1)
|
||||
)
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
.overlay(
|
||||
ZStack {
|
||||
if item.streamUrl.hasPrefix("file://") {
|
||||
Image(systemName: "arrow.down.app.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(.white)
|
||||
.background(Color.black.cornerRadius(6))
|
||||
.padding(8)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(8)
|
||||
}
|
||||
},
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
.frame(width: 280, height: 157.03)
|
||||
}
|
||||
|
|
@ -377,11 +394,11 @@ struct ContinueWatchingCell: View {
|
|||
.onAppear {
|
||||
updateProgress()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(
|
||||
for: UIApplication.didBecomeActiveNotification)) {
|
||||
_ in
|
||||
.onChange(of: scenePhase) { newPhase in
|
||||
if newPhase == .active {
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateProgress() {
|
||||
|
|
@ -525,34 +542,45 @@ struct BookmarkItemView: View {
|
|||
isDetailActive = true
|
||||
}) {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.imageUrl))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: item.imageUrl)) { state in
|
||||
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)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.aspectRatio(2 / 3, contentMode: .fit)
|
||||
.shimmering()
|
||||
.aspectRatio(2/3, contentMode: .fit)
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(0.72, contentMode: .fill)
|
||||
.frame(width: 162, height: 243)
|
||||
.cornerRadius(12)
|
||||
.clipped()
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
KFImage(URL(string: module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
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 {
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by seiike on 01/06/2025.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct AnilistMatchPopupView: View {
|
||||
let seriesTitle: String
|
||||
|
|
@ -32,7 +32,6 @@ struct AnilistMatchPopupView: View {
|
|||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// (Optional) A hidden header; can be omitted if empty
|
||||
Text("".uppercased())
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
|
|
@ -62,11 +61,20 @@ struct AnilistMatchPopupView: View {
|
|||
HStack(spacing: 12) {
|
||||
if let cover = result["cover"] as? String,
|
||||
let url = URL(string: cover) {
|
||||
KFImage(url)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 50, height: 70)
|
||||
.cornerRadius(6)
|
||||
LazyImage(url: url) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.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) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import AVFoundation
|
||||
|
||||
struct EpisodeCell: View {
|
||||
|
|
@ -264,14 +264,28 @@ struct EpisodeCell: View {
|
|||
private var episodeThumbnail: some View {
|
||||
ZStack {
|
||||
if let url = URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl) {
|
||||
KFImage(url)
|
||||
.onFailure { error in
|
||||
Logger.shared.log("Failed to load episode image: \(error)", type: "Error")
|
||||
LazyImage(url: url) { state in
|
||||
if let image = state.imageContainer?.image {
|
||||
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 {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SafariServices
|
||||
|
||||
private let tmdbFetcher = TMDBFetcher()
|
||||
|
|
@ -60,7 +60,7 @@ struct MediaInfoView: View {
|
|||
@State private var isMatchingPresented = false
|
||||
@State private var matchedTitle: String? = nil
|
||||
|
||||
@StateObject private var jsController = JSController.shared
|
||||
@ObservedObject private var jsController = JSController.shared
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject var tabBarController: TabBarController
|
||||
|
|
@ -85,7 +85,6 @@ struct MediaInfoView: View {
|
|||
@State private var isBulkDownloading: Bool = false
|
||||
@State private var bulkDownloadProgress: String = ""
|
||||
@State private var tmdbType: TMDBFetcher.MediaType? = nil
|
||||
@State private var latestProgress: Double = 0.0
|
||||
|
||||
private var isGroupedBySeasons: Bool {
|
||||
return groupedEpisodes().count > 1
|
||||
|
|
@ -123,7 +122,6 @@ struct MediaInfoView: View {
|
|||
.navigationBarHidden(true)
|
||||
.ignoresSafeArea(.container, edges: .top)
|
||||
.onAppear {
|
||||
updateLatestProgress()
|
||||
buttonRefreshTrigger.toggle()
|
||||
|
||||
let savedID = UserDefaults.standard.integer(forKey: "custom_anilist_id_\(href)")
|
||||
|
|
@ -168,6 +166,34 @@ struct MediaInfoView: View {
|
|||
.onDisappear(){
|
||||
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) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
activeFetchID = nil
|
||||
|
|
@ -214,54 +240,25 @@ struct MediaInfoView: View {
|
|||
private var mainScrollView: some View {
|
||||
ScrollView {
|
||||
ZStack(alignment: .top) {
|
||||
KFImage(URL(string: imageUrl))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: UIScreen.main.bounds.width, height: 700)
|
||||
.clipped()
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.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) {
|
||||
Rectangle()
|
||||
.fill(Color.clear)
|
||||
.frame(height: 450)
|
||||
.frame(height: 400)
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
headerSection
|
||||
if !episodeLinks.isEmpty {
|
||||
|
|
@ -275,15 +272,15 @@ struct MediaInfoView: View {
|
|||
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.3), location: 0.1),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.6), location: 0.3),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.9), location: 0.7),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.5), location: 0.2),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.8), location: 0.5),
|
||||
.init(color: (colorScheme == .dark ? Color.black : Color.white), location: 1.0)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.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()
|
||||
|
|
@ -359,12 +356,10 @@ struct MediaInfoView: View {
|
|||
UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(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"))
|
||||
updateLatestProgress()
|
||||
} else {
|
||||
UserDefaults.standard.set(0.0, forKey: "lastPlayedTime_\(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"))
|
||||
updateLatestProgress()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
|
|
@ -587,34 +582,25 @@ struct MediaInfoView: View {
|
|||
@ViewBuilder
|
||||
private var playAndBookmarkSection: some View {
|
||||
HStack(spacing: 12) {
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(Color.accentColor)
|
||||
.frame(height: 48)
|
||||
|
||||
Button(action: {
|
||||
playFirstUnwatchedEpisode()
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
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))
|
||||
Button(action: {
|
||||
playFirstUnwatchedEpisode()
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "play.fill")
|
||||
.foregroundColor(colorScheme == .dark ? .black : .white)
|
||||
Text(startWatchingText)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(colorScheme == .dark ? .black : .white)
|
||||
}
|
||||
.disabled(isFetchingEpisode)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(Color.accentColor)
|
||||
)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 25))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.stroke(Color.accentColor, lineWidth: 0)
|
||||
)
|
||||
.disabled(isFetchingEpisode)
|
||||
|
||||
Button(action: {
|
||||
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
|
||||
private var noEpisodesSection: some View {
|
||||
VStack(spacing: 8) {
|
||||
|
|
@ -1000,46 +974,53 @@ struct MediaInfoView: View {
|
|||
.padding(.vertical, 50)
|
||||
}
|
||||
|
||||
private var continueWatchingText: String {
|
||||
for ep in episodeLinks {
|
||||
let last = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
||||
let total = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||
let progress = total > 0 ? last / total : 0
|
||||
|
||||
if progress > 0 && progress < 0.9 {
|
||||
return "Continue Watching Episode \(ep.number)"
|
||||
private var startWatchingText: String {
|
||||
let indices = finishedAndUnfinishedIndices()
|
||||
let finished = indices.finished
|
||||
let unfinished = indices.unfinished
|
||||
|
||||
if episodeLinks.count == 1 {
|
||||
if let unfinishedIndex = unfinished {
|
||||
return "Continue Watching"
|
||||
}
|
||||
return "Start Watching"
|
||||
}
|
||||
|
||||
for ep in episodeLinks {
|
||||
let last = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
||||
let total = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||
let progress = total > 0 ? last / total : 0
|
||||
|
||||
if progress < 0.9 {
|
||||
return "Start Watching Episode \(ep.number)"
|
||||
}
|
||||
if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 {
|
||||
let nextEp = episodeLinks[finishedIndex + 1]
|
||||
return "Start Watching Episode \(nextEp.number)"
|
||||
}
|
||||
|
||||
if let unfinishedIndex = unfinished {
|
||||
let currentEp = episodeLinks[unfinishedIndex]
|
||||
return "Continue Watching Episode \(currentEp.number)"
|
||||
}
|
||||
|
||||
return "Start Watching"
|
||||
}
|
||||
|
||||
private func playFirstUnwatchedEpisode() {
|
||||
for ep in episodeLinks {
|
||||
let last = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
||||
let total = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||
let progress = total > 0 ? last / total : 0
|
||||
|
||||
if progress < 0.9 {
|
||||
selectedEpisodeNumber = ep.number
|
||||
fetchStream(href: ep.href)
|
||||
return
|
||||
}
|
||||
let indices = finishedAndUnfinishedIndices()
|
||||
let finished = indices.finished
|
||||
let unfinished = indices.unfinished
|
||||
|
||||
if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 {
|
||||
let nextEp = episodeLinks[finishedIndex + 1]
|
||||
selectedEpisodeNumber = nextEp.number
|
||||
fetchStream(href: nextEp.href)
|
||||
return
|
||||
}
|
||||
|
||||
if let first = episodeLinks.first {
|
||||
selectedEpisodeNumber = first.number
|
||||
fetchStream(href: first.href)
|
||||
if let unfinishedIndex = unfinished {
|
||||
let ep = episodeLinks[unfinishedIndex]
|
||||
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.subtitles = subtitles ?? ""
|
||||
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
|
||||
default:
|
||||
break
|
||||
|
|
@ -1384,10 +1368,16 @@ struct MediaInfoView: View {
|
|||
episodeImageUrl: selectedEpisodeImage,
|
||||
headers: headers ?? nil
|
||||
)
|
||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||
Logger.shared.log("Opening custom media player with stream URL: \(url), and subtitles URL: \(String(describing: subtitles))", type: "Stream")
|
||||
customMediaPlayer.modalPresentationStyle = .overFullScreen
|
||||
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()
|
||||
}
|
||||
|
||||
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 Kingfisher
|
||||
|
||||
struct SearchItem: Identifiable {
|
||||
let id = UUID()
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by paul on 28/05/25.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct SearchResultsGrid: View {
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
|
|
@ -32,12 +32,22 @@ struct SearchResultsGrid: View {
|
|||
ForEach(items) { item in
|
||||
NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule)) {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.imageUrl))
|
||||
.resizable()
|
||||
.aspectRatio(0.72, contentMode: .fill)
|
||||
.frame(width: cellWidth, height: cellWidth * 1.5)
|
||||
.cornerRadius(12)
|
||||
.clipped()
|
||||
LazyImage(url: URL(string: item.imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(0.72, contentMode: .fill)
|
||||
.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 {
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ModuleButtonModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 27/01/25.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ModuleSelectorMenu: View {
|
||||
let selectedModule: ScrapingModule?
|
||||
|
|
@ -27,11 +27,19 @@ struct ModuleSelectorMenu: View {
|
|||
onModuleSelected(module.id.uuidString)
|
||||
} label: {
|
||||
HStack {
|
||||
KFImage(URL(string: module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
.cornerRadius(4)
|
||||
LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
.cornerRadius(4)
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color(.systemGray5))
|
||||
}
|
||||
}
|
||||
|
||||
Text(module.metadata.sourceName)
|
||||
if module.id.uuidString == selectedModuleId {
|
||||
Image(systemName: "checkmark")
|
||||
|
|
@ -48,29 +56,37 @@ struct ModuleSelectorMenu: View {
|
|||
Text(selectedModule.metadata.sourceName)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
KFImage(URL(string: selectedModule.metadata.iconUrl))
|
||||
.resizable()
|
||||
.frame(width: 36, height: 36)
|
||||
.clipShape(Circle())
|
||||
.background(
|
||||
LazyImage(url: URL(string: selectedModule.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.frame(width: 36, height: 36)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.accentColor.opacity(gradientOpacity), location: 0),
|
||||
.init(color: Color.accentColor.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 0.5
|
||||
)
|
||||
)
|
||||
.matchedGeometryEffect(id: "background_circle", in: animation)
|
||||
)
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
Circle()
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.accentColor.opacity(gradientOpacity), location: 0),
|
||||
.init(color: Color.accentColor.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 0.5
|
||||
)
|
||||
)
|
||||
.matchedGeometryEffect(id: "background_circle", in: animation)
|
||||
)
|
||||
} else {
|
||||
Text("Select Module")
|
||||
.font(.headline)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 26/05/25.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
fileprivate struct SettingsSection<Content: View>: View {
|
||||
let title: String
|
||||
|
|
@ -66,14 +66,18 @@ struct SettingsViewAbout: View {
|
|||
VStack(spacing: 24) {
|
||||
SettingsSection(title: "App Info", footer: "Sora/Sulfur will always remain free with no ADs!") {
|
||||
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"))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/Sora/Assets.xcassets/AppIcons/AppIcon_Default.appiconset/darkmode.png")) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(20)
|
||||
.shadow(radius: 5)
|
||||
} else {
|
||||
ProgressView()
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(20)
|
||||
.shadow(radius: 5)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Sora")
|
||||
|
|
@ -96,13 +100,17 @@ struct SettingsViewAbout: View {
|
|||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/100066266?v=4"))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: "https://avatars.githubusercontent.com/u/100066266?v=4")) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
ProgressView()
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("cranci1")
|
||||
|
|
@ -205,13 +213,17 @@ struct ContributorView: View {
|
|||
}
|
||||
}) {
|
||||
HStack {
|
||||
KFImage(URL(string: contributor.avatarUrl))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: contributor.avatarUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
ProgressView()
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
Text(contributor.login)
|
||||
.font(.headline)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
fileprivate struct SettingsSection<Content: View>: View {
|
||||
let title: String
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
fileprivate struct SettingsSection<Content: View>: View {
|
||||
let title: String
|
||||
|
|
@ -67,11 +67,19 @@ fileprivate struct ModuleListItemView: View {
|
|||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
KFImage(URL(string: module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
.padding(.trailing, 10)
|
||||
LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.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) {
|
||||
HStack(alignment: .bottom, spacing: 4) {
|
||||
|
|
@ -211,7 +219,7 @@ struct SettingsViewModule: View {
|
|||
.navigationTitle("Modules")
|
||||
.navigationBarItems(trailing:
|
||||
HStack(spacing: 16) {
|
||||
if didReceiveDefaultPageLink && !moduleManager.modules.isEmpty {
|
||||
if didReceiveDefaultPageLink {
|
||||
Button(action: {
|
||||
showLibrary = true
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
// Created by Francesco on 23/03/25.
|
||||
//
|
||||
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Security
|
||||
import Kingfisher
|
||||
|
||||
fileprivate struct SettingsSection<Content: View>: View {
|
||||
let title: String
|
||||
|
|
@ -120,18 +120,21 @@ struct SettingsViewTrackers: View {
|
|||
SettingsSection(title: "AniList") {
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png"))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) { state in
|
||||
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)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 60, height: 60)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.clipShape(Rectangle())
|
||||
.cornerRadius(10)
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("AniList.co")
|
||||
|
|
@ -212,18 +215,21 @@ struct SettingsViewTrackers: View {
|
|||
SettingsSection(title: "Trakt") {
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
KFImage(URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png"))
|
||||
.placeholder {
|
||||
LazyImage(url: URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png")) { state in
|
||||
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)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 60, height: 60)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.clipShape(Rectangle())
|
||||
.cornerRadius(10)
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Trakt.tv")
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@
|
|||
1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; };
|
||||
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.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 */; };
|
||||
13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */; };
|
||||
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 */; };
|
||||
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.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 */; };
|
||||
13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; };
|
||||
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; };
|
||||
|
|
@ -191,9 +192,10 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13637B8D2DE0ECCC00BDA2FC /* Kingfisher in Frameworks */,
|
||||
13AF34B62DF6CB5900C77880 /* NukeUI in Frameworks */,
|
||||
13637B902DE0ECD200BDA2FC /* Drops in Frameworks */,
|
||||
13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */,
|
||||
13AF34B42DF6CB5900C77880 /* Nuke in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -614,9 +616,10 @@
|
|||
);
|
||||
name = Sulfur;
|
||||
packageProductDependencies = (
|
||||
13637B8C2DE0ECCC00BDA2FC /* Kingfisher */,
|
||||
13637B8F2DE0ECD200BDA2FC /* Drops */,
|
||||
13637B922DE0ECDB00BDA2FC /* MarqueeLabel */,
|
||||
13AF34B32DF6CB5900C77880 /* Nuke */,
|
||||
13AF34B52DF6CB5900C77880 /* NukeUI */,
|
||||
);
|
||||
productName = Sora;
|
||||
productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */;
|
||||
|
|
@ -643,12 +646,13 @@
|
|||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 133D7C612D2BE2500075467E;
|
||||
packageReferences = (
|
||||
13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */,
|
||||
13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */,
|
||||
13AF34B22DF6CB5900C77880 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
);
|
||||
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -920,8 +924,9 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
|
|
@ -962,8 +967,9 @@
|
|||
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
|
|
@ -994,14 +1000,6 @@
|
|||
/* End XCConfigurationList 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" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/omaralbeik/Drops.git";
|
||||
|
|
@ -1014,18 +1012,21 @@
|
|||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/cbpowell/MarqueeLabel";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 4.2.1;
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
13AF34B22DF6CB5900C77880 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kean/Nuke.git";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
13637B8C2DE0ECCC00BDA2FC /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
13637B8F2DE0ECD200BDA2FC /* Drops */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */;
|
||||
|
|
@ -1036,6 +1037,16 @@
|
|||
package = 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "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 */
|
||||
};
|
||||
rootObject = 133D7C622D2BE2500075467E /* Project object */;
|
||||
|
|
|
|||
|
|
@ -1,34 +1,32 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Drops",
|
||||
"repositoryURL": "https://github.com/omaralbeik/Drops.git",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"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"
|
||||
}
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "drops",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/omaralbeik/Drops.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "5824681795286c36bdc4a493081a63e64e2a064e"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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