mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-28 13:28:45 +00:00
shooting like im curry or lebron 🏀 (#144)
This commit is contained in:
parent
991ff443c2
commit
ceea5c9206
6 changed files with 493 additions and 77 deletions
|
|
@ -60,6 +60,20 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
return UserDefaults.standard.bool(forKey: "doubleTapSeekEnabled")
|
||||
}
|
||||
|
||||
private var isPipAutoEnabled: Bool {
|
||||
UserDefaults.standard.bool(forKey: "pipAutoEnabled")
|
||||
}
|
||||
|
||||
private var isPipButtonVisible: Bool {
|
||||
if UserDefaults.standard.object(forKey: "pipButtonVisible") == nil {
|
||||
return true
|
||||
}
|
||||
return UserDefaults.standard.bool(forKey: "pipButtonVisible")
|
||||
}
|
||||
private var pipController: AVPictureInPictureController?
|
||||
private var pipButton: UIButton!
|
||||
|
||||
|
||||
var portraitButtonVisibleConstraints: [NSLayoutConstraint] = []
|
||||
var portraitButtonHiddenConstraints: [NSLayoutConstraint] = []
|
||||
var landscapeButtonVisibleConstraints: [NSLayoutConstraint] = []
|
||||
|
|
@ -259,6 +273,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
setupAudioSession()
|
||||
updateSkipButtonsVisibility()
|
||||
setupHoldSpeedIndicator()
|
||||
setupPipIfSupported()
|
||||
|
||||
view.bringSubviewToFront(subtitleStackView)
|
||||
|
||||
|
|
@ -1189,6 +1204,53 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
private func setupPipIfSupported() {
|
||||
guard AVPictureInPictureController.isPictureInPictureSupported() else {
|
||||
return
|
||||
}
|
||||
let pipPlayerLayer = AVPlayerLayer(player: playerViewController.player)
|
||||
pipPlayerLayer.frame = playerViewController.view.layer.bounds
|
||||
pipPlayerLayer.videoGravity = .resizeAspect
|
||||
|
||||
playerViewController.view.layer.insertSublayer(pipPlayerLayer, at: 0)
|
||||
pipController = AVPictureInPictureController(playerLayer: pipPlayerLayer)
|
||||
pipController?.delegate = self
|
||||
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
|
||||
let Image = UIImage(systemName: "pip", withConfiguration: config)
|
||||
pipButton = UIButton(type: .system)
|
||||
pipButton.setImage(Image, for: .normal)
|
||||
pipButton.tintColor = .white
|
||||
pipButton.addTarget(self, action: #selector(pipButtonTapped(_:)), for: .touchUpInside)
|
||||
|
||||
pipButton.layer.shadowColor = UIColor.black.cgColor
|
||||
pipButton.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
pipButton.layer.shadowOpacity = 0.6
|
||||
pipButton.layer.shadowRadius = 4
|
||||
pipButton.layer.masksToBounds = false
|
||||
|
||||
controlsContainerView.addSubview(pipButton)
|
||||
pipButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// NEW: pin pipButton to the left of lockButton:
|
||||
NSLayoutConstraint.activate([
|
||||
pipButton.centerYAnchor.constraint(equalTo: dimButton.centerYAnchor),
|
||||
pipButton.trailingAnchor.constraint(equalTo: dimButton.leadingAnchor, constant: -8),
|
||||
pipButton.widthAnchor.constraint(equalToConstant: 44),
|
||||
pipButton.heightAnchor.constraint(equalToConstant: 44)
|
||||
])
|
||||
|
||||
pipButton.isHidden = !isPipButtonVisible
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(startPipIfNeeded),
|
||||
name: UIApplication.willResignActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func setupMenuButton() {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
|
||||
let image = UIImage(systemName: "text.bubble", withConfiguration: config)
|
||||
|
|
@ -1645,6 +1707,24 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func pipButtonTapped(_ sender: UIButton) {
|
||||
guard let pip = pipController else { return }
|
||||
if pip.isPictureInPictureActive {
|
||||
pip.stopPictureInPicture()
|
||||
} else {
|
||||
pip.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func startPipIfNeeded() {
|
||||
guard isPipAutoEnabled,
|
||||
let pip = pipController,
|
||||
!pip.isPictureInPictureActive else {
|
||||
return
|
||||
}
|
||||
pip.startPictureInPicture()
|
||||
}
|
||||
|
||||
@objc private func lockTapped() {
|
||||
controlsLocked.toggle()
|
||||
|
||||
|
|
@ -2494,8 +2574,25 @@ class GradientOverlayButton: UIButton {
|
|||
}
|
||||
}
|
||||
|
||||
extension CustomMediaPlayerViewController: AVPictureInPictureControllerDelegate {
|
||||
func pictureInPictureControllerWillStartPictureInPicture(_ pipController: AVPictureInPictureController) {
|
||||
pipButton.alpha = 0.5
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(_ pipController: AVPictureInPictureController) {
|
||||
pipButton.alpha = 1.0
|
||||
}
|
||||
|
||||
func pictureInPictureController(_ pipController: AVPictureInPictureController,
|
||||
failedToStartPictureInPictureWithError error: Error) {
|
||||
|
||||
Logger.shared.log("PiP failed to start: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
// yes? Like the plural of the famous american rapper ye? -IBHRAD
|
||||
// low taper fade the meme is massive -cranci
|
||||
// cranci still doesnt have a job -seiike
|
||||
// The mind is the source of good and evil, only you yourself can decide which you will bring yourself. -seiike
|
||||
// guys watch Clannad already - ibro
|
||||
// May the Divine Providence bestow its infinite mercy upon your soul, and may eternal grace find you beyond the shadows of this mortal realm. - paul, 15/11/2005 - 13/05/2023
|
||||
// May the Divine Providence bestow its infinite mercy upon your soul, and may eternal grace find you beyond the shadows of this mortal realm. - paul, 15/11/2005 - 13/05/2023
|
||||
// this dumbass ↑ defo used gpt
|
||||
|
|
|
|||
213
Sora/Views/MediaInfoView/AnilistMatchPopupView.swift
Normal file
213
Sora/Views/MediaInfoView/AnilistMatchPopupView.swift
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
//
|
||||
// AnilistMatchPopupView.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by seiike on 01/06/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct AnilistMatchPopupView: View {
|
||||
let seriesTitle: String
|
||||
let onSelect: (Int) -> Void
|
||||
|
||||
@State private var results: [[String: Any]] = []
|
||||
@State private var isLoading = true
|
||||
|
||||
@AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
private var isLightMode: Bool {
|
||||
selectedAppearance == .light
|
||||
|| (selectedAppearance == .system && colorScheme == .light)
|
||||
}
|
||||
|
||||
@State private var manualIDText: String = ""
|
||||
@State private var showingManualIDAlert = false
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// (Optional) A hidden header; can be omitted if empty
|
||||
Text("".uppercased())
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.horizontal, 10)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
} else if results.isEmpty {
|
||||
Text("No matches found")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.gray)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
} else {
|
||||
LazyVStack(spacing: 15) {
|
||||
ForEach(results.indices, id: \.self) { index in
|
||||
let result = results[index]
|
||||
|
||||
Button(action: {
|
||||
if let id = result["id"] as? Int {
|
||||
onSelect(id)
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 12) {
|
||||
if let cover = result["cover"] as? String,
|
||||
let url = URL(string: cover) {
|
||||
KFImage(url)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 50, height: 70)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(result["title"] as? String ?? "Unknown")
|
||||
.font(.body)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if let english = result["title_english"] as? String {
|
||||
Text(english)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(11)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: Color.accentColor.opacity(0.25), location: 0),
|
||||
.init(color: Color.accentColor.opacity(0), location: 1)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 0.5
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 15))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 16)
|
||||
}
|
||||
}
|
||||
|
||||
if !results.isEmpty {
|
||||
Text("Tap a title to override the current match.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
.navigationTitle("AniList Match")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
.foregroundColor(isLightMode ? .black : .white)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
manualIDText = ""
|
||||
showingManualIDAlert = true
|
||||
}) {
|
||||
Image(systemName: "number")
|
||||
.foregroundColor(isLightMode ? .black : .white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Set Custom AniList ID", isPresented: $showingManualIDAlert, actions: {
|
||||
TextField("AniList ID", text: $manualIDText)
|
||||
.keyboardType(.numberPad)
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Save", action: {
|
||||
if let idInt = Int(manualIDText.trimmingCharacters(in: .whitespaces)) {
|
||||
onSelect(idInt)
|
||||
dismiss()
|
||||
}
|
||||
})
|
||||
}, message: {
|
||||
Text("Enter the AniList ID for this media")
|
||||
})
|
||||
}
|
||||
.onAppear(perform: fetchMatches)
|
||||
}
|
||||
|
||||
private func fetchMatches() {
|
||||
let query = """
|
||||
query {
|
||||
Page(page: 1, perPage: 6) {
|
||||
media(search: "\(seriesTitle)", type: ANIME) {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
guard let url = URL(string: "https://graphql.anilist.co") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: ["query": query])
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, _, _ in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
|
||||
guard let data = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataDict = json["data"] as? [String: Any],
|
||||
let page = dataDict["Page"] as? [String: Any],
|
||||
let mediaList = page["media"] as? [[String: Any]] else {
|
||||
return
|
||||
}
|
||||
|
||||
self.results = mediaList.map { media in
|
||||
let titleInfo = media["title"] as? [String: Any]
|
||||
let cover = (media["coverImage"] as? [String: Any])?["large"] as? String
|
||||
|
||||
return [
|
||||
"id": media["id"] ?? 0,
|
||||
"title": titleInfo?["romaji"] ?? "Unknown",
|
||||
"title_english": titleInfo?["english"],
|
||||
"cover": cover
|
||||
]
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
|
@ -231,6 +231,15 @@ struct EpisodeCell: View {
|
|||
.onChange(of: progress) { _ in
|
||||
updateProgress()
|
||||
}
|
||||
.onChange(of: itemID) { newID in
|
||||
// 1) Clear any cached title/image so that the UI shows the loading spinner:
|
||||
loadedFromCache = false
|
||||
isLoading = true
|
||||
retryAttempts = maxRetryAttempts // reset retries if you want
|
||||
|
||||
// 2) Call the same logic you already use to pull per-episode info:
|
||||
fetchEpisodeDetails()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
updateDownloadStatus()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ struct MediaInfoView: View {
|
|||
|
||||
@State private var isModuleSelectorPresented = false
|
||||
@State private var isError = false
|
||||
@State private var isMatchingPresented = false
|
||||
@State private var matchedTitle: String? = nil
|
||||
|
||||
@StateObject private var jsController = JSController.shared
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
|
|
@ -150,6 +152,10 @@ struct MediaInfoView: View {
|
|||
.onAppear {
|
||||
buttonRefreshTrigger.toggle()
|
||||
|
||||
let savedID = UserDefaults.standard.integer(forKey: "custom_anilist_id_\(href)")
|
||||
if savedID != 0 {
|
||||
customAniListID = savedID }
|
||||
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
|
||||
if !hasFetched {
|
||||
|
|
@ -212,14 +218,14 @@ struct MediaInfoView: View {
|
|||
.fill(Color.gray.opacity(0.3))
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(ImageUpscaler.lanczosProcessor(scale: 3, sharpeningIntensity: 1, sharpeningRadius: 1))
|
||||
.setProcessor(ImageUpscaler.lanczosProcessor(scale: 3, sharpeningIntensity: 1.5, sharpeningRadius: 0.8))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: UIScreen.main.bounds.width, height: 600)
|
||||
.clipped()
|
||||
KFImage(URL(string: imageUrl))
|
||||
.placeholder { EmptyView() }
|
||||
.setProcessor(ImageUpscaler.lanczosProcessor(scale: 3, sharpeningIntensity: 1, sharpeningRadius: 1))
|
||||
.setProcessor(ImageUpscaler.lanczosProcessor(scale: 3, sharpeningIntensity: 1.5, sharpeningRadius: 0.8))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: UIScreen.main.bounds.width, height: 600)
|
||||
|
|
@ -458,16 +464,22 @@ struct MediaInfoView: View {
|
|||
@ViewBuilder
|
||||
private var menuButton: some View {
|
||||
Menu {
|
||||
Button(action: {
|
||||
showCustomIDAlert()
|
||||
}) {
|
||||
Label("Set Custom AniList ID", systemImage: "number")
|
||||
// Show current match (title if available, else ID)
|
||||
if let id = itemID ?? customAniListID {
|
||||
let labelText = (matchedTitle?.isEmpty == false ? matchedTitle! : "\(id)")
|
||||
Text("Matched with: \(labelText)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
if let customID = customAniListID {
|
||||
|
||||
Divider()
|
||||
|
||||
if let _ = customAniListID {
|
||||
Button(action: {
|
||||
customAniListID = nil
|
||||
itemID = nil
|
||||
matchedTitle = nil
|
||||
fetchItemID(byTitle: cleanTitle(title)) { result in
|
||||
switch result {
|
||||
case .success(let id):
|
||||
|
|
@ -490,12 +502,30 @@ struct MediaInfoView: View {
|
|||
Label("Open in AniList", systemImage: "link")
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
isMatchingPresented = true
|
||||
}) {
|
||||
Label("Match with AniList", systemImage: "magnifyingglass")
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button(action: {
|
||||
Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: "Debug")
|
||||
DropManager.shared.showDrop(title: "Debug Info Logged", subtitle: "", duration: 1.0, icon: UIImage(systemName: "terminal"))
|
||||
Logger.shared.log("""
|
||||
Debug Info:
|
||||
Title: \(title)
|
||||
Href: \(href)
|
||||
Module: \(module.metadata.sourceName)
|
||||
AniList ID: \(itemID ?? -1)
|
||||
Custom ID: \(customAniListID ?? -1)
|
||||
Matched Title: \(matchedTitle ?? "—")
|
||||
""", type: "Debug")
|
||||
DropManager.shared.showDrop(
|
||||
title: "Debug Info Logged",
|
||||
subtitle: "",
|
||||
duration: 1.0,
|
||||
icon: UIImage(systemName: "terminal")
|
||||
)
|
||||
}) {
|
||||
Label("Log Debug Info", systemImage: "terminal")
|
||||
}
|
||||
|
|
@ -509,6 +539,16 @@ struct MediaInfoView: View {
|
|||
.clipShape(Circle())
|
||||
.circularGradientOutline()
|
||||
}
|
||||
.sheet(isPresented: $isMatchingPresented) {
|
||||
AnilistMatchPopupView(seriesTitle: title) { selectedID in
|
||||
// 1) Assign the new AniList ID:
|
||||
self.customAniListID = selectedID
|
||||
self.itemID = selectedID
|
||||
UserDefaults.standard.set(selectedID, forKey: "custom_anilist_id_\(href)")
|
||||
|
||||
isMatchingPresented = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -638,41 +678,43 @@ struct MediaInfoView: View {
|
|||
private var seasonsEpisodeList: some View {
|
||||
let seasons = groupedEpisodes()
|
||||
if !seasons.isEmpty, selectedSeason < seasons.count {
|
||||
ForEach(seasons[selectedSeason]) { ep in
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
|
||||
let defaultBannerImageValue = getBannerImageBasedOnAppearance()
|
||||
|
||||
EpisodeCell(
|
||||
episodeIndex: selectedSeason,
|
||||
episode: ep.href,
|
||||
episodeID: ep.number - 1,
|
||||
progress: progress,
|
||||
itemID: itemID ?? 0,
|
||||
totalEpisodes: episodeLinks.count,
|
||||
defaultBannerImage: defaultBannerImageValue,
|
||||
module: module,
|
||||
parentTitle: title,
|
||||
showPosterURL: imageUrl,
|
||||
isMultiSelectMode: isMultiSelectMode,
|
||||
isSelected: selectedEpisodes.contains(ep.number),
|
||||
onSelectionChanged: { isSelected in
|
||||
if isSelected {
|
||||
selectedEpisodes.insert(ep.number)
|
||||
} else {
|
||||
selectedEpisodes.remove(ep.number)
|
||||
LazyVStack(spacing: 15) {
|
||||
ForEach(seasons[selectedSeason]) { ep in
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
|
||||
let defaultBannerImageValue = getBannerImageBasedOnAppearance()
|
||||
|
||||
EpisodeCell(
|
||||
episodeIndex: selectedSeason,
|
||||
episode: ep.href,
|
||||
episodeID: ep.number - 1,
|
||||
progress: progress,
|
||||
itemID: itemID ?? 0,
|
||||
totalEpisodes: episodeLinks.count,
|
||||
defaultBannerImage: defaultBannerImageValue,
|
||||
module: module,
|
||||
parentTitle: title,
|
||||
showPosterURL: imageUrl,
|
||||
isMultiSelectMode: isMultiSelectMode,
|
||||
isSelected: selectedEpisodes.contains(ep.number),
|
||||
onSelectionChanged: { isSelected in
|
||||
if isSelected {
|
||||
selectedEpisodes.insert(ep.number)
|
||||
} else {
|
||||
selectedEpisodes.remove(ep.number)
|
||||
}
|
||||
},
|
||||
onTap: { imageUrl in
|
||||
episodeTapAction(ep: ep, imageUrl: imageUrl)
|
||||
},
|
||||
onMarkAllPrevious: {
|
||||
markAllPreviousEpisodesAsWatched(ep: ep, inSeason: true)
|
||||
}
|
||||
},
|
||||
onTap: { imageUrl in
|
||||
episodeTapAction(ep: ep, imageUrl: imageUrl)
|
||||
},
|
||||
onMarkAllPrevious: {
|
||||
markAllPreviousEpisodesAsWatched(ep: ep, inSeason: true)
|
||||
}
|
||||
)
|
||||
)
|
||||
.disabled(isFetchingEpisode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("No episodes available")
|
||||
|
|
@ -721,43 +763,86 @@ struct MediaInfoView: View {
|
|||
|
||||
@ViewBuilder
|
||||
private var flatEpisodeList: some View {
|
||||
ForEach(episodeLinks.indices.filter { selectedRange.contains($0) }, id: \.self) { i in
|
||||
let ep = episodeLinks[i]
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
|
||||
EpisodeCell(
|
||||
episodeIndex: i,
|
||||
episode: ep.href,
|
||||
episodeID: ep.number - 1,
|
||||
progress: progress,
|
||||
itemID: itemID ?? 0,
|
||||
totalEpisodes: episodeLinks.count,
|
||||
defaultBannerImage: getBannerImageBasedOnAppearance(),
|
||||
module: module,
|
||||
parentTitle: title,
|
||||
showPosterURL: imageUrl,
|
||||
isMultiSelectMode: isMultiSelectMode,
|
||||
isSelected: selectedEpisodes.contains(ep.number),
|
||||
onSelectionChanged: { isSelected in
|
||||
if isSelected {
|
||||
selectedEpisodes.insert(ep.number)
|
||||
} else {
|
||||
selectedEpisodes.remove(ep.number)
|
||||
LazyVStack(spacing: 15) {
|
||||
ForEach(episodeLinks.indices.filter { selectedRange.contains($0) }, id: \.self) { i in
|
||||
let ep = episodeLinks[i]
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
|
||||
let defaultBannerImageValue = getBannerImageBasedOnAppearance()
|
||||
|
||||
EpisodeCell(
|
||||
episodeIndex: i,
|
||||
episode: ep.href,
|
||||
episodeID: ep.number - 1,
|
||||
progress: progress,
|
||||
itemID: itemID ?? 0,
|
||||
totalEpisodes: episodeLinks.count,
|
||||
defaultBannerImage: defaultBannerImageValue,
|
||||
module: module,
|
||||
parentTitle: title,
|
||||
showPosterURL: imageUrl,
|
||||
isMultiSelectMode: isMultiSelectMode,
|
||||
isSelected: selectedEpisodes.contains(ep.number),
|
||||
onSelectionChanged: { isSelected in
|
||||
if isSelected {
|
||||
selectedEpisodes.insert(ep.number)
|
||||
} else {
|
||||
selectedEpisodes.remove(ep.number)
|
||||
}
|
||||
},
|
||||
onTap: { imageUrl in
|
||||
episodeTapAction(ep: ep, imageUrl: imageUrl)
|
||||
},
|
||||
onMarkAllPrevious: {
|
||||
markAllPreviousEpisodesInFlatList(ep: ep, index: i)
|
||||
}
|
||||
},
|
||||
onTap: { imageUrl in
|
||||
episodeTapAction(ep: ep, imageUrl: imageUrl)
|
||||
},
|
||||
onMarkAllPrevious: {
|
||||
markAllPreviousEpisodesInFlatList(ep: ep, index: i)
|
||||
}
|
||||
)
|
||||
)
|
||||
.disabled(isFetchingEpisode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchAniListTitle(id: Int) {
|
||||
let query = """
|
||||
query ($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
let variables: [String: Any] = ["id": id]
|
||||
|
||||
guard let url = URL(string: "https://graphql.anilist.co") else { return }
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = try? JSONSerialization.data(withJSONObject: ["query": query, "variables": variables])
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, _, _ in
|
||||
guard
|
||||
let data = data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let dataDict = json["data"] as? [String: Any],
|
||||
let media = dataDict["Media"] as? [String: Any],
|
||||
let titleDict = media["title"] as? [String: Any]
|
||||
else { return }
|
||||
|
||||
let english = titleDict["english"] as? String
|
||||
let romaji = titleDict["romaji"] as? String
|
||||
let finalTitle = (english?.isEmpty == false ? english! : (romaji ?? "Unknown"))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
matchedTitle = finalTitle
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
|
||||
private func markAllPreviousEpisodesInFlatList(ep: EpisodeLink, index: Int) {
|
||||
let userDefaults = UserDefaults.standard
|
||||
var updates = [String: Double]()
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ struct SettingsViewPlayer: View {
|
|||
@AppStorage("skip85Visible") private var skip85Visible: Bool = true
|
||||
@AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false
|
||||
@AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true
|
||||
@AppStorage("pipButtonVisible") private var pipButtonVisible: Bool = true
|
||||
|
||||
private let mediaPlayers = ["Default", "Sora", "VLC", "OutPlayer", "Infuse", "nPlayer", "SenPlayer", "IINA"]
|
||||
private let inAppPlayers = ["Default", "Sora"]
|
||||
|
|
@ -235,6 +236,13 @@ struct SettingsViewPlayer: View {
|
|||
isOn: $holdForPauseEnabled,
|
||||
showDivider: false
|
||||
)
|
||||
|
||||
SettingsToggleRow(
|
||||
icon: "pip",
|
||||
title: "Show PiP Button",
|
||||
isOn: $pipButtonVisible,
|
||||
showDivider: false
|
||||
)
|
||||
}
|
||||
|
||||
SettingsSection(title: "Speed Settings") {
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@
|
|||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; };
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; };
|
||||
1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */; };
|
||||
1E47859B2DEBC5960095BF2F /* AnilistMatchPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */; };
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
|
||||
1EA64DCD2DE5030100AC14BC /* ImageUpscaler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */; };
|
||||
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; };
|
||||
|
|
@ -178,6 +179,7 @@
|
|||
13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||
1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = "<group>"; };
|
||||
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchPopupView.swift; sourceTree = "<group>"; };
|
||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
|
||||
1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUpscaler.swift; sourceTree = "<group>"; };
|
||||
1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -361,6 +363,7 @@
|
|||
133D7C7F2D2BE2630075467E /* MediaInfoView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */,
|
||||
138AA1B52D2D66EC0021F9DF /* EpisodeCell */,
|
||||
133D7C802D2BE2630075467E /* MediaInfoView.swift */,
|
||||
);
|
||||
|
|
@ -730,6 +733,7 @@
|
|||
0402DA172DE7B7B8003BB42C /* SearchViewComponents.swift in Sources */,
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
||||
0402DA0E2DE7AA01003BB42C /* TabBarController.swift in Sources */,
|
||||
1E47859B2DEBC5960095BF2F /* AnilistMatchPopupView.swift in Sources */,
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
|
||||
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
||||
0457C5972DE7712A000AFBD9 /* DeviceScaleModifier.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue