├── Repository ├── ActualVersion.txt └── Logo.png ├── .gitignore ├── Yandex Music ├── Assets.xcassets │ ├── Contents.json │ └── App Icon.appiconset │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 512.png │ │ ├── 16@2x.png │ │ ├── 32@2x.png │ │ ├── 128@2x.png │ │ ├── 256@2x.png │ │ ├── 512@2x.png │ │ └── Contents.json ├── en.lproj │ ├── InfoPlist.strings │ └── Localizable.strings ├── ru.lproj │ ├── InfoPlist.strings │ └── Localizable.strings ├── List.entitlements ├── Framework │ ├── Utils │ │ ├── WeakPointer.swift │ │ └── MediaKey.swift │ ├── EventHelper.Target.swift │ ├── Controls │ │ ├── YMMenu.swift │ │ ├── YMButton.swift │ │ ├── YMMenuItem.swift │ │ ├── YMTextField.swift │ │ └── YMApplication.swift │ ├── EventHelper.Message.swift │ ├── Extensions │ │ └── Bool.swift │ ├── Constants.swift │ ├── EventHelper.swift │ ├── StorageHelper.swift │ ├── TerminalHelper.swift │ ├── UpdateHelper.swift │ └── LocalizedString.swift ├── GoogleService-Info.plist ├── SettingsViewController.swift ├── AppDelegate.swift ├── MainViewController.swift └── Full.storyboard ├── Yandex Music.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ │ └── Yandex Music.xcscheme └── project.pbxproj └── README.md /Repository/ActualVersion.txt: -------------------------------------------------------------------------------- 1 | 10101 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xcuserdatad 3 | -------------------------------------------------------------------------------- /Repository/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Repository/Logo.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/128.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/16.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/256.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/32.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/512.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/16@2x.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/32@2x.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/128@2x.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/256@2x.png -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/debug45/Yandex-Music/HEAD/Yandex Music/Assets.xcassets/App Icon.appiconset/512@2x.png -------------------------------------------------------------------------------- /Yandex Music/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Yandex Music 4 | 5 | Created by Sergey Moskvin on 28.01.2023. 6 | 7 | */ 8 | 9 | "CFBundleName" = "Yandex Music"; 10 | -------------------------------------------------------------------------------- /Yandex Music/ru.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | Yandex Music 4 | 5 | Created by Sergey Moskvin on 28.01.2023. 6 | 7 | */ 8 | 9 | "CFBundleName" = "Я.Музыка"; 10 | -------------------------------------------------------------------------------- /Yandex Music.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Yandex Music/List.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Yandex Music.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Utils/WeakPointer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakPointer.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | final class WeakPointer { 9 | 10 | private(set) weak var object: AnyObject? 11 | 12 | // MARK: Life Cycle 13 | 14 | init(_ object: AnyObject) { 15 | self.object = object 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Yandex Music/Framework/EventHelper.Target.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventHelper.Target.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | extension EventHelper { 9 | 10 | typealias Target = EventHelperTarget 11 | 12 | } 13 | 14 | // MARK: - 15 | 16 | protocol EventHelperTarget: AnyObject { 17 | 18 | func handleMessage(_ message: EventHelper.Message) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Yandex Music.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ___FILENAME___ 8 | // ___WORKSPACENAME___ 9 | // 10 | // Created by ___FULLUSERNAME___ on ___DATE___. 11 | // 12 | 13 | 14 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Controls/YMMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YMMenu.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 28.01.2023. 6 | // 7 | 8 | import AppKit 9 | 10 | @IBDesignable final class YMMenu: NSMenu { 11 | 12 | @IBInspectable var titleKey: String? { 13 | didSet { 14 | if let titleKey { 15 | title = NSLocalizedString(titleKey, comment: "") 16 | } else { 17 | title = "" 18 | } 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Controls/YMButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YMButton.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 28.01.2023. 6 | // 7 | 8 | import AppKit 9 | 10 | @IBDesignable final class YMButton: NSButton { 11 | 12 | @IBInspectable var titleKey: String? { 13 | didSet { 14 | if let titleKey { 15 | title = NSLocalizedString(titleKey, comment: "") 16 | } else { 17 | title = "" 18 | } 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Controls/YMMenuItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YMMenuItem.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 28.01.2023. 6 | // 7 | 8 | import AppKit 9 | 10 | @IBDesignable final class YMMenuItem: NSMenuItem { 11 | 12 | @IBInspectable var titleKey: String? { 13 | didSet { 14 | if let titleKey { 15 | title = NSLocalizedString(titleKey, comment: "") 16 | } else { 17 | title = "" 18 | } 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Controls/YMTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YMTextField.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 28.01.2023. 6 | // 7 | 8 | import AppKit 9 | 10 | @IBDesignable final class YMTextField: NSTextField { 11 | 12 | @IBInspectable var titleKey: String? { 13 | didSet { 14 | if let titleKey { 15 | stringValue = NSLocalizedString(titleKey, comment: "") 16 | } else { 17 | stringValue = "" 18 | } 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Yandex Music/Framework/EventHelper.Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventHelper.Message.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | extension EventHelper { 9 | 10 | enum Message { 11 | 12 | case updateBackMenuBarItem(isEnabled: Bool) 13 | case updateForwardMenuBarItem(isEnabled: Bool) 14 | 15 | case backMenuBarItemDidSelect 16 | case forwardMenuBarItemDidSelect 17 | case homeMenuBarItemDidSelect 18 | case reloadPageMenuBarItemDidSelect 19 | 20 | case globalMediaKeyDidPress(_ mediaKey: MediaKey) 21 | case resetBuiltInBrowser 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Extensions/Bool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bool.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 13.02.2022. 6 | // 7 | 8 | extension Bool { 9 | 10 | private static let binaryValues = [ 11 | false: "0", 12 | true: "1" 13 | ] 14 | 15 | // MARK: Life Cycle 16 | 17 | init?(binaryValue: String) { 18 | if let value = Self.binaryValues.first(where: { $0.value == binaryValue })?.key { 19 | self = value 20 | } else { 21 | return nil 22 | } 23 | } 24 | 25 | // MARK: Properties 26 | 27 | var binaryValue: String { 28 | return Self.binaryValues[self]! 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Yandex Music/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PLIST_VERSION 6 | 1 7 | BUNDLE_ID 8 | debug45.Yandex-Music 9 | PROJECT_ID 10 | yandex-music-85e24 11 | STORAGE_BUCKET 12 | yandex-music-85e24.appspot.com 13 | IS_ADS_ENABLED 14 | 15 | IS_ANALYTICS_ENABLED 16 | 17 | IS_APPINVITE_ENABLED 18 | 19 | IS_GCM_ENABLED 20 | 21 | IS_SIGNIN_ENABLED 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Utils/MediaKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaKey.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | import IOKit 9 | 10 | enum MediaKey { 11 | 12 | case playPause 13 | 14 | case previousTrack 15 | case nextTrack 16 | 17 | // MARK: Life Cycle 18 | 19 | init?(systemCode: Int) { 20 | switch Int32(systemCode) { 21 | case NX_KEYTYPE_PLAY: 22 | self = .playPause 23 | 24 | case NX_KEYTYPE_REWIND: 25 | self = .previousTrack 26 | case NX_KEYTYPE_FAST: 27 | self = .nextTrack 28 | 29 | default: 30 | return nil 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 28.01.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | final class Constants { 11 | 12 | // MARK: Life Cycle 13 | 14 | private init() { } 15 | 16 | // MARK: Properties 17 | 18 | static let baseDomains = [ 19 | (languageCode: "en", host: "yandex.com"), 20 | (languageCode: "ru", host: "yandex.ru") 21 | ] 22 | 23 | static let repositoryURL = URL(string: "https://github.com/debug45/Yandex-Music")! 24 | static let releasesURL = URL(string: "https://github.com/debug45/Yandex-Music/releases")! 25 | 26 | static let actualVersionDataURL = URL(string: "https://raw.githubusercontent.com/debug45/Yandex-Music/master/Repository/ActualVersion.txt")! 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Yandex Music/Framework/Controls/YMApplication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YMApplication.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | import Cocoa 9 | 10 | final class YMApplication: NSApplication { 11 | 12 | override func sendEvent(_ event: NSEvent) { 13 | if event.type == .systemDefined && event.subtype.rawValue == 8 { 14 | let keyCode = (event.data1 & 0xFFFF0000) >> 16 15 | let flags = event.data1 & 0x0000FFFF 16 | 17 | if (flags & 0xFF00) >> 8 == 0xA, let mediaKey = MediaKey(systemCode: keyCode) { 18 | let message = EventHelper.Message.globalMediaKeyDidPress(mediaKey) 19 | EventHelper.instance.report(message) 20 | } 21 | } 22 | 23 | super.sendEvent(event) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Yandex Music/Framework/EventHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventHelper.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | final class EventHelper { 9 | 10 | static let instance = EventHelper() 11 | 12 | private var targets: [WeakPointer] = [] 13 | 14 | // MARK: Life Cycle 15 | 16 | private init() { } 17 | 18 | // MARK: Functions 19 | 20 | func addTarget(_ target: Target) { 21 | guard !targets.contains(where: { $0.object === target }) else { 22 | return 23 | } 24 | 25 | let pointer = WeakPointer(target) 26 | targets.append(pointer) 27 | } 28 | 29 | func report(_ message: Message) { 30 | var index = 0 31 | 32 | while index < targets.count { 33 | let pointer = targets[index] 34 | 35 | guard let target = pointer.object as? Target else { 36 | targets.remove(at: index) 37 | continue 38 | } 39 | 40 | target.handleMessage(message) 41 | index += 1 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Yandex Music/Framework/StorageHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageHelper.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 13.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | final class StorageHelper { 11 | 12 | private static let userDefaults = UserDefaults() 13 | 14 | // MARK: Life Cycle 15 | 16 | private init() { } 17 | 18 | // MARK: Properties 19 | 20 | private static let _isFirstLaunch = "isFirstLaunch" 21 | 22 | static var isFirstLaunch: Bool? { 23 | get { 24 | guard let binaryValue = getFromUserDefaults(forKey: _isFirstLaunch) else { 25 | return nil 26 | } 27 | 28 | return .init(binaryValue: binaryValue) 29 | } set { 30 | setToUserDefaults(newValue?.binaryValue, forKey: _isFirstLaunch) 31 | } 32 | } 33 | 34 | // MARK: Functions 35 | 36 | private static func getFromUserDefaults(forKey key: String) -> String? { 37 | return userDefaults.string(forKey: key) 38 | } 39 | 40 | private static func setToUserDefaults(_ value: String?, forKey key: String) { 41 | if let value { 42 | userDefaults.set(value, forKey: key) 43 | } else { 44 | userDefaults.removeObject(forKey: key) 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Yandex Music/SettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewController.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | import Cocoa 9 | 10 | final class SettingsViewController: NSViewController { 11 | 12 | @IBOutlet private weak var systemMusicSuppressionCheckbox: NSButton! 13 | 14 | private var isFirstAppearance = true 15 | 16 | // MARK: Life Cycle 17 | 18 | override func viewWillAppear() { 19 | super.viewWillAppear() 20 | 21 | view.window?.title = LocalizedString.Scene.Settings.title 22 | systemMusicSuppressionCheckbox.state = TerminalHelper.checkIsSystemMusicAppLaunchAgentLoaded() == false ? .on : .off 23 | 24 | guard isFirstAppearance else { 25 | return 26 | } 27 | 28 | isFirstAppearance = false 29 | view.window?.center() 30 | } 31 | 32 | // MARK: Builder Actions 33 | 34 | @IBAction private func systemMusicSuppressionCheckboxDidPress(_ sender: Any) { 35 | TerminalHelper.updateSystemMusicAppLaunchAgent(isLoaded: systemMusicSuppressionCheckbox.state == .off) 36 | } 37 | 38 | @IBAction private func resetBuiltInBrowserButtonDidPress(_ sender: Any) { 39 | EventHelper.instance.report(.resetBuiltInBrowser) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Yandex Music/Framework/TerminalHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TerminalHelper.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 12.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | final class TerminalHelper { 11 | 12 | // MARK: Life Cycle 13 | 14 | private init() { } 15 | 16 | // MARK: Functions 17 | 18 | static func checkIsSystemMusicAppLaunchAgentLoaded() -> Bool? { 19 | if let output = TerminalHelper.runCommand(""" 20 | launchctl list 21 | """) { 22 | return output.contains("com.apple.rcd") 23 | } 24 | 25 | return nil 26 | } 27 | 28 | static func updateSystemMusicAppLaunchAgent(isLoaded: Bool) { 29 | let verdict = isLoaded ? "load" : "unload" 30 | 31 | _ = TerminalHelper.runCommand(""" 32 | launchctl \(verdict) -w /System/Library/LaunchAgents/com.apple.rcd.plist 33 | """) 34 | } 35 | 36 | private static func runCommand(_ command: String) -> String? { 37 | let process = Process() 38 | 39 | process.arguments = ["-c", command] 40 | process.launchPath = "/bin/sh" 41 | 42 | let pipe = Pipe() 43 | process.standardOutput = pipe 44 | 45 | let fileHandle = pipe.fileHandleForReading 46 | process.launch() 47 | 48 | let outputData = fileHandle.readDataToEndOfFile() 49 | return String(data: outputData, encoding: .utf8) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Yandex Music/Assets.xcassets/App Icon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Yandex Music/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Yandex Music 4 | 5 | Created by Sergey Moskvin on 28.01.2023. 6 | 7 | */ 8 | 9 | "Scene.Main.Title" = "Yandex Music"; 10 | "Scene.Main.Error.Title" = "Failed to load the page"; 11 | "Scene.Main.Error.TryAgainButton" = "Try again"; 12 | 13 | "Scene.Settings.Title" = "Settings"; 14 | "Scene.Settings.SystemMusicSuppressionCheckbox" = "Prevent autorun of the ”Music“ system app"; 15 | "Scene.Settings.ResetBrowserButton" = "Reset built-in browser"; 16 | 17 | "Alert.Update.Title" = "A new app version is available"; 18 | "Alert.Update.Description" = "Do you want to find out what changes does it include and download the update?"; 19 | "Alert.Update.Button.ShowDetails" = "Show details"; 20 | "Alert.Update.Button.Later" = "Later"; 21 | 22 | "MainMenu.App" = "Yandex Music"; 23 | "MainMenu.App.About" = "About Yandex Music"; 24 | "MainMenu.App.Preferences" = "Settings…"; 25 | "MainMenu.App.Services" = "Services"; 26 | "MainMenu.App.Hide" = "Hide Yandex Music"; 27 | "MainMenu.App.HideOthers" = "Hide Others"; 28 | "MainMenu.App.ShowAll" = "Show All"; 29 | "MainMenu.App.Quit" = "Quit Yandex Music"; 30 | 31 | "MainMenu.Edit" = "Edit"; 32 | "MainMenu.Edit.Undo" = "Undo"; 33 | "MainMenu.Edit.Redo" = "Redo"; 34 | "MainMenu.Edit.Cut" = "Cut"; 35 | "MainMenu.Edit.Copy" = "Copy"; 36 | "MainMenu.Edit.Paste" = "Paste"; 37 | "MainMenu.Edit.Delete" = "Delete"; 38 | "MainMenu.Edit.SelectAll" = "Select All"; 39 | 40 | "MainMenu.View" = "View"; 41 | "MainMenu.View.Back" = "Back"; 42 | "MainMenu.View.Forward" = "Forward"; 43 | "MainMenu.View.Home" = "Home"; 44 | "MainMenu.View.ReloadPage" = "Reload Page"; 45 | "MainMenu.View.EnterFullScreen" = "Enter Full Screen"; 46 | 47 | "MainMenu.Window" = "Window"; 48 | "MainMenu.Window.Close" = "Close"; 49 | "MainMenu.Window.Minimize" = "Minimise"; 50 | "MainMenu.Window.Zoom" = "Zoom"; 51 | "MainMenu.Window.BringAllToFront" = "Bring All to Front"; 52 | 53 | "MainMenu.Help" = "Help"; 54 | "MainMenu.Help.CodeRepository" = "Repository on GitHub"; 55 | 56 | "DockMenu.PlayPause" = "Play\U2009/\U2009Pause"; 57 | "DockMenu.NextTrack" = "Next track"; 58 | "DockMenu.PreviousTrack" = "Previous track"; 59 | -------------------------------------------------------------------------------- /Yandex Music/ru.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Yandex Music 4 | 5 | Created by Sergey Moskvin on 28.01.2023. 6 | 7 | */ 8 | 9 | "Scene.Main.Title" = "Я.Музыка"; 10 | "Scene.Main.Error.Title" = "Не\U00A0удалось загрузить страницу"; 11 | "Scene.Main.Error.TryAgainButton" = "Повторить попытку"; 12 | 13 | "Scene.Settings.Title" = "Настройки"; 14 | "Scene.Settings.SystemMusicSuppressionCheckbox" = "Блокировать автозапуск системной «Музыки»"; 15 | "Scene.Settings.ResetBrowserButton" = "Сбросить встроенный браузер"; 16 | 17 | "Alert.Update.Title" = "Доступна новая версия приложения"; 18 | "Alert.Update.Description" = "Хотите узнать, какие изменения в\U00A0неё вошли, и\U00A0скачать обновление?"; 19 | "Alert.Update.Button.ShowDetails" = "Подробнее"; 20 | "Alert.Update.Button.Later" = "Не\U00A0сейчас"; 21 | 22 | "MainMenu.App" = "Я.Музыка"; 23 | "MainMenu.App.About" = "О\U00A0приложении Я.Музыка"; 24 | "MainMenu.App.Preferences" = "Настройки…"; 25 | "MainMenu.App.Services" = "Службы"; 26 | "MainMenu.App.Hide" = "Скрыть Я.Музыка"; 27 | "MainMenu.App.HideOthers" = "Скрыть остальные"; 28 | "MainMenu.App.ShowAll" = "Показать\U00A0все"; 29 | "MainMenu.App.Quit" = "Завершить Я.Музыка"; 30 | 31 | "MainMenu.Edit" = "Правка"; 32 | "MainMenu.Edit.Undo" = "Отменить"; 33 | "MainMenu.Edit.Redo" = "Повторить"; 34 | "MainMenu.Edit.Cut" = "Вырезать"; 35 | "MainMenu.Edit.Copy" = "Скопировать"; 36 | "MainMenu.Edit.Paste" = "Вставить"; 37 | "MainMenu.Edit.Delete" = "Удалить"; 38 | "MainMenu.Edit.SelectAll" = "Выбрать\U00A0всё"; 39 | 40 | "MainMenu.View" = "Вид"; 41 | "MainMenu.View.Back" = "Назад"; 42 | "MainMenu.View.Forward" = "Вперёд"; 43 | "MainMenu.View.Home" = "Домашняя страница"; 44 | "MainMenu.View.ReloadPage" = "Перезагрузить страницу"; 45 | "MainMenu.View.EnterFullScreen" = "Перейти в\U00A0полноэкранный режим"; 46 | 47 | "MainMenu.Window" = "Окно"; 48 | "MainMenu.Window.Close" = "Закрыть"; 49 | "MainMenu.Window.Minimize" = "Свернуть"; 50 | "MainMenu.Window.Zoom" = "Изменить масштаб"; 51 | "MainMenu.Window.BringAllToFront" = "Все\U00A0окна\U00A0— на\U00A0передний план"; 52 | 53 | "MainMenu.Help" = "Справка"; 54 | "MainMenu.Help.CodeRepository" = "Репозиторий на\U00A0GitHub"; 55 | 56 | "DockMenu.PlayPause" = "Воспроизведение\U2009/\U2009пауза"; 57 | "DockMenu.NextTrack" = "Следующий трек"; 58 | "DockMenu.PreviousTrack" = "Предыдущий трек"; 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Яндекс Музыка для macOS 4 | [⬇️ Скачать](https://github.com/debug45/Yandex-Music/releases) 5 | 6 | Это не нативное приложение, работающее через какое-то серверное API, а просто сайт Я.Музыки в красивой и удобной обёртке. Однако есть и приятные бонусы… 7 | 8 | ## Преимущества над сайтом 9 | 10 | - Не нужно держать постоянно открытую вкладку браузера. Приложение работает в отдельном окне, которое можно настраивать, сворачивать или скрывать. 11 | - Реализована полная поддержка медиаклавиш клавиатуры — ставить на паузу или переключать треки можно даже при свёрнутом окне приложения. 12 | - Если пользоваться медиаклавишами неудобно, все те же функции доступны в меню иконки приложения в Dock, что всё равно намного быстрее и удобнее перехода в браузер для каждого переключения трека или установки специальных браузерных расширений. 13 | 14 | ## Чем это приложение лучше аналогов 15 | - Внутри используется WebKit (движок Safari), а не какой-нибудь Chromium, благодаря чему приложение совсем не нагружает процессор и память, не уменьшает время работы MacBook от аккумулятора и уж тем более не задействует дискретную графику. 16 | - Бинарник приложения полностью нативен — написан на чистом Swift без использования каких бы то ни было кросс-платформенных фреймворков. 17 | - Оптимизировано как для чипов Apple Silicon, так и для процессоров Intel. 18 | - У приложения аккуратный дизайн и оригинальная иконка, идентичная официальной. 19 | 20 | ## Системные требования 21 | 22 | - Чип Apple Silicon или процессор Intel 23 | - macOS 11 Big Sur или новее 24 | 25 | ## Дополнительная информация 26 | Чтобы клавиша воспроизведения / паузы на клавиатуре не открывала автоматически системную «Музыку», моё приложение отключает системный агент запуска `com.apple.rcd`. 27 | 28 | Вернуть его обратно при необходимости можно специальной галочкой в настройках либо же самостоятельно через терминал, используя для этого команду `launchctl load -w /System/Library/LaunchAgents/com.apple.rcd.plist`. 29 | 30 | ## Заключение 31 | 32 | Если вы обнаружили какую-то проблему, пожалуйста, [сообщите](https://github.com/debug45/Yandex-Music/issues/new) о ней. 33 | 34 | Я не претендую ни на какие права на Яндекс Музыку или её контент — просто написал небольшое решение проблемы, с которой столкнулся лично. Однако все права на мой собственный код в этом репозитории я оставляю за собой. 35 | 36 | Если приложение вам понравилось, пожалуйста, поставьте звёздочку этому репозиторию. Спасибо! 👍 37 | -------------------------------------------------------------------------------- /Yandex Music/Framework/UpdateHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateHelper.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 13.02.2022. 6 | // 7 | 8 | import AppKit 9 | 10 | final class UpdateHelper { 11 | 12 | // MARK: Life Cycle 13 | 14 | private init() { } 15 | 16 | // MARK: Functions 17 | 18 | static func checkNewVersionAvailability() { 19 | DispatchQueue.global(qos: .utility).async { 20 | guard 21 | let response = try? String(contentsOf: Constants.actualVersionDataURL), 22 | 23 | let newVersion = Int(response), 24 | let currentVersion = getCurrentVersion(), 25 | 26 | newVersion > currentVersion 27 | else { 28 | return 29 | } 30 | 31 | DispatchQueue.main.async { 32 | let alert = NSAlert() 33 | alert.alertStyle = .warning 34 | 35 | let localizedString = LocalizedString.Alert.Update.self 36 | 37 | alert.messageText = localizedString.title 38 | alert.informativeText = localizedString.description 39 | 40 | alert.addButton(withTitle: localizedString.Button.showDetails) 41 | alert.addButton(withTitle: localizedString.Button.later) 42 | 43 | switch alert.runModal() { 44 | case .alertFirstButtonReturn: 45 | NSWorkspace.shared.open(Constants.releasesURL) 46 | 47 | default: 48 | break 49 | } 50 | } 51 | } 52 | } 53 | 54 | private static func getCurrentVersion() -> Int? { 55 | guard 56 | let infoDictionary = Bundle.main.infoDictionary, 57 | let numberComponents = (infoDictionary["CFBundleShortVersionString"] as? String)?.split(separator: ".") 58 | else { 59 | return nil 60 | } 61 | 62 | var result = 0 63 | 64 | if numberComponents.count >= 2 { 65 | if let major = Int(numberComponents[0]) { 66 | result += major * 1_00_00 67 | } 68 | 69 | if let minor = Int(numberComponents[1]) { 70 | result += minor * 1_00 71 | } 72 | 73 | if numberComponents.count >= 3, let patch = Int(numberComponents[2]) { 74 | result += patch 75 | } 76 | } 77 | 78 | return result > 0 ? result : nil 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Yandex Music.xcodeproj/xcshareddata/xcschemes/Yandex Music.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Yandex Music.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329", 3 | "pins" : [ 4 | { 5 | "identity" : "abseil-cpp-binary", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/google/abseil-cpp-binary.git", 8 | "state" : { 9 | "revision" : "748c7837511d0e6a507737353af268484e1745e2", 10 | "version" : "1.2024011601.1" 11 | } 12 | }, 13 | { 14 | "identity" : "app-check", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/google/app-check.git", 17 | "state" : { 18 | "revision" : "3b62f154d00019ae29a71e9738800bb6f18b236d", 19 | "version" : "10.19.2" 20 | } 21 | }, 22 | { 23 | "identity" : "firebase-ios-sdk", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/firebase/firebase-ios-sdk", 26 | "state" : { 27 | "revision" : "03189348b7798fe94b892a35883f1dc745814fe0", 28 | "version" : "10.28.0" 29 | } 30 | }, 31 | { 32 | "identity" : "googleappmeasurement", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/google/GoogleAppMeasurement.git", 35 | "state" : { 36 | "revision" : "fe727587518729046fc1465625b9afd80b5ab361", 37 | "version" : "10.28.0" 38 | } 39 | }, 40 | { 41 | "identity" : "googledatatransport", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/google/GoogleDataTransport.git", 44 | "state" : { 45 | "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", 46 | "version" : "9.4.0" 47 | } 48 | }, 49 | { 50 | "identity" : "googleutilities", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/google/GoogleUtilities.git", 53 | "state" : { 54 | "revision" : "57a1d307f42df690fdef2637f3e5b776da02aad6", 55 | "version" : "7.13.3" 56 | } 57 | }, 58 | { 59 | "identity" : "grpc-binary", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/google/grpc-binary.git", 62 | "state" : { 63 | "revision" : "e9fad491d0673bdda7063a0341fb6b47a30c5359", 64 | "version" : "1.62.2" 65 | } 66 | }, 67 | { 68 | "identity" : "gtm-session-fetcher", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/google/gtm-session-fetcher.git", 71 | "state" : { 72 | "revision" : "96d7cc73a71ce950723aa3c50ce4fb275ae180b8", 73 | "version" : "3.1.0" 74 | } 75 | }, 76 | { 77 | "identity" : "interop-ios-for-google-sdks", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/google/interop-ios-for-google-sdks.git", 80 | "state" : { 81 | "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", 82 | "version" : "100.0.0" 83 | } 84 | }, 85 | { 86 | "identity" : "leveldb", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/firebase/leveldb.git", 89 | "state" : { 90 | "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", 91 | "version" : "1.22.2" 92 | } 93 | }, 94 | { 95 | "identity" : "nanopb", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/firebase/nanopb.git", 98 | "state" : { 99 | "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", 100 | "version" : "2.30909.0" 101 | } 102 | }, 103 | { 104 | "identity" : "promises", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/google/promises.git", 107 | "state" : { 108 | "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", 109 | "version" : "2.4.0" 110 | } 111 | }, 112 | { 113 | "identity" : "swift-protobuf", 114 | "kind" : "remoteSourceControl", 115 | "location" : "https://github.com/apple/swift-protobuf.git", 116 | "state" : { 117 | "revision" : "ab3a58b7209a17d781c0d1dbb3e1ff3da306bae8", 118 | "version" : "1.20.3" 119 | } 120 | } 121 | ], 122 | "version" : 3 123 | } 124 | -------------------------------------------------------------------------------- /Yandex Music/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | import Cocoa 9 | import FirebaseAnalytics 10 | import FirebaseCore 11 | 12 | @main final class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | @IBOutlet private weak var backMenuBarItem: NSMenuItem! 15 | @IBOutlet private weak var forwardMenuBarItem: NSMenuItem! 16 | 17 | // MARK: Life Cycle 18 | 19 | func applicationDidFinishLaunching(_ notification: Notification) { 20 | configureFirebase() 21 | 22 | EventHelper.instance.addTarget(self) 23 | UpdateHelper.checkNewVersionAvailability() 24 | 25 | guard StorageHelper.isFirstLaunch != false else { 26 | return 27 | } 28 | 29 | StorageHelper.isFirstLaunch = false 30 | TerminalHelper.updateSystemMusicAppLaunchAgent(isLoaded: false) 31 | } 32 | 33 | func applicationDockMenu(_ sender: NSApplication) -> NSMenu? { 34 | let menu = NSMenu() 35 | let localizedString = LocalizedString.DockMenu.self 36 | 37 | var selector = #selector(playPauseDockMenuItemDidSelect) 38 | menu.addItem(withTitle: localizedString.playPause, action: selector, keyEquivalent: "") 39 | 40 | selector = #selector(nextTrackDockMenuItemDidSelect) 41 | menu.addItem(withTitle: localizedString.nextTrack, action: selector, keyEquivalent: "") 42 | 43 | selector = #selector(previousTrackDockMenuItemDidSelect) 44 | menu.addItem(withTitle: localizedString.previousTrack, action: selector, keyEquivalent: "") 45 | 46 | return menu 47 | } 48 | 49 | // MARK: Builder Actions 50 | 51 | @IBAction private func backMenuBarItemDidSelect(_ sender: Any) { 52 | EventHelper.instance.report(.backMenuBarItemDidSelect) 53 | } 54 | 55 | @IBAction private func forwardMenuBarItemDidSelect(_ sender: Any) { 56 | EventHelper.instance.report(.forwardMenuBarItemDidSelect) 57 | } 58 | 59 | @IBAction private func homeMenuBarItemDidSelect(_ sender: Any) { 60 | EventHelper.instance.report(.homeMenuBarItemDidSelect) 61 | } 62 | 63 | @IBAction private func reloadPageMenuBarItemDidSelect(_ sender: Any) { 64 | EventHelper.instance.report(.reloadPageMenuBarItemDidSelect) 65 | } 66 | 67 | @IBAction private func codeRepositoryMenuBarItemDidSelect(_ sender: Any) { 68 | NSWorkspace.shared.open(Constants.repositoryURL) 69 | } 70 | 71 | // MARK: Functions 72 | 73 | private func configureFirebase() { 74 | let secretDataKeys = [ 75 | "CLIENT_ID", 76 | "REVERSED_CLIENT_ID", 77 | "API_KEY", 78 | "GCM_SENDER_ID", 79 | "GOOGLE_APP_ID" 80 | ] 81 | 82 | guard 83 | let path = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist"), 84 | let availableKeys = NSDictionary(contentsOfFile: path)?.allKeys.compactMap({ $0 as? String }), 85 | secretDataKeys.allSatisfy({ availableKeys.contains($0) }) 86 | else { 87 | return 88 | } 89 | 90 | FirebaseApp.configure() 91 | Analytics.setAnalyticsCollectionEnabled(true) 92 | } 93 | 94 | @objc private func playPauseDockMenuItemDidSelect(_ sender: Any) { 95 | let message = EventHelper.Message.globalMediaKeyDidPress(.playPause) 96 | EventHelper.instance.report(message) 97 | } 98 | 99 | @objc private func previousTrackDockMenuItemDidSelect(_ sender: Any) { 100 | let message = EventHelper.Message.globalMediaKeyDidPress(.previousTrack) 101 | EventHelper.instance.report(message) 102 | } 103 | 104 | @objc private func nextTrackDockMenuItemDidSelect(_ sender: Any) { 105 | let message = EventHelper.Message.globalMediaKeyDidPress(.nextTrack) 106 | EventHelper.instance.report(message) 107 | } 108 | 109 | } 110 | 111 | // MARK: - Event Helper Target 112 | 113 | extension AppDelegate: EventHelper.Target { 114 | 115 | func handleMessage(_ message: EventHelper.Message) { 116 | switch message { 117 | case let .updateBackMenuBarItem(isEnabled): 118 | backMenuBarItem.action = isEnabled ? #selector(backMenuBarItemDidSelect) : nil 119 | case let .updateForwardMenuBarItem(isEnabled): 120 | forwardMenuBarItem.action = isEnabled ? #selector(forwardMenuBarItemDidSelect) : nil 121 | 122 | default: 123 | break 124 | } 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /Yandex Music/Framework/LocalizedString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedString.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 28.01.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LocalizedString { 11 | 12 | enum Scene { 13 | 14 | enum Main { 15 | 16 | static let title = NSLocalizedString("Scene.Main.Title", comment: "") 17 | 18 | enum Error { 19 | 20 | static let title = NSLocalizedString("Scene.Main.Error.Title", comment: "") 21 | static let tryAgainButton = NSLocalizedString("Scene.Main.Error.TryAgainButton", comment: "") 22 | 23 | } 24 | 25 | } 26 | 27 | enum Settings { 28 | 29 | static let title = NSLocalizedString("Scene.Settings.Title", comment: "") 30 | static let systemMusicSuppressionCheckbox = NSLocalizedString("Scene.Settings.SystemMusicSuppressionCheckbox", comment: "") 31 | static let resetBrowserButton = NSLocalizedString("Scene.Settings.ResetBrowserButton", comment: "") 32 | 33 | } 34 | 35 | } 36 | 37 | enum Alert { 38 | enum Update { 39 | 40 | static let title = NSLocalizedString("Alert.Update.Title", comment: "") 41 | static let description = NSLocalizedString("Alert.Update.Description", comment: "") 42 | 43 | enum Button { 44 | 45 | static let showDetails = NSLocalizedString("Alert.Update.Button.ShowDetails", comment: "") 46 | static let later = NSLocalizedString("Alert.Update.Button.Later", comment: "") 47 | 48 | } 49 | 50 | } 51 | } 52 | 53 | enum MainMenu { 54 | 55 | static let app = NSLocalizedString("MainMenu.App", comment: "") 56 | 57 | enum App { 58 | 59 | static let about = NSLocalizedString("MainMenu.App.About", comment: "") 60 | static let preferences = NSLocalizedString("MainMenu.App.Preferences", comment: "") 61 | static let services = NSLocalizedString("MainMenu.App.Services", comment: "") 62 | static let hide = NSLocalizedString("MainMenu.App.Hide", comment: "") 63 | static let hideOthers = NSLocalizedString("MainMenu.App.HideOthers", comment: "") 64 | static let showAll = NSLocalizedString("MainMenu.App.ShowAll", comment: "") 65 | static let quit = NSLocalizedString("MainMenu.App.Quit", comment: "") 66 | 67 | } 68 | 69 | static let edit = NSLocalizedString("MainMenu.Edit", comment: "") 70 | 71 | enum Edit { 72 | 73 | static let undo = NSLocalizedString("MainMenu.Edit.Undo", comment: "") 74 | static let redo = NSLocalizedString("MainMenu.Edit.Redo", comment: "") 75 | static let cut = NSLocalizedString("MainMenu.Edit.Cut", comment: "") 76 | static let copy = NSLocalizedString("MainMenu.Edit.Copy", comment: "") 77 | static let paste = NSLocalizedString("MainMenu.Edit.Paste", comment: "") 78 | static let delete = NSLocalizedString("MainMenu.Edit.Delete", comment: "") 79 | static let selectAll = NSLocalizedString("MainMenu.Edit.SelectAll", comment: "") 80 | 81 | } 82 | 83 | static let view = NSLocalizedString("MainMenu.View", comment: "") 84 | 85 | enum View { 86 | 87 | static let back = NSLocalizedString("MainMenu.View.Back", comment: "") 88 | static let forward = NSLocalizedString("MainMenu.View.Forward", comment: "") 89 | static let home = NSLocalizedString("MainMenu.View.Home", comment: "") 90 | static let reloadPage = NSLocalizedString("MainMenu.View.ReloadPage", comment: "") 91 | static let enterFullScreen = NSLocalizedString("MainMenu.View.EnterFullScreen", comment: "") 92 | 93 | } 94 | 95 | static let window = NSLocalizedString("MainMenu.Window", comment: "") 96 | 97 | enum Window { 98 | 99 | static let close = NSLocalizedString("MainMenu.Window.Close", comment: "") 100 | static let minimize = NSLocalizedString("MainMenu.Window.Minimize", comment: "") 101 | static let zoom = NSLocalizedString("MainMenu.Window.Zoom", comment: "") 102 | static let bringAllToFront = NSLocalizedString("MainMenu.Window.BringAllToFront", comment: "") 103 | 104 | } 105 | 106 | static let help = NSLocalizedString("MainMenu.Help", comment: "") 107 | 108 | enum Help { 109 | static let codeRepository = NSLocalizedString("MainMenu.Help.CodeRepository", comment: "") 110 | } 111 | 112 | } 113 | 114 | enum DockMenu { 115 | 116 | static let playPause = NSLocalizedString("DockMenu.PlayPause", comment: "") 117 | static let nextTrack = NSLocalizedString("DockMenu.NextTrack", comment: "") 118 | static let previousTrack = NSLocalizedString("DockMenu.PreviousTrack", comment: "") 119 | 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /Yandex Music/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // Yandex Music 4 | // 5 | // Created by Sergey Moskvin on 11.02.2022. 6 | // 7 | 8 | import Cocoa 9 | import WebKit 10 | 11 | final class MainViewController: NSViewController { 12 | 13 | @IBOutlet private weak var webView: WKWebView! 14 | 15 | @IBOutlet private weak var loadingIndicator: NSProgressIndicator! 16 | @IBOutlet private weak var errorView: NSView! 17 | 18 | private let homeURL = { 19 | let baseDomain = Constants.baseDomains.first(where: { $0.languageCode == Locale.current.languageCode })?.host 20 | ?? Constants.baseDomains.first?.host ?? "" 21 | 22 | return URL(string: "https://music." + baseDomain)! 23 | } () 24 | 25 | private let allowedDomains = Constants.baseDomains.flatMap { 26 | return ["music." + $0.host, "passport." + $0.host] 27 | } 28 | 29 | private var isFirstAppearance = true 30 | 31 | // MARK: Life Cycle 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | configure() 36 | } 37 | 38 | override func viewWillAppear() { 39 | super.viewWillAppear() 40 | view.window?.title = LocalizedString.Scene.Main.title 41 | } 42 | 43 | override func viewDidAppear() { 44 | super.viewDidAppear() 45 | 46 | guard isFirstAppearance else { 47 | return 48 | } 49 | 50 | isFirstAppearance = false 51 | 52 | view.window?.delegate = self 53 | loadingIndicator.startAnimation(self) 54 | 55 | goHome() 56 | } 57 | 58 | // MARK: Builder Actions 59 | 60 | @IBAction private func tryAgainButtonDidPress(_ sender: Any) { 61 | resetVisibleState() 62 | reloadPage() 63 | } 64 | 65 | // MARK: Functions 66 | 67 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 68 | guard (object as? WKWebView) == webView else { 69 | return 70 | } 71 | 72 | var message: EventHelper.Message? 73 | 74 | switch keyPath { 75 | case #keyPath(WKWebView.canGoBack): 76 | message = .updateBackMenuBarItem(isEnabled: webView.canGoBack) 77 | case #keyPath(WKWebView.canGoForward): 78 | message = .updateForwardMenuBarItem(isEnabled: webView.canGoForward) 79 | 80 | default: 81 | break 82 | } 83 | 84 | if let message { 85 | EventHelper.instance.report(message) 86 | } 87 | } 88 | 89 | private func configure() { 90 | for keyPath in [ 91 | #keyPath(WKWebView.canGoBack), 92 | #keyPath(WKWebView.canGoForward) 93 | ] { 94 | webView.addObserver(self, forKeyPath: keyPath, options: [.initial, .new], context: nil) 95 | } 96 | 97 | webView.navigationDelegate = self 98 | webView.uiDelegate = self 99 | 100 | EventHelper.instance.addTarget(self) 101 | } 102 | 103 | private func goHome() { 104 | let request = URLRequest(url: homeURL) 105 | webView.load(request) 106 | } 107 | 108 | private func reloadPage() { 109 | if webView.url != nil { 110 | webView.reload() 111 | } else { 112 | goHome() 113 | } 114 | } 115 | 116 | private func resetVisibleState() { 117 | webView.isHidden = true 118 | errorView.isHidden = true 119 | 120 | loadingIndicator.startAnimation(self) 121 | } 122 | 123 | private func checkForRedirectionFromLoginToSettings(url: URL) -> Bool { 124 | let suitablePaths = Constants.baseDomains.flatMap { 125 | let value = "music.\($0.host)/settings" 126 | 127 | let allowedCharacters = CharacterSet.urlHostAllowed 128 | let encoded = value.addingPercentEncoding(withAllowedCharacters: allowedCharacters)! 129 | 130 | return [ 131 | value, 132 | encoded, 133 | encoded.addingPercentEncoding(withAllowedCharacters: allowedCharacters)! 134 | ] 135 | } 136 | 137 | let url = url.absoluteString 138 | return suitablePaths.contains(where: { url.contains($0) }) && url.contains("from-passport") 139 | } 140 | 141 | private func clickWebButton(javaScriptClass: String, completion: ((Bool) -> Void)? = nil) { 142 | webView.evaluateJavaScript(""" 143 | document.querySelector('.\(javaScriptClass)').click(); 144 | """) { _, error in 145 | completion?(error == nil) 146 | } 147 | } 148 | 149 | } 150 | 151 | // MARK: - WebKit Navigation Delegate 152 | 153 | extension MainViewController: WKNavigationDelegate { 154 | 155 | func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { 156 | var result = WKNavigationActionPolicy.allow 157 | 158 | guard let url = navigationAction.request.url else { 159 | return result 160 | } 161 | 162 | switch navigationAction.navigationType { 163 | case .linkActivated: 164 | if !allowedDomains.contains(where: { url.absoluteString.contains($0) }) { 165 | NSWorkspace.shared.open(url) 166 | result = .cancel 167 | } 168 | 169 | case .formSubmitted: 170 | if checkForRedirectionFromLoginToSettings(url: url) { 171 | DispatchQueue.main.async { 172 | self.goHome() 173 | } 174 | 175 | result = .cancel 176 | } 177 | 178 | default: 179 | break 180 | } 181 | 182 | return result 183 | } 184 | 185 | func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { 186 | loadingIndicator.stopAnimation(self) 187 | webView.isHidden = false 188 | } 189 | 190 | func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { 191 | let error = error as NSError 192 | 193 | if error.code == 102, let url = error.userInfo["NSErrorFailingURLKey"] as? URL, checkForRedirectionFromLoginToSettings(url: url) { 194 | return 195 | } 196 | 197 | loadingIndicator.stopAnimation(self) 198 | webView.isHidden = true 199 | 200 | errorView.isHidden = false 201 | } 202 | 203 | } 204 | 205 | // MARK: - WebKit UI Delegate 206 | 207 | extension MainViewController: WKUIDelegate { 208 | 209 | func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { 210 | if navigationAction.targetFrame == nil { 211 | webView.load(navigationAction.request) 212 | } 213 | 214 | return nil 215 | } 216 | 217 | } 218 | 219 | // MARK: - Event Helper Target 220 | 221 | extension MainViewController: EventHelper.Target { 222 | 223 | func handleMessage(_ message: EventHelper.Message) { 224 | switch message { 225 | case .backMenuBarItemDidSelect: 226 | webView.goBack() 227 | case .forwardMenuBarItemDidSelect: 228 | webView.goForward() 229 | 230 | case .homeMenuBarItemDidSelect: 231 | if !errorView.isHidden { 232 | resetVisibleState() 233 | } 234 | 235 | goHome() 236 | 237 | case .reloadPageMenuBarItemDidSelect: 238 | if !errorView.isHidden { 239 | resetVisibleState() 240 | } 241 | 242 | reloadPage() 243 | 244 | case let .globalMediaKeyDidPress(mediaKey): 245 | switch mediaKey { 246 | case .playPause: 247 | clickWebButton(javaScriptClass: "player-controls__btn_play") { result in 248 | guard !result else { 249 | return 250 | } 251 | 252 | self.clickWebButton(javaScriptClass: "player-controls__btn_pause") 253 | } 254 | 255 | case .previousTrack: 256 | clickWebButton(javaScriptClass: "player-controls__btn_prev") 257 | case .nextTrack: 258 | clickWebButton(javaScriptClass: "player-controls__btn_next") 259 | } 260 | 261 | case .resetBuiltInBrowser: 262 | let webViewStore = WKWebsiteDataStore.default() 263 | let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes() 264 | 265 | webViewStore.fetchDataRecords(ofTypes: dataTypes) { records in 266 | webViewStore.removeData(ofTypes: dataTypes, for: records) { 267 | self.resetVisibleState() 268 | self.goHome() 269 | } 270 | } 271 | 272 | default: 273 | break 274 | } 275 | } 276 | 277 | } 278 | 279 | // MARK: - Window Delegate 280 | 281 | extension MainViewController: NSWindowDelegate { 282 | 283 | func windowShouldClose(_ sender: NSWindow) -> Bool { 284 | NSApplication.shared.terminate(self) 285 | return true 286 | } 287 | 288 | } 289 | -------------------------------------------------------------------------------- /Yandex Music.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A504CAD92985345C0043F0FC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A504CADB2985345C0043F0FC /* Localizable.strings */; }; 11 | A504CADE2985361A0043F0FC /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = A504CADD2985361A0043F0FC /* LocalizedString.swift */; }; 12 | A504CAE029853B680043F0FC /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A504CADF29853B680043F0FC /* Constants.swift */; }; 13 | A504CAE32985465D0043F0FC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = A504CAE52985465D0043F0FC /* InfoPlist.strings */; }; 14 | A504CAE829854BA00043F0FC /* YMButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A504CAE729854BA00043F0FC /* YMButton.swift */; }; 15 | A504CAED298551EE0043F0FC /* YMTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A504CAEC298551EE0043F0FC /* YMTextField.swift */; }; 16 | A504CAEF298553970043F0FC /* YMMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A504CAEE298553970043F0FC /* YMMenuItem.swift */; }; 17 | A504CAF1298554230043F0FC /* YMMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A504CAF0298554230043F0FC /* YMMenu.swift */; }; 18 | A504CAF529855EF70043F0FC /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = A504CAF429855EF70043F0FC /* FirebaseAnalytics */; }; 19 | A504CAF729855EF70043F0FC /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = A504CAF629855EF70043F0FC /* FirebaseCrashlytics */; }; 20 | A50AE66527B5B4F900F01BFA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50AE66427B5B4F900F01BFA /* AppDelegate.swift */; }; 21 | A50AE66727B5B4F900F01BFA /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50AE66627B5B4F900F01BFA /* MainViewController.swift */; }; 22 | A50AE66927B5B4FA00F01BFA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A50AE66827B5B4FA00F01BFA /* Assets.xcassets */; }; 23 | A53E0B7827B646DE006B2253 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53E0B7727B646DE006B2253 /* SettingsViewController.swift */; }; 24 | A55A79AC27B7C5EF0072BFAF /* TerminalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55A79AB27B7C5EF0072BFAF /* TerminalHelper.swift */; }; 25 | A570E48B27B6BF5500BF6E73 /* Full.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A570E48A27B6BF5500BF6E73 /* Full.storyboard */; }; 26 | A570E4A927B700C400BF6E73 /* YMApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = A570E4A827B700C400BF6E73 /* YMApplication.swift */; }; 27 | A570E4AD27B7020500BF6E73 /* MediaKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = A570E4AC27B7020500BF6E73 /* MediaKey.swift */; }; 28 | A570E4AF27B7035900BF6E73 /* EventHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A570E4AE27B7035900BF6E73 /* EventHelper.swift */; }; 29 | A570E4B227B703C600BF6E73 /* WeakPointer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A570E4B127B703C600BF6E73 /* WeakPointer.swift */; }; 30 | A570E4B427B7042600BF6E73 /* EventHelper.Target.swift in Sources */ = {isa = PBXBuildFile; fileRef = A570E4B327B7042600BF6E73 /* EventHelper.Target.swift */; }; 31 | A570E4B627B7046600BF6E73 /* EventHelper.Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = A570E4B527B7046600BF6E73 /* EventHelper.Message.swift */; }; 32 | A57B7614298560B40070B4F4 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A57B7613298560B40070B4F4 /* GoogleService-Info.plist */; }; 33 | A5C26A4D27B910060078A610 /* StorageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C26A4C27B910060078A610 /* StorageHelper.swift */; }; 34 | A5C26A5027B911300078A610 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C26A4F27B911300078A610 /* Bool.swift */; }; 35 | A5C26A5227B917290078A610 /* UpdateHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C26A5127B917290078A610 /* UpdateHelper.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | A504CADA2985345C0043F0FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 40 | A504CADC2985345F0043F0FC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 41 | A504CADD2985361A0043F0FC /* LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; 42 | A504CADF29853B680043F0FC /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 43 | A504CAE42985465D0043F0FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 44 | A504CAE6298546620043F0FC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; 45 | A504CAE729854BA00043F0FC /* YMButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YMButton.swift; sourceTree = ""; }; 46 | A504CAEC298551EE0043F0FC /* YMTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YMTextField.swift; sourceTree = ""; }; 47 | A504CAEE298553970043F0FC /* YMMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YMMenuItem.swift; sourceTree = ""; }; 48 | A504CAF0298554230043F0FC /* YMMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YMMenu.swift; sourceTree = ""; }; 49 | A50AE66127B5B4F900F01BFA /* Yandex Music.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Yandex Music.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | A50AE66427B5B4F900F01BFA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 51 | A50AE66627B5B4F900F01BFA /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 52 | A50AE66827B5B4FA00F01BFA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | A50AE66D27B5B4FA00F01BFA /* List.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = List.entitlements; sourceTree = ""; }; 54 | A53E0B7727B646DE006B2253 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 55 | A55A79AB27B7C5EF0072BFAF /* TerminalHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalHelper.swift; sourceTree = ""; }; 56 | A570E48A27B6BF5500BF6E73 /* Full.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Full.storyboard; sourceTree = ""; }; 57 | A570E4A827B700C400BF6E73 /* YMApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YMApplication.swift; sourceTree = ""; }; 58 | A570E4AC27B7020500BF6E73 /* MediaKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaKey.swift; sourceTree = ""; }; 59 | A570E4AE27B7035900BF6E73 /* EventHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHelper.swift; sourceTree = ""; }; 60 | A570E4B127B703C600BF6E73 /* WeakPointer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakPointer.swift; sourceTree = ""; }; 61 | A570E4B327B7042600BF6E73 /* EventHelper.Target.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHelper.Target.swift; sourceTree = ""; }; 62 | A570E4B527B7046600BF6E73 /* EventHelper.Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHelper.Message.swift; sourceTree = ""; }; 63 | A57B7613298560B40070B4F4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 64 | A5C26A4C27B910060078A610 /* StorageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageHelper.swift; sourceTree = ""; }; 65 | A5C26A4F27B911300078A610 /* Bool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bool.swift; sourceTree = ""; }; 66 | A5C26A5127B917290078A610 /* UpdateHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateHelper.swift; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | A50AE65E27B5B4F900F01BFA /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | A504CAF529855EF70043F0FC /* FirebaseAnalytics in Frameworks */, 75 | A504CAF729855EF70043F0FC /* FirebaseCrashlytics in Frameworks */, 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | A504CAEB29854DDF0043F0FC /* Controls */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | A570E4A827B700C400BF6E73 /* YMApplication.swift */, 86 | A504CAE729854BA00043F0FC /* YMButton.swift */, 87 | A504CAF0298554230043F0FC /* YMMenu.swift */, 88 | A504CAEE298553970043F0FC /* YMMenuItem.swift */, 89 | A504CAEC298551EE0043F0FC /* YMTextField.swift */, 90 | ); 91 | path = Controls; 92 | sourceTree = ""; 93 | }; 94 | A50AE65827B5B4F900F01BFA = { 95 | isa = PBXGroup; 96 | children = ( 97 | A50AE66327B5B4F900F01BFA /* Yandex Music */, 98 | A50AE66227B5B4F900F01BFA /* Products */, 99 | ); 100 | sourceTree = ""; 101 | }; 102 | A50AE66227B5B4F900F01BFA /* Products */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | A50AE66127B5B4F900F01BFA /* Yandex Music.app */, 106 | ); 107 | name = Products; 108 | sourceTree = ""; 109 | }; 110 | A50AE66327B5B4F900F01BFA /* Yandex Music */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | A50AE66427B5B4F900F01BFA /* AppDelegate.swift */, 114 | A50AE66627B5B4F900F01BFA /* MainViewController.swift */, 115 | A53E0B7727B646DE006B2253 /* SettingsViewController.swift */, 116 | A570E4A727B700AE00BF6E73 /* Framework */, 117 | A50AE66827B5B4FA00F01BFA /* Assets.xcassets */, 118 | A570E48A27B6BF5500BF6E73 /* Full.storyboard */, 119 | A57B7613298560B40070B4F4 /* GoogleService-Info.plist */, 120 | A50AE66D27B5B4FA00F01BFA /* List.entitlements */, 121 | A504CAE52985465D0043F0FC /* InfoPlist.strings */, 122 | A504CADB2985345C0043F0FC /* Localizable.strings */, 123 | ); 124 | path = "Yandex Music"; 125 | sourceTree = ""; 126 | }; 127 | A570E4A727B700AE00BF6E73 /* Framework */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | A504CAEB29854DDF0043F0FC /* Controls */, 131 | A5C26A4E27B911250078A610 /* Extensions */, 132 | A570E4B727B7065D00BF6E73 /* Utils */, 133 | A504CADF29853B680043F0FC /* Constants.swift */, 134 | A570E4AE27B7035900BF6E73 /* EventHelper.swift */, 135 | A570E4B527B7046600BF6E73 /* EventHelper.Message.swift */, 136 | A570E4B327B7042600BF6E73 /* EventHelper.Target.swift */, 137 | A504CADD2985361A0043F0FC /* LocalizedString.swift */, 138 | A5C26A4C27B910060078A610 /* StorageHelper.swift */, 139 | A55A79AB27B7C5EF0072BFAF /* TerminalHelper.swift */, 140 | A5C26A5127B917290078A610 /* UpdateHelper.swift */, 141 | ); 142 | path = Framework; 143 | sourceTree = ""; 144 | }; 145 | A570E4B727B7065D00BF6E73 /* Utils */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | A570E4AC27B7020500BF6E73 /* MediaKey.swift */, 149 | A570E4B127B703C600BF6E73 /* WeakPointer.swift */, 150 | ); 151 | path = Utils; 152 | sourceTree = ""; 153 | }; 154 | A5C26A4E27B911250078A610 /* Extensions */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | A5C26A4F27B911300078A610 /* Bool.swift */, 158 | ); 159 | path = Extensions; 160 | sourceTree = ""; 161 | }; 162 | /* End PBXGroup section */ 163 | 164 | /* Begin PBXNativeTarget section */ 165 | A50AE66027B5B4F900F01BFA /* Yandex Music */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = A50AE67027B5B4FA00F01BFA /* Build configuration list for PBXNativeTarget "Yandex Music" */; 168 | buildPhases = ( 169 | A50AE65D27B5B4F900F01BFA /* Sources */, 170 | A50AE65E27B5B4F900F01BFA /* Frameworks */, 171 | A50AE65F27B5B4F900F01BFA /* Resources */, 172 | ); 173 | buildRules = ( 174 | ); 175 | dependencies = ( 176 | ); 177 | name = "Yandex Music"; 178 | packageProductDependencies = ( 179 | A504CAF429855EF70043F0FC /* FirebaseAnalytics */, 180 | A504CAF629855EF70043F0FC /* FirebaseCrashlytics */, 181 | ); 182 | productName = "Yandex Music"; 183 | productReference = A50AE66127B5B4F900F01BFA /* Yandex Music.app */; 184 | productType = "com.apple.product-type.application"; 185 | }; 186 | /* End PBXNativeTarget section */ 187 | 188 | /* Begin PBXProject section */ 189 | A50AE65927B5B4F900F01BFA /* Project object */ = { 190 | isa = PBXProject; 191 | attributes = { 192 | BuildIndependentTargetsInParallel = 1; 193 | LastSwiftUpdateCheck = 1320; 194 | LastUpgradeCheck = 1540; 195 | TargetAttributes = { 196 | A50AE66027B5B4F900F01BFA = { 197 | CreatedOnToolsVersion = 13.2.1; 198 | }; 199 | }; 200 | }; 201 | buildConfigurationList = A50AE65C27B5B4F900F01BFA /* Build configuration list for PBXProject "Yandex Music" */; 202 | compatibilityVersion = "Xcode 13.0"; 203 | developmentRegion = en; 204 | hasScannedForEncodings = 0; 205 | knownRegions = ( 206 | ru, 207 | en, 208 | ); 209 | mainGroup = A50AE65827B5B4F900F01BFA; 210 | packageReferences = ( 211 | A504CAF329855EF70043F0FC /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 212 | ); 213 | productRefGroup = A50AE66227B5B4F900F01BFA /* Products */; 214 | projectDirPath = ""; 215 | projectRoot = ""; 216 | targets = ( 217 | A50AE66027B5B4F900F01BFA /* Yandex Music */, 218 | ); 219 | }; 220 | /* End PBXProject section */ 221 | 222 | /* Begin PBXResourcesBuildPhase section */ 223 | A50AE65F27B5B4F900F01BFA /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | A50AE66927B5B4FA00F01BFA /* Assets.xcassets in Resources */, 228 | A504CAE32985465D0043F0FC /* InfoPlist.strings in Resources */, 229 | A57B7614298560B40070B4F4 /* GoogleService-Info.plist in Resources */, 230 | A504CAD92985345C0043F0FC /* Localizable.strings in Resources */, 231 | A570E48B27B6BF5500BF6E73 /* Full.storyboard in Resources */, 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | /* End PBXResourcesBuildPhase section */ 236 | 237 | /* Begin PBXSourcesBuildPhase section */ 238 | A50AE65D27B5B4F900F01BFA /* Sources */ = { 239 | isa = PBXSourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | A570E4B627B7046600BF6E73 /* EventHelper.Message.swift in Sources */, 243 | A570E4AF27B7035900BF6E73 /* EventHelper.swift in Sources */, 244 | A570E4AD27B7020500BF6E73 /* MediaKey.swift in Sources */, 245 | A5C26A5227B917290078A610 /* UpdateHelper.swift in Sources */, 246 | A504CADE2985361A0043F0FC /* LocalizedString.swift in Sources */, 247 | A504CAE029853B680043F0FC /* Constants.swift in Sources */, 248 | A504CAE829854BA00043F0FC /* YMButton.swift in Sources */, 249 | A570E4B427B7042600BF6E73 /* EventHelper.Target.swift in Sources */, 250 | A570E4B227B703C600BF6E73 /* WeakPointer.swift in Sources */, 251 | A5C26A5027B911300078A610 /* Bool.swift in Sources */, 252 | A5C26A4D27B910060078A610 /* StorageHelper.swift in Sources */, 253 | A504CAF1298554230043F0FC /* YMMenu.swift in Sources */, 254 | A570E4A927B700C400BF6E73 /* YMApplication.swift in Sources */, 255 | A50AE66727B5B4F900F01BFA /* MainViewController.swift in Sources */, 256 | A55A79AC27B7C5EF0072BFAF /* TerminalHelper.swift in Sources */, 257 | A53E0B7827B646DE006B2253 /* SettingsViewController.swift in Sources */, 258 | A50AE66527B5B4F900F01BFA /* AppDelegate.swift in Sources */, 259 | A504CAEF298553970043F0FC /* YMMenuItem.swift in Sources */, 260 | A504CAED298551EE0043F0FC /* YMTextField.swift in Sources */, 261 | ); 262 | runOnlyForDeploymentPostprocessing = 0; 263 | }; 264 | /* End PBXSourcesBuildPhase section */ 265 | 266 | /* Begin PBXVariantGroup section */ 267 | A504CADB2985345C0043F0FC /* Localizable.strings */ = { 268 | isa = PBXVariantGroup; 269 | children = ( 270 | A504CADA2985345C0043F0FC /* en */, 271 | A504CADC2985345F0043F0FC /* ru */, 272 | ); 273 | name = Localizable.strings; 274 | sourceTree = ""; 275 | }; 276 | A504CAE52985465D0043F0FC /* InfoPlist.strings */ = { 277 | isa = PBXVariantGroup; 278 | children = ( 279 | A504CAE42985465D0043F0FC /* en */, 280 | A504CAE6298546620043F0FC /* ru */, 281 | ); 282 | name = InfoPlist.strings; 283 | sourceTree = ""; 284 | }; 285 | /* End PBXVariantGroup section */ 286 | 287 | /* Begin XCBuildConfiguration section */ 288 | A50AE66E27B5B4FA00F01BFA /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | ALWAYS_SEARCH_USER_PATHS = NO; 292 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 293 | CLANG_ANALYZER_NONNULL = YES; 294 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 295 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 296 | CLANG_CXX_LIBRARY = "libc++"; 297 | CLANG_ENABLE_MODULES = YES; 298 | CLANG_ENABLE_OBJC_ARC = YES; 299 | CLANG_ENABLE_OBJC_WEAK = YES; 300 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 301 | CLANG_WARN_BOOL_CONVERSION = YES; 302 | CLANG_WARN_COMMA = YES; 303 | CLANG_WARN_CONSTANT_CONVERSION = YES; 304 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 305 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 306 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 307 | CLANG_WARN_EMPTY_BODY = YES; 308 | CLANG_WARN_ENUM_CONVERSION = YES; 309 | CLANG_WARN_INFINITE_RECURSION = YES; 310 | CLANG_WARN_INT_CONVERSION = YES; 311 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 312 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 313 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 314 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 315 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 316 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 317 | CLANG_WARN_STRICT_PROTOTYPES = YES; 318 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 319 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 320 | CLANG_WARN_UNREACHABLE_CODE = YES; 321 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 322 | COPY_PHASE_STRIP = NO; 323 | DEAD_CODE_STRIPPING = YES; 324 | DEBUG_INFORMATION_FORMAT = dwarf; 325 | ENABLE_STRICT_OBJC_MSGSEND = YES; 326 | ENABLE_TESTABILITY = YES; 327 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 328 | GCC_C_LANGUAGE_STANDARD = gnu11; 329 | GCC_DYNAMIC_NO_PIC = NO; 330 | GCC_NO_COMMON_BLOCKS = YES; 331 | GCC_OPTIMIZATION_LEVEL = 0; 332 | GCC_PREPROCESSOR_DEFINITIONS = ( 333 | "DEBUG=1", 334 | "$(inherited)", 335 | ); 336 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 337 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 338 | GCC_WARN_UNDECLARED_SELECTOR = YES; 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 340 | GCC_WARN_UNUSED_FUNCTION = YES; 341 | GCC_WARN_UNUSED_VARIABLE = YES; 342 | MACOSX_DEPLOYMENT_TARGET = 11.0; 343 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 344 | MTL_FAST_MATH = YES; 345 | ONLY_ACTIVE_ARCH = YES; 346 | SDKROOT = macosx; 347 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 348 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 349 | }; 350 | name = Debug; 351 | }; 352 | A50AE66F27B5B4FA00F01BFA /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ALWAYS_SEARCH_USER_PATHS = NO; 356 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 357 | CLANG_ANALYZER_NONNULL = YES; 358 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 359 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 360 | CLANG_CXX_LIBRARY = "libc++"; 361 | CLANG_ENABLE_MODULES = YES; 362 | CLANG_ENABLE_OBJC_ARC = YES; 363 | CLANG_ENABLE_OBJC_WEAK = YES; 364 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 365 | CLANG_WARN_BOOL_CONVERSION = YES; 366 | CLANG_WARN_COMMA = YES; 367 | CLANG_WARN_CONSTANT_CONVERSION = YES; 368 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 369 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 370 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 371 | CLANG_WARN_EMPTY_BODY = YES; 372 | CLANG_WARN_ENUM_CONVERSION = YES; 373 | CLANG_WARN_INFINITE_RECURSION = YES; 374 | CLANG_WARN_INT_CONVERSION = YES; 375 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 376 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 377 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 379 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 384 | CLANG_WARN_UNREACHABLE_CODE = YES; 385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 386 | COPY_PHASE_STRIP = NO; 387 | DEAD_CODE_STRIPPING = YES; 388 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 389 | ENABLE_NS_ASSERTIONS = NO; 390 | ENABLE_STRICT_OBJC_MSGSEND = YES; 391 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 392 | GCC_C_LANGUAGE_STANDARD = gnu11; 393 | GCC_NO_COMMON_BLOCKS = YES; 394 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 395 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 396 | GCC_WARN_UNDECLARED_SELECTOR = YES; 397 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 398 | GCC_WARN_UNUSED_FUNCTION = YES; 399 | GCC_WARN_UNUSED_VARIABLE = YES; 400 | MACOSX_DEPLOYMENT_TARGET = 11.0; 401 | MTL_ENABLE_DEBUG_INFO = NO; 402 | MTL_FAST_MATH = YES; 403 | SDKROOT = macosx; 404 | SWIFT_COMPILATION_MODE = wholemodule; 405 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 406 | }; 407 | name = Release; 408 | }; 409 | A50AE67127B5B4FA00F01BFA /* Debug */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon"; 413 | CODE_SIGN_ENTITLEMENTS = "Yandex Music/List.entitlements"; 414 | CODE_SIGN_IDENTITY = "-"; 415 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 416 | CODE_SIGN_STYLE = Automatic; 417 | COMBINE_HIDPI_IMAGES = YES; 418 | CURRENT_PROJECT_VERSION = 10; 419 | DEAD_CODE_STRIPPING = YES; 420 | DEVELOPMENT_TEAM = XHE6H7N7QS; 421 | ENABLE_HARDENED_RUNTIME = YES; 422 | GENERATE_INFOPLIST_FILE = YES; 423 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; 424 | INFOPLIST_KEY_NSMainStoryboardFile = Full; 425 | INFOPLIST_KEY_NSPrincipalClass = Yandex_Music.YMApplication; 426 | LD_RUNPATH_SEARCH_PATHS = ( 427 | "$(inherited)", 428 | "@executable_path/../Frameworks", 429 | ); 430 | MACOSX_DEPLOYMENT_TARGET = 11.0; 431 | MARKETING_VERSION = 1.1.1; 432 | PRODUCT_BUNDLE_IDENTIFIER = "debug45.Yandex-Music"; 433 | PRODUCT_NAME = "$(TARGET_NAME)"; 434 | SWIFT_EMIT_LOC_STRINGS = YES; 435 | SWIFT_VERSION = 5.0; 436 | }; 437 | name = Debug; 438 | }; 439 | A50AE67227B5B4FA00F01BFA /* Release */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon"; 443 | CODE_SIGN_ENTITLEMENTS = "Yandex Music/List.entitlements"; 444 | CODE_SIGN_IDENTITY = "-"; 445 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 446 | CODE_SIGN_STYLE = Automatic; 447 | COMBINE_HIDPI_IMAGES = YES; 448 | CURRENT_PROJECT_VERSION = 10; 449 | DEAD_CODE_STRIPPING = YES; 450 | DEVELOPMENT_TEAM = XHE6H7N7QS; 451 | ENABLE_HARDENED_RUNTIME = YES; 452 | GENERATE_INFOPLIST_FILE = YES; 453 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; 454 | INFOPLIST_KEY_NSMainStoryboardFile = Full; 455 | INFOPLIST_KEY_NSPrincipalClass = Yandex_Music.YMApplication; 456 | LD_RUNPATH_SEARCH_PATHS = ( 457 | "$(inherited)", 458 | "@executable_path/../Frameworks", 459 | ); 460 | MACOSX_DEPLOYMENT_TARGET = 11.0; 461 | MARKETING_VERSION = 1.1.1; 462 | PRODUCT_BUNDLE_IDENTIFIER = "debug45.Yandex-Music"; 463 | PRODUCT_NAME = "$(TARGET_NAME)"; 464 | SWIFT_EMIT_LOC_STRINGS = YES; 465 | SWIFT_VERSION = 5.0; 466 | }; 467 | name = Release; 468 | }; 469 | /* End XCBuildConfiguration section */ 470 | 471 | /* Begin XCConfigurationList section */ 472 | A50AE65C27B5B4F900F01BFA /* Build configuration list for PBXProject "Yandex Music" */ = { 473 | isa = XCConfigurationList; 474 | buildConfigurations = ( 475 | A50AE66E27B5B4FA00F01BFA /* Debug */, 476 | A50AE66F27B5B4FA00F01BFA /* Release */, 477 | ); 478 | defaultConfigurationIsVisible = 0; 479 | defaultConfigurationName = Release; 480 | }; 481 | A50AE67027B5B4FA00F01BFA /* Build configuration list for PBXNativeTarget "Yandex Music" */ = { 482 | isa = XCConfigurationList; 483 | buildConfigurations = ( 484 | A50AE67127B5B4FA00F01BFA /* Debug */, 485 | A50AE67227B5B4FA00F01BFA /* Release */, 486 | ); 487 | defaultConfigurationIsVisible = 0; 488 | defaultConfigurationName = Release; 489 | }; 490 | /* End XCConfigurationList section */ 491 | 492 | /* Begin XCRemoteSwiftPackageReference section */ 493 | A504CAF329855EF70043F0FC /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { 494 | isa = XCRemoteSwiftPackageReference; 495 | repositoryURL = "https://github.com/firebase/firebase-ios-sdk"; 496 | requirement = { 497 | kind = exactVersion; 498 | version = 10.28.0; 499 | }; 500 | }; 501 | /* End XCRemoteSwiftPackageReference section */ 502 | 503 | /* Begin XCSwiftPackageProductDependency section */ 504 | A504CAF429855EF70043F0FC /* FirebaseAnalytics */ = { 505 | isa = XCSwiftPackageProductDependency; 506 | package = A504CAF329855EF70043F0FC /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 507 | productName = FirebaseAnalytics; 508 | }; 509 | A504CAF629855EF70043F0FC /* FirebaseCrashlytics */ = { 510 | isa = XCSwiftPackageProductDependency; 511 | package = A504CAF329855EF70043F0FC /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 512 | productName = FirebaseCrashlytics; 513 | }; 514 | /* End XCSwiftPackageProductDependency section */ 515 | }; 516 | rootObject = A50AE65927B5B4F900F01BFA /* Project object */; 517 | } 518 | -------------------------------------------------------------------------------- /Yandex Music/Full.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 333 | 334 | 335 | 336 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 438 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | --------------------------------------------------------------------------------