mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
feat: add submit intro functionality and UI support
This commit is contained in:
parent
b46706a588
commit
849c265a3f
8 changed files with 440 additions and 54 deletions
|
|
@ -67,6 +67,12 @@ public func nuvio_player_set_is_series(_ ptr: UnsafeMutableRawPointer, _ value:
|
||||||
DispatchQueue.main.async { p.state.isSeries = value }
|
DispatchQueue.main.async { p.state.isSeries = value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_cdecl("nuvio_player_set_submit_intro_enabled")
|
||||||
|
public func nuvio_player_set_submit_intro_enabled(_ ptr: UnsafeMutableRawPointer, _ enabled: Bool) {
|
||||||
|
let p = player(ptr)
|
||||||
|
DispatchQueue.main.async { p.state.canSubmitIntro = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
@_cdecl("nuvio_player_load_file")
|
@_cdecl("nuvio_player_load_file")
|
||||||
public func nuvio_player_load_file(
|
public func nuvio_player_load_file(
|
||||||
_ ptr: UnsafeMutableRawPointer,
|
_ ptr: UnsafeMutableRawPointer,
|
||||||
|
|
@ -347,6 +353,31 @@ public func nuvio_player_pop_next_episode_pressed(_ ptr: UnsafeMutableRawPointer
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_cdecl("nuvio_player_pop_submit_intro_requested")
|
||||||
|
public func nuvio_player_pop_submit_intro_requested(_ ptr: UnsafeMutableRawPointer) -> Bool {
|
||||||
|
let p = player(ptr)
|
||||||
|
if p.state.submitIntroRequested {
|
||||||
|
p.state.submitIntroRequested = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@_cdecl("nuvio_player_get_submit_intro_segment_type")
|
||||||
|
public func nuvio_player_get_submit_intro_segment_type(_ ptr: UnsafeMutableRawPointer) -> UnsafePointer<CChar>? {
|
||||||
|
return retainAndReturn(player(ptr).state.submitIntroSegmentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@_cdecl("nuvio_player_get_submit_intro_start_sec")
|
||||||
|
public func nuvio_player_get_submit_intro_start_sec(_ ptr: UnsafeMutableRawPointer) -> Double {
|
||||||
|
return player(ptr).state.submitIntroStartSec
|
||||||
|
}
|
||||||
|
|
||||||
|
@_cdecl("nuvio_player_get_submit_intro_end_sec")
|
||||||
|
public func nuvio_player_get_submit_intro_end_sec(_ ptr: UnsafeMutableRawPointer) -> Double {
|
||||||
|
return player(ptr).state.submitIntroEndSec
|
||||||
|
}
|
||||||
|
|
||||||
@_cdecl("nuvio_player_is_addon_subtitles_fetch_requested")
|
@_cdecl("nuvio_player_is_addon_subtitles_fetch_requested")
|
||||||
public func nuvio_player_is_addon_subtitles_fetch_requested(_ ptr: UnsafeMutableRawPointer) -> Bool {
|
public func nuvio_player_is_addon_subtitles_fetch_requested(_ ptr: UnsafeMutableRawPointer) -> Bool {
|
||||||
let p = player(ptr)
|
let p = player(ptr)
|
||||||
|
|
@ -690,3 +721,17 @@ public func nuvio_player_show_episode_streams(
|
||||||
p.state.showEpisodeStreams = true
|
p.state.showEpisodeStreams = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@_cdecl("nuvio_player_dismiss_panels")
|
||||||
|
public func nuvio_player_dismiss_panels(_ ptr: UnsafeMutableRawPointer) {
|
||||||
|
let p = player(ptr)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
p.state.showSourcesPanel = false
|
||||||
|
p.state.showEpisodesPanel = false
|
||||||
|
p.state.showEpisodeStreams = false
|
||||||
|
p.state.showSubtitlePanel = false
|
||||||
|
p.state.showAudioPanel = false
|
||||||
|
p.state.showSubmitIntroPanel = false
|
||||||
|
p.state.controlsVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,13 @@ struct NuvioControlsView: View {
|
||||||
var onAddSubtitleUrl: ((String) -> Void)?
|
var onAddSubtitleUrl: ((String) -> Void)?
|
||||||
var onRemoveExternalAndSelect: ((Int) -> Void)?
|
var onRemoveExternalAndSelect: ((Int) -> Void)?
|
||||||
var onFetchAddonSubtitles: (() -> Void)?
|
var onFetchAddonSubtitles: (() -> Void)?
|
||||||
|
var onSubmitIntro: ((String, Double, Double) -> Void)?
|
||||||
|
|
||||||
@State private var isDragging = false
|
@State private var isDragging = false
|
||||||
@State private var dragPosition: Double = 0
|
@State private var dragPosition: Double = 0
|
||||||
|
@State private var submitIntroSegmentType = "intro"
|
||||||
|
@State private var submitIntroStartTime = "00:00"
|
||||||
|
@State private var submitIntroEndTime = "00:00"
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
|
|
@ -77,8 +81,12 @@ struct NuvioControlsView: View {
|
||||||
Color.clear
|
Color.clear
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
if state.controlsLocked {
|
||||||
|
state.lockedOverlayVisible = true
|
||||||
|
} else {
|
||||||
state.controlsVisible.toggle()
|
state.controlsVisible.toggle()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
topGradient
|
topGradient
|
||||||
|
|
@ -99,10 +107,10 @@ struct NuvioControlsView: View {
|
||||||
.padding(.bottom, metrics.sliderBottomOffset)
|
.padding(.bottom, metrics.sliderBottomOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.opacity(state.controlsVisible ? 1 : 0)
|
.opacity(state.controlsVisible && !state.controlsLocked ? 1 : 0)
|
||||||
.animation(.easeInOut(duration: 0.25), value: state.controlsVisible)
|
.animation(.easeInOut(duration: 0.25), value: state.controlsVisible)
|
||||||
|
|
||||||
if state.skipButtonType != nil {
|
if !state.controlsLocked && state.skipButtonType != nil {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack {
|
HStack {
|
||||||
|
|
@ -114,7 +122,7 @@ struct NuvioControlsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.showNextEpisode {
|
if !state.controlsLocked && state.showNextEpisode {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack {
|
HStack {
|
||||||
|
|
@ -143,6 +151,14 @@ struct NuvioControlsView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state.showSubmitIntroPanel {
|
||||||
|
submitIntroModal
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.controlsLocked && state.lockedOverlayVisible {
|
||||||
|
lockedOverlay
|
||||||
|
}
|
||||||
|
|
||||||
if state.showSourcesPanel {
|
if state.showSourcesPanel {
|
||||||
NuvioSourcesPanel(state: state) {
|
NuvioSourcesPanel(state: state) {
|
||||||
state.showSourcesPanel = false
|
state.showSourcesPanel = false
|
||||||
|
|
@ -237,19 +253,46 @@ struct NuvioControlsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: onClose) {
|
HStack(spacing: 10) {
|
||||||
Image(systemName: "xmark")
|
if state.canSubmitIntro {
|
||||||
.font(.system(size: metrics.headerIconSize, weight: .semibold))
|
headerIconButton(icon: "flag.fill", size: metrics.headerIconSize) {
|
||||||
|
state.showSubmitIntroPanel = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headerIconButton(icon: state.controlsLocked ? "lock.open.fill" : "lock.fill", size: metrics.headerIconSize) {
|
||||||
|
if state.controlsLocked {
|
||||||
|
state.controlsLocked = false
|
||||||
|
state.lockedOverlayVisible = false
|
||||||
|
state.controlsVisible = true
|
||||||
|
} else {
|
||||||
|
state.controlsLocked = true
|
||||||
|
state.controlsVisible = false
|
||||||
|
state.lockedOverlayVisible = false
|
||||||
|
state.showSubtitlePanel = false
|
||||||
|
state.showAudioPanel = false
|
||||||
|
state.showSourcesPanel = false
|
||||||
|
state.showEpisodesPanel = false
|
||||||
|
state.showSubmitIntroPanel = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headerIconButton(icon: "xmark", size: metrics.headerIconSize, action: onClose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 28)
|
||||||
|
.padding(.top, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerIconButton(icon: String, size: CGFloat, action: @escaping () -> Void) -> some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: size, weight: .semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.frame(width: metrics.headerIconSize + 16, height: metrics.headerIconSize + 16)
|
.frame(width: size + 16, height: size + 16)
|
||||||
.background(Color.black.opacity(0.35))
|
.background(Color.black.opacity(0.35))
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 28)
|
|
||||||
.padding(.top, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
func centerControls(metrics: ControlMetrics) -> some View {
|
func centerControls(metrics: ControlMetrics) -> some View {
|
||||||
HStack(spacing: metrics.centerGap) {
|
HStack(spacing: metrics.centerGap) {
|
||||||
|
|
@ -662,6 +705,183 @@ struct NuvioControlsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lockedOverlay: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.black.opacity(0.45)
|
||||||
|
.onTapGesture {
|
||||||
|
state.lockedOverlayVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
state.controlsLocked = false
|
||||||
|
state.lockedOverlayVisible = false
|
||||||
|
state.controlsVisible = true
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Image(systemName: "lock.open.fill")
|
||||||
|
.font(.system(size: 18, weight: .semibold))
|
||||||
|
Text("Unlock")
|
||||||
|
.font(.system(size: 15, weight: .bold))
|
||||||
|
}
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 22)
|
||||||
|
.padding(.vertical, 14)
|
||||||
|
.background(Color.white.opacity(0.16))
|
||||||
|
.clipShape(Capsule())
|
||||||
|
.overlay(Capsule().stroke(Color.white.opacity(0.18), lineWidth: 1))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var submitIntroModal: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.black.opacity(0.6)
|
||||||
|
.onTapGesture { state.showSubmitIntroPanel = false }
|
||||||
|
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
HStack {
|
||||||
|
Text("Submit Timestamps")
|
||||||
|
.font(.system(size: 18, weight: .bold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Spacer()
|
||||||
|
Button(action: { state.showSubmitIntroPanel = false }) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.background(Color.white.opacity(0.15))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
submitSegmentButton(title: "Intro", value: "intro")
|
||||||
|
submitSegmentButton(title: "Recap", value: "recap")
|
||||||
|
submitSegmentButton(title: "Outro", value: "outro")
|
||||||
|
}
|
||||||
|
|
||||||
|
submitTimeRow(title: "Start Time", text: $submitIntroStartTime)
|
||||||
|
submitTimeRow(title: "End Time", text: $submitIntroEndTime)
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button(action: { state.showSubmitIntroPanel = false }) {
|
||||||
|
Text("Cancel")
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundColor(.white.opacity(0.75))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color.white.opacity(0.08))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
guard let start = parseSubmitTime(submitIntroStartTime),
|
||||||
|
let end = parseSubmitTime(submitIntroEndTime),
|
||||||
|
end > start else { return }
|
||||||
|
onSubmitIntro?(submitIntroSegmentType, start, end)
|
||||||
|
state.showSubmitIntroPanel = false
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "paperplane.fill")
|
||||||
|
.font(.system(size: 13, weight: .bold))
|
||||||
|
Text("Submit")
|
||||||
|
.font(.system(size: 14, weight: .bold))
|
||||||
|
}
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color.blue)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.frame(width: 420)
|
||||||
|
.background(Color(red: 0.12, green: 0.12, blue: 0.12))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 24))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.stroke(Color.white.opacity(0.1), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
if submitIntroStartTime == "00:00" && submitIntroEndTime == "00:00" {
|
||||||
|
submitIntroStartTime = formatSubmitTime(Double(state.positionMs) / 1000.0)
|
||||||
|
submitIntroEndTime = formatSubmitTime(Double(state.positionMs) / 1000.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitSegmentButton(title: String, value: String) -> some View {
|
||||||
|
let selected = submitIntroSegmentType == value
|
||||||
|
return Button(action: { submitIntroSegmentType = value }) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 13, weight: .bold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(selected ? Color.white.opacity(0.2) : Color.white.opacity(0.08))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitTimeRow(title: String, text: Binding<String>) -> some View {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundColor(.white.opacity(0.75))
|
||||||
|
.frame(width: 92, alignment: .leading)
|
||||||
|
TextField("00:00", text: text)
|
||||||
|
.textFieldStyle(.plain)
|
||||||
|
.font(.system(size: 15, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(Color.white.opacity(0.08))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
Button(action: {
|
||||||
|
text.wrappedValue = formatSubmitTime(Double(state.positionMs) / 1000.0)
|
||||||
|
}) {
|
||||||
|
Text("Now")
|
||||||
|
.font(.system(size: 12, weight: .bold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(Color.white.opacity(0.12))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSubmitTime(_ value: String) -> Double? {
|
||||||
|
let parts = value.split(separator: ":").map(String.init)
|
||||||
|
guard parts.count == 2 || parts.count == 3 else { return nil }
|
||||||
|
let numbers = parts.compactMap(Double.init)
|
||||||
|
guard numbers.count == parts.count else { return nil }
|
||||||
|
if numbers.contains(where: { $0 < 0 }) { return nil }
|
||||||
|
if numbers.count == 2 {
|
||||||
|
return numbers[0] * 60 + numbers[1]
|
||||||
|
}
|
||||||
|
return numbers[0] * 3600 + numbers[1] * 60 + numbers[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSubmitTime(_ seconds: Double) -> String {
|
||||||
|
let total = max(Int(seconds.rounded(.down)), 0)
|
||||||
|
let h = total / 3600
|
||||||
|
let m = (total / 60) % 60
|
||||||
|
let s = total % 60
|
||||||
|
if h > 0 {
|
||||||
|
return "\(h):\(String(format: "%02d", m)):\(String(format: "%02d", s))"
|
||||||
|
}
|
||||||
|
return "\(String(format: "%02d", m)):\(String(format: "%02d", s))"
|
||||||
|
}
|
||||||
|
|
||||||
var subtitleModal: some View {
|
var subtitleModal: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black.opacity(0.6)
|
Color.black.opacity(0.6)
|
||||||
|
|
@ -740,12 +960,14 @@ struct NuvioControlsView: View {
|
||||||
lang: "",
|
lang: "",
|
||||||
isSelected: !state.subtitleTracks.contains(where: { $0.selected })
|
isSelected: !state.subtitleTracks.contains(where: { $0.selected })
|
||||||
) {
|
) {
|
||||||
|
state.selectedAddonSubtitleId = nil
|
||||||
onRemoveExternalAndSelect?(-1)
|
onRemoveExternalAndSelect?(-1)
|
||||||
state.showSubtitlePanel = false
|
state.showSubtitlePanel = false
|
||||||
}
|
}
|
||||||
ForEach(Array(state.subtitleTracks.enumerated()), id: \.element.id) { _, track in
|
ForEach(Array(state.subtitleTracks.enumerated()), id: \.element.id) { _, track in
|
||||||
let label = track.title.isEmpty ? (track.lang.isEmpty ? "Track \(track.id)" : track.lang) : track.title
|
let label = track.title.isEmpty ? (track.lang.isEmpty ? "Track \(track.id)" : track.lang) : track.title
|
||||||
trackRow(label: label, lang: track.lang, isSelected: track.selected) {
|
trackRow(label: label, lang: track.lang, isSelected: track.selected) {
|
||||||
|
state.selectedAddonSubtitleId = nil
|
||||||
onRemoveExternalAndSelect?(track.id)
|
onRemoveExternalAndSelect?(track.id)
|
||||||
state.showSubtitlePanel = false
|
state.showSubtitlePanel = false
|
||||||
}
|
}
|
||||||
|
|
@ -898,9 +1120,9 @@ struct NuvioControlsView: View {
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
state.subtitleStyleTextColor = 0
|
state.subtitleStyleTextColor = 0
|
||||||
state.subtitleStyleFontSize = 30
|
state.subtitleStyleFontSize = 18
|
||||||
state.subtitleStyleOutlineEnabled = true
|
state.subtitleStyleOutlineEnabled = false
|
||||||
state.subtitleStyleBottomOffset = 5
|
state.subtitleStyleBottomOffset = 20
|
||||||
applyCurrentSubtitleStyle()
|
applyCurrentSubtitleStyle()
|
||||||
}) {
|
}) {
|
||||||
Text("Reset Defaults")
|
Text("Reset Defaults")
|
||||||
|
|
@ -1002,5 +1224,3 @@ private extension Double {
|
||||||
return min(max(self, range.lowerBound), range.upperBound)
|
return min(max(self, range.lowerBound), range.upperBound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ final class NuvioPlayerState: ObservableObject {
|
||||||
|
|
||||||
@Published var controlsVisible: Bool = true
|
@Published var controlsVisible: Bool = true
|
||||||
@Published var cursorHidden: Bool = false
|
@Published var cursorHidden: Bool = false
|
||||||
|
@Published var controlsLocked: Bool = false
|
||||||
|
@Published var lockedOverlayVisible: Bool = false
|
||||||
|
|
||||||
@Published var title: String = ""
|
@Published var title: String = ""
|
||||||
@Published var streamTitle: String = ""
|
@Published var streamTitle: String = ""
|
||||||
|
|
@ -105,9 +107,9 @@ final class NuvioPlayerState: ObservableObject {
|
||||||
@Published var gestureFeedback: GestureFeedbackState? = nil
|
@Published var gestureFeedback: GestureFeedbackState? = nil
|
||||||
|
|
||||||
@Published var subtitleStyleTextColor: Int = 0
|
@Published var subtitleStyleTextColor: Int = 0
|
||||||
@Published var subtitleStyleFontSize: Int = 30
|
@Published var subtitleStyleFontSize: Int = 18
|
||||||
@Published var subtitleStyleOutlineEnabled: Bool = true
|
@Published var subtitleStyleOutlineEnabled: Bool = false
|
||||||
@Published var subtitleStyleBottomOffset: Int = 5
|
@Published var subtitleStyleBottomOffset: Int = 20
|
||||||
|
|
||||||
@Published var addonSubtitles: [AddonSubtitleInfo] = []
|
@Published var addonSubtitles: [AddonSubtitleInfo] = []
|
||||||
@Published var addonSubtitlesLoading: Bool = false
|
@Published var addonSubtitlesLoading: Bool = false
|
||||||
|
|
@ -120,6 +122,8 @@ final class NuvioPlayerState: ObservableObject {
|
||||||
@Published var showEpisodesPanel: Bool = false
|
@Published var showEpisodesPanel: Bool = false
|
||||||
@Published var hasVideoId: Bool = false
|
@Published var hasVideoId: Bool = false
|
||||||
@Published var isSeries: Bool = false
|
@Published var isSeries: Bool = false
|
||||||
|
@Published var canSubmitIntro: Bool = false
|
||||||
|
@Published var showSubmitIntroPanel: Bool = false
|
||||||
|
|
||||||
@Published var sourceStreams: [NuvioStreamInfo] = []
|
@Published var sourceStreams: [NuvioStreamInfo] = []
|
||||||
@Published var sourceAddonGroups: [NuvioAddonGroupInfo] = []
|
@Published var sourceAddonGroups: [NuvioAddonGroupInfo] = []
|
||||||
|
|
@ -148,6 +152,10 @@ final class NuvioPlayerState: ObservableObject {
|
||||||
var episodeFilterChanged: Bool = false
|
var episodeFilterChanged: Bool = false
|
||||||
var episodeReloadRequested: Bool = false
|
var episodeReloadRequested: Bool = false
|
||||||
var episodeBackRequested: Bool = false
|
var episodeBackRequested: Bool = false
|
||||||
|
var submitIntroRequested: Bool = false
|
||||||
|
var submitIntroSegmentType: String = "intro"
|
||||||
|
var submitIntroStartSec: Double = 0
|
||||||
|
var submitIntroEndSec: Double = 0
|
||||||
|
|
||||||
var resizeModeLabel: String {
|
var resizeModeLabel: String {
|
||||||
switch resizeMode {
|
switch resizeMode {
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,12 @@ final class NuvioPlayerWindow {
|
||||||
},
|
},
|
||||||
onFetchAddonSubtitles: { [weak self] in
|
onFetchAddonSubtitles: { [weak self] in
|
||||||
self?.state.addonSubtitlesFetchRequested = true
|
self?.state.addonSubtitlesFetchRequested = true
|
||||||
|
},
|
||||||
|
onSubmitIntro: { [weak self] segmentType, startSec, endSec in
|
||||||
|
self?.state.submitIntroSegmentType = segmentType
|
||||||
|
self?.state.submitIntroStartSec = startSec
|
||||||
|
self?.state.submitIntroEndSec = endSec
|
||||||
|
self?.state.submitIntroRequested = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
hostingView = NSHostingView(rootView: controlsView)
|
hostingView = NSHostingView(rootView: controlsView)
|
||||||
|
|
@ -140,11 +146,18 @@ final class NuvioPlayerWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMouseMoved() {
|
func handleMouseMoved() {
|
||||||
|
if state.controlsLocked {
|
||||||
|
return
|
||||||
|
}
|
||||||
showControls()
|
showControls()
|
||||||
scheduleHideControls()
|
scheduleHideControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMouseClicked() {
|
func handleMouseClicked() {
|
||||||
|
if state.controlsLocked {
|
||||||
|
state.lockedOverlayVisible = true
|
||||||
|
return
|
||||||
|
}
|
||||||
if state.controlsVisible {
|
if state.controlsVisible {
|
||||||
hideControlsNow()
|
hideControlsNow()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -154,6 +167,14 @@ final class NuvioPlayerWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleKeyDown(_ event: NSEvent) -> Bool {
|
func handleKeyDown(_ event: NSEvent) -> Bool {
|
||||||
|
if state.controlsLocked {
|
||||||
|
if event.keyCode == 53 {
|
||||||
|
close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
state.lockedOverlayVisible = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
switch event.keyCode {
|
switch event.keyCode {
|
||||||
case 53:
|
case 53:
|
||||||
close()
|
close()
|
||||||
|
|
@ -236,6 +257,10 @@ final class NuvioPlayerWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showControls() {
|
private func showControls() {
|
||||||
|
if state.controlsLocked {
|
||||||
|
state.lockedOverlayVisible = true
|
||||||
|
return
|
||||||
|
}
|
||||||
state.controlsVisible = true
|
state.controlsVisible = true
|
||||||
if state.cursorHidden {
|
if state.cursorHidden {
|
||||||
NSCursor.unhide()
|
NSCursor.unhide()
|
||||||
|
|
@ -246,6 +271,9 @@ final class NuvioPlayerWindow {
|
||||||
private func hideControlsNow() {
|
private func hideControlsNow() {
|
||||||
hideTimer?.invalidate()
|
hideTimer?.invalidate()
|
||||||
hideTimer = nil
|
hideTimer = nil
|
||||||
|
if state.controlsLocked {
|
||||||
|
return
|
||||||
|
}
|
||||||
state.controlsVisible = false
|
state.controlsVisible = false
|
||||||
if !state.cursorHidden {
|
if !state.cursorHidden {
|
||||||
NSCursor.hide()
|
NSCursor.hide()
|
||||||
|
|
@ -255,9 +283,12 @@ final class NuvioPlayerWindow {
|
||||||
|
|
||||||
private func scheduleHideControls() {
|
private func scheduleHideControls() {
|
||||||
hideTimer?.invalidate()
|
hideTimer?.invalidate()
|
||||||
|
if state.controlsLocked {
|
||||||
|
return
|
||||||
|
}
|
||||||
hideTimer = Timer.scheduledTimer(withTimeInterval: 3.5, repeats: false) { [weak self] _ in
|
hideTimer = Timer.scheduledTimer(withTimeInterval: 3.5, repeats: false) { [weak self] _ in
|
||||||
guard let self, self.state.isPlaying else { return }
|
guard let self, self.state.isPlaying else { return }
|
||||||
if self.state.showSubtitlePanel || self.state.showAudioPanel || self.state.showSourcesPanel || self.state.showEpisodesPanel { return }
|
if self.state.showSubtitlePanel || self.state.showAudioPanel || self.state.showSourcesPanel || self.state.showEpisodesPanel || self.state.showSubmitIntroPanel { return }
|
||||||
self.state.controlsVisible = false
|
self.state.controlsVisible = false
|
||||||
if !self.state.cursorHidden {
|
if !self.state.cursorHidden {
|
||||||
NSCursor.hide()
|
NSCursor.hide()
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ interface PlayerEngineController {
|
||||||
logo: String? = null,
|
logo: String? = null,
|
||||||
) {}
|
) {}
|
||||||
fun setPlayerFlags(hasVideoId: Boolean, isSeries: Boolean) {}
|
fun setPlayerFlags(hasVideoId: Boolean, isSeries: Boolean) {}
|
||||||
|
fun setSubmitIntroEnabled(enabled: Boolean) {}
|
||||||
fun showSkipButton(type: String, endTimeMs: Long) {}
|
fun showSkipButton(type: String, endTimeMs: Long) {}
|
||||||
fun hideSkipButton() {}
|
fun hideSkipButton() {}
|
||||||
fun showNextEpisode(
|
fun showNextEpisode(
|
||||||
|
|
@ -40,6 +41,7 @@ interface PlayerEngineController {
|
||||||
) {}
|
) {}
|
||||||
fun hideNextEpisode() {}
|
fun hideNextEpisode() {}
|
||||||
fun setOnNextEpisodeRequestedCallback(callback: () -> Unit) {}
|
fun setOnNextEpisodeRequestedCallback(callback: () -> Unit) {}
|
||||||
|
fun setOnSubmitIntroSubmittedCallback(callback: (segmentType: String, startSec: Double, endSec: Double) -> Unit) {}
|
||||||
fun setOnCloseCallback(callback: () -> Unit) {}
|
fun setOnCloseCallback(callback: () -> Unit) {}
|
||||||
fun setOnAddonSubtitlesFetchCallback(callback: () -> Unit) {}
|
fun setOnAddonSubtitlesFetchCallback(callback: () -> Unit) {}
|
||||||
fun pushAddonSubtitles(subtitles: List<AddonSubtitle>, isLoading: Boolean) {}
|
fun pushAddonSubtitles(subtitles: List<AddonSubtitle>, isLoading: Boolean) {}
|
||||||
|
|
@ -69,6 +71,7 @@ interface PlayerEngineController {
|
||||||
currentStreamUrl: String?,
|
currentStreamUrl: String?,
|
||||||
) {}
|
) {}
|
||||||
fun showEpisodeStreamsView(season: Int?, episode: Int?, title: String?) {}
|
fun showEpisodeStreamsView(season: Int?, episode: Int?, title: String?) {}
|
||||||
|
fun dismissNativePanels() {}
|
||||||
fun switchSource(url: String, audioUrl: String?, headersJson: String?) {}
|
fun switchSource(url: String, audioUrl: String?, headersJson: String?) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,9 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
val allEpisodes = remember(playerMetaVideos) { playerMetaVideos }
|
val allEpisodes = remember(playerMetaVideos) { playerMetaVideos }
|
||||||
val isSeries = parentMetaType == "series"
|
val isSeries = parentMetaType == "series"
|
||||||
|
val canSubmitIntro = isSeries &&
|
||||||
|
playerSettingsUiState.introSubmitEnabled &&
|
||||||
|
playerSettingsUiState.introDbApiKey.isNotBlank()
|
||||||
|
|
||||||
// Skip intro/outro/recap state
|
// Skip intro/outro/recap state
|
||||||
var skipIntervals by remember { mutableStateOf<List<SkipInterval>>(emptyList()) }
|
var skipIntervals by remember { mutableStateOf<List<SkipInterval>>(emptyList()) }
|
||||||
|
|
@ -1097,6 +1100,11 @@ fun PlayerScreen(
|
||||||
SubtitleRepository.fetchAddonSubtitles(type, videoId)
|
SubtitleRepository.fetchAddonSubtitles(type, videoId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resolveSubmitIntroImdbId(): String? =
|
||||||
|
activeVideoId?.split(":")?.firstOrNull()?.takeIf { it.startsWith("tt") }
|
||||||
|
?: parentMetaId.takeIf { it.startsWith("tt") }
|
||||||
|
?: metaUiState.meta?.id?.takeIf { it.startsWith("tt") }
|
||||||
|
|
||||||
LaunchedEffect(activeSourceUrl, activeSourceAudioUrl, activeSourceHeaders, activeSourceResponseHeaders) {
|
LaunchedEffect(activeSourceUrl, activeSourceAudioUrl, activeSourceHeaders, activeSourceResponseHeaders) {
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
playerController = null
|
playerController = null
|
||||||
|
|
@ -1127,6 +1135,39 @@ fun PlayerScreen(
|
||||||
playerController?.applySubtitleStyle(subtitleStyle)
|
playerController?.applySubtitleStyle(subtitleStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
playerController,
|
||||||
|
title,
|
||||||
|
activeStreamTitle,
|
||||||
|
activeProviderName,
|
||||||
|
activeSeasonNumber,
|
||||||
|
activeEpisodeNumber,
|
||||||
|
activeEpisodeTitle,
|
||||||
|
backdropArtwork,
|
||||||
|
logo,
|
||||||
|
activeVideoId,
|
||||||
|
parentMetaType,
|
||||||
|
) {
|
||||||
|
playerController?.setMetadata(
|
||||||
|
title = title,
|
||||||
|
streamTitle = activeStreamTitle,
|
||||||
|
providerName = activeProviderName,
|
||||||
|
seasonNumber = activeSeasonNumber,
|
||||||
|
episodeNumber = activeEpisodeNumber,
|
||||||
|
episodeTitle = activeEpisodeTitle,
|
||||||
|
artwork = backdropArtwork,
|
||||||
|
logo = logo,
|
||||||
|
)
|
||||||
|
playerController?.setPlayerFlags(
|
||||||
|
hasVideoId = activeVideoId != null,
|
||||||
|
isSeries = parentMetaType == "series",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(playerController, canSubmitIntro) {
|
||||||
|
playerController?.setSubmitIntroEnabled(canSubmitIntro)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(playerController, addonSubtitles, isLoadingAddonSubtitles) {
|
LaunchedEffect(playerController, addonSubtitles, isLoadingAddonSubtitles) {
|
||||||
playerController?.pushAddonSubtitles(addonSubtitles, isLoadingAddonSubtitles)
|
playerController?.pushAddonSubtitles(addonSubtitles, isLoadingAddonSubtitles)
|
||||||
}
|
}
|
||||||
|
|
@ -1618,6 +1659,22 @@ fun PlayerScreen(
|
||||||
nextEpisodeAutoPlayJob?.cancel()
|
nextEpisodeAutoPlayJob?.cancel()
|
||||||
playNextEpisode()
|
playNextEpisode()
|
||||||
}
|
}
|
||||||
|
controller.setOnSubmitIntroSubmittedCallback { segmentType, startSec, endSec ->
|
||||||
|
val season = activeSeasonNumber ?: return@setOnSubmitIntroSubmittedCallback
|
||||||
|
val episode = activeEpisodeNumber ?: return@setOnSubmitIntroSubmittedCallback
|
||||||
|
val imdbId = resolveSubmitIntroImdbId() ?: return@setOnSubmitIntroSubmittedCallback
|
||||||
|
if (endSec <= startSec) return@setOnSubmitIntroSubmittedCallback
|
||||||
|
scope.launch {
|
||||||
|
SkipIntroRepository.submitIntro(
|
||||||
|
imdbId = imdbId,
|
||||||
|
season = season,
|
||||||
|
episode = episode,
|
||||||
|
startSec = startSec,
|
||||||
|
endSec = endSec,
|
||||||
|
segmentType = segmentType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
controller.setOnAddonSubtitlesFetchCallback {
|
controller.setOnAddonSubtitlesFetchCallback {
|
||||||
if (contentType != null && activeVideoId != null) {
|
if (contentType != null && activeVideoId != null) {
|
||||||
SubtitleRepository.fetchAddonSubtitles(contentType, activeVideoId!!)
|
SubtitleRepository.fetchAddonSubtitles(contentType, activeVideoId!!)
|
||||||
|
|
@ -1638,20 +1695,6 @@ fun PlayerScreen(
|
||||||
val stream = allStreams.firstOrNull { it.directPlaybackUrl == url }
|
val stream = allStreams.firstOrNull { it.directPlaybackUrl == url }
|
||||||
?: return@setOnSourceStreamSelectedCallback
|
?: return@setOnSourceStreamSelectedCallback
|
||||||
switchToSource(stream)
|
switchToSource(stream)
|
||||||
val headers = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request)
|
|
||||||
val headersJson = headers.takeIf { it.isNotEmpty() }?.entries
|
|
||||||
?.joinToString(",", "{", "}") { (k, v) -> "\"${k}\":\"${v}\"" }
|
|
||||||
controller.switchSource(url, null, headersJson)
|
|
||||||
controller.setMetadata(
|
|
||||||
title = title,
|
|
||||||
streamTitle = activeStreamTitle,
|
|
||||||
providerName = activeProviderName,
|
|
||||||
seasonNumber = activeSeasonNumber,
|
|
||||||
episodeNumber = activeEpisodeNumber,
|
|
||||||
episodeTitle = activeEpisodeTitle,
|
|
||||||
artwork = backdropArtwork,
|
|
||||||
logo = logo,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
controller.setOnSourceFilterChangedCallback { addonId ->
|
controller.setOnSourceFilterChangedCallback { addonId ->
|
||||||
PlayerStreamsRepository.selectSourceFilter(addonId)
|
PlayerStreamsRepository.selectSourceFilter(addonId)
|
||||||
|
|
@ -1678,6 +1721,17 @@ fun PlayerScreen(
|
||||||
controller.setOnEpisodeSelectedCallback { episodeId ->
|
controller.setOnEpisodeSelectedCallback { episodeId ->
|
||||||
val episode = playerMetaVideos.firstOrNull { it.id == episodeId }
|
val episode = playerMetaVideos.firstOrNull { it.id == episodeId }
|
||||||
?: return@setOnEpisodeSelectedCallback
|
?: return@setOnEpisodeSelectedCallback
|
||||||
|
val downloadedEpisode = DownloadsRepository.findPlayableDownload(
|
||||||
|
parentMetaId = parentMetaId,
|
||||||
|
seasonNumber = episode.season,
|
||||||
|
episodeNumber = episode.episode,
|
||||||
|
videoId = episode.id,
|
||||||
|
)
|
||||||
|
if (downloadedEpisode != null) {
|
||||||
|
switchToDownloadedEpisode(downloadedEpisode, episode)
|
||||||
|
controller.dismissNativePanels()
|
||||||
|
return@setOnEpisodeSelectedCallback
|
||||||
|
}
|
||||||
episodeStreamsPanelState = episodeStreamsPanelState.copy(
|
episodeStreamsPanelState = episodeStreamsPanelState.copy(
|
||||||
showStreams = true,
|
showStreams = true,
|
||||||
selectedEpisode = episode,
|
selectedEpisode = episode,
|
||||||
|
|
@ -1698,20 +1752,6 @@ fun PlayerScreen(
|
||||||
val episode = playerMetaVideos.firstOrNull { it.id == episodeStreamsPanelState.selectedEpisode?.id }
|
val episode = playerMetaVideos.firstOrNull { it.id == episodeStreamsPanelState.selectedEpisode?.id }
|
||||||
?: return@setOnEpisodeStreamSelectedCallback
|
?: return@setOnEpisodeStreamSelectedCallback
|
||||||
switchToEpisodeStream(stream, episode)
|
switchToEpisodeStream(stream, episode)
|
||||||
val headers = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request)
|
|
||||||
val headersJson = headers.takeIf { it.isNotEmpty() }?.entries
|
|
||||||
?.joinToString(",", "{", "}") { (k, v) -> "\"${k}\":\"${v}\"" }
|
|
||||||
controller.switchSource(url, null, headersJson)
|
|
||||||
controller.setMetadata(
|
|
||||||
title = title,
|
|
||||||
streamTitle = activeStreamTitle,
|
|
||||||
providerName = activeProviderName,
|
|
||||||
seasonNumber = activeSeasonNumber,
|
|
||||||
episodeNumber = activeEpisodeNumber,
|
|
||||||
episodeTitle = activeEpisodeTitle,
|
|
||||||
artwork = backdropArtwork,
|
|
||||||
logo = logo,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
controller.setOnEpisodeFilterChangedCallback { addonId ->
|
controller.setOnEpisodeFilterChangedCallback { addonId ->
|
||||||
PlayerStreamsRepository.selectEpisodeStreamsFilter(addonId)
|
PlayerStreamsRepository.selectEpisodeStreamsFilter(addonId)
|
||||||
|
|
@ -1821,7 +1861,7 @@ fun PlayerScreen(
|
||||||
},
|
},
|
||||||
onSourcesClick = if (activeVideoId != null) { { openSourcesPanel() } } else null,
|
onSourcesClick = if (activeVideoId != null) { { openSourcesPanel() } } else null,
|
||||||
onEpisodesClick = if (isSeries) { { openEpisodesPanel() } } else null,
|
onEpisodesClick = if (isSeries) { { openEpisodesPanel() } } else null,
|
||||||
onSubmitIntroClick = if (isSeries && playerSettingsUiState.introSubmitEnabled && playerSettingsUiState.introDbApiKey.isNotBlank()) { { showSubmitIntroModal = true } } else null,
|
onSubmitIntroClick = if (canSubmitIntro) { { showSubmitIntroModal = true } } else null,
|
||||||
onScrubChange = { positionMs -> scrubbingPositionMs = positionMs },
|
onScrubChange = { positionMs -> scrubbingPositionMs = positionMs },
|
||||||
onScrubFinished = { positionMs ->
|
onScrubFinished = { positionMs ->
|
||||||
scrubbingPositionMs = null
|
scrubbingPositionMs = null
|
||||||
|
|
@ -2075,9 +2115,7 @@ fun PlayerScreen(
|
||||||
|
|
||||||
val season = activeSeasonNumber
|
val season = activeSeasonNumber
|
||||||
val episode = activeEpisodeNumber
|
val episode = activeEpisodeNumber
|
||||||
val imdbId = activeVideoId?.split(":")?.firstOrNull()?.takeIf { it.startsWith("tt") }
|
val imdbId = resolveSubmitIntroImdbId()
|
||||||
?: parentMetaId.takeIf { it.startsWith("tt") }
|
|
||||||
?: metaUiState.meta?.id?.takeIf { it.startsWith("tt") }
|
|
||||||
|
|
||||||
if (showSubmitIntroModal && season != null && episode != null && !imdbId.isNullOrBlank()) {
|
if (showSubmitIntroModal && season != null && episode != null && !imdbId.isNullOrBlank()) {
|
||||||
com.nuvio.app.features.player.skip.SubmitIntroDialog(
|
com.nuvio.app.features.player.skip.SubmitIntroDialog(
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
|
||||||
var onEpisodeReloadCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
|
var onEpisodeReloadCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
var onEpisodeBackCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
|
var onEpisodeBackCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
var onNextEpisodeRequestedCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
|
var onNextEpisodeRequestedCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
|
var onSubmitIntroSubmittedCallback by remember {
|
||||||
|
mutableStateOf<((String, Double, Double) -> Unit)?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(playerPtr) {
|
DisposableEffect(playerPtr) {
|
||||||
bridge.nuvio_player_show(playerPtr)
|
bridge.nuvio_player_show(playerPtr)
|
||||||
|
|
@ -71,6 +74,14 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(playWhenReady) {
|
||||||
|
if (playWhenReady) {
|
||||||
|
bridge.nuvio_player_play(playerPtr)
|
||||||
|
} else {
|
||||||
|
bridge.nuvio_player_pause(playerPtr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(resizeMode) {
|
LaunchedEffect(resizeMode) {
|
||||||
val mode = when (resizeMode) {
|
val mode = when (resizeMode) {
|
||||||
PlayerResizeMode.Fit -> 0
|
PlayerResizeMode.Fit -> 0
|
||||||
|
|
@ -116,6 +127,11 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
|
||||||
label = bridge.nuvio_player_get_subtitle_track_label(playerPtr, index) ?: "",
|
label = bridge.nuvio_player_get_subtitle_track_label(playerPtr, index) ?: "",
|
||||||
language = bridge.nuvio_player_get_subtitle_track_lang(playerPtr, index),
|
language = bridge.nuvio_player_get_subtitle_track_lang(playerPtr, index),
|
||||||
isSelected = bridge.nuvio_player_is_subtitle_track_selected(playerPtr, index),
|
isSelected = bridge.nuvio_player_is_subtitle_track_selected(playerPtr, index),
|
||||||
|
isForced = inferForcedSubtitleTrack(
|
||||||
|
label = bridge.nuvio_player_get_subtitle_track_label(playerPtr, index),
|
||||||
|
language = bridge.nuvio_player_get_subtitle_track_lang(playerPtr, index),
|
||||||
|
trackId = bridge.nuvio_player_get_subtitle_track_id(playerPtr, index).toString(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,6 +215,10 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
|
||||||
bridge.nuvio_player_set_is_series(playerPtr, isSeries)
|
bridge.nuvio_player_set_is_series(playerPtr, isSeries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setSubmitIntroEnabled(enabled: Boolean) {
|
||||||
|
bridge.nuvio_player_set_submit_intro_enabled(playerPtr, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
override fun showSkipButton(type: String, endTimeMs: Long) {
|
override fun showSkipButton(type: String, endTimeMs: Long) {
|
||||||
bridge.nuvio_player_show_skip_button(playerPtr, type, endTimeMs)
|
bridge.nuvio_player_show_skip_button(playerPtr, type, endTimeMs)
|
||||||
}
|
}
|
||||||
|
|
@ -225,6 +245,10 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
|
||||||
onNextEpisodeRequestedCallback = callback
|
onNextEpisodeRequestedCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setOnSubmitIntroSubmittedCallback(callback: (String, Double, Double) -> Unit) {
|
||||||
|
onSubmitIntroSubmittedCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
override fun setOnCloseCallback(callback: () -> Unit) {
|
override fun setOnCloseCallback(callback: () -> Unit) {
|
||||||
onCloseCallback = callback
|
onCloseCallback = callback
|
||||||
}
|
}
|
||||||
|
|
@ -378,13 +402,17 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
|
||||||
bridge.nuvio_player_show_episode_streams(playerPtr, season ?: 0, episode ?: 0, title)
|
bridge.nuvio_player_show_episode_streams(playerPtr, season ?: 0, episode ?: 0, title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dismissNativePanels() {
|
||||||
|
bridge.nuvio_player_dismiss_panels(playerPtr)
|
||||||
|
}
|
||||||
|
|
||||||
override fun switchSource(url: String, audioUrl: String?, headersJson: String?) {
|
override fun switchSource(url: String, audioUrl: String?, headersJson: String?) {
|
||||||
bridge.nuvio_player_load_file(playerPtr, url, audioUrl, headersJson)
|
bridge.nuvio_player_load_file(playerPtr, url, audioUrl, headersJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(controller) {
|
LaunchedEffect(controller, sourceUrl) {
|
||||||
onControllerReady(controller)
|
onControllerReady(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,6 +452,13 @@ internal object MacOSMpvPlayerBackend : DesktopPlaybackBackend {
|
||||||
if (bridge.nuvio_player_pop_next_episode_pressed(playerPtr)) {
|
if (bridge.nuvio_player_pop_next_episode_pressed(playerPtr)) {
|
||||||
onNextEpisodeRequestedCallback?.invoke()
|
onNextEpisodeRequestedCallback?.invoke()
|
||||||
}
|
}
|
||||||
|
if (bridge.nuvio_player_pop_submit_intro_requested(playerPtr)) {
|
||||||
|
onSubmitIntroSubmittedCallback?.invoke(
|
||||||
|
bridge.nuvio_player_get_submit_intro_segment_type(playerPtr) ?: "intro",
|
||||||
|
bridge.nuvio_player_get_submit_intro_start_sec(playerPtr),
|
||||||
|
bridge.nuvio_player_get_submit_intro_end_sec(playerPtr),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (bridge.nuvio_player_pop_sources_open_requested(playerPtr)) {
|
if (bridge.nuvio_player_pop_sources_open_requested(playerPtr)) {
|
||||||
onSourcesRequestedCallback?.invoke()
|
onSourcesRequestedCallback?.invoke()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ internal interface MacOSMPVBridgeLib : Library {
|
||||||
|
|
||||||
fun nuvio_player_set_has_video_id(player: Pointer, value: Boolean)
|
fun nuvio_player_set_has_video_id(player: Pointer, value: Boolean)
|
||||||
fun nuvio_player_set_is_series(player: Pointer, value: Boolean)
|
fun nuvio_player_set_is_series(player: Pointer, value: Boolean)
|
||||||
|
fun nuvio_player_set_submit_intro_enabled(player: Pointer, enabled: Boolean)
|
||||||
|
|
||||||
fun nuvio_player_load_file(
|
fun nuvio_player_load_file(
|
||||||
player: Pointer,
|
player: Pointer,
|
||||||
|
|
@ -120,6 +121,10 @@ internal interface MacOSMPVBridgeLib : Library {
|
||||||
|
|
||||||
fun nuvio_player_is_closed(player: Pointer): Boolean
|
fun nuvio_player_is_closed(player: Pointer): Boolean
|
||||||
fun nuvio_player_pop_next_episode_pressed(player: Pointer): Boolean
|
fun nuvio_player_pop_next_episode_pressed(player: Pointer): Boolean
|
||||||
|
fun nuvio_player_pop_submit_intro_requested(player: Pointer): Boolean
|
||||||
|
fun nuvio_player_get_submit_intro_segment_type(player: Pointer): String?
|
||||||
|
fun nuvio_player_get_submit_intro_start_sec(player: Pointer): Double
|
||||||
|
fun nuvio_player_get_submit_intro_end_sec(player: Pointer): Double
|
||||||
fun nuvio_player_is_addon_subtitles_fetch_requested(player: Pointer): Boolean
|
fun nuvio_player_is_addon_subtitles_fetch_requested(player: Pointer): Boolean
|
||||||
fun nuvio_player_set_addon_subtitles_loading(player: Pointer, loading: Boolean)
|
fun nuvio_player_set_addon_subtitles_loading(player: Pointer, loading: Boolean)
|
||||||
fun nuvio_player_clear_addon_subtitles(player: Pointer)
|
fun nuvio_player_clear_addon_subtitles(player: Pointer)
|
||||||
|
|
@ -159,4 +164,5 @@ internal interface MacOSMPVBridgeLib : Library {
|
||||||
fun nuvio_player_add_episode_addon_group(player: Pointer, id: String, addonName: String, addonId: String, isLoading: Boolean, hasError: Boolean)
|
fun nuvio_player_add_episode_addon_group(player: Pointer, id: String, addonName: String, addonId: String, isLoading: Boolean, hasError: Boolean)
|
||||||
fun nuvio_player_set_episode_selected_filter(player: Pointer, addonId: String?)
|
fun nuvio_player_set_episode_selected_filter(player: Pointer, addonId: String?)
|
||||||
fun nuvio_player_show_episode_streams(player: Pointer, season: Int, episode: Int, title: String?)
|
fun nuvio_player_show_episode_streams(player: Pointer, season: Int, episode: Int, title: String?)
|
||||||
|
fun nuvio_player_dismiss_panels(player: Pointer)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue