├── 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 |
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 | 
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 |

11 |
12 |
13 |
14 |
15 |
![]()
16 |
![]()
17 |
18 |
19 |
00:00
20 |
00:00
21 |
22 |
26 |
27 |
Title
28 |
29 |
Band - Album
30 |
31 |
32 |
33 |
44 |
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 |
--------------------------------------------------------------------------------