subs loader added now module support 😭

This commit is contained in:
cranci1 2025-02-15 17:08:01 +01:00
parent 9aa56fa8a3
commit 1f48612a67
3 changed files with 162 additions and 2 deletions

View file

@ -40,6 +40,7 @@
13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; };
13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; };
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */; };
13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */; };
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBEFD92D5F7D1200D011EE /* String.swift */; };
13D842522D4523B800EBBFA6 /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 13D842512D4523B800EBBFA6 /* Drops */; };
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; };
@ -88,6 +89,7 @@
13B7F4C02D58FFDD0045714A /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = "<group>"; };
13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingManager.swift; sourceTree = "<group>"; };
13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingItem.swift; sourceTree = "<group>"; };
13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VTTSubtitlesLoader.swift; sourceTree = "<group>"; };
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>"; };
@ -360,6 +362,7 @@
children = (
13EA2BD22D32D97400C1EBD7 /* Components */,
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */,
13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */,
);
path = CustomPlayer;
sourceTree = "<group>";
@ -474,6 +477,7 @@
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */,
13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */,
13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */,
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,

View file

@ -5,8 +5,8 @@
// Created by Francesco on 20/12/24.
//
import SwiftUI
import AVKit
import SwiftUI
struct CustomVideoPlayer: UIViewControllerRepresentable {
let player: AVPlayer
@ -35,15 +35,35 @@ struct CustomMediaPlayer: View {
@State private var timeObserverToken: Any?
@State private var isVideoLoaded = false
@State private var showWatchNextButton = true
@ObservedObject private var subtitlesLoader = VTTSubtitlesLoader()
@AppStorage("subtitleForegroundColor") private var subtitleForegroundColor: String = "white"
@AppStorage("subtitleBackgroundEnabled") private var subtitleBackgroundEnabled: Bool = true
@AppStorage("subtitleFontSize") private var subtitleFontSize: Double = 20.0
@AppStorage("subtitleShadowRadius") private var subtitleShadowRadius: Double = 1.0
private var subtitleFGColor: Color {
switch subtitleForegroundColor {
case "white": return Color.white
case "yellow": return Color.yellow
case "green": return Color.green
case "purple": return Color.purple
case "blue": return Color.blue
case "red": return Color.red
default: return Color.white
}
}
@Environment(\.presentationMode) var presentationMode
let module: ScrapingModule
let fullUrl: String
let title: String
let episodeNumber: Int
let subtitlesURL: String?
let onWatchNext: () -> Void
init(module: ScrapingModule, urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void) {
init(module: ScrapingModule, urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void, subtitlesURL: String?) {
guard let url = URL(string: urlString) else {
fatalError("Invalid URL string")
}
@ -61,6 +81,7 @@ struct CustomMediaPlayer: View {
self.title = title
self.episodeNumber = episodeNumber
self.onWatchNext = onWatchNext
self.subtitlesURL = subtitlesURL ?? ""
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
if lastPlayedTime > 0 {
@ -85,6 +106,10 @@ struct CustomMediaPlayer: View {
startUpdatingCurrentTime()
setInitialPlayerRate()
addPeriodicTimeObserver(fullURL: fullUrl)
if let url = subtitlesURL, !url.isEmpty {
subtitlesLoader.load(from: url)
}
}
.edgesIgnoringSafeArea(.all)
.overlay(
@ -141,6 +166,20 @@ struct CustomMediaPlayer: View {
}
}
VStack {
Spacer()
if let currentCue = subtitlesLoader.cues.first(where: { currentTime >= $0.startTime && currentTime <= $0.endTime }) {
Text(currentCue.text)
.font(.system(size: CGFloat(subtitleFontSize)))
.multilineTextAlignment(.center)
.padding(8)
.background(subtitleBackgroundEnabled ? Color.black.opacity(0.6) : Color.clear)
.foregroundColor(subtitleFGColor)
.cornerRadius(5)
.shadow(color: Color.black, radius: CGFloat(subtitleShadowRadius))
}
}
VStack {
Spacer()
VStack {
@ -202,6 +241,38 @@ struct CustomMediaPlayer: View {
.foregroundColor(.white)
.font(.system(size: 15))
}
Menu {
Menu("Subtitle Foreground Color") {
Button("White") { subtitleForegroundColor = "white" }
Button("Yellow") { subtitleForegroundColor = "yellow" }
Button("Green") { subtitleForegroundColor = "green" }
Button("Blue") { subtitleForegroundColor = "blue" }
Button("Red") { subtitleForegroundColor = "red" }
Button("Purple") { subtitleForegroundColor = "purple" }
}
Menu("Subtitle Font Size") {
Button("16") { subtitleFontSize = 16 }
Button("18") { subtitleFontSize = 18 }
Button("20") { subtitleFontSize = 20 }
Button("22") { subtitleFontSize = 22 }
Button("24") { subtitleFontSize = 24 }
}
Menu("Subtitle Shadow Intensity") {
Button("None") { subtitleShadowRadius = 0 }
Button("Low") { subtitleShadowRadius = 1 }
Button("Medium") { subtitleShadowRadius = 3 }
Button("High") { subtitleShadowRadius = 6 }
}
Button(action: {
subtitleBackgroundEnabled.toggle()
}) {
Text(subtitleBackgroundEnabled ? "Disable Background" : "Enable Background")
}
} label: {
Image(systemName: "text.bubble")
.foregroundColor(.white)
.font(.system(size: 15))
}
}
}
.padding(.trailing, 32)

View file

@ -0,0 +1,85 @@
//
// VTTSubtitlesLoader.swift
// Sora
//
// Created by Francesco on 15/02/25.
//
import Combine
import Foundation
struct SubtitleCue: Identifiable {
let id = UUID()
let startTime: Double
let endTime: Double
let text: String
}
class VTTSubtitlesLoader: ObservableObject {
@Published var cues: [SubtitleCue] = []
func load(from urlString: String) {
guard let url = URL(string: urlString) else { return }
URLSession.custom.dataTask(with: url) { data, _, error in
guard let data = data,
let vttContent = String(data: data, encoding: .utf8),
error == nil else { return }
DispatchQueue.main.async {
self.cues = self.parseVTT(content: vttContent)
}
}.resume()
}
private func parseVTT(content: String) -> [SubtitleCue] {
var cues: [SubtitleCue] = []
let lines = content.components(separatedBy: .newlines)
var index = 0
while index < lines.count {
let line = lines[index].trimmingCharacters(in: .whitespaces)
if line.isEmpty || line == "WEBVTT" {
index += 1
continue
}
if !line.contains("-->") {
index += 1
if index >= lines.count { break }
}
let timeLine = lines[index]
let times = timeLine.components(separatedBy: "-->")
if times.count < 2 {
index += 1
continue
}
let startTime = parseTimecode(times[0].trimmingCharacters(in: .whitespaces))
let endTime = parseTimecode(times[1].trimmingCharacters(in: .whitespaces))
index += 1
var cueText = ""
while index < lines.count && !lines[index].trimmingCharacters(in: .whitespaces).isEmpty {
cueText += lines[index] + "\n"
index += 1
}
cues.append(SubtitleCue(startTime: startTime, endTime: endTime, text: cueText.trimmingCharacters(in: .whitespacesAndNewlines)))
}
return cues
}
private func parseTimecode(_ timeString: String) -> Double {
let parts = timeString.components(separatedBy: ":")
var seconds = 0.0
if parts.count == 3,
let h = Double(parts[0]),
let m = Double(parts[1]),
let s = Double(parts[2].replacingOccurrences(of: ",", with: ".")) {
seconds = h * 3600 + m * 60 + s
} else if parts.count == 2,
let m = Double(parts[0]),
let s = Double(parts[1].replacingOccurrences(of: ",", with: ".")) {
seconds = m * 60 + s
}
return seconds
}
}