Using the (tweaked) annotation feature in waveform-playlist.tags/v0.1.0
@@ -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; | |||
} |
@@ -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); | |||
} |
@@ -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(); | |||
} |
@@ -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'; |
@@ -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(); |
@@ -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", | |||
@@ -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": [ | |||