mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 00:22:12 +00:00
This commit is contained in:
parent
e13333afe7
commit
eb65912998
1 changed files with 197 additions and 194 deletions
|
|
@ -40,233 +40,236 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationView {
|
||||||
ScrollView {
|
VStack {
|
||||||
if !continueWatchingItems.isEmpty {
|
ScrollView {
|
||||||
LazyVStack(alignment: .leading) {
|
if !continueWatchingItems.isEmpty {
|
||||||
Text("Continue Watching")
|
LazyVStack(alignment: .leading) {
|
||||||
.font(.headline)
|
Text("Continue Watching")
|
||||||
.padding(.horizontal, 8)
|
.font(.headline)
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
.padding(.horizontal, 8)
|
||||||
HStack(spacing: 8) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
ForEach(Array(continueWatchingItems.reversed())) { item in
|
HStack(spacing: 8) {
|
||||||
Button(action: {
|
ForEach(Array(continueWatchingItems.reversed())) { item in
|
||||||
if UserDefaults.standard.string(forKey: "externalPlayer") == "Sora" {
|
Button(action: {
|
||||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
if UserDefaults.standard.string(forKey: "externalPlayer") == "Sora" {
|
||||||
module: item.module,
|
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||||
urlString: item.streamUrl,
|
module: item.module,
|
||||||
fullUrl: item.fullUrl,
|
urlString: item.streamUrl,
|
||||||
title: item.mediaTitle,
|
fullUrl: item.fullUrl,
|
||||||
episodeNumber: item.episodeNumber,
|
title: item.mediaTitle,
|
||||||
onWatchNext: { },
|
episodeNumber: item.episodeNumber,
|
||||||
subtitlesURL: item.subtitles,
|
onWatchNext: { },
|
||||||
episodeImageUrl: item.imageUrl
|
subtitlesURL: item.subtitles,
|
||||||
)
|
episodeImageUrl: item.imageUrl
|
||||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
)
|
||||||
|
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
||||||
let rootVC = windowScene.windows.first?.rootViewController {
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}) {
|
||||||
let videoPlayerViewController = VideoPlayerViewController(module: item.module)
|
VStack(alignment: .leading) {
|
||||||
videoPlayerViewController.streamUrl = item.streamUrl
|
ZStack {
|
||||||
videoPlayerViewController.fullUrl = item.fullUrl
|
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl))
|
||||||
videoPlayerViewController.episodeImageUrl = item.imageUrl
|
.placeholder {
|
||||||
videoPlayerViewController.episodeNumber = item.episodeNumber
|
RoundedRectangle(cornerRadius: 10)
|
||||||
videoPlayerViewController.mediaTitle = item.mediaTitle
|
.fill(Color.gray.opacity(0.3))
|
||||||
videoPlayerViewController.subtitles = item.subtitles ?? ""
|
.frame(width: 240, height: 135)
|
||||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
.shimmering()
|
||||||
|
}
|
||||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||||
let rootVC = windowScene.windows.first?.rootViewController {
|
.resizable()
|
||||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
.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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}
|
||||||
VStack(alignment: .leading) {
|
}
|
||||||
ZStack {
|
.padding(.horizontal, 8)
|
||||||
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl))
|
}
|
||||||
|
.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 {
|
.placeholder {
|
||||||
RoundedRectangle(cornerRadius: 10)
|
RoundedRectangle(cornerRadius: 10)
|
||||||
.fill(Color.gray.opacity(0.3))
|
.fill(Color.gray.opacity(0.3))
|
||||||
.frame(width: 240, height: 135)
|
.frame(width: 130, height: 195)
|
||||||
.shimmering()
|
.shimmering()
|
||||||
}
|
}
|
||||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(16/9, contentMode: .fill)
|
.scaledToFill()
|
||||||
.frame(width: 240, height: 135)
|
.frame(width: 130, height: 195)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.clipped()
|
.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)
|
Text(item.title.romaji)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.lineLimit(2)
|
.frame(width: 130)
|
||||||
|
.lineLimit(1)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
.foregroundColor(.primary)
|
.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)
|
.padding(.horizontal, 8)
|
||||||
}
|
}
|
||||||
.frame(height: 190)
|
|
||||||
}
|
HStack(alignment: .bottom, spacing: 5) {
|
||||||
}
|
Text("Trending")
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
.font(.headline)
|
||||||
HStack(alignment: .bottom, spacing: 5) {
|
Text("on \(trendingDateString)")
|
||||||
Text("Seasonal")
|
.font(.subheadline)
|
||||||
.font(.headline)
|
.foregroundColor(.gray)
|
||||||
Text("of \(currentDeviceSeasonAndYear.season) \(String(format: "%d", currentDeviceSeasonAndYear.year))")
|
}
|
||||||
.font(.subheadline)
|
.padding(.horizontal, 8)
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
.padding(.horizontal, 8)
|
HStack(spacing: 8) {
|
||||||
.padding(.top, 8)
|
if trendingItems.isEmpty {
|
||||||
|
ForEach(0..<5, id: \.self) { _ in
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
HomeSkeletonCell()
|
||||||
HStack(spacing: 8) {
|
}
|
||||||
if aniListItems.isEmpty {
|
} else {
|
||||||
ForEach(0..<5, id: \.self) { _ in
|
ForEach(trendingItems, id: \.id) { item in
|
||||||
HomeSkeletonCell()
|
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||||
}
|
VStack {
|
||||||
} else {
|
KFImage(URL(string: item.coverImage.large))
|
||||||
ForEach(aniListItems, id: \.id) { item in
|
.placeholder {
|
||||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
RoundedRectangle(cornerRadius: 10)
|
||||||
VStack {
|
.fill(Color.gray.opacity(0.3))
|
||||||
KFImage(URL(string: item.coverImage.large))
|
.frame(width: 130, height: 195)
|
||||||
.placeholder {
|
.shimmering()
|
||||||
RoundedRectangle(cornerRadius: 10)
|
}
|
||||||
.fill(Color.gray.opacity(0.3))
|
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||||
.frame(width: 130, height: 195)
|
.resizable()
|
||||||
.shimmering()
|
.scaledToFill()
|
||||||
}
|
.frame(width: 130, height: 195)
|
||||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
.cornerRadius(10)
|
||||||
.resizable()
|
.clipped()
|
||||||
.scaledToFill()
|
|
||||||
.frame(width: 130, height: 195)
|
Text(item.title.romaji)
|
||||||
.cornerRadius(10)
|
.font(.caption)
|
||||||
.clipped()
|
.frame(width: 130)
|
||||||
|
.lineLimit(1)
|
||||||
Text(item.title.romaji)
|
.multilineTextAlignment(.center)
|
||||||
.font(.caption)
|
.foregroundColor(.primary)
|
||||||
.frame(width: 130)
|
}
|
||||||
.lineLimit(1)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
|
||||||
}
|
}
|
||||||
|
.padding(.bottom, 16)
|
||||||
HStack(alignment: .bottom, spacing: 5) {
|
}
|
||||||
Text("Trending")
|
.navigationTitle("Home")
|
||||||
.font(.headline)
|
}
|
||||||
Text("on \(trendingDateString)")
|
.onAppear {
|
||||||
.font(.subheadline)
|
continueWatchingItems = ContinueWatchingManager.shared.fetchItems()
|
||||||
.foregroundColor(.gray)
|
AnilistServiceSeasonalAnime().fetchSeasonalAnime { items in
|
||||||
}
|
if let items = items {
|
||||||
.padding(.horizontal, 8)
|
aniListItems = items
|
||||||
|
|
||||||
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)
|
AnilistServiceTrendingAnime().fetchTrendingAnime { items in
|
||||||
}
|
if let items = items {
|
||||||
.navigationTitle("Home")
|
trendingItems = items
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
continueWatchingItems = ContinueWatchingManager.shared.fetchItems()
|
|
||||||
AnilistServiceSeasonalAnime().fetchSeasonalAnime { items in
|
|
||||||
if let items = items {
|
|
||||||
aniListItems = items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AnilistServiceTrendingAnime().fetchTrendingAnime { items in
|
|
||||||
if let items = items {
|
|
||||||
trendingItems = items
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func markContinueWatchingItemAsWatched(item: ContinueWatchingItem) {
|
private func markContinueWatchingItemAsWatched(item: ContinueWatchingItem) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue