does even work yay
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run

This commit is contained in:
cranci1 2025-01-07 16:53:36 +01:00
parent fdbb3e1edc
commit 0d9ff05bb6
6 changed files with 352 additions and 86 deletions

View file

@ -21,6 +21,8 @@
133D7C932D2BE2640075467E /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C892D2BE2640075467E /* Modules.swift */; };
133D7C942D2BE2640075467E /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C8B2D2BE2640075467E /* JSController.swift */; };
133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; };
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; };
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -38,6 +40,8 @@
133D7C872D2BE2640075467E /* URLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = "<group>"; };
133D7C892D2BE2640075467E /* Modules.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Modules.swift; sourceTree = "<group>"; };
133D7C8B2D2BE2640075467E /* JSController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = "<group>"; };
138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = "<group>"; };
138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -105,6 +109,7 @@
133D7C7F2D2BE2630075467E /* MediaInfoView */ = {
isa = PBXGroup;
children = (
138AA1B52D2D66EC0021F9DF /* EpisodeCell */,
133D7C802D2BE2630075467E /* MediaInfoView.swift */,
);
path = MediaInfoView;
@ -152,6 +157,15 @@
path = Loaders;
sourceTree = "<group>";
};
138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = {
isa = PBXGroup;
children = (
138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */,
138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */,
);
path = EpisodeCell;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -233,12 +247,14 @@
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */,
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */,
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */,
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -8,10 +8,40 @@
import Foundation
extension URLSession {
static let userAgents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.2365.92",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.128",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3; rv:123.0) Gecko/20100101 Firefox/123.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.2; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (iPad; CPU OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Android 14; Mobile; rv:123.0) Gecko/123.0 Firefox/123.0",
"Mozilla/5.0 (Android 13; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0"
]
static let randomUserAgent: String = {
userAgents.randomElement() ?? userAgents[0]
}()
static let custom: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
"User-Agent": randomUserAgent
]
return URLSession(configuration: configuration)
}()

View file

@ -36,7 +36,7 @@ class JSController: ObservableObject {
return
}
URLSession.custom.dataTask(with: url) { [weak self] data, response, error in
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
guard let self = self else { return }
if let error = error {
@ -69,4 +69,44 @@ class JSController: ObservableObject {
}
}.resume()
}
func fetchDetails(url: String, completion: @escaping ([MediaItem]) -> Void) {
guard let url = URL(string: url) else {
completion([])
return
}
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
guard let self = self else { return }
if let error = error {
print("Network error: \(error)")
DispatchQueue.main.async { completion([]) }
return
}
guard let data = data, let html = String(data: data, encoding: .utf8) else {
print("Failed to decode HTML")
DispatchQueue.main.async { completion([]) }
return
}
if let parseFunction = self.context.objectForKeyedSubscript("extractDetails"),
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
let resultItems = results.map { item in
MediaItem(
description: item["description"] ?? "",
aliases: item["aliases"] ?? "",
airdate: item["airdate"] ?? ""
)
}
DispatchQueue.main.async {
completion(resultItems)
}
} else {
print("Failed to parse results")
DispatchQueue.main.async { completion([]) }
}
}.resume()
}
}

View file

@ -0,0 +1,36 @@
//
// CircularProgressBar.swift
// Sora
//
// Created by Francesco on 18/12/24.
//
import SwiftUI
struct CircularProgressBar: View {
var progress: Double
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 5.0)
.opacity(0.3)
.foregroundColor(Color.accentColor)
Circle()
.trim(from: 0.0, to: CGFloat(min(progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.accentColor)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: progress)
if progress >= 0.90 {
Image(systemName: "checkmark")
.font(.system(size: 12))
} else {
Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0))
.font(.system(size: 12))
}
}
}
}

View file

@ -0,0 +1,119 @@
//
// EpisodeCell.swift
// Sora
//
// Created by Francesco on 18/12/24.
//
import SwiftUI
import Kingfisher
struct EpisodeCell: View {
let episode: String
let episodeID: Int
let imageUrl: String
let progress: Double
let itemID: Int
@State private var episodeTitle: String = ""
@State private var episodeImageUrl: String = ""
@State private var isLoading: Bool = true
var body: some View {
HStack {
ZStack {
KFImage(URL(string: episodeImageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : episodeImageUrl))
.resizable()
.aspectRatio(16/9, contentMode: .fill)
.frame(width: 100, height: 56)
.cornerRadius(8)
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
}
VStack(alignment: .leading) {
Text("Episode \(episodeID + 1)")
.font(.system(size: 15))
if !episodeTitle.isEmpty {
Text(episodeTitle)
.font(.system(size: 13))
.foregroundColor(.secondary)
}
}
Spacer()
CircularProgressBar(progress: progress)
.frame(width: 40, height: 40)
}
.onAppear {
fetchEpisodeDetails()
}
}
func fetchEpisodeDetails() {
let cacheKey = "episodeDetails_\(itemID)_\(episodeID)"
if let cachedData = UserDefaults.standard.data(forKey: cacheKey) {
parseEpisodeDetails(data: cachedData)
return
}
guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(itemID)") else {
isLoading = false
return
}
URLSession.custom.dataTask(with: url) { data, _, error in
if let error = error {
print("Failed to fetch episode details: \(error)")
DispatchQueue.main.async {
self.isLoading = false
}
return
}
guard let data = data else {
print("No data received")
DispatchQueue.main.async {
self.isLoading = false
}
return
}
UserDefaults.standard.set(data, forKey: cacheKey)
self.parseEpisodeDetails(data: data)
}.resume()
}
func parseEpisodeDetails(data: Data) {
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
guard let json = jsonObject as? [String: Any],
let episodes = json["episodes"] as? [String: Any],
let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any],
let title = episodeDetails["title"] as? [String: String],
let image = episodeDetails["image"] as? String else {
print("Invalid response format")
DispatchQueue.main.async {
self.isLoading = false
}
return
}
DispatchQueue.main.async {
self.episodeTitle = title["en"] ?? ""
self.episodeImageUrl = image
self.isLoading = false
}
} catch {
print("Failed to parse JSON: \(error)")
DispatchQueue.main.async {
self.isLoading = false
}
}
}
}

View file

@ -8,6 +8,13 @@
import SwiftUI
import Kingfisher
struct MediaItem: Identifiable {
let id = UUID()
let description: String
let aliases: String
let airdate: String
}
struct MediaInfoView: View {
let title: String
let imageUrl: String
@ -25,104 +32,122 @@ struct MediaInfoView: View {
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
@ObservedObject var jsController = JSController()
var body: some View {
Group {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .top, spacing: 10) {
KFImage(URL(string: imageUrl))
.resizable()
.aspectRatio(2/3, contentMode: .fill)
.cornerRadius(10)
.frame(width: 150, height: 225)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 17))
.fontWeight(.bold)
if isLoading {
ProgressView()
.padding()
} else {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .top, spacing: 10) {
KFImage(URL(string: imageUrl))
.resizable()
.aspectRatio(2/3, contentMode: .fill)
.cornerRadius(10)
.frame(width: 150, height: 225)
if !aliases.isEmpty && aliases != title {
Text(aliases)
.font(.system(size: 13))
.foregroundColor(.secondary)
}
Spacer()
HStack(alignment: .center, spacing: 12) {
Text(module.metadata.sourceName)
.font(.system(size: 13))
.padding(4)
.background(Capsule().fill(Color.accentColor.opacity(0.4)))
Button(action: {
}) {
Image(systemName: "ellipsis.circle")
.resizable()
.frame(width: 20, height: 20)
}
Button(action: {
}) {
Image(systemName: "safari")
.resizable()
.frame(width: 20, height: 20)
}
}
}
}
if !synopsis.isEmpty {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .center) {
Text("Synopsis")
.font(.system(size: 18))
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 17))
.fontWeight(.bold)
if !aliases.isEmpty && aliases != title {
Text(aliases)
.font(.system(size: 13))
.foregroundColor(.secondary)
}
Spacer()
Button(action: {
showFullSynopsis.toggle()
}) {
Text(showFullSynopsis ? "Less" : "More")
.font(.system(size: 14))
HStack(alignment: .center, spacing: 12) {
Text(module.metadata.sourceName)
.font(.system(size: 13))
.padding(4)
.background(Capsule().fill(Color.accentColor.opacity(0.4)))
Button(action: {
}) {
Image(systemName: "ellipsis.circle")
.resizable()
.frame(width: 20, height: 20)
}
Button(action: {
}) {
Image(systemName: "safari")
.resizable()
.frame(width: 20, height: 20)
}
}
}
Text(synopsis)
.lineLimit(showFullSynopsis ? nil : 4)
.font(.system(size: 14))
}
}
HStack {
Button(action: {
}) {
HStack {
Image(systemName: "play.fill")
.foregroundColor(.primary)
Text("Start Watching")
.font(.headline)
.foregroundColor(.primary)
}
.padding()
.frame(maxWidth: .infinity)
.background(Color.accentColor)
.cornerRadius(10)
}
Button(action: {
}) {
Image(systemName: "bookmark")
.resizable()
.frame(width: 20, height: 27)
if !synopsis.isEmpty {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .center) {
Text("Synopsis")
.font(.system(size: 18))
.fontWeight(.bold)
Spacer()
Button(action: {
showFullSynopsis.toggle()
}) {
Text(showFullSynopsis ? "Less" : "More")
.font(.system(size: 14))
}
}
Text(synopsis)
.lineLimit(showFullSynopsis ? nil : 4)
.font(.system(size: 14))
}
}
HStack {
Button(action: {
}) {
HStack {
Image(systemName: "play.fill")
.foregroundColor(.primary)
Text("Start Watching")
.font(.headline)
.foregroundColor(.primary)
}
.padding()
.frame(maxWidth: .infinity)
.background(Color.accentColor)
.cornerRadius(10)
}
Button(action: {
}) {
Image(systemName: "bookmark")
.resizable()
.frame(width: 20, height: 27)
}
}
}
.padding()
.navigationBarTitleDisplayMode(.inline)
.navigationBarTitle(title)
.navigationViewStyle(StackNavigationViewStyle())
}
.padding()
.navigationBarTitleDisplayMode(.inline)
.navigationBarTitle(title)
.navigationViewStyle(StackNavigationViewStyle())
}
}
.onAppear {
jsController.fetchDetails(url: href) { items in
if let item = items.first {
print("Fetched item: \(item)")
self.synopsis = item.description
self.aliases = item.aliases
self.airdate = item.airdate
}
self.isLoading = false
}
}
}