testflgith (#162)

* Main (#158)

* dwo (#132)

* bug fixes  (#127)

* yeah @realdoomsboygaming fault

* fixes

* freaky ahh update

---------

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

* opds

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

* yeah nice xcode

* yeah fuck ts

* yeah

* ok this shit is really fucked then

---------

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

* Yeah idk what i am even doing at this point

* opsi

* would this wrk?

* test UI mode

* should be better now

* my bad sorru

* ok should be good now + better quality parsesr

* ohhhh i forgot my bad

* ok now its fixed

* who know if this works 😭

* ohhh yeah my bad

* ok should work now

* brooo come one 😭

* who tf does this work 😭

* ok yeah im done bro 😭

* oh yeah my bad ok

* audio track please work holy moly

* ok yeah no audio for now

* ok please this time audio should work

* Revert "ok please this time audio should work"

This reverts commit a14d7db5ea.

* d

* Updated Dark and Light mode thumbnails  (#159)

* Update CustomPlayer.swift

* Update README.md

* boom shakalaka (#160)

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

same with seasons

* fixed memory leak from urldelegate

---------

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

* yeah idk tf is ts 😭

* migrated to NukeUI from KingFisher

* why was it even imported 😭

* Update README.md

* POP THE CHAMPAGNE 🍾 (#161)

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

* updated Nuke to branch + adjusted some stuffs

* yeah this is not needed

* Update README.md

---------

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

View file

@ -64,13 +64,13 @@ Sora does not include any modules by default. You will need to find and add the
## Acknowledgements
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

View file

@ -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])

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)")
}

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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) {

View file

@ -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))

View file

@ -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)
}
}

View file

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

View file

@ -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()

View file

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

View file

@ -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)

View file

@ -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)

View file

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

View file

@ -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
}) {

View file

@ -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")

View file

@ -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 */;

View file

@ -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