Browse Source

Display bookmarked fragments

Using the (tweaked) annotation feature in waveform-playlist.
tags/v0.1.0
Gerben 4 years ago
parent
commit
eb362b4b40
7 changed files with 250 additions and 15 deletions
  1. +76
    -0
      app/assets/main.css
  2. +56
    -0
      app/display-bookmarks/background.js
  3. +67
    -0
      app/display-bookmarks/contentscript.js
  4. +3
    -2
      app/scripts/background.js
  5. +2
    -0
      app/scripts/contentscript.js
  6. +43
    -11
      package-lock.json
  7. +3
    -2
      package.json

+ 76
- 0
app/assets/main.css View File

@@ -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;
}

+ 56
- 0
app/display-bookmarks/background.js View File

@@ -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);
}

+ 67
- 0
app/display-bookmarks/contentscript.js View File

@@ -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();
}

+ 3
- 2
app/scripts/background.js View File

@@ -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';

+ 2
- 0
app/scripts/contentscript.js View File

@@ -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();

+ 43
- 11
package-lock.json View File

@@ -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",


+ 3
- 2
package.json View File

@@ -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": [


Loading…
Cancel
Save