diff --git a/iina.xcodeproj/project.pbxproj b/iina.xcodeproj/project.pbxproj index 1d638065c1..d91b2ddf35 100644 --- a/iina.xcodeproj/project.pbxproj +++ b/iina.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1326717E20852D0D000FA7E2 /* SubChooseViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1326718020852D0D000FA7E2 /* SubChooseViewController.xib */; }; + 3412DB8E2C2FC64C00BBC142 /* Equalizations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3412DB8D2C2FC64800BBC142 /* Equalizations.swift */; }; 34ACA04B29DBB0090030C09C /* LogWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 34ACA04D29DBB0090030C09C /* LogWindowController.xib */; }; 34ACAB722C142A0500F871A0 /* libintl.8.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34ACAB2D2C142A0300F871A0 /* libintl.8.dylib */; }; 34ACAB732C142A0500F871A0 /* libgraphite2.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 34ACAB2E2C142A0300F871A0 /* libgraphite2.3.dylib */; }; @@ -652,6 +653,7 @@ 2FE1FCAE1F98D3DA004C4D6B /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; 2FE1FCB01F98D3DB004C4D6B /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/FilterPresets.strings; sourceTree = ""; }; 2FE1FCB11F98D3DB004C4D6B /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/KeyBinding.strings; sourceTree = ""; }; + 3412DB8D2C2FC64800BBC142 /* Equalizations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Equalizations.swift; sourceTree = ""; }; 348B000028DC688D0045F683 /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/MiniPlayerWindowController.strings"; sourceTree = ""; }; 348B000128DC688D0045F683 /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/GuideWindowController.strings"; sourceTree = ""; }; 348B000228DC688E0045F683 /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/CropSettingsViewController.strings"; sourceTree = ""; }; @@ -2441,6 +2443,7 @@ 84AABE8A1DBF634600D138FD /* CharEncoding.swift */, 8450403F1E0ACADD0079C194 /* MPVNode.swift */, 842F55A41EA17E7E0081D475 /* IINACommand.swift */, + 3412DB8D2C2FC64800BBC142 /* Equalizations.swift */, ); name = Data; sourceTree = ""; @@ -3189,6 +3192,7 @@ E35306FC214813B7008FE492 /* JavascriptPluginInstance.swift in Sources */, 84A0BA971D2FA1CE00BC8DA1 /* PlayerCore.swift in Sources */, 84D377671D737F58007F7396 /* MPVChapter.swift in Sources */, + 3412DB8E2C2FC64C00BBC142 /* Equalizations.swift in Sources */, 84A886E61E24F3BD008755BB /* OnlineSubtitle.swift in Sources */, 847C62CA1DC13CDA00E1EF16 /* PrefGeneralViewController.swift in Sources */, E3530700214908DD008FE492 /* JavascriptAPIHttp.swift in Sources */, diff --git a/iina/Base.lproj/Localizable.strings b/iina/Base.lproj/Localizable.strings index 9cc8429c6c..7b1080330b 100644 --- a/iina/Base.lproj/Localizable.strings +++ b/iina/Base.lproj/Localizable.strings @@ -16,6 +16,9 @@ "general.username" = "Username"; "general.password" = "Password"; +"input.value_is_empty" = "Value cannot be empty."; +"input.already_exists" = "Value already exists."; + // Guide Window "guide.highlights" = "Highlights"; "guide.basic_settings" = "Basic Settings"; @@ -48,6 +51,35 @@ "quicksetting.hdr" = "HDR"; "quicksetting.hide" = "Hide"; +// EQ Presets +"eq.preset.flat" = "Flat"; +"eq.preset.acoustic" = "Acoustic"; +"eq.preset.classical" = "Classical"; +"eq.preset.dance" = "Dance"; +"eq.preset.deep" = "Deep"; +"eq.preset.electronic" = "Electronic"; +"eq.preset.hip_hop" = "Hip Hop"; +"eq.preset.increase_bass" = "Increase Bass"; +"eq.preset.increase_treble" = "Increase Treble"; +"eq.preset.increase_vocal" = "Increase Vocal"; +"eq.preset.jazz" = "Jazz"; +"eq.preset.latin" = "Latin"; +"eq.preset.loundness" = "Loundness"; +"eq.preset.lounge" = "Lounge"; +"eq.preset.piano" = "Piano"; +"eq.preset.pop" = "Pop"; +"eq.preset.rnb" = "R&B"; +"eq.preset.reduce_bass" = "Reduce Bass"; +"eq.preset.reduce_treble" = "Reduce Treble"; +"eq.preset.rock" = "Rock"; +"eq.preset.small_speaker" = "Small Speaker"; +"eq.preset.spoken_word" = "Spoken Word"; + +"alert.eq.new_profile.title" = "New EQ Profile"; +"alert.eq.new_profile.message" = "Please enter a profile name."; +"alert.eq.rename.title" = "Rename Currnet Profile"; +"alert.eq.rename.message" = "Please enter a new profile name."; + // OSDMessage.swift "osd.pause" = "Pause"; "osd.resume" = "Resume"; diff --git a/iina/Base.lproj/QuickSettingViewController.xib b/iina/Base.lproj/QuickSettingViewController.xib index 1aee849559..3bd7b1145c 100644 --- a/iina/Base.lproj/QuickSettingViewController.xib +++ b/iina/Base.lproj/QuickSettingViewController.xib @@ -1,8 +1,8 @@ - + - + @@ -35,6 +35,7 @@ + @@ -90,13 +91,13 @@ - + - + - - + + @@ -1184,7 +1185,7 @@ - + @@ -1196,7 +1197,7 @@ - + @@ -1207,7 +1208,7 @@ - + @@ -1215,7 +1216,7 @@ - + @@ -1223,7 +1224,7 @@ - + @@ -1231,7 +1232,7 @@ - + @@ -1243,7 +1244,7 @@ - + @@ -1276,99 +1277,211 @@ - + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + @@ -1402,171 +1515,115 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + - + + + + + + + + + - + + - + - + - - + + + - + - + + + + + + - - + + + + @@ -1661,7 +1718,7 @@ - + @@ -1703,7 +1760,7 @@ - + @@ -1745,7 +1802,7 @@ - + @@ -1817,7 +1874,7 @@ - + @@ -1859,7 +1916,7 @@ - + @@ -1901,7 +1958,7 @@ - + @@ -1943,7 +2000,7 @@ - + @@ -1951,7 +2008,7 @@ - + @@ -1959,7 +2016,7 @@ - + @@ -2008,7 +2065,7 @@ - + @@ -2019,7 +2076,7 @@ - + @@ -2027,7 +2084,7 @@ - + @@ -2035,7 +2092,7 @@ - + @@ -2043,7 +2100,7 @@ - + @@ -2058,7 +2115,7 @@ - + @@ -2066,7 +2123,7 @@ - + @@ -2086,7 +2143,7 @@ - + @@ -2115,7 +2172,7 @@ - + @@ -2123,7 +2180,7 @@ - + @@ -2151,7 +2208,7 @@ - + @@ -2202,7 +2259,7 @@ - + @@ -2210,7 +2267,7 @@ - + @@ -2218,7 +2275,7 @@ - + @@ -2226,7 +2283,7 @@ - + @@ -2234,7 +2291,7 @@ - + @@ -2242,7 +2299,7 @@ - + @@ -2272,7 +2329,7 @@ - + @@ -2280,7 +2337,7 @@ - + @@ -2486,11 +2543,11 @@ - + - + diff --git a/iina/Equalizations.swift b/iina/Equalizations.swift new file mode 100644 index 0000000000..0b3d35f739 --- /dev/null +++ b/iina/Equalizations.swift @@ -0,0 +1,68 @@ +// +// Untitled.swift +// iina +// +// Created by Yuze Jiang on 2024/06/29. +// Copyright © 2024 lhc. All rights reserved. +// + +class EQProfile: Codable { + var gains = [Double](repeatElement(0.0, count: 10)) + + init(_ values: [Double]) { + gains = values + } + + init(fromCurrentSliders sliders: [NSSlider]) { + gains = sliders.map { $0.doubleValue } + } +} + +class PresetEQProfile: EQProfile { + let localizationKey: String + let name: String + + init(_ name: String, _ values: [Double]) { + self.localizationKey = "eq.preset." + name + self.name = NSLocalizedString(localizationKey, comment: localizationKey) + super.init(values) + } + + required init(from decoder: any Decoder) throws { + fatalError("init(from:) has not been implemented") + } +} + +let presetEQs: [PresetEQProfile] = [ + .init("flat", [0,0,0,0,0,0,0,0,0,0]), + .init("acoustic", [5.0, 4.9, 3.95,1.05,2.15,1.75,3.5,4.1,3.55,2.15]), + .init("classical", [4.75,3.75,3,2.5,-1.5,-1.5,0,2.25,3.25,3.75]), + .init("dance", [3.57,6.55,4.99,0,1.92,3.65,5.15,4.54,3.59,0]), + .init("deep",[4.95,3.55,1.75,1.00,2.85,2.50,1.45,-2.15,-3.55,-4.60]), + .init("electronic", [4.25,3.8,1.2,0,-2.15,2.25,0.85,1.25,3.95,4.8]), + .init("hip_hop", [5,4.25,1.5,3,-1,-1,1.5,-0.5,2,3]), + .init("increase_bass", [5.5,4.25,3.5,2.5,1.25,0,0,0,0,0]), + .init("increase_treble", [0,0,0,0,0,1.25,2.5,3.5,4.25,5.5]), + .init("increase_vocal",[-1.50,-3.00,-3.00,1.50,3.75,3.75,3.00,1.50,0,-1.50]), + .init("jazz", [4,3,1.5,2.25,-1.5,-1.5,0,1.5,3,3.75]), + .init("latin",[4.5,3,0,0,-1.5,-1.5,-1.5,0,3,4.5]), + .init("loundness",[6,4,0,0,-2,0,-1,-5,5,1]), + .init("lounge",[-3.00,-1.50,-0.50,1.50,4.00,2.50,0,-1.50,2.00,1.00]), + .init("piano", [3,2,0,2.5,3,1.5,3.5,4.5,3,3.5]), + .init("pop", [-1.5,-1,0,2,4,4,2,0,-1,-1.5]), + .init("rnb", [2.62,6.92,5.65,1.33,-2.19,-1.50,2.32,2.65,3.0,3.75]), + .init("reduce_bass", [-5.5,-4.25,-3.5,-2.5,-1.25,0,0,0,0,0]), + .init("reduce_treble", [0,0,0,0,0,-1.25,-2.5,-3.5,-4.25,-5.5]), + .init("rock",[5,4,3,1.5,-0.5,-1,0.5,2.5,3.5,4.5]), + .init("small_speaker",[5.5,4.25,3.5,2.5,1.25,0,-1.25,-2.5,-3.5,-4.25]), + .init("spoken_word", [-3.46,-0.47,0,0.69,3.46,4.61,4.84,4.28,2.54,0]), +] + +var userEQs: Dictionary = [:] { + didSet { + let encoder = JSONEncoder() + if let encoded = try? encoder.encode(userEQs) { + UserDefaults.standard.set(encoded, forKey: Preference.Key.userEQPresets.rawValue) + } + } +} diff --git a/iina/PlaybackInfo.swift b/iina/PlaybackInfo.swift index fe1125f0c5..3512f7c04f 100644 --- a/iina/PlaybackInfo.swift +++ b/iina/PlaybackInfo.swift @@ -120,7 +120,7 @@ class PlaybackInfo { var cropFilter: MPVFilter? var flipFilter: MPVFilter? var mirrorFilter: MPVFilter? - var audioEqFilters: [MPVFilter?]? + var audioEqFilter: MPVFilter? var delogoFilter: MPVFilter? var deinterlace: Bool = false diff --git a/iina/PlayerCore.swift b/iina/PlayerCore.swift index d43a79b474..69767108fa 100644 --- a/iina/PlayerCore.swift +++ b/iina/PlayerCore.swift @@ -1347,19 +1347,13 @@ class PlayerCore: NSObject { } func setAudioEq(fromGains gains: [Double]) { - let channelCount = mpv.getInt(MPVProperty.audioParamsChannelCount) - let freqList = [31.25, 62.5, 125, 250, 500, 1000, 2000, 4000, 8000, 16000] - let filters = freqList.enumerated().map { (index, freq) -> MPVFilter in - let string = [Int](0.. Bool { addAudioFilter(filter.stringFormat) } /// Add an audio filter given as a string. /// - Parameter filter: The filter to add. /// - Returns: `true` if the filter was successfully added, `false` otherwise. + @discardableResult func addAudioFilter(_ filter: String) -> Bool { log("Adding audio filter \(filter)...") var result = true @@ -1539,6 +1535,7 @@ class PlayerCore: NSObject { /// remove a filter. /// - Parameter filter: The filter to remove. /// - Returns: `true` if the filter was successfully removed, `false` otherwise. + @discardableResult func removeAudioFilter(_ filter: MPVFilter) -> Bool { removeAudioFilter(filter.stringFormat) } /// Remove an audio filter given as a string. @@ -1550,6 +1547,7 @@ class PlayerCore: NSObject { /// methods that identify the filter to be removed based on its position in the filter list are the preferred way to remove a filter. /// - Parameter filter: The filter to remove. /// - Returns: `true` if the filter was successfully removed, `false` otherwise. + @discardableResult func removeAudioFilter(_ filter: String) -> Bool { log("Removing audio filter \(filter)...") var result = true @@ -2509,12 +2507,7 @@ class PlayerCore: NSObject { for filter in audioFilters { guard let label = filter.label else { continue } if label.hasPrefix(Constants.FilterName.audioEq) { - if info.audioEqFilters == nil { - info.audioEqFilters = Array(repeating: nil, count: 10) - } - if let index = Int(String(label.last!)) { - info.audioEqFilters![index] = filter - } + info.audioEqFilter = filter } } } diff --git a/iina/Preference.swift b/iina/Preference.swift index c76e627131..081aea8a49 100644 --- a/iina/Preference.swift +++ b/iina/Preference.swift @@ -187,6 +187,8 @@ struct Preference { static let replayGainClip = Key("replayGainClip") static let replayGainFallback = Key("replayGainFallback") + static let userEQPresets = Key("userEQPresets") + // Subtitle static let subAutoLoadIINA = Key("subAutoLoadIINA") diff --git a/iina/QuickSettingViewController.swift b/iina/QuickSettingViewController.swift index a9f8cc8fa0..f21d4bb89d 100644 --- a/iina/QuickSettingViewController.swift +++ b/iina/QuickSettingViewController.swift @@ -8,6 +8,13 @@ import Cocoa +fileprivate let eqUserDefinedProfileMenuItemTag = 0 +fileprivate let eqPresetProfileMenuItemTag = 1 +fileprivate let eqDeleteMenuItemTag = -1 +fileprivate let eqRenameMenuItemTag = -2 +fileprivate let eqSaveMenuItemTag = -3 +fileprivate let eqCustomMenuItemTag = 1000 + class QuickSettingViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, SidebarViewController { override var nibName: NSNib.Name { return NSNib.Name("QuickSettingViewController") @@ -152,6 +159,7 @@ class QuickSettingViewController: NSViewController, NSTableViewDataSource, NSTab @IBOutlet weak var customSubDelayTextField: NSTextField! @IBOutlet weak var subSegmentedControl: NSSegmentedControl! + @IBOutlet weak var eqPopUpButton: NSPopUpButton! @IBOutlet weak var audioEqSlider1: NSSlider! @IBOutlet weak var audioEqSlider2: NSSlider! @IBOutlet weak var audioEqSlider3: NSSlider! @@ -181,6 +189,12 @@ class QuickSettingViewController: NSViewController, NSTableViewDataSource, NSTab private var pluginTabsStackView: NSStackView! private var pluginTabs: [String: SidebarTabView] = [:] + private lazy var eqSliders: [NSSlider] = [audioEqSlider1, audioEqSlider2, audioEqSlider3, audioEqSlider4, audioEqSlider5, + audioEqSlider6, audioEqSlider7, audioEqSlider8, audioEqSlider9, audioEqSlider10] + + private var lastUsedProfileName: String = "" + private var inputString: String = "" + var downShift: CGFloat = 0 { didSet { buttonTopConstraint.constant = downShift @@ -214,6 +228,18 @@ class QuickSettingViewController: NSViewController, NSTableViewDataSource, NSTab switchHorizontalLine2.wantsLayer = true switchHorizontalLine2.layer?.opacity = 0.5 + if let data = UserDefaults.standard.data(forKey: Preference.Key.userEQPresets.rawValue), + let dict = try? JSONDecoder().decode(Dictionary.self, from: data) { + userEQs = dict + } + + eqPopUpButton.menu!.delegate = self + presetEQs.forEach { preset in + eqPopUpButton.menu?.addItem(withTitle: preset.name, tag: eqPresetProfileMenuItemTag, obj: preset.localizationKey) + } + eqPopUpButton.selectItem(withTag: eqCustomMenuItemTag) + lastUsedProfileName = eqPopUpButton.selectedItem!.title + func observe(_ name: Notification.Name, block: @escaping (Notification) -> Void) { observers.append(NotificationCenter.default.addObserver(forName: name, object: player, queue: .main, using: block)) } @@ -431,16 +457,17 @@ class QuickSettingViewController: NSViewController, NSTableViewDataSource, NSTab } private func updateAudioEqState() { - if let filters = player.info.audioEqFilters { - withAllAudioEqSliders { slider in - if let gain = filters[slider.tag]?.stringFormat.dropLast().split(separator: "=").last { + if let filter = player.info.audioEqFilter { + let filters = filter.stringFormat.split(separator: ",") + zip(filters, eqSliders).forEach { (filter, slider) in + if let gain = filter.dropLast().split(separator: "=").last { slider.doubleValue = Double(gain) ?? 0 } else { slider.doubleValue = 0 } } } else { - withAllAudioEqSliders { $0.doubleValue = 0 } + eqSliders.forEach { $0.doubleValue = 0 } } } @@ -631,8 +658,7 @@ class QuickSettingViewController: NSViewController, NSTableViewDataSource, NSTab } private func withAllAudioEqSliders(_ block: (NSSlider) -> Void) { - [audioEqSlider1, audioEqSlider2, audioEqSlider3, audioEqSlider4, audioEqSlider5, - audioEqSlider6, audioEqSlider7, audioEqSlider8, audioEqSlider9, audioEqSlider10].forEach { + eqSliders.forEach { block($0) } } @@ -825,27 +851,20 @@ class QuickSettingViewController: NSViewController, NSTableViewDataSource, NSTab redraw(indicator: audioDelaySliderIndicator, constraint: audioDelaySliderConstraint, slider: audioDelaySlider, value: "\(sender.stringValue)s") } - @IBAction func audioEqSliderAction(_ sender: NSSlider) { - player.setAudioEq(fromGains: [ - audioEqSlider1.doubleValue, - audioEqSlider2.doubleValue, - audioEqSlider3.doubleValue, - audioEqSlider4.doubleValue, - audioEqSlider5.doubleValue, - audioEqSlider6.doubleValue, - audioEqSlider7.doubleValue, - audioEqSlider8.doubleValue, - audioEqSlider9.doubleValue, - audioEqSlider10.doubleValue, - ]) - } - - @IBAction func resetAudioEqAction(_ sender: AnyObject) { - player.removeAudioEqFilter() - updateAudioEqState() + func applyEQ(_ profile: EQProfile) { + zip(eqSliders, profile.gains).forEach { (slider, gain) in + slider.doubleValue = gain + } + player.setAudioEq(fromGains: profile.gains) } + + @IBAction func audioEqSliderAction(_ sender: NSSlider) { + player.setAudioEq(fromGains: eqSliders.map { $0.doubleValue }) + eqPopUpButton.selectItem(withTag: eqCustomMenuItemTag) + } + // MARK: Sub tab @IBAction func hideSubAction(_ sender: NSSwitch) { @@ -1021,6 +1040,96 @@ class QuickSettingViewController: NSViewController, NSTableViewDataSource, NSTab } +extension QuickSettingViewController: NSMenuDelegate { + private func promptAudioEQProfileName(isNewProfile: Bool) -> String? { + let key = isNewProfile ? "eq.new_profile" : "eq.rename" + let nameList = eqPopUpButton.itemArray + .filter{ $0.tag == eqPresetProfileMenuItemTag || $0.tag == eqUserDefinedProfileMenuItemTag } + .map{ $0.title } + let validator: Utility.InputValidator = { input in + if input.isEmpty { + return .valueIsEmpty + } + if nameList.contains( where: { $0 == input } ) { + return .valueAlreadyExists + } else { + return .ok + } + } + var inputString: String? + Utility.quickPromptPanel(key, validator: validator, callback: { inputString = $0 }) + return inputString + } + + func findItem(_ name: String, _ tag: Int = eqUserDefinedProfileMenuItemTag) -> NSMenuItem? { + return eqPopUpButton.itemArray.filter{ $0.tag == tag }.first { $0.title == name } + } + + @IBAction func eqPopUpButtonAction(_ sender: NSPopUpButton) { + let tag = sender.selectedTag() + let name = sender.titleOfSelectedItem + let representedObject = sender.selectedItem?.representedObject as? String + switch tag { + case eqSaveMenuItemTag: + if let inputString = promptAudioEQProfileName(isNewProfile: true) { + let newProfile = EQProfile(fromCurrentSliders: eqSliders) + userEQs[inputString] = newProfile + menuNeedsUpdate(eqPopUpButton.menu!) + eqPopUpButton.select(findItem(inputString)) + } else { + eqPopUpButton.select(findItem(lastUsedProfileName)) + } + case eqRenameMenuItemTag: + if let inputString = promptAudioEQProfileName(isNewProfile: false) { + let profile = userEQs.removeValue(forKey: lastUsedProfileName) + userEQs[inputString] = profile + menuNeedsUpdate(eqPopUpButton.menu!) + eqPopUpButton.select(findItem(inputString)) + } else { + eqPopUpButton.select(findItem(lastUsedProfileName)) + } + case eqDeleteMenuItemTag: + userEQs.removeValue(forKey: lastUsedProfileName) + menuNeedsUpdate(eqPopUpButton.menu!) + eqPopUpButton.selectItem(withTag: eqCustomMenuItemTag) + case eqCustomMenuItemTag: + lastUsedProfileName = sender.selectedItem!.title + case eqPresetProfileMenuItemTag: + guard let preset = presetEQs.first(where: { $0.localizationKey == representedObject }) else { break } + lastUsedProfileName = preset.name + applyEQ(preset) + default: // user defined EQ Profiles + guard let pair = userEQs.first(where: { $0.0 == name }) else { break } + lastUsedProfileName = pair.0 + applyEQ(pair.1) + } + } + + func menuNeedsUpdate(_ menu: NSMenu) { + let tag = eqPopUpButton.selectedTag() + let saveItem = menu.item(withTag: eqSaveMenuItemTag)! + let editingItems = [menu.item(withTag: eqRenameMenuItemTag)!, menu.item(withTag: eqDeleteMenuItemTag)!] + + editingItems.forEach { $0.isEnabled = (tag == eqUserDefinedProfileMenuItemTag) } + saveItem.isEnabled = (tag == eqCustomMenuItemTag) + + let selectedName = eqPopUpButton.titleOfSelectedItem! + let selectedTag = eqPopUpButton.selectedTag() + var items = menu.items + items.removeAll { $0.tag == eqUserDefinedProfileMenuItemTag } + if !userEQs.isEmpty { + items.append(NSMenuItem.separator()) + } + menu.items = items + userEQs.forEach { (name, eq) in + menu.addItem(withTitle: name, tag: eqUserDefinedProfileMenuItemTag) + } + eqPopUpButton.select(findItem(selectedName, selectedTag)) + eqPopUpButton.itemArray.forEach { $0.state = .off } + eqPopUpButton.selectedItem?.state = .on + } +} + class QuickSettingView: NSView { override func mouseDown(with event: NSEvent) {} diff --git a/iina/Utility.swift b/iina/Utility.swift index 25d41c0a48..b6606b1bec 100644 --- a/iina/Utility.swift +++ b/iina/Utility.swift @@ -25,6 +25,15 @@ class Utility { static let blacklistExt = supportedFileExt[.sub]! + multipleFilePlaylistExt static let lut3dExt = ["3dl", "cube", "dat", "m3d"] + enum ValidationResult { + case ok + case valueIsEmpty + case valueAlreadyExists + case custom(String) + } + + typealias InputValidator = (T) -> ValidationResult + // MARK: - Logs, alerts @available(*, deprecated, message: "showAlert(message:alertStyle:) is deprecated, use showAlert(_ key:comment:arguments:alertStyle:) instead") @@ -226,33 +235,83 @@ class Utility { - Returns: Whether user dismissed the panel by clicking OK. Only works when using `.modal` mode. */ @discardableResult - static func quickPromptPanel(_ key: String, titleComment: String? = nil, messageComment: String? = nil, inputValue: String? = nil, sheetWindow: NSWindow? = nil, callback: @escaping (String) -> Void) -> Bool { + static func quickPromptPanel(_ key: String, titleComment: String? = nil, messageComment: String? = nil, + inputValue: String? = nil, validator: InputValidator? = nil, + sheetWindow: NSWindow? = nil, callback: @escaping (String) -> Void) -> Bool { let panel = NSAlert() let titleKey = "alert." + key + ".title" let messageKey = "alert." + key + ".message" panel.messageText = NSLocalizedString(titleKey, comment: titleComment ?? titleKey) panel.informativeText = NSLocalizedString(messageKey, comment: messageComment ?? messageKey) - let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 240, height: 24)) + + // accessory view + let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 240, height: 16)) + input.translatesAutoresizingMaskIntoConstraints = false input.lineBreakMode = .byClipping input.usesSingleLineMode = true input.cell?.isScrollable = true if let inputValue = inputValue { input.stringValue = inputValue } - panel.accessoryView = input - panel.addButton(withTitle: NSLocalizedString("general.ok", comment: "OK")) - panel.addButton(withTitle: NSLocalizedString("general.cancel", comment: "Cancel")) + let stackView = NSStackView(frame: NSRect(x: 0, y: 0, width: 240, height: 20)) + stackView.orientation = .vertical + stackView.alignment = .centerX + stackView.addArrangedSubview(input) + + // buttons + let okButton = panel.addButton(withTitle: NSLocalizedString("general.ok", comment: "OK")) + let _ = panel.addButton(withTitle: NSLocalizedString("general.cancel", comment: "Cancel")) panel.window.initialFirstResponder = input + // validation + var observer: NSObjectProtocol? + if let validator = validator { + let label = NSTextField(labelWithString: "label") + label.textColor = .secondaryLabelColor + label.font = .systemFont(ofSize: NSFont.smallSystemFontSize) + stackView.addArrangedSubview(label) + stackView.frame = NSRect(x: 0, y: 0, width: 240, height: 42) + + let validateInput = { + switch validator(input.stringValue) { + case .ok: + okButton.isEnabled = true + label.stringValue = "" + case .valueIsEmpty: + okButton.isEnabled = false + label.stringValue = NSLocalizedString("input.value_is_empty", comment: "Value is empty.") + case .valueAlreadyExists: + okButton.isEnabled = false + label.stringValue = NSLocalizedString("input.already_exists", comment: "Value already exists.") + case .custom(let message): + label.stringValue = message + okButton.isEnabled = false + } + } + observer = NotificationCenter.default.addObserver(forName: NSControl.textDidChangeNotification, object: input, queue: .main) { _ in + validateInput() + } + validateInput() + } + + stackView.translatesAutoresizingMaskIntoConstraints = true + panel.accessoryView = stackView + if let sheetWindow = sheetWindow { panel.beginSheetModal(for: sheetWindow) { response in if response == .alertFirstButtonReturn { callback(input.stringValue) } + if let observer = observer { + NotificationCenter.default.removeObserver(observer) + } } } else { if panel.runModal() == .alertFirstButtonReturn { callback(input.stringValue) + if let observer = observer { + NotificationCenter.default.removeObserver(observer) + } return true } } diff --git a/iina/en.lproj/Localizable.strings b/iina/en.lproj/Localizable.strings index 1a47625aec..4eeef3b0e1 100644 --- a/iina/en.lproj/Localizable.strings +++ b/iina/en.lproj/Localizable.strings @@ -16,6 +16,9 @@ "general.username" = "Username"; "general.password" = "Password"; +"input.value_is_empty" = "Value cannot be empty."; +"input.already_exists" = "Value already exists."; + // Guide Window "guide.highlights" = "Highlights"; "guide.basic_settings" = "Basic Settings"; @@ -48,6 +51,35 @@ "quicksetting.hdr" = "HDR"; "quicksetting.hide" = "Hide"; +// EQ Presets +"eq.preset.flat" = "Flat"; +"eq.preset.acoustic" = "Acoustic"; +"eq.preset.classical" = "Classical"; +"eq.preset.dance" = "Dance"; +"eq.preset.deep" = "Deep"; +"eq.preset.electronic" = "Electronic"; +"eq.preset.hip_hop" = "Hip Hop"; +"eq.preset.increase_bass" = "Increase Bass"; +"eq.preset.increase_treble" = "Increase Treble"; +"eq.preset.increase_vocal" = "Increase Vocal"; +"eq.preset.jazz" = "Jazz"; +"eq.preset.latin" = "Latin"; +"eq.preset.loundness" = "Loundness"; +"eq.preset.lounge" = "Lounge"; +"eq.preset.piano" = "Piano"; +"eq.preset.pop" = "Pop"; +"eq.preset.rnb" = "R&B"; +"eq.preset.reduce_bass" = "Reduce Bass"; +"eq.preset.reduce_treble" = "Reduce Treble"; +"eq.preset.rock" = "Rock"; +"eq.preset.small_speaker" = "Small Speaker"; +"eq.preset.spoken_word" = "Spoken Word"; + +"alert.eq.new_profile.title" = "New EQ Profile"; +"alert.eq.new_profile.message" = "Please enter a profile name."; +"alert.eq.rename.title" = "Rename Currnet Profile"; +"alert.eq.rename.message" = "Please enter a new profile name."; + // OSDMessage.swift "osd.pause" = "Pause"; "osd.resume" = "Resume"; diff --git a/iina/en.lproj/QuickSettingViewController.strings b/iina/en.lproj/QuickSettingViewController.strings index 5a034ad26b..02f062b66a 100644 --- a/iina/en.lproj/QuickSettingViewController.strings +++ b/iina/en.lproj/QuickSettingViewController.strings @@ -49,9 +49,6 @@ /* Class = "NSTextFieldCell"; title = "BORDER"; ObjectID = "GlZ-YU-dNm"; */ "GlZ-YU-dNm.title" = "BORDER"; -/* Class = "NSTextFieldCell"; title = "31.25"; ObjectID = "HBF-J7-Tie"; */ -"HBF-J7-Tie.title" = "31.25"; - /* Class = "NSTextFieldCell"; title = "-5s"; ObjectID = "IYU-eq-dls"; */ "YCG-xK-eAs.title" = "-5s"; @@ -216,3 +213,15 @@ /* Class = "NSTextFieldCell"; title = "HDR"; ObjectID = "UW4-by-XRS"; */ "UW4-by-XRS.title" = "HDR"; + +/* Class = "NSMenuItem"; title = "Save the current EQ as a preset…"; ObjectID = "sE0-LN-T8i"; */ +"sE0-LN-T8i.title" = "Save the current EQ as a preset…"; + +/* Class = "NSMenuItem"; title = "Rename the current EQ preset…"; ObjectID = "2tD-0L-UUs"; */ +"2tD-0L-UUs.title" = "Rename the current EQ preset…"; + +/* Class = "NSMenuItem"; title = "Remove the current EQ preset"; ObjectID = "dvI-Rj-7qF"; */ +"dvI-Rj-7qF.title" = "Remove the current EQ preset"; + +/* Class = "NSMenuItem"; title = "Manual"; ObjectID = "LPh-nJ-GjN"; */ +"LPh-nJ-GjN.title" = "Manual";