├── icons ├── icon-48.png ├── icon-96.png ├── next-48.png ├── next-96.png ├── prev-48.png ├── prev-96.png ├── pause-48.png ├── pause-96.png ├── play-arrow-48.png └── play-arrow-96.png ├── images └── screen.png ├── popup ├── ym-image.png ├── controls.html ├── controls.css └── controls.js ├── vscode.code-workspace ├── options ├── options.css ├── options.html └── options.js ├── README.md ├── handler.js ├── .gitignore ├── manifest.json ├── _locales ├── ru │ └── messages.json └── en │ └── messages.json ├── contextMenu.js ├── hotKeyHandler.js └── injector.js /icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/icon-48.png -------------------------------------------------------------------------------- /icons/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/icon-96.png -------------------------------------------------------------------------------- /icons/next-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/next-48.png -------------------------------------------------------------------------------- /icons/next-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/next-96.png -------------------------------------------------------------------------------- /icons/prev-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/prev-48.png -------------------------------------------------------------------------------- /icons/prev-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/prev-96.png -------------------------------------------------------------------------------- /images/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/images/screen.png -------------------------------------------------------------------------------- /icons/pause-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/pause-48.png -------------------------------------------------------------------------------- /icons/pause-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/pause-96.png -------------------------------------------------------------------------------- /popup/ym-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/popup/ym-image.png -------------------------------------------------------------------------------- /vscode.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /icons/play-arrow-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/play-arrow-48.png -------------------------------------------------------------------------------- /icons/play-arrow-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzhard/YandexMusicFirefoxControls/HEAD/icons/play-arrow-96.png -------------------------------------------------------------------------------- /options/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 25em; 3 | font-family: "Open Sans Light", sans-serif; 4 | font-size: 1.5em; 5 | font-weight: 300; 6 | } 7 | 8 | .title { 9 | font-size: 1.2em; 10 | margin-bottom: 0.5em; 11 | } 12 | 13 | label { 14 | float: left; 15 | } 16 | 17 | input { 18 | margin: 0.5em; 19 | } -------------------------------------------------------------------------------- /options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
Notifications
13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yandex Music Controls 2 | 3 | Extension to control Yandex Music tabs. 4 | 5 | This extension provides you ability to control your Yandex.Music tabs. 6 | 7 | ## Features 8 | 9 | * Pause/Play, Next/Previous track 10 | * Change track repeat / shuffle 11 | * Like and Dislike 12 | * Hot keys 13 | 14 | ![Preview](./images/screen.png) 15 | 16 | ## Install 17 | 18 | This Extention could be simply Installed from [Mozilla Addons page](https://addons.mozilla.org/ru/firefox/addon/yandexmusicplayer/) 19 | 20 | Also for debugging install as a [temporary extention](https://extensionworkshop.com/documentation/develop/temporary-installation-in-firefox/) 21 | 22 | ## Contributing 23 | 24 | This repository is open for contribution. You are welcome with PRs 25 | -------------------------------------------------------------------------------- /options/options.js: -------------------------------------------------------------------------------- 1 | const notificationsChk = document.querySelector("#notifications"); 2 | document.getElementById("notificationsTitle").textContent = browser.i18n.getMessage("notificationsTitle"); 3 | document.getElementById("showNotificationsLabel").firstChild.textContent = browser.i18n.getMessage("showNotificationsLabel"); 4 | function updateUI(restoredSettings) { 5 | notificationsChk.checked = restoredSettings.showNotifications; 6 | } 7 | 8 | function onError(e) { 9 | console.error(e); 10 | } 11 | 12 | const gettingStoredSettings = browser.storage.local.get({"showNotifications": false}); 13 | gettingStoredSettings.then(updateUI, onError); 14 | 15 | notificationsChk.onchange = () => { 16 | browser.storage.local.set({"showNotifications": notificationsChk.checked}); 17 | }; -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | function onTrackChanged() { 2 | window.postMessage({type: "track", msg: getTrack()}); 3 | } 4 | 5 | function onControlsChanged() { 6 | window.postMessage({type: "controls", msg: externalAPI.getControls()}); 7 | } 8 | 9 | function onProgressChanged() { 10 | window.postMessage({type: "progress", msg: externalAPI.getProgress()}); 11 | } 12 | 13 | externalAPI.on(externalAPI.EVENT_TRACK, onTrackChanged); 14 | externalAPI.on(externalAPI.EVENT_CONTROLS, onControlsChanged); 15 | externalAPI.on(externalAPI.EVENT_PROGRESS, onProgressChanged); 16 | 17 | function getTrack() { 18 | return { 19 | isPlaying: externalAPI.isPlaying(), 20 | currentTrack: externalAPI.getCurrentTrack(), 21 | controlState: externalAPI.getControls(), 22 | progress: externalAPI.getProgress(), 23 | volume: externalAPI.getVolume(), 24 | repeat: externalAPI.getRepeat(), 25 | shuffle: externalAPI.getShuffle() 26 | }; 27 | } 28 | 29 | function isLiked() { 30 | return externalAPI.getCurrentTrack().liked 31 | } 32 | 33 | function isDisliked() { 34 | return externalAPI.getCurrentTrack().disliked 35 | } 36 | 37 | function getShuffle() { 38 | return externalAPI.getShuffle() 39 | } 40 | 41 | function getRepeat() { 42 | return externalAPI.getRepeat() 43 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/tasks.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | .idea/ 11 | 12 | # Sensitive or high-churn files 13 | .idea/**/dataSources/ 14 | .idea/**/dataSources.ids 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | .idea/**/dbnavigator.xml 20 | 21 | # Gradle 22 | .idea/**/gradle.xml 23 | .idea/**/libraries 24 | 25 | # CMake 26 | cmake-build-debug/ 27 | cmake-build-release/ 28 | 29 | # Mongo Explorer plugin 30 | .idea/**/mongoSettings.xml 31 | 32 | # File-based project format 33 | *.iws 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | 53 | # Editor-based Rest Client 54 | .idea/httpRequests 55 | 56 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "description": "__MSG_extensionDescription__", 5 | "version": "1.2.1", 6 | "default_locale": "en", 7 | "icons": { 8 | "48": "icons/icon-48.png", 9 | "96": "icons/icon-96.png" 10 | }, 11 | "applications": { 12 | "gecko": { 13 | "id": "ymc@dzhard.github.org", 14 | "strict_min_version": "57.0" 15 | } 16 | }, 17 | "permissions": [ 18 | "*://music.yandex.ru/*", 19 | "*://music.yandex.by/*", 20 | "*://music.yandex.com/*", 21 | "*://music.yandex.kz/*", 22 | "*://music.yandex.ua/*", 23 | "activeTab", 24 | "tabs", 25 | "notifications", 26 | "storage", 27 | "contextMenus" 28 | ], 29 | "options_ui": { 30 | "page": "options/options.html", 31 | "browser_style": true 32 | }, 33 | "browser_action": { 34 | "default_icon": "icons/icon-48.png", 35 | "default_title": "__MSG_extensionName__", 36 | "default_popup": "popup/controls.html" 37 | }, 38 | "content_scripts": [ 39 | { 40 | "matches": [ 41 | "*://music.yandex.ru/*", 42 | "*://music.yandex.by/*", 43 | "*://music.yandex.com/*", 44 | "*://music.yandex.kz/*", 45 | "*://music.yandex.ua/*" 46 | ], 47 | "js": [ 48 | "injector.js" 49 | ] 50 | } 51 | ], 52 | "background": { 53 | "scripts": [ 54 | "hotKeyHandler.js", 55 | "contextMenu.js" 56 | ] 57 | }, 58 | "browser_specific_settings": { 59 | "gecko": { 60 | "id": "yamusic@dzhard.github.com", 61 | "strict_min_version": "57.0" 62 | } 63 | }, 64 | "commands": { 65 | "play-pause": { 66 | "suggested_key": { 67 | "default": "Alt+P" 68 | }, 69 | "description": "Play/Pause" 70 | }, 71 | "next": { 72 | "suggested_key": { 73 | "default": "Alt+W" 74 | }, 75 | "description": "Next" 76 | }, 77 | "prev": { 78 | "suggested_key": { 79 | "default": "Alt+Q" 80 | }, 81 | "description": "Previous" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /popup/controls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | Image 11 | 12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 |
00:00
20 |
00:00
21 |
22 |
23 |
24 |
25 |
26 |
27 | Title 28 |
29 |

Band - Album 30 |

31 |
32 |
33 |
34 |
35 | fast_rewind 36 |
37 |
38 | play_arrow 39 |
40 |
41 | fast_forward 42 |
43 |
44 |
45 |
46 | favorite 47 |
48 |
49 | shuffle 50 |
51 |
52 | repeat_one 53 |
54 |
55 | block 56 |
57 |
58 | sms 59 |
60 |
61 | volume_up 62 | 63 | 65 | 66 |
67 |
68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Яндекс.Музыка Управление", 4 | "description": "Yandex Music Controls and hotkeys." 5 | }, 6 | "extensionDescription": { 7 | "message": "Управление для Яндекс Музыка", 8 | "description": "Description of the extension." 9 | }, 10 | "notificationTitle": { 11 | "message": "Яндекс Музыка", 12 | "description": "Notification title." 13 | }, 14 | "notificationContent": { 15 | "message": "Сейчас играет: $ARTIST$ - $TRACK$.", 16 | "description": "Tells the user which track is playing.", 17 | "placeholders": { 18 | "artist": { 19 | "content": "$1", 20 | "example": "неизвестный исполнитель" 21 | }, 22 | "track": { 23 | "content": "$2", 24 | "example": "неизвестный трек" 25 | } 26 | } 27 | }, 28 | "cmPrev": { 29 | "message": "Предыдущий ($1)", 30 | "description": "Previous track" 31 | }, 32 | "cmNext": { 33 | "message": "Следующий ($1)", 34 | "description": "Next track" 35 | }, 36 | "cmPlay": { 37 | "message": "Играть ($1)", 38 | "description": "Play track" 39 | }, 40 | "cmPause": { 41 | "message": "Пауза ($1)", 42 | "description": "Pause track" 43 | }, 44 | "cmOptions": { 45 | "message": "Опции", 46 | "description": "Options" 47 | }, 48 | "prev": { 49 | "message": "Предыдущий", 50 | "description": "Previous track" 51 | }, 52 | "next": { 53 | "message": "Следующий", 54 | "description": "Next track" 55 | }, 56 | "play": { 57 | "message": "Играть", 58 | "description": "Play track" 59 | }, 60 | "pause": { 61 | "message": "Пауза", 62 | "description": "Pause track" 63 | }, 64 | "like": { 65 | "message": "Нравится", 66 | "description": "Like" 67 | }, 68 | "shuffle": { 69 | "message": "Перемешать", 70 | "description": "Shuffle" 71 | }, 72 | "repeat": { 73 | "message": "Повторять", 74 | "description": "Repeat" 75 | }, 76 | "repeat_one": { 77 | "message": "Повторять один", 78 | "description": "Repeat" 79 | }, 80 | "dislike": { 81 | "message": "Блокировать трек", 82 | "description": "Block track" 83 | }, 84 | "volume": { 85 | "message": "Громкость", 86 | "description": "Volume" 87 | }, 88 | "notifications": { 89 | "message": "Уведомления", 90 | "description": "Notifications" 91 | }, 92 | "notificationsTitle": { 93 | "message": "Уведомления", 94 | "description": "Notifications" 95 | }, 96 | "showNotificationsLabel": { 97 | "message": "Показывать уведомления", 98 | "description": "Show Notifications" 99 | }, 100 | "openYaMusicMessage": { 101 | "message": "Открыть Яндекс Музыка", 102 | "description": "Open Yandex.Music" 103 | } 104 | } -------------------------------------------------------------------------------- /contextMenu.js: -------------------------------------------------------------------------------- 1 | let menuPlayName; 2 | let menuPauseName; 3 | let menuPrevName; 4 | let menuNextName; 5 | let activeTab; 6 | 7 | browser.commands.getAll() 8 | .then(res => { 9 | res.forEach(sc => { 10 | switch (sc.name) { 11 | case'play-pause': 12 | menuPlayName = sc.shortcut; 13 | menuPauseName = sc.shortcut; 14 | break; 15 | case'prev': 16 | menuPrevName = sc.shortcut; 17 | break; 18 | case'next': 19 | menuNextName = sc.shortcut; 20 | break 21 | } 22 | }); 23 | }) 24 | .then(() => buildMenu()); 25 | 26 | function buildMenu() { 27 | browser.contextMenus.create({ 28 | icons: { 29 | "48": "/icons/play-arrow-48.png", 30 | "96": "/icons/play-arrow-96.png" 31 | }, 32 | id: "play-pause-menu", 33 | title: menuPlayName, 34 | contexts: ["browser_action"], 35 | onclick: () => { 36 | pause(activeTab); 37 | } 38 | }); 39 | browser.contextMenus.create({ 40 | icons: { 41 | "48": "/icons/next-48.png", 42 | "96": "/icons/next-96.png" 43 | }, 44 | id: "play-next-menu", 45 | title: browser.i18n.getMessage("cmNext", menuNextName), 46 | contexts: ["browser_action"], 47 | onclick: () => { 48 | next(activeTab) 49 | } 50 | }); 51 | browser.contextMenus.create({ 52 | icons: { 53 | "48": "/icons/prev-48.png", 54 | "96": "/icons/prev-96.png" 55 | }, 56 | id: "play-prev-menu", 57 | title: browser.i18n.getMessage("cmPrev", menuPrevName), 58 | contexts: ["browser_action"], 59 | onclick: () => { 60 | prev(activeTab) 61 | } 62 | }); 63 | browser.contextMenus.create({ 64 | id: "options-menu", 65 | title: browser.i18n.getMessage("cmOptions"), 66 | contexts: ["browser_action"], 67 | onclick: () => browser.runtime.openOptionsPage() 68 | }); 69 | 70 | browser.contextMenus.onShown.addListener(() => { 71 | requestAllTabs() 72 | .then(r => { 73 | activeTab = r; 74 | updateMenus(activeTab); 75 | }) 76 | }); 77 | } 78 | 79 | function updateMenus(r) { 80 | if (r !== undefined && r != null) { 81 | if (r.isPlaying) { 82 | browser.contextMenus.update("play-pause-menu", { 83 | title: browser.i18n.getMessage("cmPause", menuPauseName), 84 | icons: { 85 | "48": "/icons/pause-48.png", 86 | "96": "/icons/pause-96.png" 87 | } 88 | }); 89 | } else { 90 | browser.contextMenus.update("play-pause-menu", { 91 | title: browser.i18n.getMessage("cmPlay", menuPlayName), 92 | icons: { 93 | "48": "/icons/play-arrow-48.png", 94 | "96": "/icons/play-arrow-96.png" 95 | } 96 | }); 97 | } 98 | 99 | browser.contextMenus.refresh(); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Yandex Music Controls and hotkeys.", 4 | "description": "Yandex Music Controls and hotkeys." 5 | }, 6 | "extensionDescription": { 7 | "message": "Extension to control Yandex Music tabs.", 8 | "description": "Description of the extension." 9 | }, 10 | "notificationTitle": { 11 | "message": "Yandex Music", 12 | "description": "Notification title." 13 | }, 14 | "notificationContent": { 15 | "message": "Now playing: $ARTIST$ - $TRACK$.", 16 | "description": "Tells the user which track is playing.", 17 | "placeholders": { 18 | "artist" : { 19 | "content" : "$1", 20 | "example" : "unknown artist" 21 | }, 22 | "track" : { 23 | "content" : "$2", 24 | "example" : "unknown track" 25 | } 26 | } 27 | }, 28 | "cmPrev": { 29 | "message": "Previous ($1)", 30 | "description": "Previous track (context menu)" 31 | }, 32 | "cmNext": { 33 | "message": "Next ($1)", 34 | "description": "Next track (context menu)" 35 | }, 36 | "cmPlay": { 37 | "message": "Play ($1)", 38 | "description": "Play track (context menu)" 39 | }, 40 | "cmPause": { 41 | "message": "Pause ($1)", 42 | "description": "Pause track (context menu)" 43 | }, 44 | "cmOptions": { 45 | "message": "Options", 46 | "description": "Options (context menu)" 47 | }, 48 | "prev": { 49 | "message": "Previous", 50 | "description": "Previous track" 51 | }, 52 | "next": { 53 | "message": "Next", 54 | "description": "Next track" 55 | }, 56 | "play": { 57 | "message": "Play", 58 | "description": "Play track" 59 | }, 60 | "pause": { 61 | "message": "Pause", 62 | "description": "Pause track" 63 | }, 64 | "like": { 65 | "message": "Like", 66 | "description": "Like" 67 | }, 68 | "shuffle": { 69 | "message": "Shuffle", 70 | "description": "Shuffle" 71 | }, 72 | "repeat": { 73 | "message": "Repeat", 74 | "description": "Repeat" 75 | }, 76 | "repeat_one": { 77 | "message": "Repeat one", 78 | "description": "Repeat" 79 | }, 80 | "dislike": { 81 | "message": "Block", 82 | "description": "Block track" 83 | }, 84 | "volume": { 85 | "message": "Volume", 86 | "description": "Volume" 87 | }, 88 | "notifications": { 89 | "message": "Notifications", 90 | "description": "Notifications" 91 | }, 92 | "notificationsTitle": { 93 | "message": "Notifications", 94 | "description": "Notifications" 95 | }, 96 | "showNotificationsLabel": { 97 | "message": "Show Notifications", 98 | "description": "Show Notifications" 99 | }, 100 | "openYaMusicMessage": { 101 | "message": "Open Yandex Music", 102 | "description": "Open Yandex Music" 103 | } 104 | } -------------------------------------------------------------------------------- /hotKeyHandler.js: -------------------------------------------------------------------------------- 1 | function requestTabsByDomain(domain) { 2 | return browser.tabs.query({url: domain}) 3 | .then( 4 | (ymTabs) => { 5 | return ymTabs == null || ymTabs.length === 0 ? [] : ymTabs; 6 | }, (error) => []) 7 | } 8 | 9 | function findActiveAndCall(responses) { 10 | let activePlayer = null; 11 | for (const rs of responses) { 12 | let response = rs.response; 13 | let tab = rs.tab; 14 | if (response == null) { 15 | continue; 16 | } 17 | if (response.currentTrack !== undefined) { 18 | if (response.isPlaying) { 19 | activePlayer = { 20 | response: response, 21 | tab: tab, 22 | isPlaying: response.isPlaying 23 | }; 24 | break; 25 | } else { 26 | if ((response.progress !== undefined && response.progress.position > 0) 27 | || activePlayer === null) { 28 | activePlayer = { 29 | response: response, 30 | tab: tab, 31 | isPlaying: response.isPlaying 32 | }; 33 | } 34 | } 35 | } 36 | } 37 | return new Promise((resolve, reject) => { 38 | resolve(activePlayer); 39 | }) 40 | } 41 | 42 | function requestAllTabs() { 43 | return Promise.all([ 44 | requestTabsByDomain("https://music.yandex.ru/*"), 45 | requestTabsByDomain("https://music.yandex.by/*"), 46 | requestTabsByDomain("https://music.yandex.com/*"), 47 | requestTabsByDomain("https://music.yandex.kz/*"), 48 | requestTabsByDomain("https://music.yandex.ua/*") 49 | ]) 50 | .then(responses => responses.flat(1)) 51 | .then(resp => { 52 | return loadAllTabs(resp) 53 | }); 54 | } 55 | 56 | function pause(activeTab) { 57 | sendAction(activeTab, "pause") 58 | } 59 | 60 | function next(activeTab) { 61 | sendAction(activeTab, "next") 62 | } 63 | 64 | function prev(activeTab) { 65 | sendAction(activeTab, "prev") 66 | } 67 | 68 | function sendAction(activeTab, action) { 69 | if (activeTab !== undefined && activeTab !=null) { 70 | browser.tabs.sendMessage(activeTab.tab.id, {action: action}) 71 | } else { 72 | requestAllTabs() 73 | .then(r => { 74 | browser.tabs.sendMessage(r.tab.id, {action: action}) 75 | }) 76 | } 77 | } 78 | 79 | function loadAllTabs(tabs) { 80 | if (tabs.length === 0) { 81 | return Promise.reject('no tabs found') 82 | } 83 | if (tabs.length === 1) { 84 | return browser.tabs.sendMessage(tabs[0].id, {action: "state"}) 85 | .then(rs => { 86 | return { 87 | response: rs, 88 | tab: tabs[0] 89 | } 90 | }) 91 | .catch(error => { 92 | return { 93 | response: null, 94 | tab: tabs[0], 95 | error: error 96 | } 97 | }); 98 | } 99 | 100 | let promises = []; 101 | for (const tab of tabs) { 102 | let promise = browser.tabs.sendMessage(tab.id, {action: "state"}) 103 | .then(rs => { 104 | return { 105 | response: rs, 106 | tab: tab 107 | } 108 | }, error => { 109 | return { 110 | response: null, 111 | tab: tab, 112 | error: error 113 | } 114 | }); 115 | 116 | promises.push(promise); 117 | } 118 | 119 | return Promise.all(promises) 120 | .then(rs => findActiveAndCall(rs)); 121 | 122 | } 123 | 124 | function handleHotKey(cmd) { 125 | switch (cmd) { 126 | case 'play-pause': 127 | pause(); 128 | break; 129 | case 'next': 130 | next(); 131 | break; 132 | case 'prev': 133 | prev(); 134 | break; 135 | } 136 | } 137 | 138 | browser.commands.onCommand.addListener(handleHotKey); -------------------------------------------------------------------------------- /injector.js: -------------------------------------------------------------------------------- 1 | //inject script into a page to be able to work with externalAPI 2 | var el = document.createElement('script'); 3 | el.setAttribute('type', 'text/javascript'); 4 | el.src = browser.extension.getURL('handler.js'); 5 | document.body.appendChild(el); 6 | 7 | if (!browser.runtime.onMessage.hasListener(onRecievedMessage)) { 8 | browser.runtime.onMessage.addListener(onRecievedMessage); 9 | } 10 | 11 | window.addEventListener("message", receiveMessage, false); 12 | 13 | /** 14 | * handle messages from injected script 15 | * @param event 16 | */ 17 | function receiveMessage(event) { 18 | if (event.data) { 19 | if (event.data.type === "track") { 20 | browser.runtime.sendMessage("yamusic@dzhard.github.com", event.data); 21 | browser.storage.local.get({"showNotifications": false}).then( 22 | storage => { 23 | if (storage.showNotifications) { 24 | showNotification(); 25 | } 26 | } 27 | ) 28 | } else if (event.data.type === "controls") { 29 | browser.runtime.sendMessage("yamusic@dzhard.github.com", event.data); 30 | } else if (event.data.type === "progress") { 31 | browser.runtime.sendMessage("yamusic@dzhard.github.com", event.data); 32 | } 33 | } 34 | } 35 | 36 | function showNotification() { 37 | if (Notification.permission === "granted") { 38 | trackNotification(); 39 | } else if (Notification.permission !== 'denied') { 40 | Notification.requestPermission(function (permission) { 41 | if (permission === "granted") { 42 | trackNotification(); 43 | } 44 | }); 45 | } 46 | } 47 | 48 | function trackNotification() { 49 | let track = window.wrappedJSObject.getTrack().currentTrack; 50 | let artists = []; 51 | for (let i = 0; i < track.artists.length; i++) { 52 | artists.push(track.artists[i].title); 53 | } 54 | 55 | let body = browser.i18n.getMessage("notificationContent", [artists.join(", "), track.title]); 56 | let title = browser.i18n.getMessage("notificationTitle"); 57 | 58 | let notification = new Notification(title, {"body": body}); 59 | 60 | setTimeout(notification.close.bind(notification), 2500); 61 | } 62 | 63 | /** 64 | * Handle messages from popup. 65 | * @param message 66 | * @param sender 67 | * @param sendResponse 68 | */ 69 | function onRecievedMessage(message, sender, sendResponse) { 70 | let externalAPI = window.wrappedJSObject.externalAPI; 71 | console.log('requested event:' + message.action); 72 | switch (message.action) { 73 | case "next": 74 | externalAPI.next(); 75 | break; 76 | case "prev": 77 | externalAPI.prev(); 78 | break; 79 | case "pause": 80 | externalAPI.togglePause(); 81 | sendResponse(window.wrappedJSObject.getTrack()); 82 | break; 83 | case "play": 84 | externalAPI.play(); 85 | sendResponse(window.wrappedJSObject.getTrack()); 86 | break; 87 | case "state": 88 | sendResponse(window.wrappedJSObject.getTrack()); 89 | break; 90 | case "like": 91 | externalAPI.toggleLike(); 92 | sendResponse(window.wrappedJSObject.isLiked()); 93 | break; 94 | case "dislike": 95 | externalAPI.toggleDislike(); 96 | sendResponse(window.wrappedJSObject.isDisliked()); 97 | break; 98 | case "shuffle": 99 | externalAPI.toggleShuffle(); 100 | sendResponse(window.wrappedJSObject.getShuffle()); 101 | break; 102 | case "repeat": 103 | externalAPI.toggleRepeat(); 104 | sendResponse(window.wrappedJSObject.getRepeat()); 105 | break; 106 | case "volume": 107 | externalAPI.setVolume(message.volume); 108 | break; 109 | case "seek": 110 | externalAPI.setPosition(message.pos) 111 | break; 112 | default: 113 | console.log('unknown action requested'); 114 | break 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /popup/controls.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | background: #fafafa; 4 | } 5 | 6 | body { 7 | font-family: "Varela Round", sans-serif; 8 | width: 300px; 9 | overflow-x: hidden; 10 | } 11 | 12 | .welcome { 13 | width: 100%; 14 | margin: 10px auto; 15 | flex-direction: row; 16 | align-items: center; 17 | font-size: 18px; 18 | display: none; 19 | } 20 | 21 | .player { 22 | width: 100%; 23 | margin: 10px auto; 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | text-align: center; 28 | border-bottom: 1px solid #ccc; 29 | } 30 | 31 | .player-sm { 32 | width: 100%; 33 | margin-top: 2px; 34 | border: 0; 35 | border-bottom: 1px solid #ccc; 36 | display: flex; 37 | flex-direction: row; 38 | align-items: center; 39 | text-align: center; 40 | justify-content: space-between; 41 | } 42 | 43 | .player-sm .player-buttons-sm { 44 | right: 3px; 45 | position: relative; 46 | } 47 | 48 | .player-sm .player-buttons-sm .playerbtn i { 49 | font-size: 30px; 50 | cursor: pointer; 51 | transition: 0.15s ease-in-out; 52 | } 53 | 54 | .player-sm .player-buttons-sm .playerbtn i:hover { 55 | color: #df405a; 56 | } 57 | 58 | .player-sm .song-info { 59 | width: 80%; 60 | } 61 | 62 | .player-sm .song-info .song-band-wrapper .song-band { 63 | margin-bottom: 5px; 64 | margin-top: 5px; 65 | margin-right: 5px; 66 | text-align: start; 67 | font-size: 14px; 68 | } 69 | 70 | .player-sm .song-info .song-title { 71 | margin-bottom: 5px; 72 | margin-top: 5px; 73 | flex-direction: column; 74 | font-size: 14px; 75 | text-align: start; 76 | } 77 | 78 | .player .song-info .song-title { 79 | margin: 5px; 80 | text-align: start; 81 | font-size: 16px; 82 | cursor: pointer; 83 | } 84 | 85 | .player .song-info .song-band { 86 | margin: 5px; 87 | text-align: start; 88 | font-size: 14px; 89 | cursor: pointer; 90 | } 91 | 92 | .player .album-cover img { 93 | align-content: center; 94 | display: block; 95 | max-width: 100%; 96 | } 97 | 98 | .player .song-timer { 99 | width: 95%; 100 | display: flex; 101 | flex-direction: row; 102 | justify-content: space-between; 103 | font-size: 12px; 104 | } 105 | 106 | .player .song-progress-bar { 107 | width: 100%; 108 | background: #ddd; 109 | margin: 5px 0; 110 | } 111 | 112 | .player .song-progress-bar .inner-bar { 113 | height: 4px; 114 | background: #df405a; 115 | } 116 | 117 | .player .song-progress-bar .inner-loaded-bar { 118 | height: 2px; 119 | background: #cbcbcb; 120 | } 121 | 122 | .player .player-buttons { 123 | width: 100%; 124 | display: flex; 125 | justify-content: space-around; 126 | margin: 10px 0; 127 | } 128 | 129 | .player .player-buttons .playerbtn i { 130 | font-size: 25px; 131 | cursor: pointer; 132 | transition: 0.15s ease-in-out; 133 | } 134 | 135 | .player .player-buttons .playerbtn i:hover { 136 | color: #df405a; 137 | } 138 | 139 | .btn-toggled { 140 | color: #df405a; 141 | } 142 | 143 | .player .player-buttons .playerbtn-sm i { 144 | font-size: 15px; 145 | cursor: pointer; 146 | transition: 0.15s ease-in-out; 147 | } 148 | 149 | .player .player-buttons .playerbtn-sm i:hover { 150 | color: #df405a; 151 | } 152 | 153 | .ambilight { 154 | width: 300px; 155 | height:300px; 156 | padding: 30px; 157 | position: relative; 158 | } 159 | 160 | .ambilight .image { 161 | width: 100%; 162 | position: relative; 163 | z-index: 100; 164 | } 165 | 166 | .ambilight .light { 167 | opacity: 0.75; 168 | -webkit-filter: blur(30px); 169 | filter: blur(30px); 170 | width: 100%; 171 | height: 100%; 172 | position: absolute; 173 | top: 0; 174 | left: 0; 175 | z-index: 0; 176 | } 177 | 178 | .volume_selector { 179 | background: rgba(45, 45, 44, 0.1); 180 | padding-bottom: 5px; 181 | padding-top: 5px; 182 | border-radius: 5px; 183 | } 184 | 185 | .volume_popup { 186 | background: transparent; 187 | position: relative; 188 | display: inline-block; 189 | height: 150px; 190 | margin-top: -170px; 191 | margin-left: -17px; 192 | } 193 | 194 | .playerbtn-sm .volume_popup { 195 | visibility: hidden; 196 | position: absolute; 197 | z-index: 1; 198 | } 199 | 200 | .playerbtn-sm:hover .volume_popup { 201 | visibility: visible; 202 | } 203 | 204 | input[type=range] { 205 | -webkit-appearance: none; 206 | outline: none; 207 | } 208 | 209 | input[type=range]:focus { 210 | outline: none; 211 | } 212 | 213 | input[type=range]::-moz-focus-outer { 214 | border: 0; 215 | } 216 | 217 | input[type=range]::-moz-range-track { 218 | height: 100%; 219 | width: 3px; 220 | cursor: pointer; 221 | animate: 0.2s; 222 | box-shadow: 0px 0px 0px #000000; 223 | background: #df405a; 224 | } 225 | 226 | input[type=range]::-moz-range-thumb { 227 | height: 10px; 228 | width: 10px; 229 | border-radius: 25px; 230 | background: #df405a; 231 | cursor: pointer; 232 | } 233 | 234 | .material-icons.md-dark.md-inactive { 235 | color: rgba(0, 0, 0, 0.26); 236 | } -------------------------------------------------------------------------------- /popup/controls.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", handleDomLoaded); 2 | let activePlayer; 3 | let showNotifications; 4 | let nextTrackEnabled, prevTrackEnabled; 5 | 6 | browser.runtime.onMessage.addListener(handleMessage); 7 | 8 | function handleMessage(message, sender) { 9 | if (sender.id === "yamusic@dzhard.github.com") { 10 | if (message.type === "track") { 11 | fillPlayerData(message.msg) 12 | } 13 | if (message.type === "controls") { 14 | updateControlsState(message.msg) 15 | } 16 | if (message.type === "progress") { 17 | activePlayer.response.progress = message.msg; 18 | updateProgress(message.msg) 19 | } 20 | } 21 | } 22 | 23 | function requestTabsByDomain(domain) { 24 | return browser.tabs.query({url: domain}) 25 | .then( 26 | (ymTabs) => { 27 | return ymTabs == null || ymTabs.length === 0 ? [] : ymTabs; 28 | }, () => []) 29 | .catch(() => { 30 | return []; 31 | }) 32 | } 33 | 34 | function requestAllTabs() { 35 | return Promise.all([ 36 | requestTabsByDomain("https://music.yandex.ru/*"), 37 | requestTabsByDomain("https://music.yandex.by/*"), 38 | requestTabsByDomain("https://music.yandex.com/*"), 39 | requestTabsByDomain("https://music.yandex.kz/*"), 40 | requestTabsByDomain("https://music.yandex.ua/*") 41 | ]) 42 | .then(responses => responses.flat(1)); 43 | } 44 | 45 | function handleDomLoaded(e) { 46 | document.getElementById('player').style.display = 'none'; 47 | requestAllTabs() 48 | .then(resp => loadedYandexTabs(resp)) 49 | } 50 | 51 | function loadedYandexTabs(tabs) { 52 | if (tabs.length === 1) { 53 | let tab = tabs[0]; 54 | browser.tabs.sendMessage(tab.id, {action: "state"}) 55 | .then(response => { 56 | createBigPlayer(response, tab) 57 | }).catch(e => handleUnloadedTab(e, tab)); 58 | return; 59 | } 60 | 61 | let promises = []; 62 | if (tabs.length > 0) { 63 | for (const tab of tabs) { 64 | let promise = browser.tabs.sendMessage(tab.id, {action: "state"}) 65 | .then(rs => { 66 | return { 67 | response: rs, 68 | tab: tab 69 | } 70 | }, error => { 71 | return { 72 | response: null, 73 | tab: tab, 74 | error: error 75 | } 76 | }); 77 | 78 | promises.push(promise); 79 | } 80 | 81 | Promise.all(promises) 82 | .then(rs => fillPlayers(rs)); 83 | } else { 84 | let openYaMusicPanel = document.getElementById("welcome"); 85 | let openYaMusicMessage = document.getElementById("openYaMessage"); 86 | openYaMusicMessage.innerText = browser.i18n.getMessage("openYaMusicMessage"); 87 | openYaMusicPanel.style.display = "flex"; 88 | document.getElementById("openYaMessage").onclick = () => { 89 | browser.tabs.create({"url": "https://music.yandex.ru/"}); 90 | window.close(); 91 | } 92 | } 93 | 94 | function fillPlayers(responses) { 95 | for (const rs of responses) { 96 | let response = rs.response; 97 | let tab = rs.tab; 98 | if (response == null) { 99 | handleUnloadedTab(rs.error, tab); 100 | continue; 101 | } 102 | 103 | if (response.currentTrack !== undefined) { 104 | if (response.isPlaying) { 105 | if (activePlayer !== undefined) {//recreate last tab as a small player 106 | createSmPlayer(activePlayer.response, activePlayer.tab) 107 | } 108 | createBigPlayer(response, tab) 109 | } else { 110 | if (response.progress !== undefined && response.progress.position > 0) {// checking if it's a paused 111 | if (activePlayer !== undefined) { 112 | if (activePlayer.isPlaying) { 113 | createSmPlayer(response, tab); 114 | } else { 115 | createSmPlayer(activePlayer.response, activePlayer.tab);//recreate last tab as a small player 116 | createBigPlayer(response, tab) 117 | } 118 | } else { 119 | createBigPlayer(response, tab); 120 | } 121 | 122 | } else { 123 | createSmPlayer(response, tab); 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | function handleUnloadedTab(e, tab) { 132 | if (e.message === "Could not establish connection. Receiving end does not exist.") { 133 | let response = {}; 134 | response.currentTrack = {}; 135 | response.currentTrack.artists = []; 136 | response.currentTrack.artists[0] = {}; 137 | response.currentTrack.album = {}; 138 | response.currentTrack.artists[0].title = " "; 139 | response.currentTrack.title = " Unknown "; 140 | response.currentTrack.album.title = " "; 141 | createSmPlayer(response, tab); 142 | } 143 | } 144 | 145 | function onError(e) { 146 | console.error("fail", e); 147 | } 148 | 149 | function createBigPlayer(response, tab) { 150 | activePlayer = { 151 | response: response, 152 | tab: tab, 153 | isPlaying: response.isPlaying 154 | }; 155 | document.getElementById('player').style.display = null; 156 | 157 | fillPlayerData(response, tab.id); 158 | 159 | let playI = document.getElementById('play-pause-btn'); 160 | playI.parentElement.id = 'playI' + tab.id; 161 | togglePlayStatusIcon(response.isPlaying, playI); 162 | 163 | playI.onclick = () => { 164 | browser.tabs.sendMessage(tab.id, {action: "pause"}) 165 | .then(rs => { 166 | updatePlayButtonsState(playI, rs.isPlaying) 167 | }).catch(onError); 168 | }; 169 | 170 | let bwdBtn = document.getElementById('bwd-btn'); 171 | let fwdBtn = document.getElementById('fwd-btn'); 172 | bwdBtn.title = browser.i18n.getMessage("prev"); 173 | fwdBtn.title = browser.i18n.getMessage("next"); 174 | 175 | bwdBtn.onclick = () => { 176 | if (prevTrackEnabled) { 177 | browser.tabs.sendMessage(tab.id, {action: "prev"}); 178 | } 179 | }; 180 | fwdBtn.onclick = () => { 181 | if (nextTrackEnabled) { 182 | browser.tabs.sendMessage(tab.id, {action: "next"}); 183 | } 184 | }; 185 | 186 | let likeBtn = document.getElementById('song-like'); 187 | likeBtn.title = browser.i18n.getMessage("like"); 188 | 189 | likeBtn.onclick = () => { 190 | browser.tabs.sendMessage(tab.id, {action: "like"}) 191 | .then(rs => { 192 | toggleLikeStatusIcon(rs, likeBtn) 193 | }).catch(onError); 194 | }; 195 | toggleLikeStatusIcon(response.currentTrack.liked, likeBtn); 196 | 197 | let shuffleBtn = document.getElementById('song-shuffle'); 198 | shuffleBtn.title = browser.i18n.getMessage("shuffle"); 199 | shuffleBtn.onclick = () => { 200 | browser.tabs.sendMessage(tab.id, {action: "shuffle"}) 201 | .then(rs => { 202 | toggleShuffleStatusIcon(rs, shuffleBtn) 203 | }).catch(onError); 204 | }; 205 | toggleShuffleStatusIcon(response.shuffle, shuffleBtn); 206 | 207 | let repeatBtn = document.getElementById('song-repeat'); 208 | repeatBtn.onclick = () => { 209 | browser.tabs.sendMessage(tab.id, {action: "repeat"}) 210 | .then(rs => { 211 | toggleRepeatStatusIcon(rs, repeatBtn) 212 | }).catch(onError); 213 | }; 214 | toggleRepeatStatusIcon(response.repeat, repeatBtn); 215 | 216 | let dislikeBtn = document.getElementById('song-dislike'); 217 | dislikeBtn.title = browser.i18n.getMessage("dislike"); 218 | dislikeBtn.onclick = () => { 219 | browser.tabs.sendMessage(tab.id, {action: "dislike"}) 220 | .then(rs => { 221 | toggleDislikeStatusIcon(rs, dislikeBtn) 222 | }).catch(onError); 223 | }; 224 | 225 | let volumeSelector = document.getElementById('volume_selector'); 226 | volumeSelector.value = response.volume; 227 | volumeSelector.oninput = () => { 228 | browser.tabs.sendMessage(tab.id, {action: "volume", volume: volumeSelector.value}) 229 | .then(rs => { 230 | toggleDislikeStatusIcon(rs, dislikeBtn) 231 | }).catch(onError); 232 | }; 233 | 234 | let volumeBtn = document.getElementById('volume'); 235 | volumeBtn.title = browser.i18n.getMessage("volume"); 236 | volumeBtn.onwheel = (event) => { 237 | onWheelVolume(volumeSelector, tab, event); 238 | }; 239 | volumeSelector.onwheel = (event) => { 240 | onWheelVolume(volumeSelector, tab, event); 241 | }; 242 | 243 | let notifBtn = document.getElementById('song-notifications'); 244 | notifBtn.title = browser.i18n.getMessage("notificationsTitle"); 245 | browser.storage.local.get({"showNotifications": false}).then( 246 | storage => { 247 | showNotifications = storage.showNotifications; 248 | 249 | toggleNotificationsIcon(showNotifications, notifBtn); 250 | notifBtn.onclick = () => { 251 | showNotifications = !showNotifications; 252 | browser.storage.local.set({"showNotifications": showNotifications}) 253 | .then(() => { 254 | toggleNotificationsIcon(showNotifications, notifBtn); 255 | }).catch(onError); 256 | }; 257 | } 258 | ) 259 | 260 | } 261 | 262 | function onWheelVolume(volumeSelector, tab, event) { 263 | let vol = parseFloat(volumeSelector.value); 264 | event.deltaY < 0 ? vol = vol + 0.05 : vol = vol - 0.05; 265 | if (vol > 1) { 266 | vol = 1; 267 | } 268 | if (vol < 0) { 269 | vol = 0; 270 | } 271 | volumeSelector.value = vol; 272 | browser.tabs.sendMessage(tab.id, {action: "volume", volume: vol}) 273 | } 274 | 275 | function updatePlayButtonsState(pressedI, isPlaying) { 276 | let elementsByClassName = document.getElementsByClassName("playToggle"); 277 | for (let i = 0; i < elementsByClassName.length; i++) { 278 | let item = elementsByClassName.item(i); 279 | togglePlayStatusIcon(isPlaying && pressedI.parentElement.id === item.id, item.firstElementChild) 280 | } 281 | } 282 | 283 | function updateProgress(progress) { 284 | let songProgress = document.getElementById('songprogress'); 285 | let songLoadedProgress = document.getElementById('song-load-progress'); 286 | let currentTime = document.getElementById('currenttime'); 287 | if (progress.position !== 0 && progress.duration !== 0) { 288 | let currentPos = progress.position / progress.duration * 100; 289 | let currentLoadPos = progress.loaded / progress.duration * 100; 290 | songProgress.style.width = Math.round(currentPos) + "%"; 291 | songLoadedProgress.style.width = Math.round(currentLoadPos) + "%"; 292 | currentTime.textContent = formatTime(progress.position); 293 | } else { 294 | songProgress.style.width = "0%"; 295 | songLoadedProgress.style.width = "0%"; 296 | currentTime.textContent = "00:00"; 297 | } 298 | } 299 | 300 | function fillPlayerData(response, tabId) { 301 | document.getElementById('albumcover').setAttribute('src', 302 | 'http://' + response.currentTrack.cover.replace('%%', '200x200')); 303 | document.getElementById('albumcover-smoke').setAttribute('src', 304 | 'http://' + response.currentTrack.cover.replace('%%', '200x200')); 305 | let totalTime = document.getElementById('totaltime'); 306 | let songTitle = document.getElementById('songtitle'); 307 | let albumTitle = document.getElementById('albumtitle'); 308 | let bandTitle = document.getElementById('bandtitle'); 309 | totalTime.textContent = formatTime(response.currentTrack.duration); 310 | albumTitle.textContent = response.currentTrack.album.title; 311 | songTitle.textContent = response.currentTrack.title; 312 | bandTitle.childNodes[0].textContent = response.currentTrack.artists.length > 0 313 | ? response.currentTrack.artists[0].title + ' - ' : ""; 314 | 315 | if (tabId !== undefined) { 316 | //popup opened, add events to navigate to the tab 317 | let navigateToTab = () => { 318 | browser.tabs.update(tabId, {active: true}); 319 | close() 320 | }; 321 | songTitle.onclick = navigateToTab; 322 | albumTitle.onclick = navigateToTab; 323 | bandTitle.onclick = navigateToTab; 324 | } 325 | updateControlsState(response.controlState); 326 | updateProgress(response.progress) 327 | document.getElementById("songPb").onclick = seekProgress; 328 | } 329 | 330 | function seekProgress(e) { 331 | let songProgress = document.getElementById("songPb"); 332 | let pbwidth = parseInt(window.getComputedStyle(songProgress).width); 333 | let perc = e.offsetX / pbwidth; 334 | let curtime = Math.round(activePlayer.response.progress.duration * perc); 335 | browser.tabs.sendMessage(activePlayer.tab.id, {action: "seek", pos: curtime}); 336 | document.getElementById('songprogress').style.width = perc * 100 + "%"; 337 | } 338 | 339 | function updateControlsState(controls) { 340 | let bwdBtn = document.getElementById('bwd-btn'); 341 | let fwdBtn = document.getElementById('fwd-btn'); 342 | prevTrackEnabled = controls.prev === true; 343 | nextTrackEnabled = controls.next === true; 344 | bwdBtn.className = prevTrackEnabled ? "material-icons" : "material-icons md-dark md-inactive"; 345 | fwdBtn.className = nextTrackEnabled ? "material-icons" : "material-icons md-dark md-inactive"; 346 | } 347 | 348 | function createSmPlayer(response, tab) { 349 | let player = document.createElement("div"); 350 | player.className = "player-sm"; 351 | let songInfo = document.createElement("div"); 352 | 353 | songInfo.className = "song-info"; 354 | let title = document.createElement("h4"); 355 | 356 | let currentTrack = response.currentTrack; 357 | title.className = "song-title"; 358 | title.id = "songtitle" + tab.id; 359 | title.textContent = currentTrack.title; 360 | let bandWrapper = document.createElement("div"); 361 | bandWrapper.className = "song-band-wrapper"; 362 | let songBand = document.createElement("p"); 363 | songBand.className = "song-band"; 364 | songBand.id = "bandtitle" + tab.id; 365 | 366 | let bandName = currentTrack.artists.length > 0 ? currentTrack.artists[0].title + " - " : ""; 367 | let band = document.createTextNode(bandName); 368 | let album = document.createElement("span"); 369 | album.id = "album" + tab.id; 370 | album.className = "album"; 371 | 372 | album.textContent = currentTrack.album.title; 373 | 374 | songBand.appendChild(band); 375 | songBand.appendChild(album); 376 | bandWrapper.appendChild(songBand); 377 | songInfo.appendChild(title); 378 | songInfo.appendChild(bandWrapper); 379 | 380 | let plButtons = document.createElement("div"); 381 | plButtons.className = "player-buttons-sm"; 382 | let playBtn = document.createElement("div"); 383 | playBtn.id = 'playBtn' + tab.id; 384 | playBtn.className = "playerbtn playToggle"; 385 | let playI = document.createElement("i"); 386 | playI.className = "material-icons"; 387 | playI.textContent = "play_arrow"; 388 | playBtn.appendChild(playI); 389 | plButtons.appendChild(playBtn); 390 | player.appendChild(songInfo); 391 | player.appendChild(plButtons); 392 | 393 | let navigateToTab = () => { 394 | browser.tabs.update(tab.id, {active: true}); 395 | close() 396 | }; 397 | title.onclick = navigateToTab; 398 | songBand.onclick = navigateToTab; 399 | 400 | playI.onclick = () => { 401 | browser.tabs.sendMessage(tab.id, {action: "pause"}) 402 | .then(rs => { 403 | togglePlayStatusIcon(rs.isPlaying, playI); 404 | updatePlayButtonsState(playI, rs.isPlaying); 405 | }).catch(onError); 406 | }; 407 | document.body.appendChild(player); 408 | 409 | } 410 | 411 | function togglePlayStatusIcon(isPlaying, button) { 412 | if (isPlaying) { 413 | button.textContent = "pause"; 414 | button.title = browser.i18n.getMessage("pause"); 415 | } else { 416 | button.textContent = "play_arrow"; 417 | button.title = browser.i18n.getMessage("play"); 418 | } 419 | } 420 | 421 | function toggleLikeStatusIcon(liked, button) { 422 | button.className = liked ? "material-icons btn-toggled" : "material-icons" 423 | } 424 | 425 | function toggleNotificationsIcon(show, button) { 426 | button.className = show ? "material-icons btn-toggled" : "material-icons" 427 | } 428 | 429 | function toggleShuffleStatusIcon(shuffle, button) { 430 | button.className = shuffle ? "material-icons btn-toggled" : "material-icons" 431 | } 432 | 433 | function toggleDislikeStatusIcon(dislike, button) { 434 | button.className = dislike ? "material-icons btn-toggled" : "material-icons" 435 | } 436 | 437 | function toggleRepeatStatusIcon(repeat, button) { 438 | if (repeat) { 439 | button.className = "material-icons btn-toggled"; 440 | if (repeat === 1) { 441 | button.title = browser.i18n.getMessage("repeat_one"); 442 | button.textContent = "repeat_one" 443 | } else { 444 | button.title = browser.i18n.getMessage("repeat"); 445 | button.textContent = "repeat" 446 | } 447 | } else { 448 | button.title = browser.i18n.getMessage("repeat"); 449 | button.className = "material-icons"; 450 | button.textContent = "repeat" 451 | } 452 | } 453 | 454 | function formatTime(seconds) { 455 | let timeString = new Date(0, 0, 0, 0, 0, Math.floor(seconds), 0) 456 | .toTimeString() 457 | .slice(0, 8); 458 | return timeString.startsWith("00:") ? timeString.slice(3) : timeString; 459 | } 460 | --------------------------------------------------------------------------------