Added downlaods

This commit is contained in:
Francesco 2025-05-15 18:45:38 +02:00
parent e1a671390c
commit e875fead9b
11 changed files with 465 additions and 119 deletions

View file

@ -6,7 +6,6 @@
//
import SwiftUI
import Kingfisher
struct ContentView: View {
var body: some View {
@ -15,6 +14,10 @@ struct ContentView: View {
.tabItem {
Label("Library", systemImage: "books.vertical")
}
DownloadView()
.tabItem {
Label("Downloads", systemImage: "arrow.down.app.fill")
}
SearchView()
.tabItem {
Label("Search", systemImage: "magnifyingglass")

View file

@ -12,7 +12,8 @@ struct SoraApp: App {
@StateObject private var settings = Settings()
@StateObject private var moduleManager = ModuleManager()
@StateObject private var librarykManager = LibraryManager()
@StateObject private var downloadManager = DownloadManager()
init() {
if let userAccentColor = UserDefaults.standard.color(forKey: "accentColor") {
UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = userAccentColor
@ -26,13 +27,14 @@ struct SoraApp: App {
}
}
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(moduleManager)
.environmentObject(settings)
.environmentObject(librarykManager)
.environmentObject(downloadManager)
.accentColor(settings.accentColor)
.onAppear {
settings.updateAppearance()
@ -51,7 +53,7 @@ struct SoraApp: App {
}
}
}
private func handleURL(_ url: URL) {
guard url.scheme == "sora", let host = url.host else { return }
switch host {
@ -61,9 +63,8 @@ struct SoraApp: App {
UserDefaults.standard.set(libraryURL, forKey: "lastCommunityURL")
UserDefaults.standard.set(true, forKey: "didReceiveDefaultPageLink")
let communityView = CommunityLibraryView()
.environmentObject(moduleManager)
let communityView = CommunityLibraryView().environmentObject(moduleManager)
let hostingController = UIHostingController(rootView: communityView)
DispatchQueue.main.async {
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
@ -89,11 +90,10 @@ struct SoraApp: App {
else {
return
}
let addModuleView = ModuleAdditionSettingsView(moduleUrl: moduleURL)
.environmentObject(moduleManager)
let addModuleView = ModuleAdditionSettingsView(moduleUrl: moduleURL).environmentObject(moduleManager)
let hostingController = UIHostingController(rootView: addModuleView)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
window.rootViewController?.present(hostingController, animated: true)
@ -103,19 +103,19 @@ struct SoraApp: App {
type: "Error"
)
}
default:
break
}
}
static func handleRedirect(url: URL) {
guard let params = url.queryParameters,
let code = params["code"] else {
Logger.shared.log("Failed to extract authorization code")
return
}
Logger.shared.log("Failed to extract authorization code")
return
}
switch url.host {
case "anilist":
AniListToken.exchangeAuthorizationCodeForToken(code: code) { success in
@ -125,7 +125,7 @@ struct SoraApp: App {
Logger.shared.log("AniList token exchange failed", type: "Error")
}
}
case "trakt":
TraktToken.exchangeAuthorizationCodeForToken(code: code) { success in
if success {
@ -134,7 +134,7 @@ struct SoraApp: App {
Logger.shared.log("Trakt token exchange failed", type: "Error")
}
}
default:
Logger.shared.log("Unknown authentication service", type: "Error")
}

View file

@ -7,23 +7,54 @@
import SwiftUI
import AVKit
import Foundation
import AVFoundation
struct DownloadedAsset: Identifiable, Codable {
let id: UUID
var name: String
let downloadDate: Date
let originalURL: URL
let localURL: URL
var fileSize: Int64?
let module: ScrapingModule
init(id: UUID = UUID(), name: String, downloadDate: Date, originalURL: URL, localURL: URL, module: ScrapingModule) {
self.id = id
self.name = name
self.downloadDate = downloadDate
self.originalURL = originalURL
self.localURL = localURL
self.module = module
self.fileSize = getFileSize()
}
func getFileSize() -> Int64? {
do {
let values = try localURL.resourceValues(forKeys: [.fileSizeKey])
return Int64(values.fileSize ?? 0)
} catch {
return nil
}
}
}
class DownloadManager: NSObject, ObservableObject {
@Published var activeDownloads: [(URL, Double)] = []
@Published var localPlaybackURL: URL?
@Published var activeDownloads: [ActiveDownload] = []
@Published var savedAssets: [DownloadedAsset] = []
private var assetDownloadURLSession: AVAssetDownloadURLSession!
private var activeDownloadTasks: [URLSessionTask: URL] = [:]
private var activeDownloadTasks: [URLSessionTask: (URL, ScrapingModule)] = [:]
override init() {
super.init()
initializeDownloadSession()
loadLocalContent()
loadSavedAssets()
reconcileFileSystemAssets()
}
private func initializeDownloadSession() {
let configuration = URLSessionConfiguration.background(withIdentifier: "hls-downloader")
let configuration = URLSessionConfiguration.background(withIdentifier: "downloader-\(UUID().uuidString)")
assetDownloadURLSession = AVAssetDownloadURLSession(
configuration: configuration,
assetDownloadDelegate: self,
@ -31,65 +62,170 @@ class DownloadManager: NSObject, ObservableObject {
)
}
func downloadAsset(from url: URL) {
let asset = AVURLAsset(url: url)
func downloadAsset(from url: URL, module: ScrapingModule, headers: [String: String]? = nil) {
var urlRequest = URLRequest(url: url)
if let headers = headers {
for (key, value) in headers {
urlRequest.addValue(value, forHTTPHeaderField: key)
}
} else if headers == nil {
urlRequest.addValue(module.metadata.baseUrl, forHTTPHeaderField: "Origin")
urlRequest.addValue(module.metadata.baseUrl, forHTTPHeaderField: "Referer")
}
urlRequest.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
let asset = AVURLAsset(url: urlRequest.url!, options: ["AVURLAssetHTTPHeaderFieldsKey": urlRequest.allHTTPHeaderFields ?? [:]])
let task = assetDownloadURLSession.makeAssetDownloadTask(
asset: asset,
assetTitle: "Offline Video",
assetTitle: url.lastPathComponent,
assetArtworkData: nil,
options: nil
options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 2_000_000]
)
let download = ActiveDownload(
id: UUID(),
originalURL: url,
progress: 0,
task: task!
)
activeDownloads.append(download)
activeDownloadTasks[task!] = (url, module)
task?.resume()
activeDownloadTasks[task!] = url
}
private func loadLocalContent() {
func deleteAsset(_ asset: DownloadedAsset) {
do {
try FileManager.default.removeItem(at: asset.localURL)
savedAssets.removeAll { $0.id == asset.id }
saveAssets()
} catch {
Logger.shared.log("Error deleting asset: \(error)")
}
}
func renameAsset(_ asset: DownloadedAsset, newName: String) {
guard let index = savedAssets.firstIndex(where: { $0.id == asset.id }) else { return }
savedAssets[index].name = newName
saveAssets()
}
private func saveAssets() {
do {
let data = try JSONEncoder().encode(savedAssets)
UserDefaults.standard.set(data, forKey: "savedAssets")
} catch {
Logger.shared.log("Error saving assets: \(error)")
}
}
private func loadSavedAssets() {
guard let data = UserDefaults.standard.data(forKey: "savedAssets") else { return }
do {
savedAssets = try JSONDecoder().decode([DownloadedAsset].self, from: data)
} catch {
Logger.shared.log("Error loading saved assets: \(error)")
}
}
private func reconcileFileSystemAssets() {
guard let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
do {
let contents = try FileManager.default.contentsOfDirectory(
let fileURLs = try FileManager.default.contentsOfDirectory(
at: documents,
includingPropertiesForKeys: nil,
includingPropertiesForKeys: [.creationDateKey, .fileSizeKey],
options: .skipsHiddenFiles
)
if let localURL = contents.first(where: { $0.pathExtension == "movpkg" }) {
localPlaybackURL = localURL
}
} catch {
print("Error loading local content: \(error)")
Logger.shared.log("Error reconciling files: \(error)")
}
}
}
extension DownloadManager: AVAssetDownloadDelegate {
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
activeDownloadTasks.removeValue(forKey: assetDownloadTask)
localPlaybackURL = location
guard let (originalURL, module) = activeDownloadTasks[assetDownloadTask] else { return }
let newAsset = DownloadedAsset(
name: originalURL.lastPathComponent,
downloadDate: Date(),
originalURL: originalURL,
localURL: location,
module: module
)
savedAssets.append(newAsset)
saveAssets()
cleanupDownloadTask(assetDownloadTask)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let error = error else { return }
print("Download error: \(error.localizedDescription)")
activeDownloadTasks.removeValue(forKey: task)
Logger.shared.log("Download error: \(error.localizedDescription)")
cleanupDownloadTask(task)
}
func urlSession(_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didLoad timeRange: CMTimeRange,
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
timeRangeExpectedToLoad: CMTimeRange) {
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
guard let (originalURL, _) = activeDownloadTasks[assetDownloadTask],
let downloadIndex = activeDownloads.firstIndex(where: { $0.originalURL == originalURL }) else { return }
guard let url = activeDownloadTasks[assetDownloadTask] else { return }
let progress = loadedTimeRanges
.map { $0.timeRangeValue.duration.seconds / timeRangeExpectedToLoad.duration.seconds }
.reduce(0, +)
if let index = activeDownloads.firstIndex(where: { $0.0 == url }) {
activeDownloads[index].1 = progress
} else {
activeDownloads.append((url, progress))
activeDownloads[downloadIndex].progress = progress
}
private func cleanupDownloadTask(_ task: URLSessionTask) {
activeDownloadTasks.removeValue(forKey: task)
activeDownloads.removeAll { $0.task == task }
}
}
struct DownloadProgressView: View {
let download: ActiveDownload
var body: some View {
VStack(alignment: .leading) {
Text(download.originalURL.lastPathComponent)
.font(.subheadline)
ProgressView(value: download.progress)
.progressViewStyle(LinearProgressViewStyle())
Text("\(Int(download.progress * 100))%")
.font(.caption)
}
}
}
struct AssetRowView: View {
let asset: DownloadedAsset
var body: some View {
VStack(alignment: .leading) {
Text(asset.name)
.font(.headline)
Text("\(asset.fileSize ?? 0) bytes • \(asset.downloadDate.formatted())")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
struct ActiveDownload: Identifiable {
let id: UUID
let originalURL: URL
var progress: Double
let task: URLSessionTask
}
extension URL {
static func isValidHLSURL(string: String) -> Bool {
guard let url = URL(string: string), url.pathExtension == "m3u8" else { return false }
return true
}
}

View file

@ -6,22 +6,22 @@
//
import Foundation
// URL DELEGATE CLASS FOR FETCH API
class FetchDelegate: NSObject, URLSessionTaskDelegate
{
private let allowRedirects: Bool
init(allowRedirects: Bool) {
self.allowRedirects = allowRedirects
}
// This handles the redirection and prevents it.
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
if(allowRedirects)
{
completionHandler(request) // Allow Redirect
completionHandler(request)
}
else
{
completionHandler(nil) // Block Redirect
completionHandler(nil)
}
}
@ -63,7 +63,7 @@ extension URLSession {
configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent]
return URLSession(configuration: configuration)
}()
// return url session that redirects based on input
static func fetchData(allowRedirects:Bool) -> URLSession
{
let delegate = FetchDelegate(allowRedirects:allowRedirects)
@ -72,4 +72,3 @@ extension URLSession {
return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
}
}

View file

@ -36,7 +36,6 @@ extension JSController {
if let data = resultString.data(using: .utf8) {
do {
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("JSON DATA IS \(json) 2")
var streamUrls: [String]? = nil
var subtitleUrls: [String]? = nil
var streamUrlsAndHeaders : [[String:Any]]? = nil
@ -117,7 +116,6 @@ extension JSController {
let data = jsonString.data(using: .utf8) {
do {
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("JSON object is \(json) 1")
var streamUrls: [String]? = nil
var subtitleUrls: [String]? = nil
var streamUrlsAndHeaders : [[String:Any]]? = nil
@ -227,7 +225,6 @@ extension JSController {
let data = jsonString.data(using: .utf8) {
do {
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("JSON object is \(json) 3 ")
var streamUrls: [String]? = nil
var subtitleUrls: [String]? = nil
var streamUrlsAndHeaders : [[String:Any]]? = nil

View file

@ -301,9 +301,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
#if os(iOS) && !targetEnvironment(macCatalyst)
if #available(iOS 16.0, *) {
playerViewController.allowsVideoFrameAnalysis = false
}
#endif
if let url = subtitlesURL, !url.isEmpty {

View file

@ -5,43 +5,206 @@
// Created by Francesco on 29/04/25.
//
import SwiftUI
import AVKit
import SwiftUI
struct EmptyStateView: View {
let title: String
let systemImage: String
let description: String
var body: some View {
VStack(spacing: 16) {
Image(systemName: systemImage)
.font(.system(size: 50))
.foregroundColor(.secondary)
Text(title)
.font(.title2)
.fontWeight(.medium)
Text(description)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct DownloadView: View {
@StateObject private var viewModel = DownloadManager()
@State private var hlsURL = "https://test-streams.mux.dev/x36xhzz/url_6/193039199_mp4_h264_aac_hq_7.m3u8"
@EnvironmentObject private var downloadManager: DownloadManager
@State private var selectedAsset: DownloadedAsset?
@State private var showingDeleteAlert = false
@State private var showingRenameAlert = false
@State private var renameText = ""
@State private var selectedSegment = 0
@State private var player: AVPlayer?
@AppStorage("defaultPlayer") private var defaultPlayer: String = "Default"
enum Tab: String, CaseIterable {
case active = "Active"
case completed = "Completed"
}
var body: some View {
NavigationView {
VStack {
TextField("Enter HLS URL", text: $hlsURL)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button("Download Stream") {
viewModel.downloadAsset(from: URL(string: hlsURL)!)
}
.padding()
List(viewModel.activeDownloads, id: \.0) { (url, progress) in
VStack(alignment: .leading) {
Text(url.absoluteString)
ProgressView(value: progress)
.progressViewStyle(LinearProgressViewStyle())
Picker("Downloads", selection: $selectedSegment) {
ForEach(0..<Tab.allCases.count, id: \.self) { index in
Text(Tab.allCases[index].rawValue)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
NavigationLink("Play Offline Content") {
if let url = viewModel.localPlaybackURL {
VideoPlayer(player: AVPlayer(url: url))
if selectedSegment == 0 {
if downloadManager.activeDownloads.isEmpty {
EmptyStateView(
title: "No Active Downloads",
systemImage: "arrow.down.circle",
description: "Downloads in progress will appear here"
)
} else {
Text("No offline content available")
List {
ForEach(downloadManager.activeDownloads) { download in
VStack(alignment: .leading, spacing: 8) {
Text(download.originalURL.lastPathComponent)
.font(.subheadline)
.lineLimit(1)
ProgressView(value: download.progress) {
Text("\(Int(download.progress * 100))%")
.font(.caption2)
}
}
.padding(.vertical, 4)
}
}
}
} else {
if downloadManager.savedAssets.isEmpty {
EmptyStateView(
title: "No Completed Downloads",
systemImage: "arrow.down.circle",
description: "Completed Downloads will appear here"
)
} else {
List {
ForEach(downloadManager.savedAssets) { asset in
Button(action: { playAsset(asset) }) {
HStack {
VStack(alignment: .leading) {
Text(asset.name)
.font(.headline)
.lineLimit(1)
if let size = asset.fileSize {
Text("\(formatFileSize(size))\(formatDate(asset.downloadDate))")
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
Image(systemName: "play.circle.fill")
.font(.title2)
.foregroundColor(.accentColor)
}
}
.contextMenu {
Button(action: {
renameText = asset.name
selectedAsset = asset
showingRenameAlert = true
}) {
Label("Rename", systemImage: "pencil")
}
Button(role: .destructive, action: {
selectedAsset = asset
showingDeleteAlert = true
}) {
Label("Delete", systemImage: "trash")
}
}
}
}
}
}
.padding()
}
.navigationTitle("HLS Downloader")
.navigationTitle("Downloads")
.alert("Delete Download", isPresented: $showingDeleteAlert) {
Button("Cancel", role: .cancel) { }
Button("Delete", role: .destructive) {
if let asset = selectedAsset {
downloadManager.deleteAsset(asset)
selectedAsset = nil
}
}
} message: {
Text("Are you sure you want to delete '\(selectedAsset?.name ?? "")'?")
}
.alert("Rename Download", isPresented: $showingRenameAlert) {
TextField("Name", text: $renameText)
Button("Cancel", role: .cancel) {
showingRenameAlert = false
}
Button("Save") {
if let asset = selectedAsset {
downloadManager.renameAsset(asset, newName: renameText)
}
showingRenameAlert = false
}
}
}
}
private func playAsset(_ asset: DownloadedAsset) {
if defaultPlayer == "Default" {
let playerVC = VideoPlayerViewController(module: asset.module)
playerVC.streamUrl = asset.localURL.absoluteString
playerVC.fullUrl = asset.originalURL.absoluteString
playerVC.modalPresentationStyle = .fullScreen
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
findTopViewController.findViewController(rootVC).present(playerVC, animated: true, completion: nil)
}
} else {
let playerVC = CustomMediaPlayerViewController(
module: asset.module,
urlString: asset.localURL.absoluteString,
fullUrl: asset.originalURL.absoluteString,
title: asset.name,
episodeNumber: 1,
onWatchNext: {},
subtitlesURL: nil,
aniListID: 0,
episodeImageUrl: "",
headers: nil
)
playerVC.modalPresentationStyle = .fullScreen
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
findTopViewController.findViewController(rootVC).present(playerVC, animated: true, completion: nil)
}
}
}
private func formatFileSize(_ size: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useKB, .useMB, .useGB]
formatter.countStyle = .file
return formatter.string(fromByteCount: size)
}
private func formatDate(_ date: Date) -> String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .short
return formatter.localizedString(for: date, relativeTo: Date())
}
}

View file

@ -20,6 +20,7 @@ struct EpisodeCell: View {
let episodeID: Int
let progress: Double
let itemID: Int
let module: ScrapingModule
let onTap: (String) -> Void
let onMarkAllPrevious: () -> Void
@ -28,6 +29,11 @@ struct EpisodeCell: View {
@State private var episodeImageUrl: String = ""
@State private var isLoading: Bool = true
@State private var currentProgress: Double = 0.0
@State private var isFetchingEpisode: Bool = false
@StateObject private var jsController = JSController()
@EnvironmentObject private var moduleManager: ModuleManager
@EnvironmentObject private var downloadManager: DownloadManager
@Environment(\.colorScheme) private var colorScheme
@AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system
@ -35,17 +41,19 @@ struct EpisodeCell: View {
var defaultBannerImage: String {
let isLightMode = selectedAppearance == .light || (selectedAppearance == .system && colorScheme == .light)
return isLightMode
? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner1.png"
: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner2.png"
? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner1.png"
: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner2.png"
}
init(episodeIndex: Int, episode: String, episodeID: Int, progress: Double,
itemID: Int, onTap: @escaping (String) -> Void, onMarkAllPrevious: @escaping () -> Void) {
itemID: Int, module: ScrapingModule, onTap: @escaping (String) -> Void,
onMarkAllPrevious: @escaping () -> Void) {
self.episodeIndex = episodeIndex
self.episode = episode
self.episodeID = episodeID
self.progress = progress
self.itemID = itemID
self.module = module
self.onTap = onTap
self.onMarkAllPrevious = onMarkAllPrevious
}
@ -99,6 +107,10 @@ struct EpisodeCell: View {
Label("Mark All Previous Watched", systemImage: "checkmark.circle.fill")
}
}
Button(action: downloadEpisode) {
Label("Download Episode", systemImage: "arrow.down.circle")
}
}
.onAppear {
updateProgress()
@ -187,4 +199,70 @@ struct EpisodeCell: View {
}
}.resume()
}
private func downloadEpisode() {
isFetchingEpisode = true
Task {
do {
let jsContent = try moduleManager.getModuleContent(module)
jsController.loadScript(jsContent)
if module.metadata.asyncJS == true {
jsController.fetchStreamUrlJS(episodeUrl: episode, softsub: module.metadata.softsub == true, module: module) { result in
if let sources = result.sources, !sources.isEmpty {
let streamUrl = sources[0]["streamUrl"] as? String ?? ""
let headers = sources[0]["headers"] as? [String: String] ?? [:]
self.startDownload(url: streamUrl, headers: headers)
}
else if let streams = result.streams, !streams.isEmpty {
self.startDownload(url: streams[0])
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
}
}
} else {
jsController.fetchStreamUrl(episodeUrl: episode, softsub: module.metadata.softsub == true, module: module) { result in
if let sources = result.sources, !sources.isEmpty {
let streamUrl = sources[0]["streamUrl"] as? String ?? ""
let headers = sources[0]["headers"] as? [String: String] ?? [:]
self.startDownload(url: streamUrl, headers: headers)
}
else if let streams = result.streams, !streams.isEmpty {
self.startDownload(url: streams[0])
}
DispatchQueue.main.async {
self.isFetchingEpisode = false
}
}
}
} catch {
Logger.shared.log("Error starting download: \(error)", type: "Error")
DispatchQueue.main.async {
self.isFetchingEpisode = false
}
}
}
}
private func startDownload(url: String, headers: [String: String]? = nil) {
guard let streamUrl = URL(string: url) else {
Logger.shared.log("Invalid stream URL for download", type: "Error")
return
}
downloadManager.downloadAsset(
from: streamUrl,
module: module,
headers: headers
)
DropManager.shared.showDrop(
title: "Download Started",
subtitle: "Episode \(episodeID + 1)",
duration: 1.0,
icon: UIImage(systemName: "arrow.down.circle.fill")
)
}
}

View file

@ -300,6 +300,7 @@ struct MediaInfoView: View {
episodeID: ep.number - 1,
progress: progress,
itemID: itemID ?? 0,
module: module,
onTap: { imageUrl in
if !isFetchingEpisode {
selectedEpisodeNumber = ep.number
@ -350,6 +351,7 @@ struct MediaInfoView: View {
episodeID: ep.number - 1,
progress: progress,
itemID: itemID ?? 0,
module: module,
onTap: { imageUrl in
if !isFetchingEpisode {
selectedEpisodeNumber = ep.number

View file

@ -38,14 +38,6 @@ struct SearchView: View {
return moduleManager.modules.first { $0.id.uuidString == id }
}
private var loadingMessages: [String] = [
"Searching the depths...",
"Looking for results...",
"Fetching data...",
"Please wait...",
"Almost there..."
]
private var columnsCount: Int {
if UIDevice.current.userInterfaceIdiom == .pad {
let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height

View file

@ -74,27 +74,6 @@ struct SettingsViewPlayer: View {
}
}
Section(header: Text("Progress bar Marker Color")) {
ColorPicker("Segments Color", selection: Binding(
get: {
if let data = UserDefaults.standard.data(forKey: "segmentsColorData"),
let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor {
return Color(uiColor)
}
return .yellow
},
set: { newColor in
let uiColor = UIColor(newColor)
if let data = try? NSKeyedArchiver.archivedData(
withRootObject: uiColor,
requiringSecureCoding: false
) {
UserDefaults.standard.set(data, forKey: "segmentsColorData")
}
}
))
}
Section(header: Text("Skip Settings"), footer : Text("Double tapping the screen on it's sides will skip with the short tap setting.")) {
HStack {
Text("Tap Skip:")