import Cocoa import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { private let winampChannelName = "winamp" override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } override func applicationWillFinishLaunching(_ notification: Notification) { super.applicationWillFinishLaunching(notification) // Register the channel handler eagerly so Dart's startup menu-state sync // is not dropped. `applicationDidFinishLaunching` is never delivered in // this Flutter version, so this is the earliest reliable point. The // FlutterViewController is already attached here, but poll briefly as a // fallback for other Flutter versions where it may not be yet. registerWinampChannelWhenReady() } private func registerWinampChannelWhenReady() { guard winampChannel != nil else { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in self?.registerWinampChannelWhenReady() } return } } private var flutterController: FlutterViewController? { return mainFlutterWindow?.contentViewController as? FlutterViewController } /// Lazily-created channel back to Dart. The handler is registered once so /// Dart can report state (always-on-top, EQ/PL open, UI scale) to update /// the matching menu checkmarks. private var winampChannel: FlutterMethodChannel? { if _winampChannel == nil, let controller = flutterController { let channel = FlutterMethodChannel( name: winampChannelName, binaryMessenger: controller.engine.binaryMessenger ) channel.setMethodCallHandler(handleMethodCall) _winampChannel = channel } return _winampChannel } private var _winampChannel: FlutterMethodChannel? /// Custom cursors registered by the Dart side, keyed by skin cursor name. private var cursors: [String: NSCursor] = [:] private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "setAlwaysOnTop": if let args = call.arguments as? [String: Any], let on = args["isAlwaysOnTop"] as? Bool { setMenuItemState(id: "win-always-on-top", title: "Always on Top", on: on) } result(nil) case "setWindowShade": if let args = call.arguments as? [String: Any], let on = args["on"] as? Bool { setMenuItemState(id: "win-window-shade", title: "Window Shade", on: on) } result(nil) case "setEqOpen": if let args = call.arguments as? [String: Any], let open = args["open"] as? Bool { setMenuItemState(id: "win-equalizer", title: "Equalizer", on: open) } result(nil) case "setPlOpen": if let args = call.arguments as? [String: Any], let open = args["open"] as? Bool { setMenuItemState(id: "win-playlist", title: "Playlist", on: open) } result(nil) case "setUiScale": if let args = call.arguments as? [String: Any], let scale = args["scale"] as? Double { setUiScaleMenuState(scale) } result(nil) case "registerCursor": if let args = call.arguments as? [String: Any], let name = args["name"] as? String, let pngData = (args["png"] as? FlutterStandardTypedData)?.data, let image = NSImage(data: pngData) { let hotX = args["hotspotX"] as? Int ?? 0 let hotY = args["hotspotY"] as? Int ?? 0 cursors[name] = NSCursor(image: image, hotSpot: NSPoint(x: hotX, y: hotY)) } result(nil) case "setCursor": if let args = call.arguments as? [String: Any], let name = args["name"] as? String { if let cursor = cursors[name] { cursor.set() } else { NSCursor.arrow.set() } } result(nil) default: result(FlutterMethodNotImplemented) } } private func setMenuItemState(id: String, title: String, on: Bool) { menuItem(id: id, title: title)?.state = on ? .on : .off } private func setUiScaleMenuState(_ scale: Double) { let targetId: String let targetTitle: String if scale == 1.0 { targetId = "win-ui-scale-1x" targetTitle = "1.0x" } else if scale == 2.0 { targetId = "win-ui-scale-2x" targetTitle = "2.0x" } else { targetId = "win-ui-scale-3x" targetTitle = "3.0x" } for (id, title) in [ ("win-ui-scale-1x", "1.0x"), ("win-ui-scale-2x", "2.0x"), ("win-ui-scale-3x", "3.0x"), ] { setMenuItemState(id: id, title: title, on: id == targetId && title == targetTitle) } } private func menuItem(id: String, title: String) -> NSMenuItem? { guard let menu = NSApplication.shared.mainMenu else { return nil } return findMenuItem(id: id, title: title, in: menu) } private func findMenuItem(id: String, title: String, in menu: NSMenu) -> NSMenuItem? { for item in menu.items { if item.identifier?.rawValue == id || item.title == title { return item } if let submenu = item.submenu, let found = findMenuItem(id: id, title: title, in: submenu) { return found } } return nil } private func send(_ method: String, _ arguments: Any? = nil) { winampChannel?.invokeMethod(method, arguments: arguments) } @IBAction func toggleAlwaysOnTop(_ sender: NSMenuItem) { send("toggleAlwaysOnTop") } @IBAction func toggleWindowShade(_ sender: NSMenuItem) { send("toggleWindowShade") } @IBAction func toggleEqualizer(_ sender: NSMenuItem) { send("toggleEq") } @IBAction func togglePlaylist(_ sender: NSMenuItem) { send("togglePlaylist") } @IBAction func setUIScale1x(_ sender: NSMenuItem) { send("setUIScale", ["scale": 1.0]) } @IBAction func setUIScale2x(_ sender: NSMenuItem) { send("setUIScale", ["scale": 2.0]) } @IBAction func setUIScale3x(_ sender: NSMenuItem) { send("setUIScale", ["scale": 3.0]) } @IBAction func importSkin(_ sender: NSMenuItem) { send("importSkin") } }