Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400/* * This file is part of mpv. * * mpv is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * mpv is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mpv. If not, see <http://www.gnu.org/licenses/>. */
import Cocoa
extension MenuBar { class MenuItem: NSMenuItem { var config: Config? }
enum `Type`: Comparable { case menu case menuServices case separator case item case itemNormalSize case itemHalfSize case itemDoubleSize case itemMinimize case itemZoom }
struct Config { let name: String let key: String let modifiers: NSEvent.ModifierFlags let type: Type let action: Selector? let target: AnyObject? let command: String let url: String var configs: [Config]
init( name: String = "", key: String = "", modifiers: NSEvent.ModifierFlags = .command, type: Type = .item, action: Selector? = nil, target: AnyObject? = nil, command: String = "", url: String = "", configs: [Config] = [] ) { self.name = name self.key = key self.modifiers = modifiers self.type = configs.isEmpty ? type : .menu self.action = action self.target = target self.command = command self.url = url self.configs = configs } }}
class MenuBar: NSObject { unowned let appHub: AppHub var option: OptionHelper? { return appHub.option } let mainMenu = NSMenu(title: "Main") let servicesMenu = NSMenu(title: "Services") var menuConfigs: [Config] = [] var dynamicMenuItems: [Type: [MenuItem]] = [:] let appIcon: NSImage
@objc init(_ appHub: AppHub) { self.appHub = appHub UserDefaults.standard.set(false, forKey: "NSFullScreenMenuItemEverywhere") UserDefaults.standard.set(true, forKey: "NSDisabledDictationMenuItem") UserDefaults.standard.set(true, forKey: "NSDisabledCharacterPaletteMenuItem") NSWindow.allowsAutomaticWindowTabbing = false appIcon = appHub.getIcon()
super.init()
let appMenuConfigs = [ Config(name: "About mpv", action: #selector(about), target: self), Config(type: .separator), Config( name: "Settings…", key: ",", action: #selector(settings(_:)), target: self, url: "mpv.conf" ), Config( name: "Keyboard Shortcuts Config…", action: #selector(settings(_:)), target: self, url: "input.conf" ), Config(type: .separator), Config(name: "Services", type: .menuServices), Config(type: .separator), Config(name: "Hide mpv", key: "h", action: #selector(NSApp.hide(_:))), Config(name: "Hide Others", key: "h", modifiers: [.command, .option], action: #selector(NSApp.hideOtherApplications(_:))), Config(name: "Show All", action: #selector(NSApp.unhideAllApplications(_:))), Config(type: .separator), Config(name: "Quit and Remember Position", action: #selector(command(_:)), target: self, command: "quit-watch-later"), Config(name: "Quit mpv", key: "q", action: #selector(command(_:)), target: self, command: "quit") ]
let fileMenuConfigs = [ Config(name: "Open File…", key: "o", action: #selector(openFiles), target: self), Config(name: "Open URL…", key: "O", action: #selector(openUrl), target: self), Config(name: "Open Playlist…", action: #selector(openPlaylist), target: self), Config(type: .separator), Config(name: "Close", key: "w", action: #selector(NSWindow.performClose(_:))), Config(name: "Save Screenshot", action: #selector(command(_:)), target: self, command: "async screenshot") ]
let editMenuConfigs = [ Config(name: "Undo", key: "z", action: Selector(("undo:"))), Config(name: "Redo", key: "Z", action: Selector(("redo:"))), Config(type: .separator), Config(name: "Cut", key: "x", action: #selector(NSText.cut(_:))), Config(name: "Copy", key: "c", action: #selector(NSText.copy(_:))), Config(name: "Paste", key: "v", action: #selector(NSText.paste(_:))), Config(name: "Select All", key: "a", action: #selector(NSResponder.selectAll(_:))) ]
var viewMenuConfigs = [ Config(name: "Toggle Fullscreen", action: #selector(command(_:)), target: self, command: "cycle fullscreen"), Config(name: "Toggle Float on Top", action: #selector(command(_:)), target: self, command: "cycle ontop"), Config( name: "Toggle Visibility on All Workspaces", action: #selector(command(_:)), target: self, command: "cycle on-all-workspaces" ) ]#if HAVE_MACOS_TOUCHBAR viewMenuConfigs += [ Config(type: .separator), Config(name: "Customize Touch Bar…", action: #selector(NSApp.toggleTouchBarCustomizationPalette(_:))) ]#endif
let videoMenuConfigs = [ Config(name: "Zoom Out", action: #selector(command(_:)), target: self, command: "add panscan -0.1"), Config(name: "Zoom In", action: #selector(command(_:)), target: self, command: "add panscan 0.1"), Config(name: "Reset Zoom", action: #selector(command(_:)), target: self, command: "set panscan 0"), Config(type: .separator), Config(name: "Aspect Ratio 4:3", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"4:3\""), Config(name: "Aspect Ratio 16:9", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"16:9\""), Config(name: "Aspect Ratio 1.85:1", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"1.85:1\""), Config(name: "Aspect Ratio 2.35:1", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"2.35:1\""), Config(name: "Reset Aspect Ratio", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"-1\""), Config(type: .separator), Config(name: "Rotate Left", action: #selector(command(_:)), target: self, command: "cycle-values video-rotate 0 270 180 90"), Config(name: "Rotate Right", action: #selector(command(_:)), target: self, command: "cycle-values video-rotate 90 180 270 0"), Config(name: "Reset Rotation", action: #selector(command(_:)), target: self, command: "set video-rotate 0"), Config(type: .separator), Config(name: "Half Size", key: "0", type: .itemHalfSize), Config(name: "Normal Size", key: "1", type: .itemNormalSize), Config(name: "Double Size", key: "2", type: .itemDoubleSize) ]
let audioMenuConfigs = [ Config(name: "Next Audio Track", action: #selector(command(_:)), target: self, command: "cycle audio"), Config(name: "Previous Audio Track", action: #selector(command(_:)), target: self, command: "cycle audio down"), Config(type: .separator), Config(name: "Toggle Mute", action: #selector(command(_:)), target: self, command: "cycle mute"), Config(type: .separator), Config(name: "Play Audio Later", action: #selector(command(_:)), target: self, command: "add audio-delay 0.1"), Config(name: "Play Audio Earlier", action: #selector(command(_:)), target: self, command: "add audio-delay -0.1"), Config(name: "Reset Audio Delay", action: #selector(command(_:)), target: self, command: "set audio-delay 0.0") ]
let subtitleMenuConfigs = [ Config(name: "Next Subtitle Track", action: #selector(command(_:)), target: self, command: "cycle sub"), Config(name: "Previous Subtitle Track", action: #selector(command(_:)), target: self, command: "cycle sub down"), Config(type: .separator), Config(name: "Toggle Force Style", action: #selector(command(_:)), target: self, command: "cycle-values sub-ass-override \"force\" \"no\""), Config(type: .separator), Config(name: "Display Subtitles Later", action: #selector(command(_:)), target: self, command: "add sub-delay 0.1"), Config(name: "Display Subtitles Earlier", action: #selector(command(_:)), target: self, command: "add sub-delay -0.1"), Config(name: "Reset Subtitle Delay", action: #selector(command(_:)), target: self, command: "set sub-delay 0.0") ]
let playbackMenuConfigs = [ Config(name: "Toggle Pause", action: #selector(command(_:)), target: self, command: "cycle pause"), Config(name: "Increase Speed", action: #selector(command(_:)), target: self, command: "add speed 0.1"), Config(name: "Decrease Speed", action: #selector(command(_:)), target: self, command: "add speed -0.1"), Config(name: "Reset Speed", action: #selector(command(_:)), target: self, command: "set speed 1.0"), Config(type: .separator), Config(name: "Show Playlist", action: #selector(command(_:)), target: self, command: "show-text ${playlist} 3000"), Config(name: "Show Chapters", action: #selector(command(_:)), target: self, command: "show-text ${chapter-list} 3000"), Config(name: "Show Tracks", action: #selector(command(_:)), target: self, command: "show-text ${track-list} 3000"), Config(type: .separator), Config(name: "Next File", action: #selector(command(_:)), target: self, command: "playlist-next"), Config(name: "Previous File", action: #selector(command(_:)), target: self, command: "playlist-prev"), Config(name: "Toggle Loop File", action: #selector(command(_:)), target: self, command: "cycle-values loop-file \"inf\" \"no\""), Config(name: "Toggle Loop Playlist", action: #selector(command(_:)), target: self, command: "cycle-values loop-playlist \"inf\" \"no\""), Config(name: "Shuffle", action: #selector(command(_:)), target: self, command: "playlist-shuffle"), Config(type: .separator), Config(name: "Next Chapter", action: #selector(command(_:)), target: self, command: "add chapter 1"), Config(name: "Previous Chapter", action: #selector(command(_:)), target: self, command: "add chapter -1"), Config(type: .separator), Config(name: "Step Forward", action: #selector(command(_:)), target: self, command: "frame-step"), Config(name: "Step Backward", action: #selector(command(_:)), target: self, command: "frame-back-step") ]
let windowMenuConfigs = [ Config(name: "Minimize", key: "m", type: .itemMinimize), Config(name: "Zoom", type: .itemZoom) ]
var helpMenuConfigs = [ Config(name: "mpv Website…", action: #selector(url(_:)), target: self, url: "https://mpv.io"), Config(name: "mpv on GitHub…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv"), Config(type: .separator), Config(name: "Online Manual…", action: #selector(url(_:)), target: self, url: "https://mpv.io/manual/master/"), Config(name: "Online Wiki…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv/wiki"), Config(name: "Release Notes…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv/blob/master/RELEASE_NOTES"), Config(name: "Keyboard Shortcuts…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv/blob/master/etc/input.conf"), Config(type: .separator), Config(name: "Report Issue…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv/issues/new/choose") ] if AppHub.shared.isBundle { helpMenuConfigs += [ Config(name: "Show log File…", action: #selector(showFile(_:)), target: self, url: NSHomeDirectory() + "/Library/Logs/mpv.log") ] }
menuConfigs = [ Config(name: "Apple", configs: appMenuConfigs), Config(name: "File", configs: fileMenuConfigs), Config(name: "Edit", configs: editMenuConfigs), Config(name: "View", configs: viewMenuConfigs), Config(name: "Video", configs: videoMenuConfigs), Config(name: "Audio", configs: audioMenuConfigs), Config(name: "Subtitle", configs: subtitleMenuConfigs), Config(name: "Playback", configs: playbackMenuConfigs), Config(name: "Window", configs: windowMenuConfigs), Config(name: "Help", configs: helpMenuConfigs) ]
createMenu(parentMenu: mainMenu, configs: menuConfigs) NSApp.mainMenu = mainMenu NSApp.servicesMenu = servicesMenu }
func createMenu(parentMenu: NSMenu, configs: [Config]) { for config in configs { let item = createMenuItem(parentMenu: parentMenu, config: config)
if config.type <= .menuServices { let menu = config.type == .menuServices ? servicesMenu : NSMenu(title: config.name) item.submenu = menu createMenu(parentMenu: menu, configs: config.configs) }
if config.type > Type.item { dynamicMenuItems[config.type] = (dynamicMenuItems[config.type] ?? []) + [item] } } }
func createMenuItem(parentMenu: NSMenu, config: Config) -> MenuItem { var item = MenuItem(title: config.name, action: config.action, keyEquivalent: (option?.mac.macos_menu_shortcuts ?? true) ? config.key : "") item.config = config item.target = config.target item.keyEquivalentModifierMask = config.modifiers
if config.type == .separator { item = MenuItem.separator() as? MenuItem ?? item } parentMenu.addItem(item)
return item }
@objc func about() { NSApp.orderFrontStandardAboutPanel(options: [ .applicationName: "mpv", .applicationIcon: appIcon, .applicationVersion: String(cString: swift_mpv_version), .init(rawValue: "Copyright"): String(cString: swift_mpv_copyright) ]) }
@objc func settings(_ menuItem: MenuItem) { guard let menuConfig = menuItem.config else { return } let configPaths: [URL] = [ URL(fileURLWithPath: NSHomeDirectory() + "/.config/mpv/", isDirectory: true), URL(fileURLWithPath: NSHomeDirectory() + "/.mpv/", isDirectory: true) ]
for path in configPaths { let configFile = path.appendingPathComponent(menuConfig.url, isDirectory: false)
if FileManager.default.fileExists(atPath: configFile.path) { if NSWorkspace.shared.open(configFile) { return } NSWorkspace.shared.open(path) alert(title: "No Application found to open your config file.", text: "Please open the \(menuConfig.url) file with your preferred text editor in the now open folder to edit your config.") return }
if NSWorkspace.shared.open(path) { alert(title: "No config file found.", text: "Please create a \(menuConfig.url) file with your preferred text editor in the now open folder.") return } } }
@objc func openFiles() { let panel = NSOpenPanel() panel.allowsMultipleSelection = true panel.canChooseDirectories = true
if panel.runModal() == .OK { appHub.input.open(files: panel.urls.map { $0.path }) } }
@objc func openPlaylist() { let panel = NSOpenPanel()
if panel.runModal() == .OK, let url = panel.urls.first { appHub.input.command("loadlist \"\(url.path)\"") } }
@objc func openUrl() { let alert = NSAlert() alert.messageText = "Open URL" alert.icon = appIcon alert.addButton(withTitle: "Ok") alert.addButton(withTitle: "Cancel")
let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) input.placeholderString = "URL" alert.accessoryView = input
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { input.becomeFirstResponder() }
if alert.runModal() == .alertFirstButtonReturn && input.stringValue.count > 0 { appHub.input.open(files: [input.stringValue]) } }
@objc func command(_ menuItem: MenuItem) { guard let menuConfig = menuItem.config else { return } appHub.input.command(menuConfig.command) }
@objc func url(_ menuItem: MenuItem) { guard let menuConfig = menuItem.config, let url = URL(string: menuConfig.url) else { return } NSWorkspace.shared.open(url) }
@objc func showFile(_ menuItem: MenuItem) { guard let menuConfig = menuItem.config else { return } let url = URL(fileURLWithPath: menuConfig.url) if FileManager.default.fileExists(atPath: url.path) { NSWorkspace.shared.activateFileViewerSelecting([url]) return }
alert(title: "No log File found.", text: "You deactivated logging for the Bundle.") }
func alert(title: String, text: String) { let alert = NSAlert() alert.messageText = title alert.informativeText = text alert.icon = appIcon alert.addButton(withTitle: "Ok") alert.runModal() }
func register(_ selector: Selector, key: Type) { for menuItem in dynamicMenuItems[key] ?? [] { menuItem.action = selector } }}