mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
yes
This commit is contained in:
parent
bb9bb5da88
commit
ffc3821010
7 changed files with 563 additions and 2 deletions
|
|
@ -24,6 +24,11 @@
|
|||
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; };
|
||||
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.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 */; };
|
||||
13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */; };
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; };
|
||||
13EA2BDC2D32D9FF00C1EBD7 /* MiruDataStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
|
@ -45,6 +50,11 @@
|
|||
138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.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>"; };
|
||||
13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||
13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiruDataStruct.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -131,6 +141,7 @@
|
|||
133D7C852D2BE2640075467E /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13EA2BDA2D32D9FF00C1EBD7 /* Miru */,
|
||||
133D7C862D2BE2640075467E /* Extensions */,
|
||||
133D7C882D2BE2640075467E /* Modules */,
|
||||
133D7C8A2D2BE2640075467E /* Loaders */,
|
||||
|
|
@ -174,11 +185,39 @@
|
|||
13DC0C442D302C6A00D0F966 /* MediaPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13EA2BD02D32D97400C1EBD7 /* CustomPlayer */,
|
||||
13DC0C452D302C7500D0F966 /* VideoPlayer.swift */,
|
||||
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */,
|
||||
);
|
||||
path = MediaPlayer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13EA2BD02D32D97400C1EBD7 /* CustomPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */,
|
||||
13EA2BD22D32D97400C1EBD7 /* Components */,
|
||||
);
|
||||
path = CustomPlayer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13EA2BD22D32D97400C1EBD7 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */,
|
||||
13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13EA2BDA2D32D9FF00C1EBD7 /* Miru */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */,
|
||||
);
|
||||
path = Miru;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
@ -257,10 +296,14 @@
|
|||
files = (
|
||||
13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */,
|
||||
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */,
|
||||
13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */,
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
|
||||
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
|
||||
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
|
||||
133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */,
|
||||
13EA2BDC2D32D9FF00C1EBD7 /* MiruDataStruct.swift in Sources */,
|
||||
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */,
|
||||
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */,
|
||||
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
|
||||
|
|
@ -269,6 +312,7 @@
|
|||
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
|
||||
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
|
||||
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */,
|
||||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Double+Extension.swift
|
||||
// AppleMusicSlider
|
||||
//
|
||||
// Created by Pratik on 14/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Double {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: self) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension BinaryFloatingPoint {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.unitsStyle = style
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter.string(from: TimeInterval(self)) ?? ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// MusicProgressSlider.swift
|
||||
// Custom Seekbar
|
||||
//
|
||||
// Created by Pratik on 08/01/23.
|
||||
//
|
||||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
// I did edit just a little bit the code for my liking
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
VStack {
|
||||
ZStack(alignment: .center) {
|
||||
Capsule()
|
||||
.fill(emptyColor)
|
||||
Capsule()
|
||||
.fill(isActive ? activeFillColor : fillColor)
|
||||
.mask({
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(width: max(bounds.size.width * CGFloat((localRealProgress + localTempProgress)), 0), alignment: .leading)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(value.asTimeString(style: .positional))
|
||||
Spacer(minLength: 0)
|
||||
Text("-" + (inRange.upperBound - value).asTimeString(style: .positional))
|
||||
}
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(isActive ? fillColor : emptyColor)
|
||||
}
|
||||
.frame(width: isActive ? bounds.size.width * 1.04 : bounds.size.width, alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
}
|
||||
.frame(width: bounds.size.width, height: bounds.size.height, alignment: .center)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.updating($isActive) { _, state, _ in
|
||||
state = true
|
||||
}
|
||||
.onChanged { gesture in
|
||||
localTempProgress = T(gesture.translation.width / bounds.size.width)
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
}.onEnded { _ in
|
||||
localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0)
|
||||
localTempProgress = 0
|
||||
})
|
||||
.onChange(of: isActive) { newValue in
|
||||
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
|
||||
onEditingChanged(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = getPrgPercentage(value)
|
||||
}
|
||||
.onChange(of: value) { newValue in
|
||||
if !isActive {
|
||||
localRealProgress = getPrgPercentage(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: isActive ? height * 1.25 : height, alignment: .center)
|
||||
}
|
||||
|
||||
private var animation: Animation {
|
||||
if isActive {
|
||||
return .spring()
|
||||
} else {
|
||||
return .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
private func getPrgPercentage(_ value: T) -> T {
|
||||
let range = inRange.upperBound - inRange.lowerBound
|
||||
let correctedStartValue = value - inRange.lowerBound
|
||||
let percentage = correctedStartValue / range
|
||||
return percentage
|
||||
}
|
||||
|
||||
private func getPrgValue() -> T {
|
||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound
|
||||
}
|
||||
}
|
||||
282
Sora/MediaPlayer/CustomPlayer/CustomPlayer.swift
Normal file
282
Sora/MediaPlayer/CustomPlayer/CustomPlayer.swift
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// test2
|
||||
//
|
||||
// Created by Francesco on 20/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct CustomVideoPlayer: UIViewControllerRepresentable {
|
||||
let player: AVPlayer
|
||||
|
||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||
let controller = NormalPlayer()
|
||||
controller.player = player
|
||||
controller.showsPlaybackControls = false
|
||||
player.play()
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
||||
// yes? Like the plural of the famous american rapper ye? -IBHRAD
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomMediaPlayer: View {
|
||||
@State private var player: AVPlayer
|
||||
@State private var isPlaying = true
|
||||
@State private var currentTime: Double = 0.0
|
||||
@State private var duration: Double = 0.0
|
||||
@State private var showControls = false
|
||||
@State private var inactivityTimer: Timer?
|
||||
@State private var timeObserverToken: Any?
|
||||
@State private var isVideoLoaded = false
|
||||
@State private var showWatchNextButton = true
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
let module: ScrapingModule
|
||||
let fullUrl: String
|
||||
let title: String
|
||||
let episodeNumber: Int
|
||||
let onWatchNext: () -> Void
|
||||
|
||||
init(module: ScrapingModule, urlString: String, fullUrl: String, title: String, episodeNumber: Int, onWatchNext: @escaping () -> Void) {
|
||||
guard let url = URL(string: urlString) else {
|
||||
fatalError("Invalid URL string")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if urlString.contains("ascdn") {
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
_player = State(initialValue: AVPlayer(playerItem: AVPlayerItem(asset: asset)))
|
||||
|
||||
self.module = module
|
||||
self.fullUrl = fullUrl
|
||||
self.title = title
|
||||
self.episodeNumber = episodeNumber
|
||||
self.onWatchNext = onWatchNext
|
||||
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
|
||||
if lastPlayedTime > 0 {
|
||||
let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
|
||||
self._player.wrappedValue.seek(to: seekTime)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack {
|
||||
ZStack {
|
||||
CustomVideoPlayer(player: player)
|
||||
.onAppear {
|
||||
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 600), queue: .main) { time in
|
||||
currentTime = time.seconds
|
||||
if let itemDuration = player.currentItem?.duration.seconds, itemDuration.isFinite && !itemDuration.isNaN {
|
||||
duration = itemDuration
|
||||
isVideoLoaded = true
|
||||
}
|
||||
}
|
||||
startUpdatingCurrentTime()
|
||||
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.overlay(
|
||||
Group {
|
||||
if showControls {
|
||||
Color.black.opacity(0.5)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {
|
||||
currentTime = max(currentTime - 10, 0)
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}) {
|
||||
Image(systemName: "gobackward.10")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 25))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Button(action: {
|
||||
if isPlaying {
|
||||
player.pause()
|
||||
} else {
|
||||
player.play()
|
||||
}
|
||||
isPlaying.toggle()
|
||||
}) {
|
||||
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 45))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Button(action: {
|
||||
currentTime = min(currentTime + 10, duration)
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}) {
|
||||
Image(systemName: "goforward.10")
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 25))
|
||||
.contentShape(Rectangle())
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.2), value: showControls),
|
||||
alignment: .center
|
||||
)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
showControls.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
HStack(alignment: .bottom) {
|
||||
if showControls {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Episode \(episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
Spacer()
|
||||
if duration - currentTime <= duration * 0.10 && currentTime != duration && showWatchNextButton {
|
||||
Button(action: {
|
||||
player.pause()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
onWatchNext()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "forward.fill")
|
||||
.foregroundColor(Color.black)
|
||||
Text("Watch Next")
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(32)
|
||||
}
|
||||
.padding(.trailing, 10)
|
||||
.onAppear {
|
||||
if UserDefaults.standard.bool(forKey: "hideNextButton") {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
showWatchNextButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if showControls {
|
||||
Menu {
|
||||
Menu("Playback Speed") {
|
||||
ForEach([0.5, 1.0, 1.25, 1.5, 1.75, 2.0], id: \.self) { speed in
|
||||
Button(action: {
|
||||
player.rate = Float(speed)
|
||||
if player.timeControlStatus != .playing {
|
||||
player.pause()
|
||||
}
|
||||
}) {
|
||||
Text("\(speed, specifier: "%.2f")")
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 15))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 32)
|
||||
|
||||
if showControls {
|
||||
MusicProgressSlider(
|
||||
value: $currentTime,
|
||||
inRange: 0...duration,
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.5),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 28,
|
||||
onEditingChanged: { editing in
|
||||
if !editing && isVideoLoaded {
|
||||
player.seek(to: CMTime(seconds: currentTime, preferredTimescale: 600))
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.bottom, 10)
|
||||
.disabled(!isVideoLoaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
startUpdatingCurrentTime()
|
||||
}
|
||||
.onDisappear {
|
||||
player.pause()
|
||||
inactivityTimer?.invalidate()
|
||||
if let timeObserverToken = timeObserverToken {
|
||||
player.removeTimeObserver(timeObserverToken)
|
||||
self.timeObserverToken = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
if showControls {
|
||||
HStack {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
.contentShape(Rectangle())
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startUpdatingCurrentTime() {
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
currentTime = player.currentTime().seconds
|
||||
}
|
||||
}
|
||||
|
||||
private func addPeriodicTimeObserver(fullURL: String) {
|
||||
let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
|
||||
guard let currentItem = player.currentItem,
|
||||
currentItem.duration.seconds.isFinite else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = time.seconds
|
||||
let duration = currentItem.duration.seconds
|
||||
|
||||
UserDefaults.standard.set(currentTime, forKey: "lastPlayedTime_\(fullURL)")
|
||||
UserDefaults.standard.set(duration, forKey: "totalTime_\(fullURL)")
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Sora/MediaPlayer/NormalPlayer.swift
Normal file
77
Sora/MediaPlayer/NormalPlayer.swift
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// NormalPlayer.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
|
||||
class NormalPlayer: AVPlayerViewController {
|
||||
private var originalRate: Float = 1.0
|
||||
private var holdGesture: UILongPressGestureRecognizer?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupHoldGesture()
|
||||
setupAudioSession()
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UserDefaults.standard.bool(forKey: "AlwaysLandscape") {
|
||||
return .landscape
|
||||
} else {
|
||||
return .all
|
||||
}
|
||||
}
|
||||
|
||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private func setupHoldGesture() {
|
||||
holdGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldGesture(_:)))
|
||||
holdGesture?.minimumPressDuration = 0.5
|
||||
if let holdGesture = holdGesture {
|
||||
view.addGestureRecognizer(holdGesture)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleHoldGesture(_ gesture: UILongPressGestureRecognizer) {
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
beginHoldSpeed()
|
||||
case .ended, .cancelled:
|
||||
endHoldSpeed()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func beginHoldSpeed() {
|
||||
guard let player = player else { return }
|
||||
originalRate = player.rate
|
||||
let holdSpeed = UserDefaults.standard.float(forKey: "holdSpeedPlayer")
|
||||
player.rate = holdSpeed
|
||||
}
|
||||
|
||||
private func endHoldSpeed() {
|
||||
player?.rate = originalRate
|
||||
}
|
||||
|
||||
func setupAudioSession() {
|
||||
do {
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers)
|
||||
try audioSession.setActive(true)
|
||||
|
||||
try audioSession.overrideOutputAudioPort(.speaker)
|
||||
} catch {
|
||||
print("Failed to set up AVAudioSession: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ class VideoPlayerViewController: UIViewController {
|
|||
let module: ScrapingModule
|
||||
|
||||
var player: AVPlayer?
|
||||
var playerViewController: AVPlayerViewController?
|
||||
var playerViewController: NormalPlayer?
|
||||
var timeObserverToken: Any?
|
||||
var streamUrl: String?
|
||||
var fullUrl: String = ""
|
||||
|
|
@ -42,7 +42,7 @@ class VideoPlayerViewController: UIViewController {
|
|||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
||||
player = AVPlayer(playerItem: playerItem)
|
||||
playerViewController = AVPlayerViewController()
|
||||
playerViewController = NormalPlayer()
|
||||
playerViewController?.player = player
|
||||
addPeriodicTimeObserver(fullURL: fullUrl)
|
||||
|
||||
|
|
|
|||
26
Sora/Utils/Miru/MiruDataStruct.swift
Normal file
26
Sora/Utils/Miru/MiruDataStruct.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// MiruDataStruct.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MiruDataStruct: Codable {
|
||||
var likes: [Like]
|
||||
|
||||
struct Like: Codable {
|
||||
let anilistID: Int
|
||||
var gogoSlug: String
|
||||
let title: String
|
||||
let cover: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case anilistID = "anilist_id"
|
||||
case gogoSlug = "gogo_slug"
|
||||
case title
|
||||
case cover
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue