diff --git a/.eslintrc.json b/.eslintrc.json
index 1e12092..ea2921c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,15 +1,23 @@
{
+ "env": { "browser": true, "node":true },
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended"],
"ignorePatterns": ["dist"],
- "root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
- "ecmaVersion": 6,
+ "ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
+ "root": true,
"rules": {
+ "@typescript-eslint/ban-ts-comment": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/naming-convention": "warn",
"@typescript-eslint/semi": ["error", "never"],
- "arrow-parens": ["warn", "as-needed"],
+ "arrow-parens": ["error", "as-needed"],
"curly": ["error", "multi-or-nest"],
"eqeqeq": "warn",
"no-throw-literal": "warn",
diff --git a/.gitignore b/.gitignore
index 9166479..e3a799b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
+.token
*.vsix
-dist/
-icon/res/
+dist
+media/res
node_modules
-out
package-lock.json
-src/ref
\ No newline at end of file
+src/ref
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 0000000..ce097dd
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,7 @@
+module.exports = {
+ arrowParens: 'avoid',
+ printWidth: 120,
+ semi: false,
+ singleQuote: true,
+ trailingComma: 'none'
+}
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index c0a2258..a3cdc1e 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,5 +1,11 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
- "recommendations": ["dbaeumer.vscode-eslint"]
+ "recommendations": [
+ "dbaeumer.vscode-eslint",
+ "amodio.tsl-problem-matcher",
+ "ue.alphabetical-sorter",
+ "shardulm94.trailing-spaces",
+ "streetsidesoftware.code-spell-checker"
+ ]
}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 2b99907..0fc4e0c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,22 +6,20 @@
"version": "0.2.0",
"configurations": [
{
- "name": "Extension:webpack",
+ "name": "Launch Extension webpack",
"type": "extensionHost",
"request": "launch",
- "runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
- "preLaunchTask": "npm: webpack"
+ "preLaunchTask": "${defaultBuildTask}"
},
{
- "name": "Extension:watch",
+ "name": "Launch Extension ts-watch",
"type": "extensionHost",
"request": "launch",
- "runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
- "outFiles": ["${workspaceFolder}/out/**/*.js"],
- "preLaunchTask": "npm: watch"
+ "outFiles": ["${workspaceFolder}/dist/**/*.js"],
+ "preLaunchTask": "ts-watch"
}
]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 75a21e6..a5dd06f 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,11 +1,42 @@
// Place your settings in this file to overwrite default and user settings.
{
+ "cSpell.words": [
+ "Adblocker",
+ "Btns",
+ "bufferutil",
+ "deepskyblue",
+ "Execa",
+ "lanly",
+ "letmeplaythemusic",
+ "lmptm",
+ "playpause",
+ "soundcloud",
+ "spoticon",
+ "targetchanged",
+ "targetcreated",
+ "targetdestroyed",
+ "testid",
+ "treeview",
+ "uddir",
+ "ytmusic"
+ ],
+ "cSpell.ignorePaths": [
+ ".git/objects",
+ ".vscode",
+ ".eslintrc.json",
+ "node_modules",
+ "package-lock.json"
+ ],
+ "editor.rulers": [120],
+ "explorer.sortOrder": "type",
"files.exclude": {
+ "dist": false, // set this to true to hide the "dist" folder with the compiled JS files
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
+ "dist": true, // set this to false to include "dist" folder in search results
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
-}
\ No newline at end of file
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 305b614..b7ca355 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -5,30 +5,43 @@
"tasks": [
{
"type": "npm",
- "script": "watch",
- "problemMatcher": "$tsc-watch",
+ "script": "wp-watch",
+ "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"],
"isBackground": true,
"presentation": {
- "reveal": "never"
+ "reveal": "never",
+ "group": "watcher"
},
"group": {
"kind": "build",
"isDefault": true
- }
+ },
+ "dependsOn": ["npm: task-clean-output"]
+ },
+ {
+ "label": "ts-watch",
+ "type": "npm",
+ "script": "ts-watch",
+ "problemMatcher": "$tsc-watch",
+ "isBackground": true,
+ "presentation": { "reveal": "never" },
+ "dependsOrder": "sequence",
+ "dependsOn": ["npm: task-clean-output", "npm: task-copy-static-assets"]
},
+ // ↓↓↓↓ For vscode command palette ↓↓↓↓
{
"type": "npm",
- "script": "compile",
- "dependsOn": ["npm: copy-static-assets"]
+ "script": "test-compile",
+ "dependsOn": ["npm: task-clean-output"]
},
{
"type": "npm",
- "script": "copy-static-assets",
+ "script": "task-copy-static-assets",
"isBackground": false
},
{
"type": "npm",
- "script": "clean-output",
+ "script": "task-clean-output",
"isBackground": false
}
]
diff --git a/.vscodeignore b/.vscodeignore
index 437770f..8a67662 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -1,14 +1,13 @@
.gitignore
-.vscode/
+.token
+.vscode
**/.eslintrc.json
**/*.map
**/*.ts
**/tsconfig.json
-clearOutFiles.ts
-copyStaticAssets.ts
-icon/res/
+media/res
+media/vscodeignore
node_modules
-out/
-src/
+src
vsc-extension-quickstart.md
-webpack.config.js
\ No newline at end of file
+webpack.config.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65bbbfd..1022b6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,49 @@ All notable changes to the "LetMePlayTheMusic" extension will be documented in t
Check [Keep a Changelog](http://keepachangelog.com) for recommendations on how to structure this file.
+### References
+- https://github.com/microsoft/vscode-generator-code/tree/main/generators/app/templates/ext-command-ts
+- https://github.com/microsoft/vscode-extension-samples
+- https://www.conventionalcommits.org
+
### TODO
-- Extention settings scope
-- I18n?
-- Site English version issue
+- Brave setting removal
+- I18n
+- Playwright vs Puppeteer option
+- Seek backward/forward setting option
+- Shelljs vs Execa
+- Support for other sites
+
+## [2.0.0] - December 2021
+- Add new feature - treeview
+- Rewrite/refactor most of the code - 30+ commits
+- New float button style - old vs new in Windows
+
+- Fix Spotify bug due to it's style class's changes
+- Fix site English version issue - observes another DOM element to update playback status
+- Merge play and pause commands into one toggle function
+- Move most of the minor inject action to inject script instead
+- Re-config project's configs like eslint, tsconfig, vscode setting, and webpack (.js -> .ts)
+- Rename directories: icon -> media, scripts -> inject
+- Playback icons is sync with the site's playback icons which was behaved contrarily before
+- Webpack 5.65.0 compiled successfully in 9980 ms
+- 15 files, 1.39MB, 1.63.0
+
+### Note
+
+#### This release mostly has more significant impact on the dev side rather than like a product update.
+
+#### One funny thing is that the picture in README.md accounts for a large part of this extension's size.
+
+#### [MediaSession](https://developer.mozilla.org/en-US/docs/Web/API/MediaSession)
+- Soundcloud does update `playbackState` but only call `set` for from press play
+- Spotify does call `set` to update metadata but playbackState always *none*
+- Youtube and YTmusic update `playbackState` consistent with proxy `set` event
+
+#### Puppeteer
+- Seem Puppeteer doesn't keep track pages/tabs' order
+- If click too quick, this shows up: `Error: Execution context is not available in detached frame "about:blank" (are you trying to evaluate?)`
+ Waiting and try/catch was fruitless
## [1.4.0] - October 2020
- Add key shortcuts [#7](https://github.com/lanly-dev/VSCode-LMPTM/issues/7)
@@ -24,7 +63,7 @@ Check [Keep a Changelog](http://keepachangelog.com) for recommendations on how t
- 11 files, 141.77KB, 1.42.0
## [1.2.0] - December 2019
-- Add browser execuatble file setting [#3](https://github.com/lanly-dev/VSCode-LMPTM/issues/3)
+- Add browser executable file setting [#3](https://github.com/lanly-dev/VSCode-LMPTM/issues/3)
- Add Incognito/Private mode setting
- Add [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md) setting [#4](https://github.com/lanly-dev/VSCode-LMPTM/issues/4)
- 11 files, 192KB, 1.41.0
@@ -36,4 +75,4 @@ Check [Keep a Changelog](http://keepachangelog.com) for recommendations on how t
## [1.0.0] - August 2019
- Initial release
-- 2117 files, 5.09MB, 1.35.0
\ No newline at end of file
+- 2117 files, 5.09MB, 1.35.0
diff --git a/README.md b/README.md
index 20f00bb..2f3d961 100644
--- a/README.md
+++ b/README.md
@@ -2,27 +2,27 @@
[![Version](https://vsmarketplacebadge.apphb.com/version-short/lanly-dev.letmeplaythemusic.svg)](https://marketplace.visualstudio.com/items?itemName=lanly-dev.letmeplaythemusic)
-Tired or get annoyed from switching windows in order to pause or skip a song?
-Doesn't want to or lazy to install the extra official players to the system?
-Want to stay focus on your programing instead of those distracting actions and feelings above?
+Tired or get annoyed from switching windows in order to pause or skip a song?\
+Doesn't want to or lazy to install the extra official players to the system?\
+Or perhaps your keyboard doesn't have the handy media hotkeys?\
+Wanted to stay focus on your programing instead of those shortcomings above?
-If yes, you are in luck!
+If yes, you are in luck!\
This extension launches a Chrome/Chromium browser to the 4 popular music sites and you can control the playback from within your favorite Visual Studio Code editor.
-Extra [use case](https://github.com/lanly-dev/VSCode-LMPTM/issues/8#issuecomment-661796089) - you could use this extension to follow programming tutorials on Youtube with the handy seek forward/backward key shortcuts.
+>Extra [use case](https://github.com/lanly-dev/VSCode-LMPTM/issues/8#issuecomment-661796089) - you could use this extension to follow programming tutorials on Youtube with the handy seek forward/backward key shortcuts.
-[How to use it?](https://github.com/lanly-dev/VSCode-LMPTM/issues/1)
+>[How to use it?](https://github.com/lanly-dev/VSCode-LMPTM/issues/1)
-## Features
+
+## Features
Supports SoundCloud, Spotify, Youtube and Youtube Music
## Requirements
-
Required Chromium-based browser
## Extension Settings
-
* `lmptm.browserPath`: Specify custom browser executable file path.
* `lmptm.ignoreDisableSync`: Ignore --disable-sync, this option is specifically for [Brave](https://brave.com) browser.
* `lmptm.incognitoMode`: Specify whether to launch browser in incognito/private mode.
@@ -31,14 +31,21 @@ Required Chromium-based browser
* `lmptm.userDataDirectory`: Specify [user data directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md), this will be ignored if **User Data** setting is unchecked.
## Known Issues
-- Work only with English version of the supported music sites
- Does not work with Opera browser
-- Won't be able to login Youtube and SoundCLoud(email method)
+- Won't be able to login Youtube and SoundCloud(email method)
## Release Notes
+### 2.0.0
+- Add treeview - so now the the app can switch between tabs
+ 1. The page has to be picked before being able to use in the treeview
+ 2. You can select the treeview items that have the play/pause icon
+ 3. The emoji ⛏️ means the tab is currently picked
+ 4. First click switches the picked icon to target tab, later clicks toggles playing/paused status of the target tab media's playback
+
+- Fix spotify bugs due to it's new UI changes
### 1.4.0
-- Add key shorcuts
+- Add key shortcuts
- Add seek 5s forward/backward for Youtube shortcuts
- Add support for [Youtube Music](https://music.youtube.com/)
- Add startPages settings
diff --git a/icon/lmptm.png b/icon/lmptm.png
deleted file mode 100644
index 6be3c3f..0000000
Binary files a/icon/lmptm.png and /dev/null differ
diff --git a/media/btn1.4.png b/media/btn1.4.png
new file mode 100644
index 0000000..e07eb46
Binary files /dev/null and b/media/btn1.4.png differ
diff --git a/media/btn2.0.png b/media/btn2.0.png
new file mode 100644
index 0000000..9a3a632
Binary files /dev/null and b/media/btn2.0.png differ
diff --git a/media/capture2.0.png b/media/capture2.0.png
new file mode 100644
index 0000000..a424827
Binary files /dev/null and b/media/capture2.0.png differ
diff --git a/media/icon.svg b/media/icon.svg
new file mode 100644
index 0000000..fd236ac
--- /dev/null
+++ b/media/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/media/lmptm.png b/media/lmptm.png
new file mode 100644
index 0000000..4589810
Binary files /dev/null and b/media/lmptm.png differ
diff --git a/media/lmptm.svg b/media/lmptm.svg
new file mode 100644
index 0000000..2bc194b
--- /dev/null
+++ b/media/lmptm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/media/vscodeignore/soundcloud.svg b/media/vscodeignore/soundcloud.svg
new file mode 100644
index 0000000..95a4ef6
--- /dev/null
+++ b/media/vscodeignore/soundcloud.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/media/vscodeignore/spotify.svg b/media/vscodeignore/spotify.svg
new file mode 100644
index 0000000..44de73e
--- /dev/null
+++ b/media/vscodeignore/spotify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/media/vscodeignore/youtube.svg b/media/vscodeignore/youtube.svg
new file mode 100644
index 0000000..cd75eaa
--- /dev/null
+++ b/media/vscodeignore/youtube.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 0df0128..896664c 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
"name": "letmeplaythemusic",
"displayName": "Let Me Play The Music",
- "description": "Control playback from the popular music sites",
+ "description": "Playback control buttons for the popular music sites",
"homepage": "https://github.com/lanly-dev/VSCode-LMPTM",
- "version": "1.4.0",
+ "version": "2.0.0",
"publisher": "lanly-dev",
"engines": {
- "vscode": "^1.50.0"
+ "vscode": "^1.63.0"
},
"extensionKind": [
"ui"
@@ -15,24 +15,40 @@
"Other"
],
"keywords": [
+ "Browser",
"Music",
"Playback",
- "Browser",
- "Souncloud",
+ "SoundCloud",
"Spotify",
- "Youtube",
- "Youtube Music"
+ "Youtube Music",
+ "Youtube"
],
- "icon": "icon/lmptm.png",
+ "icon": "media/lmptm.png",
"galleryBanner": {
"color": "#000f33",
"theme": "dark"
},
"activationEvents": [
- "*"
+ "onStartupFinished"
],
"main": "./dist/extension",
"contributes": {
+ "viewsWelcome": [
+ {
+ "view": "LMPTM",
+ "contents": "[Launch $(rocket)](command:lmptm.browserLaunch)"
+ }
+ ],
+ "views": {
+ "explorer": [
+ {
+ "id": "LMPTM",
+ "name": "LMPTM",
+ "icon": "media/icon.svg",
+ "contextualTitle": "LMPTM"
+ }
+ ]
+ },
"configuration": [
{
"title": "LMPTM",
@@ -40,7 +56,7 @@
"lmptm.browserPath": {
"type": "string",
"default": null,
- "markdownDescription": "Specify custom browser executable file path."
+ "description": "Specify custom browser executable file path."
},
"lmptm.ignoreDisableSync": {
"type": "boolean",
@@ -50,7 +66,7 @@
"lmptm.incognitoMode": {
"type": "boolean",
"default": true,
- "markdownDescription": "Specify whether to launch browser in incognito/private mode."
+ "description": "Specify whether to launch browser in incognito/private mode."
},
"lmptm.startPages": {
"type": "array",
@@ -65,7 +81,7 @@
"lmptm.userData": {
"type": "boolean",
"default": false,
- "markdownDescription": "Specify if the extension could store browser's user data, if enabled, user data directory setting is required."
+ "description": "Specify if the extension could store browser's user data, if enabled, user data directory setting is required."
},
"lmptm.userDataDirectory": {
"type": "string",
@@ -85,7 +101,7 @@
},
{
"key": "win+Alt+down",
- "command": "lmptm.toggle"
+ "command": "lmptm.playPause"
},
{
"key": "win+Alt+x",
@@ -98,36 +114,42 @@
]
},
"scripts": {
- "vscode:prepublish": "webpack --mode production",
+ "vscode:prepublish": "webpack --mode production --devtool hidden-source-map",
+ "vsce-package": "vsce package",
"test-compile": "tsc -p ./",
- "watch": "tsc -watch -p ./",
- "webpack": "webpack --mode development",
- "webpack-dev": "webpack --mode development --watch",
+ "test-webpack": "webpack",
+ "ts-watch": "tsc -watch -p ./",
+ "wp-watch": "webpack --watch",
"lint": "eslint .",
"lint-fix": "eslint . --fix",
- "copy-static-assets": "ts-node tasks.ts copy",
- "clean-output": "ts-node tasks.ts clean"
+ "task-copy-static-assets": "ts-node tasks.ts copy",
+ "task-clean-output": "ts-node tasks.ts clean",
+ "copy": "npm run task-copy-static-assets",
+ "clean": "npm run task-clean-output"
},
"devDependencies": {
- "@types/node": "^14.11.8",
- "@types/puppeteer-core": "^2.0.0",
- "@types/shelljs": "^0.8.8",
- "@types/vscode": "^1.50.0",
- "@typescript-eslint/eslint-plugin": "^4.4.0",
- "@typescript-eslint/parser": "^4.4.0",
- "copy-webpack-plugin": "^6.2.1",
- "eslint": "^7.11.0",
+ "@types/copy-webpack-plugin": "^8.0.1",
+ "@types/karma-chrome-launcher": "^3.1.1",
+ "@types/node": "^16.11.13",
+ "@types/puppeteer-core": "^5.4.0",
+ "@types/shelljs": "^0.8.9",
+ "@types/vscode": "^1.63.0",
+ "@typescript-eslint/eslint-plugin": "^5.7.0",
+ "@typescript-eslint/parser": "^5.7.0",
+ "copy-webpack-plugin": "^10.1.0",
+ "css-minimizer-webpack-plugin": "^3.2.0",
+ "eslint": "^8.4.1",
"shelljs": "^0.8.4",
- "terser": "^5.3.4",
- "ts-loader": "^8.0.4",
- "ts-node": "^9.0.0",
- "typescript": "^4.0.3",
- "webpack": "^4.44.2",
- "webpack-cli": "^3.3.12"
+ "ts-loader": "^9.2.6",
+ "ts-node": "^10.4.0",
+ "typescript": "^4.5.4",
+ "vsce": "^2.5.1",
+ "webpack": "^5.65.0",
+ "webpack-cli": "^4.9.1"
},
"dependencies": {
"karma-chrome-launcher": "^3.1.0",
- "puppeteer-core": "^5.3.1"
+ "puppeteer-core": "^13.0.0"
},
"repository": {
"type": "git",
diff --git a/src/browser.ts b/src/browser.ts
index bffa979..57d79e6 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -1,30 +1,25 @@
-import * as fs from 'fs'
import * as path from 'path'
import * as puppeteer from 'puppeteer-core'
import * as vscode from 'vscode'
import { Buttons } from './buttons'
+import { TreeviewProvider } from './treeview'
import { WhichChrome } from './whichChrome'
+import { Entry } from './interfaces'
+import { HTTPResponse } from 'puppeteer-core'
-const seekMsg = 'Seeking backward/forward function is only work for Youtube videos'
-
+const SEEK_MSG = 'Seeking backward/forward function is only work for Youtube videos. 💡'
+const STATE_MSG = 'Please select the tab/page that either in playing or paused. 💡'
export class Browser {
public static activeBrowser: Browser | undefined
public static cssPath: string
public static jsPath: string
- public static launched: boolean = false
- public static uiHtmlPath: string
- public static playButtonCss = {
- soundcloud: '.playControl',
- spotify: '.control-button--circled',
- youtube: '.ytp-play-button',
- ytmusic: '#play-pause-button'
- }
+ public static launched = false
private buttons: Buttons
private currentBrowser: puppeteer.Browser
private incognitoContext: puppeteer.BrowserContext
- private pages: puppeteer.Page[] | undefined
- private selectedMusicPageBrand: string | undefined
+ private pagesStatus: Entry[]
+ private selectedMusicBrand: string | undefined
private selectedPage: puppeteer.Page | undefined
public static launch(buttons: Buttons, context: vscode.ExtensionContext) {
@@ -43,26 +38,29 @@ export class Browser {
let cPath = vscode.workspace.getConfiguration().get('lmptm.browserPath')
if (!cPath) cPath = WhichChrome.getPaths().Chrome || WhichChrome.getPaths().Chromium
+
if (!cPath) {
- vscode.window.showInformationMessage('Missing Browser! 🤔')
+ vscode.window.showInformationMessage('No Chromium or Chrome browser found. 🤔')
return
}
- const links: any = vscode.workspace.getConfiguration().get('lmptm.startPages')
- if(links.length) {
+ const links: string[] | undefined = vscode.workspace.getConfiguration().get('lmptm.startPages')
+ if (links && links.length) {
let invalid = false
links.forEach((e: string) => {
try { new URL(e) } catch (err) {
invalid = true
return
- }})
- if (invalid){
- vscode.window.showErrorMessage('You may have an invalid url on startPages setting! 🤔')
+ }
+ })
+ if (invalid) {
+ vscode.window.showErrorMessage('You may have an invalid url on startPages setting. 🤔')
return
}
}
Browser.launched = true
+ // console.log('###########################################')
puppeteer.launch({
args,
defaultViewport: null,
@@ -71,17 +69,16 @@ export class Browser {
ignoreDefaultArgs: iArgs
}).then(async (browser: puppeteer.Browser) => {
buttons.setStatusButtonText('Running $(browser)')
- Browser.cssPath = path.join(context.extensionPath, 'dist', 'scripts', 'style.css')
- Browser.jsPath = path.join(context.extensionPath, 'dist', 'scripts', 'script.js')
- Browser.uiHtmlPath = fs.readFileSync(path.join(context.extensionPath, 'dist', 'scripts', 'ui.html'), 'utf8')
+ Browser.cssPath = path.join(context.extensionPath, 'dist', 'inject', 'style.css')
+ Browser.jsPath = path.join(context.extensionPath, 'dist', 'inject', 'script.js')
const defaultPages = await browser.pages()
defaultPages[0].close() // evaluateOnNewDocument won't on this page
- Browser.activeBrowser = new Browser(browser, buttons, await browser.createIncognitoBrowserContext())
- Browser.launched = false
-
- }, error => {
+ const b = new Browser(browser, buttons, await browser.createIncognitoBrowserContext())
+ Browser.activeBrowser = b
+ TreeviewProvider.refresh()
+ }, (error: { message: string }) => {
vscode.window.showErrorMessage(error.message)
- vscode.window.showInformationMessage('Missing Chrome? 🤔')
+ vscode.window.showInformationMessage('Browser launch failed. 😲')
Browser.launched = false
})
}
@@ -90,59 +87,40 @@ export class Browser {
constructor(browser: puppeteer.Browser, buttons: Buttons, incognitoContext: puppeteer.BrowserContext) {
this.buttons = buttons
this.currentBrowser = browser
- this.pages = undefined
- this.selectedPage = undefined
+ this.pagesStatus = []
this.incognitoContext = incognitoContext
- this.currentBrowser.on('targetcreated', target => this.update('page_created', target))
- this.currentBrowser.on('targetchanged', target => this.update('page_changed', target))
- // this.currentBrowser.on('targetdestroyed', target => this.update('page_destroyed',target))
+ this.currentBrowser.on('targetcreated', async (target: puppeteer.Target) => this.update('page_created', await target.page()))
+ this.currentBrowser.on('targetchanged', async (target: puppeteer.Target) => this.update('page_changed', await target.page()))
+ // this.currentBrowser.on('targetdestroyed', target => this.update('page_destroyed', target))
this.currentBrowser.on('disconnected', () => {
this.buttons.setStatusButtonText('Launch $(rocket)')
Browser.activeBrowser = undefined
- this.buttons.dipslayPlayback(false)
+ this.buttons.displayPlayback(false)
+ TreeviewProvider.refresh()
+ Browser.launched = false
+ // console.debug('CLOSE')
})
+ // this.currentBrowser.process().once('close', () => console.debug('CLOSE!!!!!!!!'))
this.launchPages()
}
- play() {
+ // Toggle
+ async playPause() {
if (!this.selectedPage) return
- switch (this.selectedMusicPageBrand) {
- case 'soundcloud':
- case 'ytmusic':
+ const { state } = await this.getPlaybackState(this.selectedPage)
+ switch (this.selectedMusicBrand) {
+ case 'soundcloud': case 'spotify': case 'ytmusic':
this.selectedPage.keyboard.press('Space')
break
- case 'spotify':
- // @ts-ignore
- this.selectedPage.evaluate(() => spotifyAction('play'))
- break
case 'youtube':
this.selectedPage.keyboard.press('k')
- break
}
- this.buttons.setPlayButton('pause')
- }
-
- pause() {
- if (!this.selectedPage) return
- switch (this.selectedMusicPageBrand) {
- case 'soundcloud':
- case 'ytmusic':
- this.selectedPage.keyboard.press('Space')
- break
- case 'spotify':
- // @ts-ignore
- this.selectedPage.evaluate(() => spotifyAction('pause'))
- break
- case 'youtube':
- this.selectedPage.keyboard.press('k')
- break
- }
- this.buttons.setPlayButton('play')
+ this.buttons.setPlayButtonLabel(state)
}
async skip() {
if (!this.selectedPage) return
- switch (this.selectedMusicPageBrand) {
+ switch (this.selectedMusicBrand) {
case 'soundcloud':
await this.selectedPage.keyboard.down('ShiftLeft')
await this.selectedPage.keyboard.press('ArrowRight')
@@ -159,14 +137,12 @@ export class Browser {
break
case 'ytmusic':
await this.selectedPage.keyboard.press('j')
- break
}
- this.changeEventCheck()
}
async back() {
if (!this.selectedPage) return
- switch (this.selectedMusicPageBrand) {
+ switch (this.selectedMusicBrand) {
case 'soundcloud':
await this.selectedPage.keyboard.down('ShiftLeft')
await this.selectedPage.keyboard.press('ArrowLeft')
@@ -181,37 +157,26 @@ export class Browser {
break
case 'ytmusic':
await this.selectedPage.keyboard.press('j')
- break
}
- this.changeEventCheck()
}
async forward() {
if (!this.selectedPage) return
- switch (this.selectedMusicPageBrand) {
+ switch (this.selectedMusicBrand) {
case 'youtube':
await this.selectedPage.keyboard.press('ArrowRight')
break
- default: { vscode.window.showInformationMessage(seekMsg) }
+ default: vscode.window.showInformationMessage(SEEK_MSG)
}
- this.changeEventCheck()
}
async backward() {
if (!this.selectedPage) return
- switch (this.selectedMusicPageBrand) {
+ switch (this.selectedMusicBrand) {
case 'youtube':
await this.selectedPage.keyboard.press('ArrowLeft')
break
- default: { vscode.window.showInformationMessage(seekMsg) }
- }
- this.changeEventCheck()
- }
-
- async toggle() {
- if (this.selectedPage) {
- const pStt = await this.getPlayingStatus(this.selectedPage)
- pStt.status === 'play' ? this.pause() : this.play()
+ default: vscode.window.showInformationMessage(SEEK_MSG)
}
}
@@ -219,18 +184,35 @@ export class Browser {
return this.selectedPage?.title()
}
+ getPagesStatus() {
+ return this.pagesStatus
+ }
+
+ async pickTab(index: number) {
+ if (!this.pagesStatus) return
+ await this.tabOrderUpdate()
+ const { page, state } = this.pagesStatus[index]
+ if (state === 'none') {
+ vscode.window.showInformationMessage(STATE_MSG)
+ return
+ }
+ this.update('page_selected:tab', page)
+ }
+
+ // ↓↓↓↓ Private methods ↓↓↓↓
+
private async launchPages() {
- const links: any = vscode.workspace.getConfiguration().get('lmptm.startPages')
- if (links.length) {
- const p: any = []
+ const links: string[] | undefined = vscode.workspace.getConfiguration().get('lmptm.startPages')
+ if (links && links.length) {
+ const p: Promise[] = []
links.forEach(async (e: string) => {
const pg = await this.newPage()
await pg.setDefaultNavigationTimeout(0)
- p.push(pg.goto(e))
+ await pg.goto(e)
})
- await Promise.all(p)
+ await Promise.all(p) // need to wait?
}
- this.pages = await this.currentBrowser.pages()
+ TreeviewProvider.refresh()
}
private async newPage() {
@@ -239,223 +221,213 @@ export class Browser {
else return this.currentBrowser.newPage()
}
- // The button doesn't show up on the 1st launch
- private injectHtml(page: puppeteer.Page) {
- page.evaluate(uiHtmlPath => {
- do {
- // @ts-ignore
- if (!window['injected']) {
- const div = document.createElement('div')
- div.innerHTML = uiHtmlPath
- document.getElementsByTagName('body')[0].appendChild(div)
- // @ts-ignore
- window['injected'] = true
- }
- } while (!document.getElementsByTagName('body')[0])
- }, Browser.uiHtmlPath)
+ private resetFloatButton() {
+ // @ts-ignore
+ this.selectedPage?.evaluate(() => reset())
}
- private addScripts(page: puppeteer.Page) {
- page.addStyleTag({ path: Browser.cssPath })
- page.addScriptTag({ path: Browser.jsPath })
+ private clickFloatButton(thePage: puppeteer.Page) {
+ // @ts-ignore
+ thePage.evaluate(() => click())
}
- private async setupPageWatcher(page: puppeteer.Page) {
- page.evaluateOnNewDocument(uiHtmlPath => {
- window.onload = () => {
- // @ts-ignore
- if (!window['injected']) {
- const div = document.createElement('div')
- div.innerHTML = uiHtmlPath
- document.getElementsByTagName('body')[0].appendChild(div)
- // @ts-ignore
- window['injected'] = true
- }
+ private async tabOrderUpdate() {
+ // Puppeteer doesn't keep track pages' order?
+ const pages = await this.currentBrowser.pages()
+ const newPagesStatus = []
+ for (const [i, p] of pages.entries()) {
+ for (const e of this.pagesStatus) {
+ if (p !== e.page) continue
+ e.index = i
+ newPagesStatus.push(e)
}
- }, Browser.uiHtmlPath)
+ }
+ this.pagesStatus = newPagesStatus
+ TreeviewProvider.refresh()
+ }
- page.removeAllListeners('close')
- page.on('close', async () => {
- await new Promise(resolve => setTimeout(() => resolve(), 1000))
- if (Browser.activeBrowser) this.update('page_closed', page.target())
- })
+ // Get from saved
+ private getPlaybackState(page: puppeteer.Page) {
+ for (const e of this.pagesStatus)
+ if (e.page === page) return e
+ // when not found
+ return this._getPlaybackState(page)
+ }
- // @ts-ignore
- if (!page._pageBindings.has('pageSelected')) {
- page.exposeFunction('pageSelected', async e => {
- this.update('pageSelected', page.target())
- if (page !== this.selectedPage) {
- if (this.selectedPage) this.resetButton()
- this.selectedPage = page
- this.selectedMusicPageBrand = e.brand
- this.setupMusicPage()
- this.buttons.dipslayPlayback(true)
- this.buttons.setStatusButtonText(await this.selectedPage.title())
- this.changeEventCheck()
- }
- })
- }
+ // Need this?
+ private async _getPlaybackState(page: puppeteer.Page) {
+ const pageBrand = this.musicBrandCheck(page.url())
+ const state = await page.evaluate(() => navigator.mediaSession.playbackState)
+ return { brand: pageBrand, state }
+ }
+
+ private musicBrandCheck(url: string) {
+ if (url.includes('soundcloud.com')) return 'soundcloud'
+ else if (url.includes('open.spotify.com')) return 'spotify'
+ else if (url.includes('www.youtube.com/watch')) return 'youtube'
+ else if (url.includes('music.youtube.com')) return 'ytmusic'
+ else return 'other'
}
- private setupMusicPage() {
- const page = this.selectedPage
+ private async update(event: string, page: puppeteer.Page | null) {
+ // console.debug('$$$$$$$$$', event)
if (!page) return
- const brand = this.selectedMusicPageBrand
- if (!brand) return
- // @ts-ignore
- if (!page._pageBindings.has('onPlayingChangeEvent')) {
- page.exposeFunction('onPlayingChangeEvent', () => {
- this.update('play_event', page.target())
- })
+ let extra
+ if (event.includes('page_selected')) {
+ [event, extra] = event.split(':')
+ if (!extra) throw new Error('page_selected event needs source - tab|button')
+ }
+ else if (event.includes('playback_change')) {
+ [event, extra] = event.split(':')
+ if (!extra) throw new Error('playback_change event needs state - playing|paused|none')
}
- page.evaluate(playButtonCss => {
- const target = document.querySelector(playButtonCss)
- // @ts-ignore
- const observer = new MutationObserver(() => onPlayingChangeEvent())
- observer.observe(target, { attributes: true })
- // @ts-ignore
- }, Browser.playButtonCss[brand])
-
- if (brand === 'spotify') {
- page.evaluate(() => {
- const id = setInterval(() => {
- if (document.querySelectorAll('.now-playing .cover-art-image')[0]) {
- const target = document.querySelectorAll('.now-playing .cover-art-image')[0]
- // @ts-ignore
- const observer = new MutationObserver(() => onPlayingChangeEvent())
- observer.observe(target, { attributes: true })
- clearInterval(id)
- }
- }, 3000)
- })
+ switch (event) {
+ case 'page_changed': await this.pageChanged(page); break
+ case 'page_closed': this.pageClosed(page); break
+ case 'page_created': await this.pageCreated(page); break
+ case 'page_selected': await this.pageSelected(page, extra); break
+ case 'playback_changed': await this.playbackChanged(page, extra); break
+ default: vscode.window.showErrorMessage(`Unknown event - ${event}`)
}
+ TreeviewProvider.refresh()
}
- private async updatePages() {
- this.pages = await this.currentBrowser.pages()
- return this.pages[0]
- }
+ private async pageChanged(page: puppeteer.Page) {
+ await page.waitForNetworkIdle() // this somehow prevents navigation error
+ const pageURL = page.url()
+ const brand = this.musicBrandCheck(pageURL)
- private resetButton() {
- // @ts-ignore
- this.selectedPage?.evaluate(() => reset())
- }
+ // Spotify needs bypass CSP
+ // won't stick to another site (if go to another URL) after set
+ if (brand === 'spotify') this.spotifyBypassCSP(page)
- private async changeEventCheck() {
- if (!this.selectedPage) return
- const pStatus = await this.getPlayingStatus(this.selectedPage)
- if (pStatus.brand !== 'other') {
- this.selectedMusicPageBrand = pStatus.brand
- this.buttons.setPlayButton(pStatus.status)
- if(this.selectedPage)
- this.buttons.setStatusButtonText(await this.selectedPage.title())
- } else {
- if (this.selectedPage.url().includes('www.youtube.com')) this.resetButton() // See line 400
- this.buttons.setStatusButtonText('Running $(browser)')
- this.buttons.dipslayPlayback(false)
- this.selectedPage = undefined
- this.selectedMusicPageBrand = undefined
- }
- }
-
- private async getPlayingStatus(page: puppeteer.Page) {
- const pageBrand = this.musicBrandCheck(page.url())
+ const title = await page.title()
- if (pageBrand === 'other' || !this.selectedPage) return { brand: pageBrand, status: '' }
-
- else if (pageBrand === 'soundcloud') {
- const element = await this.selectedPage.$(Browser.playButtonCss.soundcloud)
- const text = await this.selectedPage.evaluate(element => element.getAttribute('title'), element)
- const stt = text.includes('Play') ? 'play' : 'pause'
- return { brand: pageBrand, status: stt }
-
- } else if (pageBrand === 'spotify') {
- const element = await this.selectedPage.$(Browser.playButtonCss.spotify)
- const text = await this.selectedPage.evaluate(element => element.getAttribute('title'), element)
- const stt = text.includes('Play') ? 'play' : 'pause'
- return { brand: pageBrand, status: stt }
-
- } else if (pageBrand === 'youtube') {
- const element = await this.selectedPage.$(Browser.playButtonCss.youtube)
- const text = await this.selectedPage.evaluate(element => element.getAttribute('aria-label'), element)
- if (!text) return { brand: pageBrand, status: 'play' } // When replay
- const stt = text.includes('Play') ? 'play' : 'pause'
- return { brand: pageBrand, status: stt }
-
- } else if (pageBrand === 'ytmusic') {
- const element = await this.selectedPage.$(Browser.playButtonCss.ytmusic)
- const text = await this.selectedPage.evaluate(element => element.getAttribute('aria-label'), element)
- if (!text) return { brand: pageBrand, status: 'play' }
- const stt = text.includes('Play') ? 'play' : 'pause'
- return { brand: pageBrand, status: stt }
-
- } else return { brand: pageBrand, status: '' }
+ for (const [i, e] of this.pagesStatus.entries()) {
+ if (e.page !== page) continue
+ if (this.selectedPage === page) this.selectedMusicBrand = brand
+ if (brand === 'other') {
+ this.pagesStatus[i].picked = false
+ this.pagesStatus[i].state = 'none'
+ this.closingHelper()
+ }
+ this.pagesStatus[i].brand = brand
+ this.pagesStatus[i].title = title
+ }
}
- private musicBrandCheck(url: string) {
- if (url.includes('soundcloud.com')) return 'soundcloud'
- else if (url.includes('open.spotify.com')) return 'spotify'
- else if (url.includes('www.youtube.com/watch')) return 'youtube'
- else if (url.includes('music.youtube.com')) return 'ytmusic'
- else return 'other'
+ private pageClosed(page: puppeteer.Page) {
+ if (page === this.selectedPage) this.closingHelper()
+ this.pagesStatus.forEach((e, i, arr) => {
+ if (e.page !== page) return
+ arr.splice(i, 1)
+ })
}
- private async closeEventUpdate() {
- this.buttons.setPlayButton('play')
- this.buttons.dipslayPlayback(false)
+ private closingHelper() {
+ this.buttons.displayPlayback(false)
this.buttons.setStatusButtonText('Running $(browser)')
this.selectedPage = undefined
- this.selectedMusicPageBrand = undefined
+ this.selectedMusicBrand = undefined
}
- private async update(event: string, target: puppeteer.Target) {
- const page = await target.page()
- if (!page) return
+ private async pageCreated(page: puppeteer.Page) {
+ const pageURL = page.url()
+ const brand = pageURL === 'about:blank' ? 'other' : this.musicBrandCheck(pageURL)
- if (event === 'page_closed') {
- if (page === this.selectedPage) this.closeEventUpdate()
- else this.updatePages()
- }
+ // Spotify needs bypass CSP
+ if (brand === 'spotify') this.spotifyBypassCSP(page)
- else if (event === 'page_created') {
- if ((this.musicBrandCheck(page.url()) === 'spotify')) {
- this.setPageBypassCSP(page, 'true')
- page.goto(page.url())
- } else this.setPageBypassCSP(page, 'false')
- await this.setupPageWatcher(page)
-
- page.on('load', async () => {
- if (this.musicBrandCheck(page.url()) === 'spotify') await this.checkSpotifyCSP(page)
- this.injectHtml(page)
- this.addScripts(page)
- if (!(this.musicBrandCheck(page.url()) === 'other')) this.setupMusicPage()
- })
+ let title = pageURL === 'about:blank' ? pageURL : await page.title()
+ if (title === '') title = 'New Tab'
+
+ const pages = await this.currentBrowser.pages()
+ for (const [index, p] of pages.entries()) {
+ if (p !== page) continue
+ this.pagesStatus.splice(index, 0, { page, brand, index, state: 'none', picked: false, title })
}
- else if (event === 'page_changed') {
- await page.waitForNavigation()
- this.changeEventCheck()
+ page.on('load', async () => {
+ page.addStyleTag({ path: Browser.cssPath })
+ page.addScriptTag({ path: Browser.jsPath })
+ })
+
+ page.removeAllListeners('close')
+ page.on('close', async () => {
+ // console.debug('page on CLOSE')
+ await new Promise(resolve => setTimeout(() => resolve(), 1000))
+ if (Browser.activeBrowser) this.update('page_closed', page)
+ })
+
+ // @ts-ignore
+ if (!page._pageBindings.has('pageSelected'))
+ page.exposeFunction('pageSelected', () => this.update('page_selected:button', page))
+
+ // @ts-ignore
+ if (!page._pageBindings.has('playbackChanged'))
+ page.exposeFunction('playbackChanged', (state: string) => this.update(`playback_changed:${state}`, page))
+
+ }
+
+ private async pageSelected(page: puppeteer.Page, source: string) {
+ this.buttons.displayPlayback(true)
+
+ // Not sure why it can't detect or wait - the error below
+ // rejected promise not handled within 1 second:
+ // Error: Execution context is not available in detached frame "about:blank"
+ // (are you trying to evaluate?)
+
+ if (this.selectedPage) {
+ const { state } = await this.getPlaybackState(this.selectedPage)
+ if (this.selectedPage === page) {
+ await this.playPause()
+ return
+ } else {
+ this.pagesStatus.forEach(e => e.page === this.selectedPage ? e.picked = false : null)
+ if (source === 'button' && state === 'playing') await this.playPause()
+ this.resetFloatButton()
+ }
}
- else if (event === 'play_event') this.changeEventCheck()
+ for (const [i, e] of this.pagesStatus.entries()) {
+ if (e.page !== page) continue
+ if (source === 'tab') {
+ this.clickFloatButton(e.page) // this will trigger page_select:button
+ break
+ } else {
+ this.selectedPage = e.page
+ this.pagesStatus[i].picked = true
+ this.selectedMusicBrand = this.musicBrandCheck(page.url())
+ const title = await this.selectedPage.title()
+ this.pagesStatus[i].title = title
+ this.buttons.setStatusButtonText(title)
+ }
+ }
}
- private async setPageBypassCSP(page: puppeteer.Page, flag: string) {
- if (page.url() === 'about:blank') return
- page.setBypassCSP(flag === 'true')
- await page.evaluate(theFlag => sessionStorage.setItem('bypassCSP', theFlag), flag)
+ private async playbackChanged(page: puppeteer.Page, state: string) {
+ for (const [i, e] of this.pagesStatus.entries()) {
+ if (e.page !== page) continue
+ if (this.selectedPage === page) {
+ this.buttons.setPlayButtonLabel(state)
+ this.buttons.setStatusButtonText(await this.selectedPage.title())
+ }
+ this.pagesStatus[i].state = state
+ this.pagesStatus[i].title = await page.title()
+ }
}
- private async checkSpotifyCSP(page: puppeteer.Page) {
- const cspFlag = await page.evaluate(() => sessionStorage.getItem('bypassCSP'))
- if (cspFlag === 'true') return
- await this.setPageBypassCSP(page, 'true')
- await page.reload()
+ private async spotifyBypassCSP(page: puppeteer.Page) {
+ page.setBypassCSP(true)
+ // for debugging
+ await page.evaluate(() => sessionStorage.setItem('bypassCSP', 'true'))
+ // this doesn't trigger page_changed again -> no infinity loop
+ page.goto(page.url())
}
// private async sleep(ms: number = 1000) {
// await new Promise(resolve => setTimeout(() => resolve(), ms))
// }
-}
\ No newline at end of file
+}
diff --git a/src/buttons.ts b/src/buttons.ts
index 9d6e8d0..6ea6ab9 100644
--- a/src/buttons.ts
+++ b/src/buttons.ts
@@ -17,8 +17,8 @@ export class Buttons {
this.backButton.text = '$(chevron-left)'
this.statusButton.text = 'Launch $(rocket)'
- this.statusButton.command = 'lmptm.browserlaunch'
- this.playButton.command = 'lmptm.play'
+ this.statusButton.command = 'lmptm.browserLaunch'
+ this.playButton.command = 'lmptm.playPause'
this.backButton.command = 'lmptm.back'
this.skipButton.command = 'lmptm.skip'
@@ -28,7 +28,7 @@ export class Buttons {
setStatusButtonText(text: string) {
if (text === 'Launch $(rocket)') {
this.statusButton.text = text
- this.statusButton.command = 'lmptm.browserlaunch'
+ this.statusButton.command = 'lmptm.browserLaunch'
} else if (text === 'Running $(browser)') {
this.statusButton.text = text
this.statusButton.command = undefined
@@ -41,16 +41,11 @@ export class Buttons {
}
}
- setPlayButton(text: string) {
- if (text === 'play') {
- this.playButton.text = '$(play)'
- this.playButton.command = 'lmptm.play'
- } else {
- this.playButton.text = '$(primitive-square)'
- this.playButton.command = 'lmptm.pause'
- }
+ setPlayButtonLabel(label: MediaSessionPlaybackState) {
+ this.playButton.text = label === 'playing' ? '$(primitive-square)' : '$(play)'
}
- dipslayPlayback(flag: boolean) {
+
+ displayPlayback(flag: boolean) {
if (flag) {
this.playButton.show()
this.skipButton.show()
diff --git a/src/extension.ts b/src/extension.ts
index cd9c6eb..6521bbd 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,19 +1,21 @@
import { commands, window, ExtensionContext } from 'vscode'
-import { Buttons } from './buttons'
import { Browser } from './browser'
+import { Buttons } from './buttons'
+import { Entry } from './interfaces'
+import { TreeviewProvider } from './treeview'
export function activate(context: ExtensionContext) {
const buttons = new Buttons()
+ const fn = ['playPause', 'skip', 'back', 'forward', 'backward'] as const
+ const rc = commands.registerCommand
+ TreeviewProvider.create()
+ const disposables = fn.map(n => rc(`lmptm.${n}`, () => Browser.activeBrowser?.[n]()))
+ context.subscriptions.concat(disposables)
context.subscriptions.concat([
- commands.registerCommand('lmptm.browserlaunch', () => Browser.launch(buttons, context)),
- commands.registerCommand('lmptm.play', () => Browser.activeBrowser?.play()),
- commands.registerCommand('lmptm.pause', () => Browser.activeBrowser?.pause()),
- commands.registerCommand('lmptm.skip', () => Browser.activeBrowser?.skip()),
- commands.registerCommand('lmptm.back', () => Browser.activeBrowser?.back()),
- commands.registerCommand('lmptm.forward', () => Browser.activeBrowser?.forward()),
- commands.registerCommand('lmptm.backward', () => Browser.activeBrowser?.backward()),
- commands.registerCommand('lmptm.toggle', () => Browser.activeBrowser?.toggle()),
- commands.registerCommand('lmptm.showTitle', showTitle)
+ rc('lmptm.browserLaunch', () => Browser.launch(buttons, context)),
+ rc('lmptm.tvRefresh', () => TreeviewProvider.refresh()),
+ rc('lmptm.showTitle', showTitle),
+ rc('lmptm.click', selection => click(selection))
])
}
@@ -23,4 +25,8 @@ async function showTitle() {
else window.showErrorMessage('Failed to retrieve title')
}
-export function deactivate() {}
+function click(selection: Entry) {
+ Browser.activeBrowser?.pickTab(selection.index)
+}
+
+// export function deactivate() { }
diff --git a/src/inject/script.js b/src/inject/script.js
new file mode 100644
index 0000000..0948fad
--- /dev/null
+++ b/src/inject/script.js
@@ -0,0 +1,217 @@
+'use strict'
+const style = 'background:deepskyblue;padding:1px 2px;border-radius:2px'
+const log = (text, ...rest) => console.log(`%c${text}`, style, ...rest)
+log(`LMPTM's script injected successfully!`)
+
+const PICK_MSG = '⛏️ Pick?'
+const playButtonAttrs = {
+ soundcloud: {
+ css: '.playControl',
+ cssCover: '.m-visible'
+ },
+ spotify: {
+ css: 'button[data-testid="control-button-playpause"]',
+ cssAll5Btns: '.player-controls button',
+ cssTitle: 'div[data-testid="now-playing-widget"]',
+ play: 'M4.018 14L14.41 8 4.018 2z'
+ },
+ youtube: { css: '.ytp-play-button' },
+ ytmusic: { css: '#play-pause-button' }
+}
+
+let observer
+const btnPick = document.createElement('button')
+btnPick.innerHTML = PICK_MSG
+btnPick.className = 'btn-pick-float'
+document.body.appendChild(btnPick)
+btnPick.addEventListener('click', click)
+
+// Duplicate tabs solution
+// TODO: need note
+let clear = false
+let loadCount = sessionStorage.getItem('load')
+loadCount === null ? (loadCount = 0) : loadCount++
+sessionStorage.setItem('load', loadCount)
+let unloadCount = sessionStorage.getItem('unload')
+unloadCount = unloadCount === null ? 0 : parseInt(unloadCount)
+if (loadCount === unloadCount) verifyPage()
+
+window.addEventListener('beforeunload', () => {
+ if (clear) {
+ sessionStorage.removeItem('load')
+ sessionStorage.removeItem('unload')
+ return
+ }
+ sessionStorage.setItem('unload', unloadCount + 1)
+})
+
+function click() {
+ const btnPick = document.querySelector('.btn-pick-float')
+ const href = window.location.href
+ let brand
+
+ if (href.includes('soundcloud.com')) {
+ if (!document.querySelector('.m-visible')) {
+ btnPick.disabled = true
+ return void showInfo(btnPick, 'soundcloud')
+ }
+ brand = 'soundcloud'
+ } else if (href.includes('open.spotify.com')) {
+ if (!navigator.mediaSession.metadata.title) {
+ btnPick.disabled = true
+ return void showInfo(btnPick, 'spotify')
+ }
+ brand = 'spotify'
+ } else if (href.includes('www.youtube.com')) {
+ if (!href.includes('/watch')) {
+ btnPick.disabled = true
+ return void showInfo(btnPick, 'youtube')
+ }
+ brand = 'youtube'
+ } else if (href.includes('music.youtube.com')) {
+ const e = 'ytmusic-app-layout[player-visible_] > [slot=player-bar]'
+ if (!document.querySelectorAll(e)[0]) {
+ btnPick.disabled = true
+ return void showInfo(btnPick, 'ytmusic')
+ }
+ brand = 'ytmusic'
+ } else {
+ btnPick.className = 'btn-pick-float error'
+ btnPick.innerHTML = 'Never mind! 😓'
+ btnPick.disabled = true
+ btnTimeoutReset(btnPick)
+ }
+
+ if (brand) {
+ window.pageSelected()
+ sessionStorage.setItem('lmptm', brand)
+ changeBtnAttr(brand)
+ clear = true
+ }
+}
+
+// For supporting other sites later?
+// function setupMediaSession() {
+// const proxyHandler = {
+// get(target, prop) {
+// log('get', prop)
+// const value = target[prop]
+// if (typeof value === 'function') {
+// const fn = value.bind(target)
+// return fn
+// }
+// return value
+// },
+// set(target, prop, value) {
+// target[prop] = value
+// log('set', prop)
+// if (prop[0] === 'playbackState') window.playbackChanged()
+// return true
+// }
+// }
+// Object.defineProperty(navigator, 'mediaSession', {
+// // eslint-disable-next-line no-undef
+// value: new Proxy(navigator.mediaSession, proxyHandler)
+// })
+// }
+
+function setupObserver(brand) {
+ const { css } = playButtonAttrs[brand]
+ if (observer) observer.disconnect()
+ const targetE = document.querySelector(css)
+ let state = getPlaybackState(brand, css)
+ if (brand === 'soundcloud' && state === 'none') state = 'paused'
+ window.playbackChanged(state)
+
+ observer = new MutationObserver(() => {
+ const state = getPlaybackState(brand)
+ window.playbackChanged(state)
+ })
+ if (targetE) observer.observe(targetE, { attributes: true })
+
+ // Spotify doesn't fire playbackChanged when skip/back a song
+ if (brand === 'spotify') {
+ const { cssTitle } = playButtonAttrs.spotify
+ const targetE2 = document.querySelector(cssTitle)
+ // Observer can watch multiple elements :)
+ if (targetE2) observer.observe(targetE2, { attributes: true })
+ }
+}
+
+function getPlaybackState(brand) {
+ // spotify doesn't update the mediaSession.playbackState
+ if (brand === 'spotify') {
+ const { css, play } = playButtonAttrs.spotify
+ const d = document.querySelector(`${css} svg path:last-child`).getAttribute('d')
+ const state = d === play ? 'paused' : 'playing'
+ return state
+ } else return navigator.mediaSession.playbackState
+}
+
+function verifyPage() {
+ const brand = sessionStorage.getItem('lmptm')
+ const href = window.location.href
+ if (!brand) return
+ if (brand === 'spotify' && href.includes('open.spotify.com')) changeBtnAttr(brand)
+ else if (brand === 'soundcloud' && href.includes('soundcloud.com')) changeBtnAttr(brand)
+ else if (brand === 'youtube' && href.includes('www.youtube.com/watch')) changeBtnAttr(brand)
+ else if (brand === 'ytmusic' && href.includes('music.youtube.com/watch')) changeBtnAttr(brand)
+ else reset()
+}
+
+function changeBtnAttr(brand) {
+ setupObserver(brand)
+ if (brand === 'ytmusic') brand = 'youtube'
+ btnPick.className = `btn-pick-float border-gray ${brand}`
+ btnPick.innerHTML = null
+}
+
+function showInfo(btnPick, brand) {
+ btnPick.className = `btn-pick-float border-gray ${brand}-info`
+ let msg = 'Something is not right...'
+ switch (brand) {
+ case 'soundcloud':
+ msg = 'Please pick a song 😉'
+ break
+ case 'spotify':
+ msg = 'Please log in and make sure the playing queue is not empty 😉'
+ break
+ case 'youtube':
+ msg = 'Please pick a video 😉'
+ break
+ case 'ytmusic':
+ msg = 'Please make sure the playing queue is not empty 😉'
+ break
+ }
+ btnPick.innerHTML = msg
+ btnTimeoutReset(btnPick)
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function spotifyAction(action) {
+ const { cssAll5Btns } = playButtonAttrs.spotify
+ const actionBtn = document.querySelectorAll(cssAll5Btns)
+ switch (action) {
+ case 'skip':
+ actionBtn[3].click()
+ break
+ case 'back':
+ actionBtn[1].click()
+ break
+ }
+}
+
+function btnTimeoutReset(btnPick) {
+ setTimeout(() => {
+ btnPick.innerHTML = '⛏️ Pick?'
+ btnPick.className = 'btn-pick-float'
+ btnPick.disabled = false
+ }, 3000)
+}
+
+function reset() {
+ const btnPick = document.querySelector('.btn-pick-float')
+ btnPick.innerHTML = '⛏️ Pick?'
+ btnPick.className = 'btn-pick-float'
+ sessionStorage.removeItem('lmptm')
+}
diff --git a/src/inject/style.css b/src/inject/style.css
new file mode 100644
index 0000000..6c61be8
--- /dev/null
+++ b/src/inject/style.css
@@ -0,0 +1,86 @@
+.btn-pick-float {
+ position: fixed;
+ right: 10px;
+ bottom: 10px;
+ transition: 200ms;
+ z-index: 2147483646;
+ border-radius: 5px;
+ border-color: deepskyblue;
+ box-shadow: 1px 1px dodgerblue;
+ background-color: deepskyblue;
+ cursor: pointer;
+ width: 75px;
+ height: 30px;
+ text-align: center;
+ color: black;
+}
+.border-gray {
+ border-color: lightgray;
+ box-shadow: 1px 1px gray;
+}
+.soundcloud {
+ background: url('data:image/svg+xml, ');
+ background-color: #f50;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 1.5em;
+ width: 2em;
+ height: 1.5em;
+ font-size: 2.5em;
+}
+.soundcloud-info {
+ background-color: #f50;
+ width: 8em;
+ height: 3.5em;
+ color: white !important;
+ font-size: 1.2em;
+}
+.spotify {
+ background: url('data:image/svg+xml, ');
+ background-color: black;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 1em;
+ width: 1.5em;
+ height: 1.5em;
+ font-size: 2em;
+}
+.spotify-info {
+ background-color: black;
+ width: 20em;
+ height: 3.5em;
+ color: #1ed761;
+ font-size: 1em;
+}
+.youtube {
+ background: url('data:image/svg+xml, ');
+ background-color: white;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 1.5em;
+ width: 2em;
+ height: 1.5em;
+ font-size: 3em;
+}
+.youtube-info {
+ background-color: white;
+ width: 8em;
+ height: 3.5em;
+ color: red;
+ font-size: 1.5em;
+}
+.ytmusic-info {
+ background-color: white;
+ width: 15em;
+ height: 3.5em;
+ color: red;
+ font-size: 1.5em;
+}
+.error {
+ border-color: orangered;
+ box-shadow: 1px 1px orangered;
+ background-color: red;
+ cursor: progress;
+ width: 10em;
+ color: white;
+}
diff --git a/src/interfaces.ts b/src/interfaces.ts
new file mode 100644
index 0000000..b9a56e3
--- /dev/null
+++ b/src/interfaces.ts
@@ -0,0 +1,9 @@
+import { Page } from 'puppeteer-core'
+export interface Entry {
+ brand: string
+ index: number
+ page: Page
+ picked: boolean
+ state: MediaSessionPlaybackState
+ title: string
+}
diff --git a/src/media/soundcloud.svg b/src/media/soundcloud.svg
deleted file mode 100644
index 1afc2b6..0000000
--- a/src/media/soundcloud.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/media/spotify.svg b/src/media/spotify.svg
deleted file mode 100644
index d55deb8..0000000
--- a/src/media/spotify.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/media/youtube.svg b/src/media/youtube.svg
deleted file mode 100644
index ed00f43..0000000
--- a/src/media/youtube.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/scripts/script.js b/src/scripts/script.js
deleted file mode 100644
index daa38f3..0000000
--- a/src/scripts/script.js
+++ /dev/null
@@ -1,155 +0,0 @@
-const btnPick = document.querySelector('.btn-pick-float')
-btnPick.addEventListener('click',check)
-
-// Duplicate tab solution
-let clear = false
-let loadCount = sessionStorage.getItem('load')
-if (loadCount === null) loadCount = 0
-else loadCount++
-sessionStorage.setItem('load',loadCount)
-let unloadCount = sessionStorage.getItem('unload')
-if (unloadCount === null) unloadCount = 0
-else unloadCount = parseInt(unloadCount)
-
-window.addEventListener('beforeunload',() => {
- if (clear) {
- sessionStorage.removeItem('load')
- sessionStorage.removeItem('unload')
- return
- }
- sessionStorage.setItem('unload',unloadCount + 1)
-})
-
-let flagPick = false
-if (loadCount === unloadCount) verifyPage()
-
-function check() {
- const btnPick = document.querySelector('.btn-pick-float')
-
- if (window.location.href.includes('soundcloud.com')) {
- if (!document.querySelectorAll('.m-visible')[0]) {
- btnPick.disabled = true
- return void soundcloudInfo(btnPick)
- }
- window.pageSelected({ brand: 'soundcloud' })
- sessionStorage.setItem('lmptm','soundcloud')
- soundcloud(btnPick)
- clear = true
-
- } else if (window.location.href.includes('open.spotify.com')) {
- if (!document.querySelectorAll('.now-playing .cover-art-image')[0]) {
- btnPick.disabled = true
- return void spotifyInfo(btnPick)
- }
- window.pageSelected({ brand: 'spotify' })
- sessionStorage.setItem('lmptm','spotify')
- spotify(btnPick)
- clear = true
-
- } else if (window.location.href.includes('www.youtube.com')) {
- if (!window.location.href.includes('/watch')) {
- btnPick.disabled = true
- return void youtubeInfo(btnPick)
- }
- window.pageSelected({ brand: 'youtube' })
- sessionStorage.setItem('lmptm','youtube')
- youtube(btnPick)
- clear = true
-
- } else if (window.location.href.includes('music.youtube.com')) {
- const e = 'ytmusic-app-layout[player-visible_] > [slot=player-bar]'
- if (!document.querySelectorAll(e)[0]) {
- btnPick.disabled = true
- return void ytmusicInfo(btnPick)
- }
- window.pageSelected({ brand: 'ytmusic' })
- sessionStorage.setItem('lmptm','ytmusic')
- youtube(btnPick)
- clear = true
-
- } else {
- btnPick.className = 'btn-pick-float error'
- btnPick.innerHTML = ' Nevermind! 😓'
- btnPick.disabled = true
- btnTimeoutReset(btnPick)
- }
-}
-
-function verifyPage() {
- const data = sessionStorage.getItem('lmptm')
- if (!data) return
- if (data === 'spotify' && window.location.href.includes('open.spotify.com')) spotify(btnPick)
- else if (data === 'soundcloud' && window.location.href.includes('soundcloud.com')) soundcloud(btnPick)
- else if (data === 'youtube' && window.location.href.includes('www.youtube.com/watch')) youtube(btnPick)
- else if (data === 'ytmusic' && window.location.href.includes('music.youtube.com/watch')) ytmusic(btnPick)
- else reset()
-}
-
-function soundcloud(btnPick) {
- btnPick.className = 'btn-pick-float soundcloud'
- btnPick.innerHTML = ''
-}
-
-function spotify(btnPick) {
- btnPick.className = 'btn-pick-float spotify'
- btnPick.innerHTML = ''
-}
-
-function youtube(btnPick) {
- btnPick.className = 'btn-pick-float youtube'
- btnPick.innerHTML = ''
-}
-
-function soundcloudInfo(btnPick) {
- btnPick.className = 'btn-pick-float soundcloud-info'
- btnPick.innerHTML = 'Please pick a song 😉'
- btnTimeoutReset(btnPick)
-}
-
-function spotifyInfo(btnPick) {
- btnPick.className = 'btn-pick-float spotify-info'
- btnPick.innerHTML = 'Please log in and make sure the playing queue is not empty! 😉'
- btnTimeoutReset(btnPick)
-}
-
-function youtubeInfo(btnPick) {
- btnPick.className = 'btn-pick-float youtube-info'
- btnPick.innerHTML = 'Please pick a video! 😉'
- btnTimeoutReset(btnPick)
-}
-
-function ytmusicInfo(btnPick) {
- btnPick.className = 'btn-pick-float ytmusic-info'
- btnPick.innerHTML = 'Please make sure the playing queue is not empty! 😉'
- btnTimeoutReset(btnPick)
-}
-
-function spotifyAction(action) {
- switch (action) {
- case 'play':
- case 'pause':
- (document.querySelector(".spoticon-play-16") || document.querySelector(".spoticon-pause-16")).click()
- break
- case 'skip':
- document.querySelector('.spoticon-skip-forward-16').click()
- break
- case 'back':
- document.querySelector('.spoticon-skip-back-16').click()
- break
- }
-}
-
-function btnTimeoutReset(btnPick) {
- setTimeout(() => {
- btnPick.innerHTML = '⛏️ Pick?'
- btnPick.className = 'btn-pick-float'
- btnPick.disabled = false
- },3000)
-}
-
-function reset() {
- const btnPick = document.querySelector('.btn-pick-float')
- btnPick.innerHTML = '⛏️ Pick?'
- btnPick.className = 'btn-pick-float'
- sessionStorage.removeItem('lmptm')
-}
\ No newline at end of file
diff --git a/src/scripts/style.css b/src/scripts/style.css
deleted file mode 100644
index 3b3607a..0000000
--- a/src/scripts/style.css
+++ /dev/null
@@ -1,92 +0,0 @@
-.btn-pick-float {
- /* box-shadow: 2px 2px 3px #999; */
- background-color: #25aff0;
- border-radius: 5px;
- width: 75px;
- height: 30px;
- right: 10px;
- bottom: 10px;
- color: black;
- cursor: pointer;
- position: fixed;
- text-align: center;
- transition: 200ms;
- z-index: 2202;
- box-shadow:
- 1px 0px #3a587f, 0px 1px #4171ae,
- 2px 1px #3a587f, 1px 2px #4171ae,
- 3px 2px #3a587f, 2px 3px #4171ae,
- 4px 3px #3a587f, 3px 4px #4171ae;
-}
-
-.soundcloud {
- background: url('data:image/svg+xml, ');
- background-color: #f50;
- background-position: center;
- background-repeat: no-repeat;
- background-size: 1.5em;
- font-size: 2.5em;
- height: 1.5em;
- width: 2em;
-}
-
-.soundcloud-info {
- background-color: #f50;
- color: white !important;
- font-size: 1.2em;
- height: 3.5em;
- width: 8em;
-}
-
-.spotify {
- background: url('data:image/svg+xml, ');
- background-color: black;
- background-position: center;
- background-repeat: no-repeat;
- background-size: 1em;
- font-size: 2.5em;
- height: 1.5em;
- width: 2em;
-}
-
-.spotify-info {
- background-color: black;
- color: #1ed761;
- font-size: 1em;
- height: 3.5em;
- width: 20em;
-}
-
-.youtube {
- background: url('data:image/svg+xml, ');
- background-color: white;
- background-position: center;
- background-repeat: no-repeat;
- background-size: 1.5em;
- font-size: 3em;
- height: 1.5em;
- width: 2em;
-}
-
-.youtube-info {
- background-color: white;
- color:red;
- font-size: 1.5em;
- height: 3.5em;
- width: 8em;
-}
-
-.ytmusic-info {
- background-color: white;
- color:red;
- font-size: 1.5em;
- height: 3.5em;
- width: 15em;
-}
-
-.error {
- background-color: red;
- color:white;
- cursor: progress;
- width: 10em;
-}
\ No newline at end of file
diff --git a/src/scripts/ui.html b/src/scripts/ui.html
deleted file mode 100644
index c5200a4..0000000
--- a/src/scripts/ui.html
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/treeview.ts b/src/treeview.ts
new file mode 100644
index 0000000..0f9e259
--- /dev/null
+++ b/src/treeview.ts
@@ -0,0 +1,63 @@
+import { Browser } from './browser'
+import { Event, EventEmitter, ThemeIcon, TreeDataProvider, TreeItem, window } from 'vscode'
+import { Entry } from './interfaces'
+
+export class TreeviewProvider implements TreeDataProvider {
+ public static tvProvider: TreeviewProvider
+
+ private _onDidChangeTreeData: EventEmitter = new EventEmitter()
+ readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event
+
+ private browser: Browser | undefined
+
+ public static create() {
+ const treeDataProvider = new TreeviewProvider()
+ window.createTreeView('LMPTM', { treeDataProvider })
+ // const tv = window.createTreeView('LMPTM', { treeDataProvider })
+ // tv.onDidChangeSelection(({ selection }) => {})
+ this.tvProvider = treeDataProvider
+ }
+
+ public static refresh() {
+ this.tvProvider.refresh()
+ }
+
+ constructor() {
+ this.browser = Browser.activeBrowser
+ }
+
+ getTreeItem(element: Entry): TreeItem {
+ return this.getItem(element)
+ }
+
+ async getChildren(): Promise {
+ if (!this.browser) return
+ const details = this.browser.getPagesStatus()
+ // console.debug('@@@@@@@@', details)
+ if (!details) return
+ return details
+ }
+
+ private getItem(element: Entry) {
+ // console.debug(element)
+
+ return new TabItem(element)
+ }
+
+ refresh(): void {
+ this.browser = Browser.activeBrowser
+ this._onDidChangeTreeData.fire(null)
+ // console.debug('refresh')
+ }
+}
+
+class TabItem extends TreeItem {
+ constructor(e: Entry) {
+ const { picked, state } = e
+ let title = e.title
+ if (picked) title = `⛏️ ${title}`
+ super(title)
+ if (state !== 'none') this.iconPath = new ThemeIcon(state === 'playing' ? 'primitive-square' : 'play')
+ this.command = { title: 'click', command: 'lmptm.click', arguments: [e] }
+ }
+}
diff --git a/src/whichChrome.ts b/src/whichChrome.ts
index 2cf4305..1ec37e9 100644
--- a/src/whichChrome.ts
+++ b/src/whichChrome.ts
@@ -4,7 +4,6 @@ interface Paths {
[key: string]: string
}
-// @ts-ignore
import * as karmaChromeLauncher from 'karma-chrome-launcher'
export class WhichChrome {
@@ -12,7 +11,8 @@ export class WhichChrome {
const chromePaths: Paths = {}
Object.keys(karmaChromeLauncher).forEach(key => {
if (key.indexOf('launcher:') !== 0) return
- const info = karmaChromeLauncher[key] && karmaChromeLauncher[key][1] && karmaChromeLauncher[key][1].prototype
+ // @ts-ignore
+ const info = karmaChromeLauncher[key][1].prototype
if (!info) return
chromePaths[info.name] = info.DEFAULT_CMD[process.platform] || null
})
diff --git a/tasks.ts b/tasks.ts
index ec385f4..5ebb943 100644
--- a/tasks.ts
+++ b/tasks.ts
@@ -1,13 +1,11 @@
import * as shell from 'shelljs'
-// clean out file
console.log(`Run task ${process.argv[2]}`)
if (process.argv[2] === 'clean') {
- console.log('Remove "out" and "dist" directory')
- shell.rm('-rf', 'out')
+ console.log('Remove "dist" directory')
shell.rm('-rf', 'dist')
-} else if (process.argv[2] === 'copy') {
- console.log('Copy static assets to "out" directories')
- shell.mkdir('-p', 'out')
- shell.cp('-R', 'src/scripts', 'out/scripts')
-} else console.log(`ಠ_ಠ What task is this? task ${process.argv[2]}`)
\ No newline at end of file
+} else if (process.argv[2] === 'copy') { // for Launch Extension ts-watch
+ console.log('Copy static assets to "dist" directories')
+ shell.mkdir('-p', 'dist')
+ shell.cp('-R', 'src/inject', 'dist/inject')
+} else console.log(`ಠ_ಠ What task is this? task ${process.argv[2]}`)
diff --git a/tsconfig.json b/tsconfig.json
index 4e376b1..338bfd1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"module": "commonjs",
- "target": "es6",
- "outDir": "out",
- "lib": ["es6", "dom"],
+ "target": "esnext",
+ "outDir": "dist",
+ "lib": ["esnext", "dom"],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */,
@@ -12,5 +12,5 @@
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"noUnusedParameters": true /* Report errors on unused parameters. */
},
- "exclude": ["node_modules", "tasks.ts"]
+ "exclude": ["node_modules", "tasks.ts", "webpack.config.ts"]
}
diff --git a/webpack.config.js b/webpack.config.js
deleted file mode 100644
index a79bc81..0000000
--- a/webpack.config.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
-
-// @ts-check
-'use strict'
-
-const path = require('path')
-const CopyPlugin = require('copy-webpack-plugin')
-const Terser = require('terser')
-
-/**@type {import('webpack').Configuration}*/
-const config = {
- target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
-
- // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
- entry: {
- extension: './src/extension.ts',
- },
- output: {
- // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
- path: path.resolve(__dirname, 'dist'),
- filename: '[name].js',
- libraryTarget: 'commonjs2',
- devtoolModuleFilenameTemplate: '../[resource-path]',
- },
- // devtool: 'source-map',
- externals: {
- vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
- bufferutil: 'commonjs bufferutil', // https://github.com/websockets/ws/issues/1220#issuecomment-433066790
- 'utf-8-validate': 'commonjs utf-8-validate',
- 'supports-color': 'commonjs supports-color',
- },
- resolve: {
- // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
- extensions: ['.ts', '.js', 'css'],
- },
- module: {
- rules: [
- {
- test: /\.ts$/,
- exclude: /node_modules/,
- use: [
- {
- loader: 'ts-loader',
- options: {
- compilerOptions: {
- module: 'es6', // override `tsconfig.json` so that TypeScript emits native JavaScript modules.
- },
- },
- },
- ],
- },
- ],
- },
- plugins: [
- new CopyPlugin({
- patterns: [
- {
- from: 'src/scripts/script.js',
- to: 'scripts',
- async transform(content, path) {
- return (await Terser.minify(content.toString())).code
- },
- },
- { from: 'src/scripts/style.css', to: 'scripts' },
- { from: 'src/scripts/ui.html', to: 'scripts' },
- ],
- }),
- ],
-}
-
-module.exports = config
diff --git a/webpack.config.ts b/webpack.config.ts
new file mode 100644
index 0000000..dfe72ce
--- /dev/null
+++ b/webpack.config.ts
@@ -0,0 +1,50 @@
+//@ts-check
+'use strict'
+import { resolve } from 'path'
+import * as CopyPlugin from 'copy-webpack-plugin'
+import * as TerserPlugin from 'terser-webpack-plugin'
+import * as CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
+
+//@ts-check
+export default {
+ mode: 'none',
+ target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
+ entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
+ output: {
+ // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
+ path: resolve(resolve(), 'dist'),
+ filename: 'extension.js',
+ libraryTarget: 'commonjs2'
+ },
+ devtool: 'source-map',
+ externals: {
+ vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
+ bufferutil: 'commonjs bufferutil', // https://github.com/websockets/ws/issues/1220#issuecomment-433066790
+ 'utf-8-validate': 'commonjs utf-8-validate'
+ },
+ resolve: {
+ // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
+ extensions: ['.ts', '.js', 'css']
+ },
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ exclude: /node_modules/,
+ use: [{ loader: 'ts-loader' }]
+ }
+ ]
+ },
+ plugins: [
+ new CopyPlugin({
+ patterns: [
+ { from: 'src/inject/script.js', to: 'inject' },
+ { from: 'src/inject/style.css', to: 'inject' }
+ ]
+ })
+ ],
+ optimization: {
+ minimize: true,
+ minimizer: [new TerserPlugin({ extractComments: false }), new CssMinimizerPlugin()]
+ }
+}