mirror of
https://github.com/cranci1/Sora.git
synced 2026-05-17 15:22:14 +00:00
This commit is contained in:
parent
fdbb3e1edc
commit
0d9ff05bb6
6 changed files with 352 additions and 86 deletions
|
|
@ -21,6 +21,8 @@
|
||||||
133D7C932D2BE2640075467E /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C892D2BE2640075467E /* Modules.swift */; };
|
133D7C932D2BE2640075467E /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C892D2BE2640075467E /* Modules.swift */; };
|
||||||
133D7C942D2BE2640075467E /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C8B2D2BE2640075467E /* JSController.swift */; };
|
133D7C942D2BE2640075467E /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C8B2D2BE2640075467E /* JSController.swift */; };
|
||||||
133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
|
@ -38,6 +40,8 @@
|
||||||
133D7C872D2BE2640075467E /* URLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -105,6 +109,7 @@
|
||||||
133D7C7F2D2BE2630075467E /* MediaInfoView */ = {
|
133D7C7F2D2BE2630075467E /* MediaInfoView */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
138AA1B52D2D66EC0021F9DF /* EpisodeCell */,
|
||||||
133D7C802D2BE2630075467E /* MediaInfoView.swift */,
|
133D7C802D2BE2630075467E /* MediaInfoView.swift */,
|
||||||
);
|
);
|
||||||
path = MediaInfoView;
|
path = MediaInfoView;
|
||||||
|
|
@ -152,6 +157,15 @@
|
||||||
path = Loaders;
|
path = Loaders;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */,
|
||||||
|
138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */,
|
||||||
|
);
|
||||||
|
path = EpisodeCell;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
|
@ -233,12 +247,14 @@
|
||||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
||||||
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
|
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
|
||||||
133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */,
|
133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */,
|
||||||
|
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */,
|
||||||
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */,
|
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */,
|
||||||
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
|
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
|
||||||
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
|
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
|
||||||
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
|
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
|
||||||
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
|
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
|
||||||
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
|
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
|
||||||
|
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,40 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension URLSession {
|
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 = {
|
static let custom: URLSession = {
|
||||||
let configuration = URLSessionConfiguration.default
|
let configuration = URLSessionConfiguration.default
|
||||||
configuration.httpAdditionalHeaders = [
|
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)
|
return URLSession(configuration: configuration)
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class JSController: ObservableObject {
|
||||||
return
|
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 }
|
guard let self = self else { return }
|
||||||
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
|
|
@ -69,4 +69,44 @@ class JSController: ObservableObject {
|
||||||
}
|
}
|
||||||
}.resume()
|
}.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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift
Normal file
119
Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
|
||||||
|
struct MediaItem: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let description: String
|
||||||
|
let aliases: String
|
||||||
|
let airdate: String
|
||||||
|
}
|
||||||
|
|
||||||
struct MediaInfoView: View {
|
struct MediaInfoView: View {
|
||||||
let title: String
|
let title: String
|
||||||
let imageUrl: String
|
let imageUrl: String
|
||||||
|
|
@ -25,104 +32,122 @@ struct MediaInfoView: View {
|
||||||
|
|
||||||
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
|
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
|
||||||
|
|
||||||
|
@ObservedObject var jsController = JSController()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
ScrollView {
|
if isLoading {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
ProgressView()
|
||||||
HStack(alignment: .top, spacing: 10) {
|
.padding()
|
||||||
KFImage(URL(string: imageUrl))
|
} else {
|
||||||
.resizable()
|
ScrollView {
|
||||||
.aspectRatio(2/3, contentMode: .fill)
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
.cornerRadius(10)
|
HStack(alignment: .top, spacing: 10) {
|
||||||
.frame(width: 150, height: 225)
|
KFImage(URL(string: imageUrl))
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(2/3, contentMode: .fill)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.frame(width: 150, height: 225)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 17))
|
.font(.system(size: 17))
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
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))
|
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
if !aliases.isEmpty && aliases != title {
|
||||||
|
Text(aliases)
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: {
|
HStack(alignment: .center, spacing: 12) {
|
||||||
showFullSynopsis.toggle()
|
Text(module.metadata.sourceName)
|
||||||
}) {
|
.font(.system(size: 13))
|
||||||
Text(showFullSynopsis ? "Less" : "More")
|
.padding(4)
|
||||||
.font(.system(size: 14))
|
.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 {
|
if !synopsis.isEmpty {
|
||||||
Button(action: {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
}) {
|
HStack(alignment: .center) {
|
||||||
HStack {
|
Text("Synopsis")
|
||||||
Image(systemName: "play.fill")
|
.font(.system(size: 18))
|
||||||
.foregroundColor(.primary)
|
.fontWeight(.bold)
|
||||||
Text("Start Watching")
|
|
||||||
.font(.headline)
|
Spacer()
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
Button(action: {
|
||||||
|
showFullSynopsis.toggle()
|
||||||
|
}) {
|
||||||
|
Text(showFullSynopsis ? "Less" : "More")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(synopsis)
|
||||||
|
.lineLimit(showFullSynopsis ? nil : 4)
|
||||||
|
.font(.system(size: 14))
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background(Color.accentColor)
|
|
||||||
.cornerRadius(10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: {
|
HStack {
|
||||||
}) {
|
Button(action: {
|
||||||
Image(systemName: "bookmark")
|
}) {
|
||||||
.resizable()
|
HStack {
|
||||||
.frame(width: 20, height: 27)
|
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)
|
.onAppear {
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue