mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
Split macOS player mode into window toggle + floating control
Replace the three-way macOS Player Mode picker (Separate Window / Floating Window / Inline) with a single 'Play in a separate window' toggle in Playback settings, and move the always-on-top choice to a pin button in the player's top bar (window mode only). Floating was really a transient window property (NSWindow.level), not a peer of the window/inline structural choice — so it belongs on a live control, not in Settings. The pin state persists across sessions. - Replace MacPlayerMode enum with macPlayerSeparateWindow / macPlayerFloating bool settings - Branch runtime presentation and window level off the two bools - Add pin.fill/pin toggle to MacOSPlayerControlsView top bar - Update localization; drop obsolete playerMode strings and enum tests
This commit is contained in:
@@ -61,7 +61,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.onChange(of: appEnvironment.navigationCoordinator.playerExpandTrigger) { _, _ in
|
.onChange(of: appEnvironment.navigationCoordinator.playerExpandTrigger) { _, _ in
|
||||||
if appEnvironment.settingsManager.macPlayerMode.usesWindow {
|
if appEnvironment.settingsManager.macPlayerSeparateWindow {
|
||||||
presentExpandedPlayerWindow(appEnvironment: appEnvironment)
|
presentExpandedPlayerWindow(appEnvironment: appEnvironment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,26 +70,33 @@ struct ContentView: View {
|
|||||||
ExpandedPlayerWindowManager.shared.hide()
|
ExpandedPlayerWindowManager.shared.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: appEnvironment.settingsManager.macPlayerMode) { oldMode, newMode in
|
.onChange(of: appEnvironment.settingsManager.macPlayerSeparateWindow) { _, separateWindow in
|
||||||
guard appEnvironment.navigationCoordinator.isPlayerExpanded else { return }
|
guard appEnvironment.navigationCoordinator.isPlayerExpanded else { return }
|
||||||
|
|
||||||
if oldMode.usesWindow && newMode.usesWindow {
|
if separateWindow {
|
||||||
ExpandedPlayerWindowManager.shared.updateWindowLevel(floating: newMode.isFloating)
|
// Inline sheet → separate window: let the sheet dismiss first, then
|
||||||
} else if oldMode.usesWindow && !newMode.usesWindow {
|
// present the window (mirrors the previous inline→window transition).
|
||||||
ExpandedPlayerWindowManager.shared.hide(animated: false)
|
|
||||||
} else if !oldMode.usesWindow && newMode.usesWindow {
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try? await Task.sleep(for: .milliseconds(300))
|
try? await Task.sleep(for: .milliseconds(300))
|
||||||
if appEnvironment.navigationCoordinator.isPlayerExpanded {
|
if appEnvironment.navigationCoordinator.isPlayerExpanded {
|
||||||
presentExpandedPlayerWindow(appEnvironment: appEnvironment)
|
presentExpandedPlayerWindow(appEnvironment: appEnvironment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Separate window → inline sheet: hide the window so the sheet binding
|
||||||
|
// (isPlayerExpanded && !separateWindow) takes over.
|
||||||
|
ExpandedPlayerWindowManager.shared.hide(animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: appEnvironment.settingsManager.macPlayerFloating) { _, floating in
|
||||||
|
guard appEnvironment.navigationCoordinator.isPlayerExpanded,
|
||||||
|
appEnvironment.settingsManager.macPlayerSeparateWindow else { return }
|
||||||
|
ExpandedPlayerWindowManager.shared.updateWindowLevel(floating: floating)
|
||||||
|
}
|
||||||
.sheet(isPresented: Binding(
|
.sheet(isPresented: Binding(
|
||||||
get: {
|
get: {
|
||||||
appEnvironment.navigationCoordinator.isPlayerExpanded &&
|
appEnvironment.navigationCoordinator.isPlayerExpanded &&
|
||||||
!appEnvironment.settingsManager.macPlayerMode.usesWindow
|
!appEnvironment.settingsManager.macPlayerSeparateWindow
|
||||||
},
|
},
|
||||||
set: { appEnvironment.navigationCoordinator.isPlayerExpanded = $0 }
|
set: { appEnvironment.navigationCoordinator.isPlayerExpanded = $0 }
|
||||||
)) {
|
)) {
|
||||||
@@ -164,7 +171,7 @@ struct ContentView: View {
|
|||||||
let isExpanded = appEnvironment.navigationCoordinator.isPlayerExpanded
|
let isExpanded = appEnvironment.navigationCoordinator.isPlayerExpanded
|
||||||
// The expanded player is a separate window in window mode, so keep the capsule
|
// The expanded player is a separate window in window mode, so keep the capsule
|
||||||
// visible alongside it. In sheet mode the sheet covers the window, so hide it.
|
// visible alongside it. In sheet mode the sheet covers the window, so hide it.
|
||||||
let usesWindow = appEnvironment.settingsManager.macPlayerMode.usesWindow
|
let usesWindow = appEnvironment.settingsManager.macPlayerSeparateWindow
|
||||||
|
|
||||||
if hasActiveVideo && (!isExpanded || usesWindow) {
|
if hasActiveVideo && (!isExpanded || usesWindow) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ enum SettingsKey: String, CaseIterable {
|
|||||||
case resolveShortLinksEnabled
|
case resolveShortLinksEnabled
|
||||||
|
|
||||||
// Platform-specific
|
// Platform-specific
|
||||||
case macPlayerMode
|
case macPlayerSeparateWindow
|
||||||
|
case macPlayerFloating
|
||||||
case playerSheetAutoResize
|
case playerSheetAutoResize
|
||||||
case listStyle
|
case listStyle
|
||||||
|
|
||||||
@@ -136,7 +137,8 @@ enum SettingsKey: String, CaseIterable {
|
|||||||
/// in both UserDefaults and iCloud, so each platform family syncs independently.
|
/// in both UserDefaults and iCloud, so each platform family syncs independently.
|
||||||
var isPlatformSpecific: Bool {
|
var isPlatformSpecific: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .preferredQuality, .cellularQuality, .allowSoftwareDecodedFormats, .macPlayerMode, .listStyle,
|
case .preferredQuality, .cellularQuality, .allowSoftwareDecodedFormats,
|
||||||
|
.macPlayerSeparateWindow, .macPlayerFloating, .listStyle,
|
||||||
// Home layout — different UI paradigms per platform
|
// Home layout — different UI paradigms per platform
|
||||||
.homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout, .homeShortcutCardStyle,
|
.homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout, .homeShortcutCardStyle,
|
||||||
.homeShortcutColorfulPalette, .homeShortcutCustomPaletteColors,
|
.homeShortcutColorfulPalette, .homeShortcutCustomPaletteColors,
|
||||||
|
|||||||
@@ -68,14 +68,30 @@ extension SettingsManager {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
var macPlayerMode: MacPlayerMode {
|
/// Whether the expanded player opens in a separate window (vs. an inline sheet).
|
||||||
|
/// Default is `true`.
|
||||||
|
var macPlayerSeparateWindow: Bool {
|
||||||
get {
|
get {
|
||||||
if let cached = _macPlayerMode { return cached }
|
if let cached = _macPlayerSeparateWindow { return cached }
|
||||||
return MacPlayerMode(rawValue: string(for: .macPlayerMode) ?? "") ?? .window
|
return bool(for: .macPlayerSeparateWindow, default: true)
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
_macPlayerMode = newValue
|
_macPlayerSeparateWindow = newValue
|
||||||
set(newValue.rawValue, for: .macPlayerMode)
|
set(newValue, for: .macPlayerSeparateWindow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the separate player window floats above other windows (always on top).
|
||||||
|
/// Toggled live from the player's top-bar pin button and remembered across sessions.
|
||||||
|
/// Default is `false`.
|
||||||
|
var macPlayerFloating: Bool {
|
||||||
|
get {
|
||||||
|
if let cached = _macPlayerFloating { return cached }
|
||||||
|
return bool(for: .macPlayerFloating, default: false)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_macPlayerFloating = newValue
|
||||||
|
set(newValue, for: .macPlayerFloating)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,37 +201,6 @@ enum DownloadQuality: String, CaseIterable, Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - macOS Player Mode
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
enum MacPlayerMode: String, CaseIterable, Codable {
|
|
||||||
case window
|
|
||||||
case floatingWindow
|
|
||||||
case inline
|
|
||||||
|
|
||||||
var displayName: String {
|
|
||||||
switch self {
|
|
||||||
case .window: return String(localized: "settings.playback.macOS.playerMode.window")
|
|
||||||
case .floatingWindow: return String(localized: "settings.playback.macOS.playerMode.floatingWindow")
|
|
||||||
case .inline: return String(localized: "settings.playback.macOS.playerMode.inline")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether this mode uses a separate window (vs sheet/inline)
|
|
||||||
var usesWindow: Bool {
|
|
||||||
switch self {
|
|
||||||
case .window, .floatingWindow: return true
|
|
||||||
case .inline: return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the window should float above other windows
|
|
||||||
var isFloating: Bool {
|
|
||||||
self == .floatingWindow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// MARK: - Haptic Feedback
|
// MARK: - Haptic Feedback
|
||||||
|
|
||||||
/// Intensity levels for haptic feedback.
|
/// Intensity levels for haptic feedback.
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ final class SettingsManager {
|
|||||||
var _preferPortraitBrowsing: Bool?
|
var _preferPortraitBrowsing: Bool?
|
||||||
#endif
|
#endif
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
var _macPlayerMode: MacPlayerMode?
|
var _macPlayerSeparateWindow: Bool?
|
||||||
|
var _macPlayerFloating: Bool?
|
||||||
var _playerSheetAutoResize: Bool?
|
var _playerSheetAutoResize: Bool?
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -456,7 +457,8 @@ final class SettingsManager {
|
|||||||
_syncSearchHistory = nil
|
_syncSearchHistory = nil
|
||||||
_searchHistoryLimit = nil
|
_searchHistoryLimit = nil
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
_macPlayerMode = nil
|
_macPlayerSeparateWindow = nil
|
||||||
|
_macPlayerFloating = nil
|
||||||
_playerSheetAutoResize = nil
|
_playerSheetAutoResize = nil
|
||||||
#endif
|
#endif
|
||||||
// miniPlayerShowVideo and miniPlayerVideoTapAction moved to preset
|
// miniPlayerShowVideo and miniPlayerVideoTapAction moved to preset
|
||||||
|
|||||||
@@ -7234,6 +7234,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"player.controls.keepOnTop" : {
|
||||||
|
"comment" : "Toggles whether the player window floats above other windows",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Keep on Top"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"player.controls.pause" : {
|
"player.controls.pause" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -12442,46 +12453,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.playback.macOS.playerMode" : {
|
"settings.playback.macOS.separateWindow" : {
|
||||||
"comment" : "macOS player mode setting",
|
"comment" : "Toggle: play the expanded video in a separate window vs. an inline sheet",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Player Mode"
|
"value" : "Play in a separate window"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.playback.macOS.playerMode.floatingWindow" : {
|
|
||||||
"comment" : "Floating window player mode - stays on top of other windows",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Floating Window"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.playback.macOS.playerMode.inline" : {
|
|
||||||
"comment" : "Inline (sheet) player mode",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Inline (Sheet)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.playback.macOS.playerMode.window" : {
|
|
||||||
"comment" : "Separate window player mode",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Separate Window"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
// Mark expanding state for mini player coordination
|
// Mark expanding state for mini player coordination
|
||||||
appEnvironment.navigationCoordinator.isPlayerExpanding = true
|
appEnvironment.navigationCoordinator.isPlayerExpanding = true
|
||||||
|
|
||||||
// Get the current player mode for window configuration
|
// Whether the window should float above other windows (always on top).
|
||||||
let mode = appEnvironment.settingsManager.macPlayerMode
|
let floating = appEnvironment.settingsManager.macPlayerFloating
|
||||||
|
|
||||||
// Host a lightweight two-phase root instead of ExpandedPlayerSheet directly.
|
// Host a lightweight two-phase root instead of ExpandedPlayerSheet directly.
|
||||||
// AppKit won't composite the window to screen until the hosted SwiftUI view
|
// AppKit won't composite the window to screen until the hosted SwiftUI view
|
||||||
@@ -133,8 +133,8 @@ final class ExpandedPlayerWindowManager: NSObject {
|
|||||||
// Make ExpandedPlayerWindowManager itself the delegate to avoid lifecycle issues
|
// Make ExpandedPlayerWindowManager itself the delegate to avoid lifecycle issues
|
||||||
window.delegate = self
|
window.delegate = self
|
||||||
|
|
||||||
// Configure window level based on mode
|
// Configure window level based on the floating preference
|
||||||
configureWindowLevel(window, floating: mode.isFloating)
|
configureWindowLevel(window, floating: floating)
|
||||||
|
|
||||||
// Center window on screen
|
// Center window on screen
|
||||||
window.center()
|
window.center()
|
||||||
|
|||||||
@@ -113,6 +113,24 @@ struct MacOSPlayerControlsView: View {
|
|||||||
|
|
||||||
Spacer(minLength: 12)
|
Spacer(minLength: 12)
|
||||||
|
|
||||||
|
// Floating (always-on-top) toggle — only meaningful for the separate
|
||||||
|
// window presentation, so hidden in the inline sheet and in fullscreen.
|
||||||
|
if let settings = appEnvironment?.settingsManager,
|
||||||
|
settings.macPlayerSeparateWindow, !isFullscreen {
|
||||||
|
Button {
|
||||||
|
settings.macPlayerFloating.toggle()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: settings.macPlayerFloating ? "pin.fill" : "pin")
|
||||||
|
.font(.system(size: 15, weight: .semibold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
|
.background(.ultraThinMaterial, in: Circle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help(Text("player.controls.keepOnTop"))
|
||||||
|
.accessibilityLabel(Text("player.controls.keepOnTop"))
|
||||||
|
}
|
||||||
|
|
||||||
if onClose != nil {
|
if onClose != nil {
|
||||||
Button {
|
Button {
|
||||||
onClose?()
|
onClose?()
|
||||||
|
|||||||
@@ -250,14 +250,10 @@ private struct BehaviorSection: View {
|
|||||||
|
|
||||||
SettingsFormSection("settings.playback.behavior.header", footer: footer) {
|
SettingsFormSection("settings.playback.behavior.header", footer: footer) {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
PlatformMenuPicker(
|
Toggle(
|
||||||
String(localized: "settings.playback.macOS.playerMode"),
|
String(localized: "settings.playback.macOS.separateWindow"),
|
||||||
selection: $settings.macPlayerMode
|
isOn: $settings.macPlayerSeparateWindow
|
||||||
) {
|
)
|
||||||
ForEach(MacPlayerMode.allCases, id: \.self) { mode in
|
|
||||||
Text(mode.displayName).tag(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle(
|
Toggle(
|
||||||
String(localized: "settings.playback.macOS.autoResizePlayer"),
|
String(localized: "settings.playback.macOS.autoResizePlayer"),
|
||||||
|
|||||||
@@ -174,38 +174,6 @@ struct SponsorBlockCategoryTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MacPlayerMode Tests (macOS only)
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
@Suite("MacPlayerMode Tests")
|
|
||||||
@MainActor
|
|
||||||
struct MacPlayerModeTests {
|
|
||||||
|
|
||||||
@Test("MacPlayerMode cases")
|
|
||||||
func allCases() {
|
|
||||||
let cases = MacPlayerMode.allCases
|
|
||||||
#expect(cases.contains(.window))
|
|
||||||
#expect(cases.contains(.inline))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("MacPlayerMode display names")
|
|
||||||
func displayNames() {
|
|
||||||
#expect(MacPlayerMode.window.displayName == "Separate Window")
|
|
||||||
#expect(MacPlayerMode.floatingWindow.displayName == "Floating Window")
|
|
||||||
#expect(MacPlayerMode.inline.displayName == "Inline (Sheet)")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test("MacPlayerMode is Codable")
|
|
||||||
func codable() throws {
|
|
||||||
for mode in MacPlayerMode.allCases {
|
|
||||||
let encoded = try JSONEncoder().encode(mode)
|
|
||||||
let decoded = try JSONDecoder().decode(MacPlayerMode.self, from: encoded)
|
|
||||||
#expect(mode == decoded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// MARK: - UserAgentGenerator Tests
|
// MARK: - UserAgentGenerator Tests
|
||||||
|
|
||||||
@Suite("UserAgentGenerator Tests")
|
@Suite("UserAgentGenerator Tests")
|
||||||
|
|||||||
Reference in New Issue
Block a user