mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
subs loader added now module support 😭
This commit is contained in:
parent
9aa56fa8a3
commit
1f48612a67
3 changed files with 162 additions and 2 deletions
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
85
Sora/Utils/MediaPlayer/CustomPlayer/VTTSubtitlesLoader.swift
Normal file
85
Sora/Utils/MediaPlayer/CustomPlayer/VTTSubtitlesLoader.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue