diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index e49787a..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "env": {
- "browser": true,
- "es6": true,
- "node": true
- },
- "rules": {
- "constructor-super": "warn",
- "jsdoc/no-undefined-types": 1,
- "no-const-assign": "warn",
- "no-this-before-super": "warn",
- "no-undef": "warn",
- "no-unreachable": "warn",
- "no-unused-vars": "warn",
- "valid-typeof": "warn"
- },
- "plugins": ["jsdoc"]
-}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8840ca7..5b11833 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,13 +1,18 @@
{
"cSpell.words": [
+ "autoplay",
"codicon",
"codicons",
"flac",
"lanly",
+ "seekbar",
"susres",
- "Typecheck",
+ "treeview",
+ "vscodeignore",
+ "wavesurfer",
"webaudio"
],
"editor.rulers": [120],
- "explorer.sortOrder": "type"
+ "explorer.sortOrder": "type",
+ "terminal.integrated.defaultProfile.windows": "Command Prompt"
}
diff --git a/.vscodeignore b/.vscodeignore
index 6978cd3..63b0346 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -2,7 +2,6 @@
.token
.vscode
**/*.map
-CHANGELOG.md
demo
jsconfig.json
lab
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdff262..bf8ceb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,19 +4,28 @@ All notable changes to the "Spectrogram" extension will be documented in this fi
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Future Works]
-- [Display duration in explorer](https://code.visualstudio.com/api/extension-guides/tree-view#view-actions)
-- Generate spectrogram faster than real time, depend on Web Audio API?
-- New icon
-- More colors
- Real-time input mode/recorder
-- Seeker bar slider
-- [Support more audio codecs](https://code.visualstudio.com/updates/v1_71#_ffmpeg-codecs-support)
+- Generate spectrogram instead real-time - [Wavesurfer](https://wavesurfer.xyz/)
+
---
+## [3.0.0] - February 2025
+- Support WAV
+- Add seekbar
+- Add color RBG config
+- Add duration in treeview
+- New icon and logo
+
+### Notes
+- Clicking treeview item some time able trigger playing the song, but due to the autoplay policy, most of the time you have to click play button inside the webview
+
+### References
+- Treeview API https://code.visualstudio.com/api/extension-guides/tree-view#view-actions
+- VS Code support codecs https://code.visualstudio.com/updates/v1_71#_ffmpeg-codecs-support
## [2.0.0] - September 2022
-- Add seeking 5s back and fort buttons
+- Add seeking 5s back and forth buttons
- Reduce extension size with Webpack
-- Support flac
+- Support FLAC
- Switch to `main` branch
- Use [Codicons](https://microsoft.github.io/vscode-codicons/dist/codicon.html)
- Use resume/suspend methods
@@ -24,19 +33,19 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
- 12 files, 278.85KB, 1.71.0
Known issues:
-- Webpack watch doesn't work as expect - it does rebuild but not each change-save
-- Vscode's debugging needs to click twice - run and restart in order to launch the app (maybe only for macOS)
+- Webpack watch doesn't work as expected - it does rebuild but not on each change-save
+- VS Code's debugging needs to click twice - run and restart in order to launch the app (maybe only for macOS)
Notes:
-- **Finally! This extension works on vscode stable version** ððððĨģðĨ
-- It turns out that vscode doesn't ship mp3 codec with its ffmpeg library (it probably got removed in the past since this extension worked before) and recently add it back in v1.71 - [Issue](https://github.com/microsoft/vscode/ssues/48494)
-- Put back resume/suspend since the issue got fixed for electron/chromium - [Issue1052747](https://bugs.chromium.org/p/chromium/issues/detail?id=1052747) | [Issue1018499](https://bugs.chromium.org/p/chromium/issues/detail?id=1018499)
-- Reduce extension size - it was funny to see previous version use webpack to minimize the 1 file - `controller.js`
-- All previous versions of this extension will not work in any recent vscode versions
+- **Finally! This extension works on VS Code stable version** ððððĨģðĨ
+- It turns out that VS Code doesn't ship MP3 codec with its FFmpeg library (it probably got removed in the past since this extension worked before) and recently added it back in v1.71 - [Issue](https://github.com/microsoft/vscode/issues/48494)
+- Put back resume/suspend since the issue got fixed for Electron/Chromium - [Issue1052747](https://bugs.chromium.org/p/chromium/issues/detail?id=1052747) | [Issue1018499](https://bugs.chromium.org/p/chromium/issues/detail?id=1018499)
+- Reduce extension size - it was funny to see the previous version use Webpack to minimize the 1 file - `controller.js`
+- All previous versions of this extension will not work in any recent VS Code versions
## [1.1.0] - December 2019
- Display duration
-- Minor bugfixes
+- Minor bug fixes
- Remove [Semantic](https://semantic-ui.com)
- Switch method suspend/resume -> start/stop
- Update CSS
@@ -58,8 +67,8 @@ References:
Notes:
- Pumped up the VS Code requirement to 1.40 due to Web Audio API bug, probably from Chrome
-- Just right after the 1st release, the Web Audio API stops working on 1.30 to 1.39 of VS code (VS code 1.38 stable build is on Electron 4 | Chrome 69)
-- The extension works (except the pause/resume function - API bug again) on VS code exploration build 1.37 with ELectron 6 | Chrome 76
+- Just right after the 1st release, the Web Audio API stops working on 1.30 to 1.39 of VS Code (VS Code 1.38 stable build is on Electron 4 | Chrome 69)
+- The extension works (except the pause/resume function - API bug again) on VS Code exploration build 1.37 with Electron 6 | Chrome 76
- This extension will not be working for a while ðĨ
---
diff --git a/README.md b/README.md
index fa158ba..d7d6a0d 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,31 @@
# Spectrogram
-This is an extension that play and display spectrogram of mp3 and flac audio files. So, are you now curious to see how your favorite songs' spectrogram looks like? ðĶðĪŠð
+This is an extension that plays and displays spectrograms of mp3 and flac audio files. Are you curious to see how your favorite songs' spectrograms look? ðĶðĪŠð

## Release Notes
+### 3.0.0
+- Added support for `WAV`
+- Added seekbar
+- Added color configuration
+- Fixed bugs
+- Improved treeview
+- New logo and icon
+
### 2.0.0
-- Add `flac` support
-- Add 5s seeking buttons
-- Extension size greatly reduce
+- Added support for `FLAC`
+- Added 5-second seeking buttons
+- Greatly reduced extension size
### 1.1.0
-- Improve UI
-- Fix pause/resume
-- Fix minor bugs
-- Refactor code
+- Improved UI
+- Fixed pause/resume
+- Fixed minor bugs
+- Refactored code
### 1.0.1
-- Fix path issue for MacOS
+- Fixed path issue for macOS
### 1.0.0
- Initial release of Spectrogram
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..9ddaa93
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,37 @@
+import globals from 'globals'
+
+export default [
+ { ignores: ['dist'] },
+ {
+ files: ['**/*.js', '**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.commonjs,
+ ...globals.node,
+ ...globals.mocha,
+ ...globals.browser
+ },
+ ecmaVersion: 2022,
+ sourceType: 'module'
+ },
+ rules: {
+ 'comma-dangle': ['error', 'never'],
+ 'constructor-super': 'warn',
+ 'eol-last': ['error', 'always'],
+ 'max-len': ['error', { code: 120 }],
+ 'no-const-assign': 'warn',
+ 'no-this-before-super': 'warn',
+ 'no-throw-literal': 'warn',
+ 'no-trailing-spaces': 'error',
+ 'no-undef': 'warn',
+ 'no-unreachable': 'warn',
+ 'no-unused-vars': 'warn',
+ 'quote-props': ['error', 'as-needed'],
+ 'valid-typeof': 'warn',
+ curly: ['error', 'multi-or-nest'],
+ eqeqeq: 'error',
+ indent: ['error', 2],
+ quotes: ['error', 'single', { allowTemplateLiterals: true }],
+ semi: ['error', 'never']
+ }
+ }]
diff --git a/jsconfig.json b/jsconfig.json
index c08832a..1b5e6b9 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"module": "commonjs",
- "target": "es6",
+ "target": "ESNext",
"checkJs": true /* Typecheck .js files. */,
- "lib": ["es6", "dom"]
+ "lib": ["ESNext", "dom"]
},
"exclude": ["node_modules", "dist"]
}
diff --git a/media/icon.svg b/media/icon.svg
new file mode 100644
index 0000000..9b4e3a5
--- /dev/null
+++ b/media/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/media/spec.png b/media/spec.png
index 5e105ef..c0e3752 100644
Binary files a/media/spec.png and b/media/spec.png differ
diff --git a/media/vscodeignore/spec.png b/media/vscodeignore/spec.png
new file mode 100644
index 0000000..5e105ef
Binary files /dev/null and b/media/vscodeignore/spec.png differ
diff --git a/package.json b/package.json
index 33fbdc1..6d41ca8 100644
--- a/package.json
+++ b/package.json
@@ -29,10 +29,69 @@
"views": {
"explorer": [
{
- "id": "spectrogram-explorer",
- "name": "spectrogram"
+ "id": "spectrogram",
+ "name": "Spectrogram",
+ "icon": "media/icon.svg",
+ "contextualTitle": "Spectrogram"
}
]
+ },
+ "commands": [
+ {
+ "command": "spectrogram.revealInFileExplorer",
+ "title": "Reveal in File Explorer"
+ }
+ ],
+ "menus": {
+ "commandPalette": [
+ {
+ "command": "spectrogram.revealInFileExplorer",
+ "when": "false"
+ }
+ ],
+ "view/item/context": [
+ {
+ "command": "spectrogram.revealInFileExplorer",
+ "when": "view == spectrogram && viewItem == fileItem",
+ "group": "navigation"
+ }
+ ]
+ },
+ "configuration": {
+ "type": "object",
+ "properties": {
+ "spectrogram.rgbColor": {
+ "type": "object",
+ "default": {
+ "r": 100,
+ "g": 0,
+ "b": 0
+ },
+ "description": "RGB color for the spectrogram",
+ "properties": {
+ "r": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "g": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255
+ },
+ "b": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255
+ }
+ }
+ },
+ "spectrogram.showDuration": {
+ "type": "boolean",
+ "default": true,
+ "description": "Show audio's duration in the treeview"
+ }
+ }
}
},
"scripts": {
@@ -41,23 +100,29 @@
"package": "webpack --mode production --devtool hidden-source-map",
"wp": "webpack",
"wp-watch": "webpack --watch",
- "task-clean-output": "ts-node tasks.js clean"
+ "task-clean-output": "ts-node tasks.js clean",
+ "lint": "eslint .",
+ "lint-sum": "eslint --format summary-chart .",
+ "lint-fix": "eslint --fix ."
+ },
+ "dependencies": {
+ "music-metadata": "^10.8.0"
},
"devDependencies": {
- "@types/node": "^18.7.23",
- "@types/vscode": "^1.71.0",
- "@types/vscode-webview": "^1.57.0",
- "@vscode/codicons": "^0.0.32",
- "copy-webpack-plugin": "^11.0.0",
- "css-minimizer-webpack-plugin": "^4.1.0",
- "eslint": "^8.24.0",
- "eslint-plugin-jsdoc": "^39.3.6",
- "pug": "^3.0.2",
+ "@types/node": "^22.10.10",
+ "@types/vscode": "^1.96.0",
+ "@types/vscode-webview": "^1.57.5",
+ "@vscode/codicons": "^0.0.36",
+ "@vscode/vsce": "^3.2.1",
+ "copy-webpack-plugin": "^12.0.2",
+ "css-minimizer-webpack-plugin": "^7.0.0",
+ "eslint": "^9.19.0",
+ "eslint-formatter-summary-chart": "^0.3.0",
+ "eslint-plugin-jsdoc": "^50.6.3",
+ "pug": "^3.0.3",
"shelljs": "^0.8.5",
- "ts-node": "^10.9.1",
- "vsce": "^2.11.0",
- "webpack": "^5.74.0",
- "webpack-cli": "^4.10.0"
+ "webpack": "^5.97.1",
+ "webpack-cli": "^6.0.1"
},
"repository": {
"type": "git",
diff --git a/src/controller.js b/src/controller.js
index 8350453..e9b0599 100644
--- a/src/controller.js
+++ b/src/controller.js
@@ -7,17 +7,18 @@ const REFRESH_ICON = ''
;(() => {
// eslint-disable-next-line no-undef
const vscode = acquireVsCodeApi()
- const canvasElement = /** @type {HTMLCanvasElement} */ (document.getElementById('canvas'))
- const canvasContext = canvasElement.getContext('2d')
+ const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('canvas'))
+ const canvasContext = canvas.getContext('2d')
- canvasElement.height = 512
+ canvas.height = 512
const susresBtn = /** @type {HTMLButtonElement} */ (document.getElementById('susresBtn'))
const backBtn = /** @type {HTMLButtonElement} */ (document.getElementById('backBtn'))
const forwardBtn = /** @type {HTMLButtonElement} */ (document.getElementById('forwardBtn'))
const durationText = document.getElementById('duration')
const fileLabel = document.getElementById('label')
+ const seekbar = /** @type {HTMLInputElement} */ (document.getElementById('seekbar'))
- let currPlayer, id, durationId
+ let currPlayer, id, durationId, rgbColor
// Receive data from vscode
window.addEventListener('message', event => {
if (currPlayer) {
@@ -26,6 +27,7 @@ const REFRESH_ICON = ''
cancelAnimationFrame(id)
clearTimeout(durationId)
}
+ rgbColor = event.data.rgbColor
currPlayer = player(event.data)
})
@@ -33,9 +35,9 @@ const REFRESH_ICON = ''
* @param {{ path: string; name: string; }} file
*/
function player(file) {
- canvasElement.width = window.innerWidth - 10
- const WIDTH = canvasElement.width
- togglePlaybackButtons('loading')
+ canvas.width = window.innerWidth - 10
+ const WIDTH = canvas.width
+ togglePlaybackButtons('LOADING')
const audioCtx = new AudioContext()
const analyser = audioCtx.createAnalyser()
analyser.smoothingTimeConstant = 0.0
@@ -45,21 +47,32 @@ const REFRESH_ICON = ''
const eightBufferLength = 8 * bufferLength
const dataArray = new Uint8Array(bufferLength)
- const imageDataFrame = canvasContext.createImageData(2, canvasElement.height)
- // TODO: note this
- for (let i = 0; i < imageDataFrame.data.length * 4; i += 8) {
- for (let j = 3; j <= 7; j++) imageDataFrame.data[i + j] = 255 // = 0,0,0,255 | 255,255,255,255
+ const imageDataFrame = canvasContext.createImageData(2, canvas.height)
+ // Initialize the imageDataFrame with alternating black and white pixels
+ for (let i = 0; i < imageDataFrame.data.length; i += 8) {
+ // Set the first pixel to black (0, 0, 0, 255)
+ // This is the background color
+ imageDataFrame.data[i] = 0
+ imageDataFrame.data[i + 1] = 0
+ imageDataFrame.data[i + 2] = 0
+ imageDataFrame.data[i + 3] = 255
+
+ // Set the second pixel to white (255, 255, 255, 255)
+ // This is the color of the vertical moving line
+ imageDataFrame.data[i + 4] = 255
+ imageDataFrame.data[i + 5] = 255
+ imageDataFrame.data[i + 6] = 255
+ imageDataFrame.data[i + 7] = 255
}
const request = new XMLHttpRequest()
request.open('GET', file.path)
request.responseType = 'arraybuffer'
- request.onload = () => audioCtx.decodeAudioData(request.response, start, onBufferError)
+ request.onload = () => audioCtx.decodeAudioData(request.response, audioCtxSetup, onBufferError)
request.send()
fileLabel.innerHTML = file.name
let source = audioCtx.createBufferSource()
- // prettier-ignore
let buffer, startAt, length, lengthMs, played = 0, isEnded = false
susresBtn.onclick = () => {
@@ -87,7 +100,7 @@ const REFRESH_ICON = ''
startAt = Date.now()
played = 0
durationWatch()
- togglePlaybackButtons('playing')
+ togglePlaybackButtons('PLAYING')
} else {
// Was suspended so resume it
audioCtx.resume().then(() => {
@@ -101,10 +114,17 @@ const REFRESH_ICON = ''
backBtn.onclick = () => seek(-5000)
forwardBtn.onclick = () => seek(5000)
+ seekbar.oninput = () => seekTo(seekbar.value)
+ seekbar.onmousemove = (event) => showHoverDuration(event)
- function start(theBuffer) {
+ function audioCtxSetup(theBuffer) {
// This prevents clicking too fast - closed before starting
if (audioCtx.state === 'closed') return
+ if (audioCtx.state === 'suspended') {
+ // https://goo.gl/7K7WLu
+ vscode.postMessage({ type: 'INFO', message: 'Please click the play button - autoplay policy' })
+ }
+
isEnded = false
buffer = theBuffer
source.buffer = theBuffer
@@ -113,12 +133,17 @@ const REFRESH_ICON = ''
source.connect(audioCtx.destination)
source.connect(analyser)
source.onended = playEnd
- source.start()
- draw()
+ if (audioCtx.state === 'running') {
+ draw()
+ togglePlaybackButtons('PLAYING')
+ } else togglePlaybackButtons('READY')
+
+ source.start()
startAt = Date.now()
durationWatch()
- togglePlaybackButtons('playing')
+ seekbar.value = '0'
+ seekbar.max = lengthMs.toString()
}
function onBufferError(err) {
@@ -150,60 +175,95 @@ const REFRESH_ICON = ''
if (audioCtx.state === 'suspended') updateDurationText()
}
+ function seekTo(ms) {
+ played = parseInt(ms)
+ if (played < 0) played = 0
+ if (played > lengthMs) played = lengthMs
+
+ source.onended = null
+ source.disconnect(audioCtx.destination)
+ source.disconnect(analyser)
+
+ source = audioCtx.createBufferSource()
+ source.buffer = buffer
+ source.connect(audioCtx.destination)
+ source.connect(analyser)
+ source.onended = playEnd
+
+ startAt = Date.now()
+ source.start(0, played / 1000)
+
+ if (audioCtx.state === 'suspended') updateDurationText()
+ }
+
function playEnd() {
isEnded = true
clearTimeout(durationId)
- togglePlaybackButtons('ended')
+ togglePlaybackButtons('ENDED')
cancelAnimationFrame(id)
- vscode.postMessage({ type: 'Finish playing' })
+ vscode.postMessage({ type: 'DONE', message: 'Playing ended' })
}
function togglePlaybackButtons(state) {
switch (state) {
- case 'loading':
- susresBtn.textContent = 'Loading'
- susresBtn.classList.add('disabled')
- susresBtn.disabled = true
- backBtn.style.display = 'none'
- forwardBtn.style.display = 'none'
- break
- case 'playing':
- susresBtn.innerHTML = PAUSE_ICON
- susresBtn.classList.remove('disabled')
- susresBtn.disabled = false
- backBtn.style.display = 'inline-block'
- forwardBtn.style.display = 'inline-block'
- break
- case 'ended':
- susresBtn.innerHTML = REFRESH_ICON
- durationText.innerHTML = null
- backBtn.style.display = 'none'
- forwardBtn.style.display = 'none'
- break
+ case 'LOADING':
+ susresBtn.textContent = 'Loading...'
+ susresBtn.classList.add('disabled')
+ susresBtn.disabled = true
+ backBtn.style.display = 'none'
+ forwardBtn.style.display = 'none'
+ seekbar.style.display = 'none'
+ break
+ case 'READY':
+ case 'PLAYING':
+ susresBtn.innerHTML = state === 'PLAYING' ? PAUSE_ICON : PLAY_ICON
+ susresBtn.classList.remove('disabled')
+ susresBtn.disabled = false
+ backBtn.style.display = 'inline-block'
+ forwardBtn.style.display = 'inline-block'
+ seekbar.style.display = 'block'
+ break
+ case 'ENDED':
+ susresBtn.innerHTML = REFRESH_ICON
+ durationText.innerHTML = null
+ backBtn.style.display = 'none'
+ forwardBtn.style.display = 'none'
+ seekbar.style.display = 'none'
+ break
}
}
function durationWatch() {
- if (audioCtx.state === 'running') {
- updateDurationText()
- durationId = setTimeout(durationWatch, 1000)
- }
+ if (audioCtx.state !== 'running') return
+ updateDurationText()
+ durationId = setTimeout(durationWatch, 1000)
}
function updateDurationText() {
const durationPlayed = Date.now() - startAt + played
durationText.innerHTML = `- ${fmtMSS(Math.trunc(durationPlayed / 1000))} | ${fmtMSS(Math.trunc(length))}`
+ seekbar.value = durationPlayed.toString()
}
function fmtMSS(s) {
return (s - (s %= 60)) / 60 + (9 < s ? ':' : ':0') + s
}
+ function showHoverDuration(event) {
+ const hoverTime = (event.offsetX / seekbar.clientWidth) * lengthMs
+ durationText.innerHTML = `- ${fmtMSS(Math.trunc(hoverTime / 1000))} | ${fmtMSS(Math.trunc(length))}`
+ }
+
let x = 0
function draw() {
id = requestAnimationFrame(draw)
analyser.getByteFrequencyData(dataArray)
- for (let i = 0, y = eightBufferLength; i < bufferLength; i++, y -= 8) imageDataFrame.data[y] = dataArray[i]
+ for (let i = 0, y = eightBufferLength; i < bufferLength; i++, y -= 8) {
+ imageDataFrame.data[y] = rgbColor.r
+ imageDataFrame.data[y + 1] = rgbColor.g
+ imageDataFrame.data[y + 2] = rgbColor.b
+ imageDataFrame.data[y + 3] = dataArray[i]
+ }
canvasContext.putImageData(imageDataFrame, x, 0)
x < WIDTH ? x++ : (x = 0)
}
diff --git a/src/extension.js b/src/extension.js
index 8b13965..16e8b1b 100644
--- a/src/extension.js
+++ b/src/extension.js
@@ -1,20 +1,20 @@
'use strict'
const path = require('path')
-// @ts-ignore
-const { ExtensionContext, Uri, window } = require('vscode')
-const { TreeView } = require('./treeView')
+const { Uri, window, workspace } = require('vscode')
+const { TreeView } = require('./treeview')
const { SpecWebviewPanel } = require('./webview')
/**
- * @param {ExtensionContext} context
+ * @param {import('vscode').ExtensionContext} context
*/
function activate(context) {
- const specExplorer = TreeView.create(context)
+ const specExplorer = TreeView.create()
specExplorer.onDidChangeSelection(file => {
try {
file.selection[0].fullFilePath
+ // eslint-disable-next-line no-unused-vars
} catch (error) {
- window.showInformationMessage('Slow down ðĩ')
+ window.showInformationMessage('Slow down ðĩâðŦ')
return
}
const { fullFilePath } = file.selection[0]
@@ -25,11 +25,13 @@ function activate(context) {
const songPath = Uri.file(fullFilePath)
SpecWebviewPanel.createOrShow(context.extensionPath)
const panel = SpecWebviewPanel.currentPanel.panel.webview.asWebviewUri(songPath)
- SpecWebviewPanel.currentPanel.panel.webview.postMessage({ path: `${panel}`, name: label })
+ const rgbColor = workspace.getConfiguration('spectrogram').get('rgbColor')
+ SpecWebviewPanel.currentPanel.panel.webview.postMessage({ path: `${panel}`, name: label, rgbColor })
SpecWebviewPanel.currentPanel.panel.webview.onDidReceiveMessage(
({ type, message }) => {
- if (type == 'finished') window.showInformationMessage('Finished Playing ð')
- else if (type == 'error') window.showErrorMessage(`${message} ðĩ`)
+ if (type === 'DONE') window.showInformationMessage(`${message} ð`)
+ else if (type === 'ERROR') window.showErrorMessage(`${message} ðĩ`)
+ else window.showInformationMessage(message)
},
undefined,
context.subscriptions
diff --git a/src/index.pug b/src/index.pug
index 15d9add..9e6873d 100644
--- a/src/index.pug
+++ b/src/index.pug
@@ -14,7 +14,7 @@ html
body
canvas#canvas
- div
+ div.text-center
button#backBtn.d-none
i.codicon.codicon-play.me-n5
i.codicon.codicon-play
@@ -22,6 +22,7 @@ html
button#forwardBtn.d-none
i.codicon.codicon-play.me-n5
i.codicon.codicon-play
+ input#seekbar(type='range', min='0', value='0', step='1')
span#label
span#duration
script(nonce=`${nonce}`, src=`${controllerUri}`)
diff --git a/src/style.css b/src/style.css
index 5b423c1..39c2047 100644
--- a/src/style.css
+++ b/src/style.css
@@ -10,6 +10,12 @@
padding: 0 5px;
}
+#seekbar {
+ cursor: pointer;
+ margin: auto;
+ width: 90%;
+}
+
body {
padding-left: 5px;
}
@@ -46,3 +52,7 @@ button:hover {
.me-n5 {
margin-right: -5px;
}
+
+.text-center {
+ text-align: center;
+}
diff --git a/src/treeview.js b/src/treeview.js
index d676467..702cacc 100644
--- a/src/treeview.js
+++ b/src/treeview.js
@@ -2,13 +2,19 @@
const vscode = require('vscode')
const path = require('path')
const fs = require('fs')
+const { loadMusicMetadata } = require('music-metadata') // Import loadMusicMetadata
class TreeView {
- static create(context) {
+ static create() {
const path = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : null
const specTreeDataProvider = new SpecTreeDataProvider(path)
- context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('spec', specTreeDataProvider))
- return vscode.window.createTreeView('spectrogram-explorer', {
+
+ vscode.commands.registerCommand('spectrogram.revealInFileExplorer', (fileItem) => {
+ const uri = vscode.Uri.file(fileItem.fullFilePath)
+ vscode.commands.executeCommand('revealFileInOS', uri)
+ })
+
+ return vscode.window.createTreeView('spectrogram', {
treeDataProvider: specTreeDataProvider,
showCollapseAll: true
})
@@ -20,6 +26,14 @@ class SpecTreeDataProvider {
this.workspaceRoot = workspaceRoot
this._onDidChangeTreeData = new vscode.EventEmitter()
this.onDidChangeTreeData = this._onDidChangeTreeData.event
+ this.showDuration = vscode.workspace.getConfiguration('spectrogram').get('showDuration', true)
+
+ vscode.workspace.onDidChangeConfiguration(event => {
+ if (event.affectsConfiguration('spectrogram.showDuration')) {
+ this.showDuration = vscode.workspace.getConfiguration('spectrogram').get('showDuration', true)
+ this.refresh()
+ }
+ })
}
refresh() {
@@ -35,20 +49,14 @@ class SpecTreeDataProvider {
vscode.window.showInformationMessage('Please open a folder')
return Promise.resolve([])
}
-
if (element) return this.getFiles(path.join(element.filePath, element.label))
else return this.getFiles(this.workspaceRoot)
}
- // ??
- provideTextDocumentContent(uri, token) {
- return uri + token
- }
-
- getFiles(thePath) {
+ async getFiles(thePath) {
// name
- const toFileItem = (name, targetPath, type) => {
- if (type == 'directory') {
+ const toFileItem = async (name, targetPath, type) => {
+ if (type === 'directory') {
let descriptionText, collapsibleState
const filesCount = fs.readdirSync(path.join(targetPath, name)).filter(this.isSupportedMedia).length
if (filesCount > 0) {
@@ -60,31 +68,51 @@ class SpecTreeDataProvider {
descriptionText = 'Empty'
}
return new fileItem(name, targetPath, collapsibleState, descriptionText)
- } else return new fileItem(name, targetPath, vscode.TreeItemCollapsibleState.None)
+ } else {
+ let descriptionText = ''
+ if (this.showDuration) descriptionText = await this.getAudioDuration(path.join(targetPath, name))
+ return new fileItem(name, targetPath, vscode.TreeItemCollapsibleState.None, descriptionText)
+ }
}
const isDirectory = name => fs.lstatSync(path.join(thePath, name)).isDirectory()
const subDirs = fs.readdirSync(thePath).filter(isDirectory)
- const mp3s = fs.readdirSync(thePath).filter(this.isSupportedMedia)
+ const audios = fs.readdirSync(thePath).filter(this.isSupportedMedia)
+
+ const subDirsItem = await Promise.all(subDirs.map(name => toFileItem(name, thePath, 'directory')))
+ const audioFilesItem = await Promise.all(audios.map(name => toFileItem(name, thePath, 'audio')))
+
+ return subDirsItem.concat(audioFilesItem)
+ }
- const subDirsItem = subDirs.map(name => toFileItem(name, thePath, 'directory'))
- const mp3filesItem = mp3s.map(name => toFileItem(name, thePath, 'audio'))
+ async getAudioDuration(filePath) {
+ const mm = await loadMusicMetadata() // Dynamically load the ESM module
+ return mm.parseFile(filePath).then(metadata => {
+ const duration = metadata.format.duration
+ return this.formatDuration(duration)
+ }).catch(err => {
+ console.error(`Error parsing file ${filePath}:`, err)
+ return 'Unknown duration'
+ })
+ }
- return subDirsItem.concat(mp3filesItem)
+ formatDuration(duration) {
+ if (!duration) return 'Unknown duration'
+ const minutes = Math.floor(duration / 60)
+ const seconds = Math.floor(duration % 60)
+ return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
}
isSupportedMedia(name) {
- if (name.indexOf('.mp3') != -1) return true
- if (name.indexOf('.flac') != -1) return true
+ return /\.(mp3|flac|wav)$/i.test(name)
}
}
class fileItem extends vscode.TreeItem {
- constructor(label, filePath, collapsibleState, descriptionText, command) {
+ constructor(label, filePath, collapsibleState, descriptionText) {
super(label, collapsibleState)
this.collapsibleState = collapsibleState
- this.command = command
- this.contextValue = 'dependency'
+ this.contextValue = 'fileItem'
this.description = descriptionText
this.filePath = filePath
diff --git a/src/webview.js b/src/webview.js
index efbed3c..e7e5dc2 100644
--- a/src/webview.js
+++ b/src/webview.js
@@ -4,19 +4,7 @@ const os = require('os')
const path = require('path')
const pug = require('pug')
-class SpecWebviewPanel {
- constructor(panel, extensionPath) {
- this.disposables = []
- this.panel = panel
- this.extensionPath = extensionPath
-
- this.panel.onDidDispose(() => this.dispose(), null, this.disposables)
- // eslint-disable-next-line no-unused-vars
- this.panel.onDidChangeViewState(event => {}, null, this.disposables)
- // eslint-disable-next-line no-unused-vars
- this.panel.webview.onDidReceiveMessage(message => {}, null, this.disposables)
- this.panel.webview.html = this.getHtmlForWebview(extensionPath)
- }
+export class SpecWebviewPanel {
static createOrShow(extensionPath) {
const column = window.activeTextEditor ? window.activeTextEditor.viewColumn : undefined
@@ -29,8 +17,11 @@ class SpecWebviewPanel {
const panelSetting = { enableScripts: true }
- if (os.platform() == 'darwin') {
- panelSetting.localResourceRoots = [Uri.file(__dirname), Uri.file(workspace.workspaceFolders[0].uri.fsPath)]
+ if (os.platform() === 'darwin') {
+ panelSetting.localResourceRoots = [
+ Uri.file(__dirname),
+ Uri.file(workspace.workspaceFolders[0].uri.fsPath)
+ ]
}
const viewColumn = column || ViewColumn.One
@@ -42,6 +33,19 @@ class SpecWebviewPanel {
SpecWebviewPanel.currentPanel = new SpecWebviewPanel(panel, extensionPath)
}
+ constructor(panel, extensionPath) {
+ this.disposables = []
+ this.panel = panel
+ this.extensionPath = extensionPath
+
+ this.panel.onDidDispose(() => this.dispose(), null, this.disposables)
+ // eslint-disable-next-line no-unused-vars
+ this.panel.onDidChangeViewState(event => {}, null, this.disposables)
+ // eslint-disable-next-line no-unused-vars
+ this.panel.webview.onDidReceiveMessage(message => {}, null, this.disposables)
+ this.panel.webview.html = this.getHtmlForWebview(extensionPath)
+ }
+
dispose() {
SpecWebviewPanel.currentPanel = undefined
@@ -71,5 +75,3 @@ function getNonce() {
for (let i = 0; i < 32; i++) text += possible.charAt(Math.floor(Math.random() * possible.length))
return text
}
-
-exports.SpecWebviewPanel = SpecWebviewPanel
diff --git a/vsc-extension-quickstart.md b/vsc-extension-quickstart.md
index e6da2a5..70c8d64 100644
--- a/vsc-extension-quickstart.md
+++ b/vsc-extension-quickstart.md
@@ -27,13 +27,16 @@
## Run tests
-* Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`.
-* Press `F5` to run the tests in a new window with your extension loaded.
-* See the output of the test result in the debug console.
-* Make changes to `src/test/suite/extension.test.js` or create new test files inside the `test/suite` folder.
- * The provided test runner will only consider files matching the name pattern `**.test.ts`.
+* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
+* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
+* See the output of the test result in the Test Results view.
+* Make changes to `test/extension.test.js` or create new test files inside the `test` folder.
+ * The provided test runner will only consider files matching the name pattern `**.test.js`.
* You can create folders inside the `test` folder to structure your tests any way you want.
+
## Go further
- * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace.
+ * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
+ * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
+ * Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users.
diff --git a/webpack.config.js b/webpack.config.js
index 8532112..a9a1067 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -4,6 +4,7 @@ const TerserPlugin = require('terser-webpack-plugin')
const path = require('path')
const CODICON_PATH = '@vscode/codicons/dist/'
+const isProduction = process.env.NODE_ENV === 'production'
module.exports = {
target: 'node',
@@ -30,7 +31,7 @@ module.exports = {
})
],
optimization: {
- minimize: true,
- minimizer: [new TerserPlugin({ extractComments: false }), new CssMinimizerPlugin()]
+ minimize: isProduction,
+ minimizer: isProduction ? [new TerserPlugin({ extractComments: false }), new CssMinimizerPlugin()] : []
}
}