mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-13 04:50:41 +00:00
HOLY MOLY MAN
This commit is contained in:
parent
e66de7f5d4
commit
2f91209647
4 changed files with 485 additions and 267 deletions
|
|
@ -61,6 +61,14 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
var watchNextButtonControlsConstraints: [NSLayoutConstraint] = []
|
||||
var isControlsVisible = false
|
||||
|
||||
var subtitleBottomConstraint: NSLayoutConstraint?
|
||||
|
||||
var subtitleBottomPadding: CGFloat = 10.0 {
|
||||
didSet {
|
||||
updateSubtitleLabelConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
init(module: ScrapingModule,
|
||||
urlString: String,
|
||||
fullUrl: String,
|
||||
|
|
@ -106,6 +114,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
|
||||
// Load persistent subtitle settings on launch
|
||||
loadSubtitleSettings()
|
||||
|
||||
setupPlayerViewController()
|
||||
setupControls()
|
||||
setupSubtitleLabel()
|
||||
|
|
@ -256,12 +268,12 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
playPauseButton.heightAnchor.constraint(equalToConstant: 50),
|
||||
|
||||
backwardButton.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor),
|
||||
backwardButton.trailingAnchor.constraint(equalTo: playPauseButton.leadingAnchor, constant: -30),
|
||||
backwardButton.trailingAnchor.constraint(equalTo: playPauseButton.leadingAnchor, constant: -50),
|
||||
backwardButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
backwardButton.heightAnchor.constraint(equalToConstant: 40),
|
||||
|
||||
forwardButton.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor),
|
||||
forwardButton.leadingAnchor.constraint(equalTo: playPauseButton.trailingAnchor, constant: 30),
|
||||
forwardButton.leadingAnchor.constraint(equalTo: playPauseButton.trailingAnchor, constant: 50),
|
||||
forwardButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
forwardButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
|
|
@ -275,14 +287,25 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
updateSubtitleLabelAppearance()
|
||||
view.addSubview(subtitleLabel)
|
||||
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
subtitleBottomConstraint = subtitleLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -subtitleBottomPadding)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
subtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
subtitleLabel.bottomAnchor.constraint(equalTo: sliderHostingController?.view.bottomAnchor ?? view.safeAreaLayoutGuide.bottomAnchor),
|
||||
subtitleBottomConstraint!,
|
||||
subtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 36),
|
||||
subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -36)
|
||||
])
|
||||
}
|
||||
|
||||
func updateSubtitleLabelConstraints() {
|
||||
subtitleBottomConstraint?.constant = -subtitleBottomPadding
|
||||
view.setNeedsLayout()
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func setupDismissButton() {
|
||||
dismissButton = UIButton(type: .system)
|
||||
dismissButton.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||
|
|
@ -374,11 +397,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
func updateSubtitleLabelAppearance() {
|
||||
subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize))
|
||||
subtitleLabel.textColor = subtitleUIColor()
|
||||
if subtitleBackgroundEnabled {
|
||||
subtitleLabel.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
||||
} else {
|
||||
subtitleLabel.backgroundColor = .clear
|
||||
}
|
||||
subtitleLabel.backgroundColor = subtitleBackgroundEnabled ? UIColor.black.withAlphaComponent(0.6) : .clear
|
||||
subtitleLabel.layer.cornerRadius = 5
|
||||
subtitleLabel.clipsToBounds = true
|
||||
subtitleLabel.layer.shadowColor = UIColor.black.cgColor
|
||||
|
|
@ -538,42 +557,120 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
if let subURL = subtitlesURL, !subURL.isEmpty {
|
||||
let foregroundActions = [
|
||||
UIAction(title: "White") { _ in self.subtitleForegroundColor = "white"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Yellow") { _ in self.subtitleForegroundColor = "yellow"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Green") { _ in self.subtitleForegroundColor = "green"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Blue") { _ in self.subtitleForegroundColor = "blue"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Red") { _ in self.subtitleForegroundColor = "red"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Purple") { _ in self.subtitleForegroundColor = "purple"; self.updateSubtitleLabelAppearance() }
|
||||
UIAction(title: "White") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "white" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Yellow") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "yellow" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Green") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "green" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Blue") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "blue" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Red") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "red" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Purple") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "purple" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
}
|
||||
]
|
||||
let colorMenu = UIMenu(title: "Subtitle Color", children: foregroundActions)
|
||||
|
||||
let fontSizeActions = [
|
||||
UIAction(title: "16") { _ in self.subtitleFontSize = 16; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "18") { _ in self.subtitleFontSize = 18; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "20") { _ in self.subtitleFontSize = 20; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "22") { _ in self.subtitleFontSize = 22; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "24") { _ in self.subtitleFontSize = 24; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "16") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 16 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "18") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 18 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "20") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 20 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "22") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 22 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "24") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 24 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Custom") { _ in self.presentCustomFontAlert() }
|
||||
]
|
||||
let fontSizeMenu = UIMenu(title: "Font Size", children: fontSizeActions)
|
||||
|
||||
let shadowActions = [
|
||||
UIAction(title: "None") { _ in self.subtitleShadowRadius = 0; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Low") { _ in self.subtitleShadowRadius = 1; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Medium") { _ in self.subtitleShadowRadius = 3; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "High") { _ in self.subtitleShadowRadius = 6; self.updateSubtitleLabelAppearance() }
|
||||
UIAction(title: "None") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 0 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Low") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 1 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Medium") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 3 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "High") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 6 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
}
|
||||
]
|
||||
let shadowMenu = UIMenu(title: "Shadow Intensity", children: shadowActions)
|
||||
|
||||
let backgroundActions = [
|
||||
UIAction(title: "Toggle") { _ in
|
||||
self.subtitleBackgroundEnabled.toggle()
|
||||
SubtitleSettingsManager.shared.update { settings in settings.backgroundEnabled.toggle() }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
}
|
||||
]
|
||||
let backgroundMenu = UIMenu(title: "Background", children: backgroundActions)
|
||||
|
||||
let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [colorMenu, fontSizeMenu, shadowMenu, backgroundMenu])
|
||||
let paddingActions = [
|
||||
UIAction(title: "10p") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 10 }
|
||||
self.loadSubtitleSettings()
|
||||
},
|
||||
UIAction(title: "20p") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 20 }
|
||||
self.loadSubtitleSettings()
|
||||
},
|
||||
UIAction(title: "30p") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 30 }
|
||||
self.loadSubtitleSettings()
|
||||
},
|
||||
UIAction(title: "Custom") { _ in self.presentCustomPaddingAlert() }
|
||||
]
|
||||
let paddingMenu = UIMenu(title: "Bottom Padding", children: paddingActions)
|
||||
|
||||
let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu])
|
||||
|
||||
menuElements = [subtitleOptionsMenu]
|
||||
}
|
||||
|
|
@ -581,6 +678,26 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
return UIMenu(title: "", children: menuElements)
|
||||
}
|
||||
|
||||
func presentCustomPaddingAlert() {
|
||||
let alert = UIAlertController(title: "Enter Custom Padding", message: nil, preferredStyle: .alert)
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = "Padding Value"
|
||||
textField.keyboardType = .numberPad
|
||||
textField.text = String(Int(self.subtitleBottomPadding))
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in
|
||||
if let text = alert.textFields?.first?.text, let intValue = Int(text) {
|
||||
let newSize = CGFloat(intValue)
|
||||
SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = newSize }
|
||||
self.loadSubtitleSettings()
|
||||
}
|
||||
}))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func presentCustomFontAlert() {
|
||||
let alert = UIAlertController(title: "Enter Custom Font Size", message: nil, preferredStyle: .alert)
|
||||
alert.addTextField { textField in
|
||||
|
|
@ -591,7 +708,8 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in
|
||||
if let text = alert.textFields?.first?.text, let newSize = Double(text) {
|
||||
self.subtitleFontSize = newSize
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = newSize }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
}
|
||||
}))
|
||||
|
|
@ -600,6 +718,15 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func loadSubtitleSettings() {
|
||||
let settings = SubtitleSettingsManager.shared.settings
|
||||
self.subtitleForegroundColor = settings.foregroundColor
|
||||
self.subtitleFontSize = settings.fontSize
|
||||
self.subtitleShadowRadius = settings.shadowRadius
|
||||
self.subtitleBackgroundEnabled = settings.backgroundEnabled
|
||||
self.subtitleBottomPadding = settings.bottomPadding
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UserDefaults.standard.bool(forKey: "alwaysLandscape") {
|
||||
return .landscape
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// SubtitleSettingsManager.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 09/03/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct SubtitleSettings: Codable {
|
||||
var foregroundColor: String = "white"
|
||||
var fontSize: Double = 20.0
|
||||
var shadowRadius: Double = 1.0
|
||||
var backgroundEnabled: Bool = true
|
||||
var bottomPadding: CGFloat = 20.0
|
||||
}
|
||||
|
||||
class SubtitleSettingsManager {
|
||||
static let shared = SubtitleSettingsManager()
|
||||
|
||||
private let userDefaultsKey = "SubtitleSettings"
|
||||
|
||||
var settings: SubtitleSettings {
|
||||
get {
|
||||
if let data = UserDefaults.standard.data(forKey: userDefaultsKey),
|
||||
let savedSettings = try? JSONDecoder().decode(SubtitleSettings.self, from: data) {
|
||||
return savedSettings
|
||||
}
|
||||
return SubtitleSettings()
|
||||
}
|
||||
set {
|
||||
if let data = try? JSONEncoder().encode(newValue) {
|
||||
UserDefaults.standard.set(data, forKey: userDefaultsKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(_ updateBlock: (inout SubtitleSettings) -> Void) {
|
||||
var currentSettings = settings
|
||||
updateBlock(¤tSettings)
|
||||
settings = currentSettings
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ struct HomeView: View {
|
|||
@State private var aniListItems: [AniListItem] = []
|
||||
@State private var trendingItems: [AniListItem] = []
|
||||
@State private var continueWatchingItems: [ContinueWatchingItem] = []
|
||||
@State private var isLoading: Bool = true
|
||||
|
||||
private var currentDeviceSeasonAndYear: (season: String, year: Int) {
|
||||
let currentDate = Date()
|
||||
|
|
@ -42,251 +43,73 @@ struct HomeView: View {
|
|||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack {
|
||||
ScrollView {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
if !continueWatchingItems.isEmpty {
|
||||
LazyVStack(alignment: .leading) {
|
||||
Text("Continue Watching")
|
||||
.font(.headline)
|
||||
.padding(.horizontal, 8)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(continueWatchingItems.reversed())) { item in
|
||||
Button(action: {
|
||||
if UserDefaults.standard.string(forKey: "externalPlayer") == "Sora" {
|
||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||
module: item.module,
|
||||
urlString: item.streamUrl,
|
||||
fullUrl: item.fullUrl,
|
||||
title: item.mediaTitle,
|
||||
episodeNumber: item.episodeNumber,
|
||||
onWatchNext: { },
|
||||
subtitlesURL: item.subtitles,
|
||||
episodeImageUrl: item.imageUrl
|
||||
)
|
||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: item.module)
|
||||
videoPlayerViewController.streamUrl = item.streamUrl
|
||||
videoPlayerViewController.fullUrl = item.fullUrl
|
||||
videoPlayerViewController.episodeImageUrl = item.imageUrl
|
||||
videoPlayerViewController.episodeNumber = item.episodeNumber
|
||||
videoPlayerViewController.mediaTitle = item.mediaTitle
|
||||
videoPlayerViewController.subtitles = item.subtitles ?? ""
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 240, height: 135)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.aspectRatio(16/9, contentMode: .fill)
|
||||
.frame(width: 240, height: 135)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
.overlay(
|
||||
KFImage(URL(string: item.module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.cornerRadius(4)
|
||||
.padding(4),
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.black.opacity(0.3))
|
||||
.blur(radius: 3)
|
||||
.frame(height: 30)
|
||||
|
||||
ProgressView(value: item.progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .white))
|
||||
.padding(.horizontal, 8)
|
||||
.scaleEffect(x: 1, y: 1.5, anchor: .center)
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(item.mediaTitle)
|
||||
.font(.caption)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.frame(width: 250, height: 190)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: { markContinueWatchingItemAsWatched(item: item) }) {
|
||||
Label("Mark as Watched", systemImage: "checkmark.circle")
|
||||
}
|
||||
Button(role: .destructive, action: { removeContinueWatchingItem(item: item) }) {
|
||||
Label("Remove Item", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.frame(height: 190)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack(alignment: .bottom, spacing: 5) {
|
||||
Text("Seasonal")
|
||||
.font(.headline)
|
||||
Text("of \(currentDeviceSeasonAndYear.season) \(String(format: "%d", currentDeviceSeasonAndYear.year))")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, 8)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
if aniListItems.isEmpty {
|
||||
ForEach(0..<5, id: \.self) { _ in
|
||||
HomeSkeletonCell()
|
||||
}
|
||||
} else {
|
||||
ForEach(aniListItems, id: \.id) { item in
|
||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||
VStack {
|
||||
KFImage(URL(string: item.coverImage.large))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 130, height: 195)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 130, height: 195)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
|
||||
Text(item.title.romaji)
|
||||
.font(.caption)
|
||||
.frame(width: 130)
|
||||
.lineLimit(1)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
|
||||
HStack(alignment: .bottom, spacing: 5) {
|
||||
Text("Trending")
|
||||
.font(.headline)
|
||||
Text("on \(trendingDateString)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
if trendingItems.isEmpty {
|
||||
ForEach(0..<5, id: \.self) { _ in
|
||||
HomeSkeletonCell()
|
||||
}
|
||||
} else {
|
||||
ForEach(trendingItems, id: \.id) { item in
|
||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||
VStack {
|
||||
KFImage(URL(string: item.coverImage.large))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 130, height: 195)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 130, height: 195)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
|
||||
Text(item.title.romaji)
|
||||
.font(.caption)
|
||||
.frame(width: 130)
|
||||
.lineLimit(1)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.navigationTitle("Home")
|
||||
}
|
||||
.onAppear {
|
||||
continueWatchingItems = ContinueWatchingManager.shared.fetchItems()
|
||||
if tracingService == "TMDB" {
|
||||
TMDBSeasonal.fetchTMDBSeasonal { items in
|
||||
if let items = items {
|
||||
aniListItems = items
|
||||
ContinueWatchingSection(items: $continueWatchingItems) { item in
|
||||
markContinueWatchingItemAsWatched(item: item)
|
||||
} removeItem: { item in
|
||||
removeContinueWatchingItem(item: item)
|
||||
}
|
||||
}
|
||||
|
||||
TMBDTrending.fetchTMDBTrending { items in
|
||||
if let items = items {
|
||||
trendingItems = items
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AnilistServiceSeasonalAnime().fetchSeasonalAnime { items in
|
||||
if let items = items {
|
||||
aniListItems = items
|
||||
}
|
||||
}
|
||||
AnilistServiceTrendingAnime().fetchTrendingAnime { items in
|
||||
if let items = items {
|
||||
trendingItems = items
|
||||
}
|
||||
}
|
||||
SeasonalSection(
|
||||
title: "Seasonal of \(currentDeviceSeasonAndYear.season) \(String(format: "%d", currentDeviceSeasonAndYear.year))",
|
||||
items: aniListItems,
|
||||
isLoading: isLoading
|
||||
)
|
||||
|
||||
TrendingSection(
|
||||
title: "Trending on \(trendingDateString)",
|
||||
items: trendingItems,
|
||||
isLoading: isLoading
|
||||
)
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.navigationTitle("Home")
|
||||
.onAppear {
|
||||
fetchData()
|
||||
}
|
||||
.refreshable {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
private func fetchData() {
|
||||
isLoading = true
|
||||
continueWatchingItems = ContinueWatchingManager.shared.fetchItems()
|
||||
|
||||
let fetchSeasonal: (@escaping ([AniListItem]?) -> Void) -> Void
|
||||
let fetchTrending: (@escaping ([AniListItem]?) -> Void) -> Void
|
||||
|
||||
if tracingService == "TMDB" {
|
||||
fetchSeasonal = TMDBSeasonal.fetchTMDBSeasonal
|
||||
fetchTrending = TMBDTrending.fetchTMDBTrending
|
||||
} else {
|
||||
fetchSeasonal = AnilistServiceSeasonalAnime().fetchSeasonalAnime
|
||||
fetchTrending = AnilistServiceTrendingAnime().fetchTrendingAnime
|
||||
}
|
||||
|
||||
fetchSeasonal { items in
|
||||
aniListItems = items ?? []
|
||||
checkLoadingState()
|
||||
}
|
||||
|
||||
fetchTrending { items in
|
||||
trendingItems = items ?? []
|
||||
checkLoadingState()
|
||||
}
|
||||
}
|
||||
|
||||
private func checkLoadingState() {
|
||||
if !aniListItems.isEmpty && !trendingItems.isEmpty {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
private func markContinueWatchingItemAsWatched(item: ContinueWatchingItem) {
|
||||
let key = "lastPlayedTime_\(item.fullUrl)"
|
||||
let totalKey = "totalTime_\(item.fullUrl)"
|
||||
|
|
@ -294,16 +117,237 @@ struct HomeView: View {
|
|||
UserDefaults.standard.set(99999999.0, forKey: totalKey)
|
||||
ContinueWatchingManager.shared.remove(item: item)
|
||||
|
||||
if let index = continueWatchingItems.firstIndex(where: { $0.id == item.id }) {
|
||||
continueWatchingItems.remove(at: index)
|
||||
}
|
||||
continueWatchingItems.removeAll { $0.id == item.id }
|
||||
}
|
||||
|
||||
private func removeContinueWatchingItem(item: ContinueWatchingItem) {
|
||||
ContinueWatchingManager.shared.remove(item: item)
|
||||
|
||||
if let index = continueWatchingItems.firstIndex(where: { $0.id == item.id }) {
|
||||
continueWatchingItems.remove(at: index)
|
||||
continueWatchingItems.removeAll { $0.id == item.id }
|
||||
}
|
||||
}
|
||||
|
||||
struct ContinueWatchingSection: View {
|
||||
@Binding var items: [ContinueWatchingItem]
|
||||
var markAsWatched: (ContinueWatchingItem) -> Void
|
||||
var removeItem: (ContinueWatchingItem) -> Void
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(alignment: .leading) {
|
||||
SectionHeader(title: "Continue Watching")
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(items.reversed())) { item in
|
||||
ContinueWatchingCell(item: item) {
|
||||
markAsWatched(item)
|
||||
} removeItem: {
|
||||
removeItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.frame(height: 190)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContinueWatchingCell: View {
|
||||
let item: ContinueWatchingItem
|
||||
var markAsWatched: () -> Void
|
||||
var removeItem: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
if UserDefaults.standard.string(forKey: "externalPlayer") == "Sora" {
|
||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||
module: item.module,
|
||||
urlString: item.streamUrl,
|
||||
fullUrl: item.fullUrl,
|
||||
title: item.mediaTitle,
|
||||
episodeNumber: item.episodeNumber,
|
||||
onWatchNext: { },
|
||||
subtitlesURL: item.subtitles,
|
||||
episodeImageUrl: item.imageUrl
|
||||
)
|
||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: item.module)
|
||||
videoPlayerViewController.streamUrl = item.streamUrl
|
||||
videoPlayerViewController.fullUrl = item.fullUrl
|
||||
videoPlayerViewController.episodeImageUrl = item.imageUrl
|
||||
videoPlayerViewController.episodeNumber = item.episodeNumber
|
||||
videoPlayerViewController.mediaTitle = item.mediaTitle
|
||||
videoPlayerViewController.subtitles = item.subtitles ?? ""
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 240, height: 135)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.aspectRatio(16/9, contentMode: .fill)
|
||||
.frame(width: 240, height: 135)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
.overlay(
|
||||
KFImage(URL(string: item.module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.cornerRadius(4)
|
||||
.padding(4),
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.black.opacity(0.3))
|
||||
.blur(radius: 3)
|
||||
.frame(height: 30)
|
||||
|
||||
ProgressView(value: item.progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .white))
|
||||
.padding(.horizontal, 8)
|
||||
.scaleEffect(x: 1, y: 1.5, anchor: .center)
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(item.mediaTitle)
|
||||
.font(.caption)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
}
|
||||
.frame(width: 240, height: 170)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: { markAsWatched() }) {
|
||||
Label("Mark as Watched", systemImage: "checkmark.circle")
|
||||
}
|
||||
Button(role: .destructive, action: { removeItem() }) {
|
||||
Label("Remove Item", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SeasonalSection: View {
|
||||
let title: String
|
||||
let items: [AniListItem]
|
||||
let isLoading: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
SectionHeader(title: title)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
if isLoading {
|
||||
ForEach(0..<5, id: \.self) { _ in
|
||||
HomeSkeletonCell()
|
||||
}
|
||||
} else {
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||
AnimeItemCell(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TrendingSection: View {
|
||||
let title: String
|
||||
let items: [AniListItem]
|
||||
let isLoading: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
SectionHeader(title: title)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
if isLoading {
|
||||
ForEach(0..<5, id: \.self) { _ in
|
||||
HomeSkeletonCell()
|
||||
}
|
||||
} else {
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||
AnimeItemCell(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SectionHeader: View {
|
||||
let title: String
|
||||
|
||||
var body: some View {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
struct AnimeItemCell: View {
|
||||
let item: AniListItem
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
KFImage(URL(string: item.coverImage.large))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 130, height: 195)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 130, height: 195)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
|
||||
Text(item.title.romaji)
|
||||
.font(.caption)
|
||||
.frame(width: 130)
|
||||
.lineLimit(1)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBEFD92D5F7D1200D011EE /* String.swift */; };
|
||||
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; };
|
||||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */; };
|
||||
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; };
|
||||
13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; };
|
||||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; };
|
||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; };
|
||||
|
|
@ -108,6 +109,7 @@
|
|||
13CBEFD92D5F7D1200D011EE /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
13D842542D45267500EBBFA6 /* DropManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropManager.swift; sourceTree = "<group>"; };
|
||||
13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleAdditionSettingsView.swift; sourceTree = "<group>"; };
|
||||
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = "<group>"; };
|
||||
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
||||
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -416,6 +418,7 @@
|
|||
13EA2BD22D32D97400C1EBD7 /* Components */,
|
||||
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */,
|
||||
13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */,
|
||||
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */,
|
||||
);
|
||||
path = CustomPlayer;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -524,6 +527,7 @@
|
|||
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
||||
1334FF562D7872E9007E289F /* SettingsViewTrackingServices.swift in Sources */,
|
||||
136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */,
|
||||
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */,
|
||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
||||
1334FF522D7871B7007E289F /* TMDBItem.swift in Sources */,
|
||||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue