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-right: 6px solid transparent; | ||||
border-bottom: 6px solid #666; | 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 initAudioPlayer from '../audio-player/contentscript.js'; | ||||
import initCreateBookmarks from '../create-bookmarks/contentscript.js'; | import initCreateBookmarks from '../create-bookmarks/contentscript.js'; | ||||
import initDisplayBookmarks from '../display-bookmarks/contentscript.js'; | |||||
async function init() { | async function init() { | ||||
const playlist = await initAudioPlayer(); | const playlist = await initAudioPlayer(); | ||||
await initCreateBookmarks(playlist); | await initCreateBookmarks(playlist); | ||||
await initDisplayBookmarks(playlist); | |||||
} | } | ||||
init(); | init(); |
@@ -3565,13 +3565,13 @@ | |||||
} | } | ||||
}, | }, | ||||
"es5-ext": { | "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": { | "requires": { | ||||
"es6-iterator": "~2.0.3", | "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": { | "es6-error": { | ||||
@@ -3591,12 +3591,12 @@ | |||||
} | } | ||||
}, | }, | ||||
"es6-symbol": { | "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": { | "requires": { | ||||
"d": "^1.0.1", | "d": "^1.0.1", | ||||
"es5-ext": "^0.10.51" | |||||
"ext": "^1.1.2" | |||||
} | } | ||||
}, | }, | ||||
"escape-string-regexp": { | "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": { | "extend-shallow": { | ||||
"version": "3.0.2", | "version": "3.0.2", | ||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", | "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", | "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", | ||||
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" | "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": { | "lodash.clonedeep": { | ||||
"version": "4.5.0", | "version": "4.5.0", | ||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", | "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", | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", | ||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" | "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": { | "lodash.flattendeep": { | ||||
"version": "4.4.0", | "version": "4.4.0", | ||||
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", | "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", | ||||
@@ -8469,15 +8494,17 @@ | |||||
} | } | ||||
}, | }, | ||||
"waveform-playlist": { | "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": { | "requires": { | ||||
"event-emitter": "^0.3.4", | "event-emitter": "^0.3.4", | ||||
"fade-curves": "^1.0.2", | "fade-curves": "^1.0.2", | ||||
"fade-maker": "^1.0.3", | "fade-maker": "^1.0.3", | ||||
"inline-worker": "^1.1.0", | "inline-worker": "^1.1.0", | ||||
"lodash.assign": "^4.0.0", | "lodash.assign": "^4.0.0", | ||||
"lodash.clamp": "^4.0.3", | |||||
"lodash.defaults": "^4.0.0", | "lodash.defaults": "^4.0.0", | ||||
"lodash.defaultsdeep": "^4.6.1", | |||||
"lodash.forown": "^4.0.0", | "lodash.forown": "^4.0.0", | ||||
"mucss": "^1.1.5", | "mucss": "^1.1.5", | ||||
"uuid": "^2.0.1", | "uuid": "^2.0.1", | ||||
@@ -8705,6 +8732,11 @@ | |||||
"integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", | "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", | ||||
"dev": true | "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": { | "which": { | ||||
"version": "1.3.1", | "version": "1.3.1", | ||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", | ||||
@@ -21,8 +21,9 @@ | |||||
"dependencies": { | "dependencies": { | ||||
"delay": "^4.3.0", | "delay": "^4.3.0", | ||||
"nanohtml": "^1.8.1", | "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": { | "ava": { | ||||
"require": [ | "require": [ | ||||