├── 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 |
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 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
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 |
--------------------------------------------------------------------------------