feat: add opening/ending autoskip and correct sponsor hover anchors
Sponsor hover labels used the overlapping chapter midpoint; retain chapter-title lookup but anchor the label to the sponsored range midpoint.
This commit is contained in:
+31
-14
@@ -77,7 +77,8 @@ final class PlayerModel: ObservableObject {
|
||||
private var metadataTask: Task<Void, Never>?
|
||||
@Published private(set) var sponsorBlockEnabled: Bool
|
||||
@Published private(set) var sponsorBlockAutoSkip: Bool
|
||||
private var sponsorSeekPosition: Double?
|
||||
@Published private(set) var openingEndingAutoSkip: Bool
|
||||
private var segmentSeekPosition: Double?
|
||||
private let preferenceDefaults: UserDefaults
|
||||
private var sponsorVideoID: String?
|
||||
private var sponsorGeneration: UInt64 = 0
|
||||
@@ -142,9 +143,12 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func updateSegmentSkip() {
|
||||
if sponsorBlockAutoSkip, state.status == PLAYER_PLAYING, effectivePlaying, !state.buffering,
|
||||
let range = sponsorToSkip,
|
||||
sponsorSeekPosition.map({ $0 >= range.lowerBound - 5 && $0 < range.upperBound }) != true {
|
||||
var automaticRange = sponsorBlockAutoSkip ? sponsorToSkip : nil
|
||||
if automaticRange == nil, openingEndingAutoSkip, let interval = currentSkipInterval {
|
||||
automaticRange = interval.start...interval.end
|
||||
}
|
||||
if let range = automaticRange, state.status == PLAYER_PLAYING, effectivePlaying, !state.buffering,
|
||||
segmentSeekPosition.map({ $0 >= range.lowerBound - 5 && $0 < range.upperBound }) != true {
|
||||
_ = seek(to: range.upperBound, userInitiated: false)
|
||||
}
|
||||
guard let interval = currentSkipInterval else {
|
||||
@@ -291,6 +295,7 @@ final class PlayerModel: ObservableObject {
|
||||
preferenceDefaults = defaults
|
||||
sponsorBlockEnabled = (defaults.object(forKey: "sponsorBlockEnabled") as? Bool) ?? configuration.sponsorBlock
|
||||
sponsorBlockAutoSkip = (defaults.object(forKey: "sponsorBlockAutoSkip") as? Bool) ?? configuration.sponsorBlockAutoSkip
|
||||
openingEndingAutoSkip = (defaults.object(forKey: "openingEndingAutoSkip") as? Bool) ?? configuration.openingEndingAutoSkip
|
||||
miruroVideoProvider = configuration.miruroVideoProvider
|
||||
miruroCaptionProvider = configuration.miruroCaptionProvider
|
||||
compactWindow = CompactWindowController(snappingEnabled: configuration.snapToEdges, defaults: defaults)
|
||||
@@ -920,6 +925,11 @@ final class PlayerModel: ObservableObject {
|
||||
preferenceDefaults.set(enabled, forKey: "sponsorBlockAutoSkip")
|
||||
}
|
||||
|
||||
func setOpeningEndingAutoSkip(_ enabled: Bool) {
|
||||
openingEndingAutoSkip = enabled
|
||||
preferenceDefaults.set(enabled, forKey: "openingEndingAutoSkip")
|
||||
}
|
||||
|
||||
private func updateSponsorRanges() {
|
||||
sponsorRanges = SponsorSegment.visibleRanges(sponsorSegments, duration: state.duration)
|
||||
}
|
||||
@@ -1105,7 +1115,7 @@ final class PlayerModel: ObservableObject {
|
||||
window?.title = fileName ?? "Terebi"
|
||||
revealControls()
|
||||
replaceMetadata(for: source)
|
||||
if !change.isQuality { sponsorSeekPosition = savedPosition }
|
||||
if !change.isQuality { segmentSeekPosition = savedPosition }
|
||||
systemPlayback?.update()
|
||||
}
|
||||
|
||||
@@ -1285,12 +1295,12 @@ final class PlayerModel: ObservableObject {
|
||||
guard canSeek, let handle, target.isFinite else { return false }
|
||||
if transition.resolvingAutomaticQuality { cancelStreamResolution() }
|
||||
automaticQuality.resetHealth()
|
||||
let previousSeek = sponsorSeekPosition
|
||||
let previousSeek = segmentSeekPosition
|
||||
// Record intent before Cast can synchronously publish a new state.
|
||||
if userInitiated { sponsorSeekPosition = target }
|
||||
if userInitiated { segmentSeekPosition = target }
|
||||
let accepted = casting.active ? seekCast(to: target) : transition.seek(target, handle: handle)
|
||||
if accepted { seekPosition = target }
|
||||
else { sponsorSeekPosition = previousSeek }
|
||||
else { segmentSeekPosition = previousSeek }
|
||||
commandError = accepted ? nil : "Could not queue the seek. Please try again."
|
||||
if !accepted { revealControls() }
|
||||
return accepted
|
||||
@@ -1300,7 +1310,7 @@ final class PlayerModel: ObservableObject {
|
||||
func stepFrame(_ direction: Int32) -> Bool {
|
||||
guard canStepFrame, let handle else { return false }
|
||||
let accepted = transition.stepFrame(direction, handle: handle)
|
||||
if accepted { sponsorSeekPosition = state.position }
|
||||
if accepted { segmentSeekPosition = state.position }
|
||||
commandError = accepted ? nil : "Could not queue the frame step. Please try again."
|
||||
revealControls()
|
||||
return accepted
|
||||
@@ -2611,8 +2621,10 @@ private struct PlaybackSlider: NSViewRepresentable {
|
||||
let lower = index == 0 ? minValue : cell.boundaries[index - 1]
|
||||
let upper = index == cell.boundaries.count ? maxValue : cell.boundaries[index]
|
||||
let middle = (lower + upper) / 2
|
||||
setHover(Hover(time: middle, x: start + (middle - minValue) / (maxValue - minValue) * width,
|
||||
sponsored: cell.sponsoredRanges.contains { $0.lowerBound <= time && time < $0.upperBound }))
|
||||
let sponsor = cell.sponsoredRanges.first { $0.lowerBound <= time && time < $0.upperBound }
|
||||
let anchor = sponsor.map { ($0.lowerBound + $0.upperBound) / 2 } ?? middle
|
||||
setHover(Hover(time: middle, x: start + (anchor - minValue) / (maxValue - minValue) * width,
|
||||
sponsored: sponsor != nil))
|
||||
}
|
||||
|
||||
func refreshHover() {
|
||||
@@ -2842,14 +2854,18 @@ func playbackSliderHitCheck() -> Bool {
|
||||
pixel(50).redComponent != pixel(30).redComponent,
|
||||
pixel(10).alphaComponent > 0, pixel(40).alphaComponent > 0 else { return false }
|
||||
cell.emphasis = []
|
||||
for (time, middle) in [(0.0, 12.5), (25.0, 50.0), (75.0, 87.5), (100.0, 87.5)] {
|
||||
for (time, middle, anchor, sponsored) in [
|
||||
(0.0, 12.5, 12.5, false), (20.0, 12.5, 25.0, true),
|
||||
(25.0, 50.0, 25.0, true), (30.0, 50.0, 25.0, true),
|
||||
(40.0, 50.0, 50.0, false), (75.0, 87.5, 87.5, false), (100.0, 87.5, 87.5, false)
|
||||
] {
|
||||
let point = slider.convert(NSPoint(x: left + time / 100 * (right - left), y: 7), to: nil)
|
||||
let event = NSEvent.mouseEvent(with: .mouseMoved, location: point, modifierFlags: [],
|
||||
timestamp: 0, windowNumber: window.windowNumber, context: nil, eventNumber: 0, clickCount: 0, pressure: 0)!
|
||||
slider.mouseMoved(with: event)
|
||||
guard hovered?.time == middle,
|
||||
hovered?.sponsored == (time == 25),
|
||||
abs(hovered!.x - (left + middle / 100 * (right - left))) < 0.5 else { return false }
|
||||
hovered?.sponsored == sponsored,
|
||||
abs(hovered!.x - (left + anchor / 100 * (right - left))) < 0.5 else { return false }
|
||||
}
|
||||
let exit = NSEvent.enterExitEvent(with: .mouseExited, location: .zero, modifierFlags: [],
|
||||
timestamp: 0, windowNumber: window.windowNumber, context: nil, eventNumber: 0, trackingNumber: 0, userData: nil)!
|
||||
@@ -3378,6 +3394,7 @@ private struct PlayerContent: View {
|
||||
Divider()
|
||||
PlayerQualityMenu(model: model)
|
||||
PlayerMiruroMenu(model: model)
|
||||
PlayerOpeningEndingSkipToggle(model: model)
|
||||
PlayerSponsorBlockMenu(model: model)
|
||||
Divider()
|
||||
if model.isCompact {
|
||||
|
||||
Reference in New Issue
Block a user