From eb362b4b40bad0292259b513bd0e8e3f64b75af9 Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 5 Nov 2019 01:15:14 +0100 Subject: [PATCH] Display bookmarked fragments Using the (tweaked) annotation feature in waveform-playlist. --- app/assets/main.css | 76 ++++++++++++++++++++++++++ app/display-bookmarks/background.js | 56 +++++++++++++++++++ app/display-bookmarks/contentscript.js | 67 +++++++++++++++++++++++ app/scripts/background.js | 5 +- app/scripts/contentscript.js | 2 + package-lock.json | 54 ++++++++++++++---- package.json | 5 +- 7 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 app/display-bookmarks/background.js create mode 100644 app/display-bookmarks/contentscript.js diff --git a/app/assets/main.css b/app/assets/main.css index 9b696a4..fee1ff3 100644 --- a/app/assets/main.css +++ b/app/assets/main.css @@ -191,3 +191,79 @@ input#volume:focus { border-right: 6px solid transparent; border-bottom: 6px solid #666; } + +.playlist .annotations .annotations-text { + display: none; +} + +.playlist .annotations a.annotation-box { + color: unset; + text-decoration: unset; +} + +.playlist .annotations .annotation-box { + background: #ffff0099; + border: 1px solid #ff8800; + border-radius: 2px; + padding: 0; + z-index: 3; +} + +.playlist .annotations .annotation-box .id { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0 10px; + box-sizing: border-box; +} + +.playlist .annotations .annotation-box .resize-handle { + display: none; +} + +.playlist .annotations .annotation-box.segment-annotation { + border-top: none; + border-left: 2px solid #ff8800; + border-right: 2px solid #ff8800; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.playlist .annotations .annotation-box.point-annotation::before { + /* Draw a triangle at the top */ + content: ''; + position: absolute; + left: 7px; + top: -7px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ff8800; +} + +.playlist .annotations .annotation-box.segment-annotation::before { + /* Draw a triangle at the top */ + content: ''; + position: absolute; + left: -7px; + top: -6px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ff8800; +} + +.playlist .annotations .annotation-box.segment-annotation::after { + /* Draw a triangle at the top */ + content: ''; + position: absolute; + right: -7px; + top: -6px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ff8800; +} diff --git a/app/display-bookmarks/background.js b/app/display-bookmarks/background.js new file mode 100644 index 0000000..98c7620 --- /dev/null +++ b/app/display-bookmarks/background.js @@ -0,0 +1,56 @@ +import whenAllSettled from 'when-all-settled'; +import { makeRemotelyCallable, remoteFunction } from 'webextension-rpc'; + +function normaliseUrl(url) { + // Remove the fragment identifier, if any. + url = url.split('#')[0] + // Ignore the URL scheme for authority based URLs (see e.g. RFC 2396 section 3). In other words, + // we disregard whether a document was loaded through https or http or whatever other protocol. + url = url.replace(/^[^:]+:\/\//, '//') + return url +} + +async function retrieveBookmarks({ url }) { + const bookmarkTree = await browser.bookmarks.getTree(); + + const toScan = [...bookmarkTree]; + const allBookmarks = []; + + while (toScan.length > 0) { + const next = toScan.shift(); + if (next.url) { + allBookmarks.push(next); + } + if (next.children) { + toScan.push(...next.children); + } + } + + const matchingBookmarks = allBookmarks.filter( + bookmark => normaliseUrl(bookmark.url) === normaliseUrl(url) + ); + + return matchingBookmarks; +} + +makeRemotelyCallable({ + retrieveBookmarks, +}); + +async function onBookmarkChange() { + // Update the bookmarks in every tab. We simply message every tab, as we do not know which ones + // contain our audio players. + const tabs = await browser.tabs.query({ status: 'complete' }); + const refreshingPromises = tabs.map(tab => + remoteFunction('displayBookmarksInPage', { tabId: tab.id })() + ); + // Wait until all tabs completed, ignoring any errors. + await whenAllSettled(refreshingPromises); +} + +browser.bookmarks.onCreated.addListener(onBookmarkChange); +browser.bookmarks.onRemoved.addListener(onBookmarkChange); +browser.bookmarks.onChanged.addListener(onBookmarkChange); +if (browser.bookmarks.onImportEnded) { + browser.bookmarks.onImportEnded.addListener(onBookmarkChange); +} diff --git a/app/display-bookmarks/contentscript.js b/app/display-bookmarks/contentscript.js new file mode 100644 index 0000000..4d78063 --- /dev/null +++ b/app/display-bookmarks/contentscript.js @@ -0,0 +1,67 @@ +import { makeRemotelyCallable, remoteFunction } from 'webextension-rpc'; +import { parseMediaFragmentIdentifier } from '../util/media-fragment-identifier'; + +const retrieveBookmarks = remoteFunction('retrieveBookmarks'); + +function fragmentIdentifierToSelector(fragmentIdentifier) { + if (fragmentIdentifier.startsWith('#')) { + fragmentIdentifier = fragmentIdentifier.substring(1); + } + const selector = { + type: 'FragmentSelector', + value: fragmentIdentifier, + }; + return selector; +} + +function selectorToTime(selector) { + if (selector.type !== 'FragmentSelector') { + throw new Error(`Unsupported selector type: '${selector.type}'`); + } + const { start, end } = parseMediaFragmentIdentifier(selector.value); + return { start, end }; +} + +export default async function init(playlist) { + const eventEmitter = playlist.getEventEmitter(); + + async function displayBookmarksInPage() { + const bookmarks = await retrieveBookmarks({ url: document.URL }); + const bookmarksWithTime = bookmarks.map(bookmark => { + const fragmentIdentifier = bookmark.url.split('#')[1]; + if (fragmentIdentifier === undefined) return null; + + const selector = fragmentIdentifierToSelector(fragmentIdentifier); + try { + const { start, end } = selectorToTime(selector); + return { bookmark, start, end }; + } catch (err) { + // Likely a fragment identifier we do not understand; skip it. + return null; + } + }).filter(value => value !== null); + + bookmarksWithTime.sort(({ start: t1 }, { start: t2 }) => t1 - t2); + + const annotations = bookmarksWithTime.map(({ bookmark, start, end }) => ({ + start, + end, + id: bookmark.title, + elementType: 'a', + elementAttributes: { + title: bookmark.title, + href: bookmark.url, + onclick: () => { eventEmitter.emit('select', start, end); }, + }, + })); + + playlist.setAnnotations({ annotations, annotationFormat: 'raw', editable: false }); + playlist.drawRequest(); + } + + makeRemotelyCallable({ + displayBookmarksInPage, + }); + + await displayBookmarksInPage(); +} diff --git a/app/scripts/background.js b/app/scripts/background.js index a5866e1..c491fee 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,2 +1,3 @@ -import '../audio-player/background.js' -import '../create-bookmarks/background.js' +import '../audio-player/background.js'; +import '../create-bookmarks/background.js'; +import '../display-bookmarks/background.js'; diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 9535b8b..5dbb372 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -1,9 +1,11 @@ import initAudioPlayer from '../audio-player/contentscript.js'; import initCreateBookmarks from '../create-bookmarks/contentscript.js'; +import initDisplayBookmarks from '../display-bookmarks/contentscript.js'; async function init() { const playlist = await initAudioPlayer(); await initCreateBookmarks(playlist); + await initDisplayBookmarks(playlist); } init(); diff --git a/package-lock.json b/package-lock.json index 46dbb7c..173daf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3565,13 +3565,13 @@ } }, "es5-ext": { - "version": "0.10.51", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.51.tgz", - "integrity": "sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ==", + "version": "0.10.52", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.52.tgz", + "integrity": "sha512-bWCbE9fbpYQY4CU6hJbJ1vSz70EClMlDgJ7BmwI+zEJhxrwjesZRPglGJlsZhu0334U3hI+gaspwksH9IGD6ag==", "requires": { "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "^1.0.0" + "es6-symbol": "~3.1.2", + "next-tick": "~1.0.0" } }, "es6-error": { @@ -3591,12 +3591,12 @@ } }, "es6-symbol": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.2.tgz", - "integrity": "sha512-/ZypxQsArlv+KHpGvng52/Iz8by3EQPxhmbuz8yFG89N/caTFBSbcXONDw0aMjy827gQg26XAjP4uXFvnfINmQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "requires": { "d": "^1.0.1", - "es5-ext": "^0.10.51" + "ext": "^1.1.2" } }, "escape-string-regexp": { @@ -3779,6 +3779,21 @@ } } }, + "ext": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.2.0.tgz", + "integrity": "sha512-0ccUQK/9e3NreLFg6K6np8aPyRgwycx+oFGtfx1dSp7Wj00Ozw9r05FgBRlzjf2XBM7LAzwgLyDscRrtSU91hA==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==" + } + } + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -5675,6 +5690,11 @@ "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" }, + "lodash.clamp": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz", + "integrity": "sha1-XCS+3u7vB1NWDcK0y0Zx+Qpt36o=" + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -5686,6 +5706,11 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" }, + "lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==" + }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -8469,15 +8494,17 @@ } }, "waveform-playlist": { - "version": "git://github.com/Treora/waveform-playlist.git#ad5d18c9d25a0db3cfa2e0d59305ee1c12018e2f", - "from": "git://github.com/Treora/waveform-playlist.git", + "version": "git://github.com/Treora/waveform-playlist.git#1d4195281824c2b2f3a586000028dad2e36fdccf", + "from": "git://github.com/Treora/waveform-playlist.git#1d41952", "requires": { "event-emitter": "^0.3.4", "fade-curves": "^1.0.2", "fade-maker": "^1.0.3", "inline-worker": "^1.1.0", "lodash.assign": "^4.0.0", + "lodash.clamp": "^4.0.3", "lodash.defaults": "^4.0.0", + "lodash.defaultsdeep": "^4.6.1", "lodash.forown": "^4.0.0", "mucss": "^1.1.5", "uuid": "^2.0.1", @@ -8705,6 +8732,11 @@ "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", "dev": true }, + "when-all-settled": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/when-all-settled/-/when-all-settled-0.1.2.tgz", + "integrity": "sha512-L45/IaOrtjm84lxBpbpdtzU49BpCo0jbKljFlWHtp/NHzN6AnV4dzVZzwDCw/R9TU36ADQxFnZDhKA4pzTlUjQ==" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 87ac778..f168b05 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,9 @@ "dependencies": { "delay": "^4.3.0", "nanohtml": "^1.8.1", - "waveform-playlist": "git://github.com/Treora/waveform-playlist", - "webextension-rpc": "^0.1.0" + "waveform-playlist": "git://github.com/Treora/waveform-playlist#1d41952", + "webextension-rpc": "^0.1.0", + "when-all-settled": "^0.1.2" }, "ava": { "require": [