HOLY MOLY MAN

This commit is contained in:
cranci1 2025-03-09 10:45:31 +01:00
parent e66de7f5d4
commit 2f91209647
4 changed files with 485 additions and 267 deletions

View file

@ -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

View file

@ -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(&currentSettings)
settings = currentSettings
}
}

View file

@ -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)
}
}
}

View file

@ -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 */,