mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-19 23:52:09 +00:00
Improved README + many things inside app for people (#181)
This commit is contained in:
parent
b56ef52ae3
commit
c15ef04c80
18 changed files with 245 additions and 134 deletions
34
README.md
34
README.md
|
|
@ -24,12 +24,12 @@
|
|||
|
||||
- [x] macOS 12.0+ support
|
||||
- [x] iOS/iPadOS 15.0+ support
|
||||
- [x] JavaScript as main Loader
|
||||
- [x] JavaScript as main loader
|
||||
- [x] Download support (HLS & MP4)
|
||||
- [x] Tracking Services (AniList, Trakt)
|
||||
- [x] Apple KeyChain support for auth Tokens
|
||||
- [x] Streams support (Jellyfin/Plex like servers)
|
||||
- [x] External Metadata providers (TMDB, AniList)
|
||||
- [x] Tracking services (AniList, Trakt)
|
||||
- [x] Apple Keychain support for auth tokens
|
||||
- [x] Streams support (Jellyfin/Plex-like servers)
|
||||
- [x] External metadata providers (TMDB, AniList)
|
||||
- [x] Background playback and Picture-in-Picture (PiP) support
|
||||
- [x] External media player support (VLC, Infuse, Outplayer, nPlayer, SenPlayer, IINA, TracyPlayer)
|
||||
|
||||
|
|
@ -49,17 +49,17 @@ Additionally, you can install the app using Xcode or using the .ipa file, which
|
|||
|
||||
## Frequently Asked Questions
|
||||
|
||||
1. **What is Sora?**
|
||||
Sora is a modular web scraping application designed to work exclusively with custom modules.
|
||||
1. **What is Sora?**
|
||||
Sora is a modular web scraping application designed to work exclusively with custom modules.
|
||||
|
||||
2. **Is Sora safe?**
|
||||
Yes, Sora is open-source and prioritizes user privacy. It does not store user data on external servers and does not collect crash logs.
|
||||
2. **Is Sora safe?**
|
||||
Yes, Sora is open-source and prioritizes user privacy. It does not store user data on external servers and does not collect crash logs.
|
||||
|
||||
3. **Will Sora ever be paid?**
|
||||
No, Sora will always remain free without subscriptions, paid content, or any type of login.
|
||||
3. **Will Sora ever be paid?**
|
||||
No, Sora will always remain free without subscriptions, paid content, or any type of login.
|
||||
|
||||
4. **How can I get modules?**
|
||||
Sora does not include any modules by default. You will need to find and add the necessary modules yourself, or create your own.
|
||||
4. **How can I get modules?**
|
||||
Sora does not include any modules by default. You will need to find and add the necessary modules yourself, or create your own.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
|
@ -95,16 +95,16 @@ along with Sora. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
## Legal
|
||||
|
||||
**_Sora is not made for Piracy! The Sora project does not condone any form of piracy._**
|
||||
**_Sora is not intended for piracy. The Sora project does not endorse or support any form of piracy._**
|
||||
|
||||
### No Liability
|
||||
|
||||
The developer(s) of this software assumes no liability for damages, legal claims, or other issues arising from the use or misuse of this software or any third-party modules. Users bear full responsibility for their actions. Use this software and modules at your own risk.
|
||||
The developer(s) of this software assume no liability for damages, legal claims, or other issues arising from the use or misuse of this software or any third-party modules. Users bear full responsibility for their actions. Use of this software and its modules is at your own risk.
|
||||
|
||||
### Third-Party Websites and Intellectual Property
|
||||
|
||||
This software is not affiliated with or endorsed by any third-party entity. Any references to third-party sites in user-generated modules do not imply endorsement. Users are responsible for verifying that their scraping activities comply with the terms of service and intellectual property rights of the sites they interact with.
|
||||
This software is not affiliated with or endorsed by any third-party entities. Any references to third-party sites in user-generated modules do not imply endorsement. Users are responsible for ensuring their scraping activities comply with the terms of service and intellectual property rights of the sites they interact with.
|
||||
|
||||
### DMCA
|
||||
|
||||
The developer(s) are not responsible for the misuse of any content inside or outside the app and shall not be responsible for the dissemination of any content within the app. Any violations should be sent to the source website or module creator. The developer is not legally responsible for any module used inside the app.
|
||||
The developer(s) are not responsible for the misuse of any content inside or outside the app and shall not be held liable for the dissemination of any content within the app. Any violations should be reported to the source website or module creator. The developer bears no legal responsibility for any module used within the app.
|
||||
|
|
|
|||
|
|
@ -41,15 +41,9 @@
|
|||
},
|
||||
"About" : {
|
||||
|
||||
},
|
||||
"Actively downloading media can be tracked from here." : {
|
||||
|
||||
},
|
||||
"Add Module" : {
|
||||
|
||||
},
|
||||
"AKA Sulfur" : {
|
||||
|
||||
},
|
||||
"All Bookmarks" : {
|
||||
|
||||
|
|
@ -59,6 +53,9 @@
|
|||
},
|
||||
"All Watching" : {
|
||||
|
||||
},
|
||||
"Also known as Sulfur" : {
|
||||
|
||||
},
|
||||
"AniList ID" : {
|
||||
|
||||
|
|
@ -145,8 +142,19 @@
|
|||
"cranci1" : {
|
||||
|
||||
},
|
||||
"DATA/LOGS" : {
|
||||
"DATA & LOGS" : {
|
||||
|
||||
},
|
||||
"DATA/LOGS" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Data & Logs"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Delete" : {
|
||||
|
||||
|
|
@ -167,15 +175,22 @@
|
|||
|
||||
},
|
||||
"Download" : {
|
||||
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Downloads"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Download Episode" : {
|
||||
"Download This Episode" : {
|
||||
|
||||
},
|
||||
"Downloads" : {
|
||||
|
||||
},
|
||||
"Enter the AniList ID for this media" : {
|
||||
"Enter the AniList ID for this series" : {
|
||||
|
||||
},
|
||||
"Episode %lld" : {
|
||||
|
|
@ -197,7 +212,14 @@
|
|||
|
||||
},
|
||||
"Failed to load contributors" : {
|
||||
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Failed to load contributors. Please try again later."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Files Downloaded" : {
|
||||
|
||||
|
|
@ -205,14 +227,57 @@
|
|||
"General" : {
|
||||
|
||||
},
|
||||
"INFOS" : {
|
||||
"General Preferences" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "General Settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"INFORMATION" : {
|
||||
|
||||
},
|
||||
"INFOS" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Join the Discord" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Join Discord Community"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"LESS" : {
|
||||
|
||||
},
|
||||
"Library" : {
|
||||
|
||||
},
|
||||
"License (GPLv3.0)" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "View License (GPLv3.0)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Loading Episode %lld..." : {
|
||||
|
||||
|
|
@ -251,16 +316,37 @@
|
|||
|
||||
},
|
||||
"MAIN" : {
|
||||
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Main Settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mark All Previous Watched" : {
|
||||
"MAIN SETTINGS" : {
|
||||
|
||||
},
|
||||
"Mark as Watched" : {
|
||||
|
||||
},
|
||||
"Mark watched" : {
|
||||
"Mark Episode as Watched" : {
|
||||
|
||||
},
|
||||
"Mark Previous Episodes as Watched" : {
|
||||
|
||||
},
|
||||
"Mark watched" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mark as Watched"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Match with AniList" : {
|
||||
|
||||
|
|
@ -285,6 +371,9 @@
|
|||
},
|
||||
"No Active Downloads" : {
|
||||
|
||||
},
|
||||
"No AniList matches found" : {
|
||||
|
||||
},
|
||||
"No Data Available" : {
|
||||
|
||||
|
|
@ -297,12 +386,17 @@
|
|||
},
|
||||
"No Episodes Available" : {
|
||||
|
||||
},
|
||||
"No items to continue watching." : {
|
||||
|
||||
},
|
||||
"No matches found" : {
|
||||
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "No matches found. Try different keywords."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"No Module Selected" : {
|
||||
|
||||
|
|
@ -310,7 +404,10 @@
|
|||
"No Modules" : {
|
||||
|
||||
},
|
||||
"No Results Found" : {
|
||||
"No Search Results Found" : {
|
||||
|
||||
},
|
||||
"Nothing to Continue Watching" : {
|
||||
|
||||
},
|
||||
"OK" : {
|
||||
|
|
@ -336,9 +433,6 @@
|
|||
},
|
||||
"Queued" : {
|
||||
|
||||
},
|
||||
"Recently watched content will appear here." : {
|
||||
|
||||
},
|
||||
"Refresh Storage Info" : {
|
||||
|
||||
|
|
@ -357,21 +451,36 @@
|
|||
},
|
||||
"Remove Item" : {
|
||||
|
||||
},
|
||||
"Report an Issue" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Report an Issue on GitHub"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Reset" : {
|
||||
|
||||
},
|
||||
"Reset AniList ID" : {
|
||||
|
||||
},
|
||||
"Reset Episode Progress" : {
|
||||
|
||||
},
|
||||
"Reset progress" : {
|
||||
|
||||
},
|
||||
"Reset Progress" : {
|
||||
|
||||
},
|
||||
"Running Sora %@ - cranci1" : {
|
||||
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Reset Progress"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Save" : {
|
||||
|
||||
|
|
@ -383,10 +492,24 @@
|
|||
|
||||
},
|
||||
"Search for something..." : {
|
||||
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Search for something..."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Search..." : {
|
||||
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Search for something..."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Season %lld" : {
|
||||
|
||||
|
|
@ -408,6 +531,9 @@
|
|||
},
|
||||
"Sora" : {
|
||||
|
||||
},
|
||||
"Sora %@ by cranci1" : {
|
||||
|
||||
},
|
||||
"Sort" : {
|
||||
|
||||
|
|
@ -433,7 +559,7 @@
|
|||
"Trakt.tv" : {
|
||||
|
||||
},
|
||||
"Try different keywords" : {
|
||||
"Try different search terms" : {
|
||||
|
||||
},
|
||||
"Use TMDB Poster Image" : {
|
||||
|
|
@ -454,7 +580,13 @@
|
|||
"You have no items saved." : {
|
||||
|
||||
},
|
||||
"Your downloaded episodes will appear here" : {
|
||||
"Your active downloads will appear here." : {
|
||||
|
||||
},
|
||||
"Your downloaded content will appear here" : {
|
||||
|
||||
},
|
||||
"Your recently watched content will appear here" : {
|
||||
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import SwiftUI
|
|||
struct SoraApp: App {
|
||||
@StateObject private var settings = Settings()
|
||||
@StateObject private var moduleManager = ModuleManager()
|
||||
@StateObject private var librarykManager = LibraryManager()
|
||||
@StateObject private var libraryManager = LibraryManager()
|
||||
@StateObject private var downloadManager = DownloadManager()
|
||||
@StateObject private var jsController = JSController.shared
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ struct SoraApp: App {
|
|||
}
|
||||
.environmentObject(moduleManager)
|
||||
.environmentObject(settings)
|
||||
.environmentObject(librarykManager)
|
||||
.environmentObject(libraryManager)
|
||||
.environmentObject(downloadManager)
|
||||
.environmentObject(jsController)
|
||||
.accentColor(settings.accentColor)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class DownloadManager: NSObject, ObservableObject {
|
|||
localPlaybackURL = localURL
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Error loading local content: \(error)", type: "Error")
|
||||
Logger.shared.log("Could not load local content: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +71,7 @@ extension DownloadManager: AVAssetDownloadDelegate {
|
|||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let error = error else { return }
|
||||
Logger.shared.log("Download failed: \(error.localizedDescription)", type: "Error")
|
||||
activeDownloadTasks.removeValue(forKey: task)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,13 +32,13 @@ enum DownloadQualityPreference: String, CaseIterable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .best:
|
||||
return "Highest available quality (largest file size)"
|
||||
return "Maximum quality available (largest file size)"
|
||||
case .high:
|
||||
return "High quality (720p or higher)"
|
||||
return "High quality (720p or better)"
|
||||
case .medium:
|
||||
return "Medium quality (480p-720p)"
|
||||
return "Medium quality (480p to 720p)"
|
||||
case .low:
|
||||
return "Lowest available quality (smallest file size)"
|
||||
return "Minimum quality available (smallest file size)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ enum M3U8StreamExtractorError: Error {
|
|||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .networkError(let error):
|
||||
return "Network error: \(error.localizedDescription)"
|
||||
return "Connection error: \(error.localizedDescription)"
|
||||
case .parsingError(let message):
|
||||
return "Parsing error: \(message)"
|
||||
return "Stream parsing error: \(message)"
|
||||
case .noStreamFound:
|
||||
return "No suitable stream found in playlist"
|
||||
return "No compatible stream found in playlist"
|
||||
case .invalidURL:
|
||||
return "Invalid stream URL"
|
||||
return "Stream URL is invalid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ class DropManager {
|
|||
let willStartImmediately = JSController.shared.willDownloadStartImmediately()
|
||||
|
||||
let message = willStartImmediately
|
||||
? "Episode \(episodeNumber) download started"
|
||||
: "Episode \(episodeNumber) queued"
|
||||
? "Episode \(episodeNumber) is now downloading"
|
||||
: "Episode \(episodeNumber) added to download queue"
|
||||
|
||||
showDrop(
|
||||
title: willStartImmediately ? "Download Started" : "Download Queued",
|
||||
|
|
|
|||
|
|
@ -21,18 +21,18 @@ extension JSController {
|
|||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
Logger.shared.log("Network error while searching: \(error)", type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
Logger.shared.log("Could not decode HTML response", type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shared.log(html,type: "HTMLStrings")
|
||||
Logger.shared.log(html, type: "HTMLStrings")
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("searchResults"),
|
||||
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
let resultItems = results.map { item in
|
||||
|
|
@ -46,7 +46,7 @@ extension JSController {
|
|||
completion(resultItems)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse results",type: "Error")
|
||||
Logger.shared.log("Could not parse search results", type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
}
|
||||
}.resume()
|
||||
|
|
@ -54,27 +54,27 @@ extension JSController {
|
|||
|
||||
func fetchJsSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)",type: "Error")
|
||||
Logger.shared.log("JavaScript exception: \(exception)", type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
guard let searchResultsFunction = context.objectForKeyedSubscript("searchResults") else {
|
||||
Logger.shared.log("No JavaScript function searchResults found",type: "Error")
|
||||
Logger.shared.log("Search function not found in module", type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let promiseValue = searchResultsFunction.call(withArguments: [keyword])
|
||||
guard let promise = promiseValue else {
|
||||
Logger.shared.log("searchResults did not return a Promise",type: "Error")
|
||||
Logger.shared.log("Search function returned invalid response", type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { result in
|
||||
|
||||
Logger.shared.log(result.toString(),type: "HTMLStrings")
|
||||
Logger.shared.log(result.toString(), type: "HTMLStrings")
|
||||
if let jsonString = result.toString(),
|
||||
let data = jsonString.data(using: .utf8) {
|
||||
do {
|
||||
|
|
@ -83,7 +83,7 @@ extension JSController {
|
|||
guard let title = item["title"] as? String,
|
||||
let imageUrl = item["image"] as? String,
|
||||
let href = item["href"] as? String else {
|
||||
Logger.shared.log("Missing or invalid data in search result item: \(item)", type: "Error")
|
||||
Logger.shared.log("Invalid search result data format", type: "Error")
|
||||
return nil
|
||||
}
|
||||
return SearchItem(title: title, imageUrl: imageUrl, href: href)
|
||||
|
|
@ -94,19 +94,19 @@ extension JSController {
|
|||
}
|
||||
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON",type: "Error")
|
||||
Logger.shared.log("Could not parse JSON response", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error: \(error)",type: "Error")
|
||||
Logger.shared.log("JSON parsing error: \(error)", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string",type: "Error")
|
||||
Logger.shared.log("Invalid search result format", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ extension JSController {
|
|||
}
|
||||
|
||||
let catchBlock: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))",type: "Error")
|
||||
Logger.shared.log("Search operation failed: \(String(describing: error.toString()))", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,9 +208,9 @@ class VideoPlayerViewController: UIViewController {
|
|||
aniListMutation.updateAnimeProgress(animeId: self.aniListID, episodeNumber: self.episodeNumber) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
Logger.shared.log("Successfully updated AniList progress for episode \(self.episodeNumber)", type: "General")
|
||||
Logger.shared.log("Updated AniList progress for Episode \(self.episodeNumber)", type: "General")
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Failed to update AniList progress: \(error.localizedDescription)", type: "Error")
|
||||
Logger.shared.log("Could not update AniList progress: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -222,9 +222,9 @@ class VideoPlayerViewController: UIViewController {
|
|||
traktMutation.markAsWatched(type: "movie", tmdbID: tmdbId) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
Logger.shared.log("Successfully updated Trakt progress for movie", type: "General")
|
||||
Logger.shared.log("Updated Trakt progress for movie", type: "General")
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Failed to update Trakt progress: \(error.localizedDescription)", type: "Error")
|
||||
Logger.shared.log("Could not update Trakt progress: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -236,9 +236,9 @@ class VideoPlayerViewController: UIViewController {
|
|||
) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
Logger.shared.log("Successfully updated Trakt progress for episode \(self.episodeNumber)", type: "General")
|
||||
Logger.shared.log("Updated Trakt progress for Episode \(self.episodeNumber)", type: "General")
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Failed to update Trakt progress: \(error.localizedDescription)", type: "Error")
|
||||
Logger.shared.log("Could not update Trakt progress: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class WebAuthenticationManager {
|
|||
if let callbackURL = callbackURL {
|
||||
completion(.success(callbackURL))
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "WebAuthenticationManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No callback URL received"])))
|
||||
completion(.failure(NSError(domain: "WebAuthenticationManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Authentication callback URL not received"])))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ struct DownloadView: View {
|
|||
.fontWeight(.medium)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text("Actively downloading media can be tracked from here.")
|
||||
Text("Your active downloads will appear here.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -167,7 +167,7 @@ struct DownloadView: View {
|
|||
.fontWeight(.medium)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text("Your downloaded episodes will appear here")
|
||||
Text("Your downloaded content will appear here")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -594,28 +594,6 @@ struct DownloadSummaryCard: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private func formatFileSize(_ size: Int64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.allowedUnits = [.useKB, .useMB, .useGB]
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: size)
|
||||
}
|
||||
|
||||
private func formatFileSizeWithUnit(_ size: Int64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.allowedUnits = [.useKB, .useMB, .useGB]
|
||||
formatter.countStyle = .file
|
||||
|
||||
let formattedString = formatter.string(fromByteCount: size)
|
||||
let components = formattedString.components(separatedBy: " ")
|
||||
if components.count == 2 {
|
||||
return "Size (\(components[1]))"
|
||||
}
|
||||
return "Size"
|
||||
}
|
||||
|
||||
|
||||
struct SummaryItem: View {
|
||||
let title: String
|
||||
let value: String
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ struct AllWatchingView: View {
|
|||
@State private var sortOption: SortOption = .dateAdded
|
||||
|
||||
enum SortOption: String, CaseIterable {
|
||||
case dateAdded = "Date Added"
|
||||
case title = "Title"
|
||||
case source = "Source"
|
||||
case progress = "Progress"
|
||||
case dateAdded = "Recently Added"
|
||||
case title = "Series Title"
|
||||
case source = "Content Source"
|
||||
case progress = "Watch Progress"
|
||||
}
|
||||
|
||||
var sortedItems: [ContinueWatchingItem] {
|
||||
|
|
|
|||
|
|
@ -91,9 +91,9 @@ struct LibraryView: View {
|
|||
Image(systemName: "play.circle")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No items to continue watching.")
|
||||
Text("Nothing to Continue Watching")
|
||||
.font(.headline)
|
||||
Text("Recently watched content will appear here.")
|
||||
Text("Your recently watched content will appear here")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ struct AnilistMatchPopupView: View {
|
|||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
} else if results.isEmpty {
|
||||
Text("No matches found")
|
||||
Text("No AniList matches found")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.gray)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
|
@ -161,7 +161,7 @@ struct AnilistMatchPopupView: View {
|
|||
}
|
||||
})
|
||||
}, message: {
|
||||
Text("Enter the AniList ID for this media")
|
||||
Text("Enter the AniList ID for this series")
|
||||
})
|
||||
}
|
||||
.onAppear(perform: fetchMatches)
|
||||
|
|
|
|||
|
|
@ -227,24 +227,24 @@ private extension EpisodeCell {
|
|||
Group {
|
||||
if progress <= 0.9 {
|
||||
Button(action: markAsWatched) {
|
||||
Label("Mark as Watched", systemImage: "checkmark.circle")
|
||||
Label("Mark Episode as Watched", systemImage: "checkmark.circle")
|
||||
}
|
||||
}
|
||||
|
||||
if progress != 0 {
|
||||
Button(action: resetProgress) {
|
||||
Label("Reset Progress", systemImage: "arrow.counterclockwise")
|
||||
Label("Reset Episode Progress", systemImage: "arrow.counterclockwise")
|
||||
}
|
||||
}
|
||||
|
||||
if episodeIndex > 0 {
|
||||
Button(action: onMarkAllPrevious) {
|
||||
Label("Mark All Previous Watched", systemImage: "checkmark.circle.fill")
|
||||
Label("Mark Previous Episodes as Watched", systemImage: "checkmark.circle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: downloadEpisode) {
|
||||
Label("Download Episode", systemImage: "arrow.down.circle")
|
||||
Label("Download This Episode", systemImage: "arrow.down.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ struct SearchStateView: View {
|
|||
Image(systemName: "magnifyingglass")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No Results Found")
|
||||
Text("No Search Results Found")
|
||||
.font(.headline)
|
||||
Text("Try different keywords")
|
||||
Text("Try different search terms")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ struct SettingsViewAbout: View {
|
|||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
SettingsSection(title: "App Info", footer: "Sora/Sulfur will always remain free with no ADs!") {
|
||||
SettingsSection(title: "App Info", footer: "Sora/Sulfur will always remain free with no ads!") {
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
LazyImage(url: URL(string: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/Sora/Assets.xcassets/AppIcon.appiconset/darkmode.png")) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
|
|
@ -81,7 +81,7 @@ struct SettingsViewAbout: View {
|
|||
Text("Sora")
|
||||
.font(.title)
|
||||
.bold()
|
||||
Text("AKA Sulfur")
|
||||
Text("Also known as Sulfur")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,29 +157,29 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("MAIN")
|
||||
Text("MAIN SETTINGS")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
NavigationLink(destination: SettingsViewGeneral()) {
|
||||
SettingsNavigationRow(icon: "gearshape", title: "General Preferences")
|
||||
SettingsNavigationRow(icon: "gearshape", title: "General Settings")
|
||||
}
|
||||
Divider().padding(.horizontal, 16)
|
||||
|
||||
NavigationLink(destination: SettingsViewPlayer()) {
|
||||
SettingsNavigationRow(icon: "play.circle", title: "Video Player")
|
||||
SettingsNavigationRow(icon: "play.circle", title: "Player Settings")
|
||||
}
|
||||
Divider().padding(.horizontal, 16)
|
||||
|
||||
NavigationLink(destination: SettingsViewDownloads()) {
|
||||
SettingsNavigationRow(icon: "arrow.down.circle", title: "Download")
|
||||
SettingsNavigationRow(icon: "arrow.down.circle", title: "Download Settings")
|
||||
}
|
||||
Divider().padding(.horizontal, 16)
|
||||
|
||||
NavigationLink(destination: SettingsViewTrackers()) {
|
||||
SettingsNavigationRow(icon: "square.stack.3d.up", title: "Trackers")
|
||||
SettingsNavigationRow(icon: "square.stack.3d.up", title: "Tracking Services")
|
||||
}
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
|
|
@ -202,7 +202,7 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("DATA/LOGS")
|
||||
Text("DATA & LOGS")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.horizontal, 20)
|
||||
|
|
@ -237,7 +237,7 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("INFOS")
|
||||
Text("INFORMATION")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.horizontal, 20)
|
||||
|
|
@ -261,7 +261,7 @@ struct SettingsView: View {
|
|||
Link(destination: URL(string: "https://discord.gg/x7hppDWFDZ")!) {
|
||||
SettingsNavigationRow(
|
||||
icon: "bubble.left.and.bubble.right",
|
||||
title: "Join the Discord",
|
||||
title: "Join Discord Community",
|
||||
isExternal: true,
|
||||
textColor: .gray
|
||||
)
|
||||
|
|
@ -271,7 +271,7 @@ struct SettingsView: View {
|
|||
Link(destination: URL(string: "https://github.com/cranci1/Sora/issues")!) {
|
||||
SettingsNavigationRow(
|
||||
icon: "exclamationmark.circle",
|
||||
title: "Report an Issue",
|
||||
title: "Report an Issue on GitHub",
|
||||
isExternal: true,
|
||||
textColor: .gray
|
||||
)
|
||||
|
|
@ -306,7 +306,7 @@ struct SettingsView: View {
|
|||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
Text("Running Sora \(version) - cranci1")
|
||||
Text("Sora \(version) by cranci1")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
|
|
|||
Loading…
Reference in a new issue