From a55a4500ee8262d4c9c50d7c769ebc8cb742bb93 Mon Sep 17 00:00:00 2001 From: Florrie Date: Sat, 23 Mar 2019 19:25:26 -0300 Subject: Rule Scope --- extension/background.html | 7 + extension/background.js | 28 +++- extension/interactive-bgm.js | 10 +- extension/lib.js | 34 ++++ extension/manifest.json | 2 +- extension/popup/index.html | 31 +++- extension/popup/main.js | 374 ++++++++++++++++++++++++++++++++++++------- extension/popup/style.css | 78 +++++++-- 8 files changed, 476 insertions(+), 88 deletions(-) create mode 100644 extension/background.html create mode 100644 extension/lib.js (limited to 'extension') diff --git a/extension/background.html b/extension/background.html new file mode 100644 index 0000000..8c0f195 --- /dev/null +++ b/extension/background.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/extension/background.js b/extension/background.js index c450c9d..3a51809 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,3 +1,5 @@ +import {getRuleScoreOnPage} from './lib.js'; + console.log('Start'); const port = browser.runtime.connectNative('interactive_bgm'); @@ -29,8 +31,8 @@ port.onMessage.addListener(({type, trackName}) => { const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); -browser.runtime.onMessage.addListener(async ({hostname, type, base64, trackName}, sender, sendResponse) => { - if (hostname) { +browser.runtime.onMessage.addListener(async ({urlString, music, type, base64, trackName}, sender, sendResponse) => { + if (urlString) { browser.storage.sync.get(['siteSettings', 'disableEverywhere']) .then(({siteSettings, disableEverywhere}) => { if (disableEverywhere) { @@ -38,12 +40,28 @@ browser.runtime.onMessage.addListener(async ({hostname, type, base64, trackName} return; } - const mode = siteSettings[hostname]; + const url = new URL(urlString); + + const allRules = siteSettings; - if (mode) { - port.postMessage(mode.map(track => ({track, volume: 100}))); + const rulesOnThisPage = allRules.map(rule => ({ + rule, + score: getRuleScoreOnPage(rule, urlString) + })).filter(obj => obj.score > 0); + + if (!rulesOnThisPage.length) { + return; } + + rulesOnThisPage.sort((a, b) => b.score - a.score); + + const {rule} = rulesOnThisPage[0]; + const {music} = rule; + + port.postMessage(music.map(track => ({track, volume: 100}))); }); + } else if (music) { + port.postMessage(music.map(track => ({track, volume: 100}))); } else if (type === 'uploadTrack' && trackName && base64) { port.postMessage({type: 'uploadTrack', trackName, base64}); return new Promise(resolve => { diff --git a/extension/interactive-bgm.js b/extension/interactive-bgm.js index 773391f..5db7d58 100644 --- a/extension/interactive-bgm.js +++ b/extension/interactive-bgm.js @@ -1,18 +1,18 @@ -const sendHostname = () => { - browser.runtime.sendMessage({hostname: location.hostname}); +const sendURL = () => { + browser.runtime.sendMessage({urlString: location.toString()}); }; -window.addEventListener('focus', sendHostname); +window.addEventListener('focus', sendURL); document.addEventListener('visibilitychange', () => { if (!document.hidden) { - sendHostname(); + sendURL(); } }); window.addEventListener('load', () => { if (!document.hidden) { - sendHostname(); + sendURL(); } }); diff --git a/extension/lib.js b/extension/lib.js new file mode 100644 index 0000000..acb5057 --- /dev/null +++ b/extension/lib.js @@ -0,0 +1,34 @@ +export function getURLParts(urlString) { + const {hostname, pathname} = new URL(urlString); + + return { + hostnameParts: hostname.split('.'), + pathnameParts: pathname.slice(1).split('/').filter(Boolean) + }; +} + +export function getRuleScoreOnPage(rule, urlString) { + const {hostnameMatch, pathnameMatch} = rule; + const {hostnameParts, pathnameParts} = getURLParts(urlString); + + // If we succeed at all, no matter how badly, the score should be non-zero! + let score = 1; + + for (let i = 0; i < hostnameMatch.length; i++) { + if (hostnameParts[hostnameParts.length - 1 - i] !== hostnameMatch[i]) { + return 0; + } + + score++; + } + + for (let i = 0; i < pathnameMatch.length; i++) { + if (pathnameParts[i] !== pathnameMatch[i]) { + return 0; + } + + score++; + } + + return score; +} diff --git a/extension/manifest.json b/extension/manifest.json index 7dbf537..dde69a4 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -34,6 +34,6 @@ ], "background": { - "scripts": ["background.js"] + "page": "background.html" } } diff --git a/extension/popup/index.html b/extension/popup/index.html index 7a9e920..fe03bff 100644 --- a/extension/popup/index.html +++ b/extension/popup/index.html @@ -9,16 +9,31 @@

Loading...

-
-

-

Tracks

- -

General Settings

-

+
+ +
+
+

+

(Applies to: *)

+

Tracks

+
    +

    General Settings

    +

    +
    +
    +

    No rule selected. Please select or create one!

    +
    +
    -

    Sorry, this page doesn't appear to have a hostname. We can't configure music here.

    +

    Sorry, this page doesn't appear to have a hostname.
    We can't configure music here.

    - + diff --git a/extension/popup/main.js b/extension/popup/main.js index 06aca18..c628447 100644 --- a/extension/popup/main.js +++ b/extension/popup/main.js @@ -1,3 +1,15 @@ +import {getRuleScoreOnPage, getURLParts} from '../lib.js'; + +function last(arr) { + return arr[arr.length - 1]; +} + +function clearChildren(el) { + while (el.firstChild) { + el.removeChild(el.firstChild); + } +} + function changeScreen(id) { for (const screen of document.getElementsByClassName('screen')) { if (screen.id === id) { @@ -8,9 +20,19 @@ function changeScreen(id) { } } +function changeContentScreen(id) { + for (const screen of document.getElementsByClassName('content-screen')) { + if (screen.id === id) { + screen.classList.add('visible'); + } else { + screen.classList.remove('visible'); + } + } +} + function loadTrackList(opts) { - const {tab, hostname, siteSettings} = opts; - const site = siteSettings[hostname] || []; + const {tab, rule, saveRule, deleteRule} = opts; + const {music} = rule; return browser.storage.sync.get('tracks').then(({tracks = []}) => { const ul = document.getElementById('track-list'); while (ul.firstChild) { @@ -32,28 +54,20 @@ function loadTrackList(opts) { label.appendChild(checkbox); checkbox.type = 'checkbox'; - checkbox.checked = site.includes(track); - checkbox.title = `Toggles whether the track "${track}" will play when this site is opened.` + checkbox.checked = music.includes(track); + checkbox.title = `Toggle whether the track "${track}" will play when this site is opened.` checkbox.addEventListener('click', () => { if (checkbox.checked) { - if (!site.includes(track)) { - site.push(track); + if (!music.includes(track)) { + music.push(track); } } else { - if (site.includes(track)) { - site.splice(site.indexOf(track), 1); + if (music.includes(track)) { + music.splice(music.indexOf(track), 1); } } - - if (!siteSettings[hostname]) { - siteSettings[hostname] = site; - } - - disableButton.style.display = 'inline-block'; - - browser.storage.sync.set({siteSettings}) - .then(() => browser.runtime.sendMessage({hostname})); + saveRule(); }); label.appendChild(document.createTextNode(' ' + track)); @@ -62,7 +76,7 @@ function loadTrackList(opts) { li.appendChild(deleteButton); deleteButton.appendChild(document.createTextNode('Delete...')); - deleteButton.title = `Deletes the track from all sites. You will be confirmed first.`; + deleteButton.title = `Delete the track from all site configuration. You will be confirmed first.`; deleteButton.addEventListener('click', () => { if (confirm(`This will delete "${track}" from ALL sites - this cannot be undone. Are you sure?`)) { @@ -91,62 +105,312 @@ function loadTrackList(opts) { actionLi.appendChild(addButton); addButton.appendChild(document.createTextNode('Create Track')); - addButton.title = `Creates a new track, which will be an option present in all sites.`; + addButton.title = `Create a new track, which will be an option present in all sites.`; addButton.addEventListener('click', () => { browser.tabs.sendMessage(tab.id, {createTrack: true}); window.close(); }); - const disableButton = document.createElement('button'); - actionLi.appendChild(disableButton); + const deleteRuleButton = document.createElement('button'); + actionLi.appendChild(deleteRuleButton); - disableButton.appendChild(document.createTextNode('Disable Site')); - disableButton.title = `Removes the entry for this site altogether. It won't change BGM when you open it again.`; + deleteRuleButton.appendChild(document.createTextNode('Delete Rule')); + deleteRuleButton.title = `Remove this rule altogether. It will be discarded and won't affect the BGM anymore.`; - disableButton.addEventListener('click', () => { - changeScreen('loading-screen'); - delete siteSettings[hostname]; - browser.storage.sync.set({siteSettings}) - .then(() => loadTrackList(opts)) - .then(() => changeScreen('main-screen')); + deleteRuleButton.addEventListener('click', () => { + if (confirm(`Are you sure you want to delete this entry? (${getWildcardRepresentation(rule)})`)) { + deleteRule(); + } }); + }); +} + +function getWildcardRepresentation(rule, urlString = rule.sourceURL) { + const {hostnameMatch, pathnameMatch} = rule; + const {hostnameParts, pathnameParts} = getURLParts(urlString); + + let string = 'http(s)://'; - if (!(hostname in siteSettings)) { - disableButton.style.display = 'none'; + if (hostnameMatch.length !== hostnameParts.length) { + if (hostnameMatch.length > 0) { + string += '*.'; + } else { + string += '*'; } - }); + } + + string += hostnameMatch.slice().reverse().join('.'); + + if (pathnameMatch.length) { + string += '/'; + } + + string += pathnameMatch.join('/'); + + if (!pathnameMatch.length || !last(pathnameMatch).includes('.')) { + string += '/*'; + } + + return string; } -Promise.all([ - (async function() { - const [[tab], {disableEverywhere: disableEverywhereStatus}] = await Promise.all([ - browser.tabs.query({active: true, currentWindow: true}), - browser.storage.sync.get('disableEverywhere') - ]); +function loadRuleScreen({tab, rule, saveRule, deleteRule}) { + if (getRuleScoreOnPage(rule, tab.url) > 0) { + rule.sourceURL = tab.url; + } + + const heading = document.getElementById('hostname'); + while (heading.firstChild) { + heading.removeChild(heading.firstChild); + } + + const hostnameSpans = []; + const pathnameSpans = []; + + const isSelected = s => s.dataset.selected === 'yes'; + + const setSelected = (s, val) => { + s.dataset.selected = val ? 'yes' : 'no'; + }; + + const toggleSelected = s => { + setSelected(s, !isSelected(s)); + }; + + const updateWildcardRepresentation = () => { + const string = getWildcardRepresentation(rule, rule.sourceURL); + + const el = document.getElementById('wildcard-representation'); + while (el.firstChild) { + el.removeChild(el.firstChild); + } + el.appendChild(document.createTextNode(string)); + }; + + const addPart = (span, allSpans, save) => { + heading.appendChild(span); + + span.style.cursor = 'pointer'; + + const getSubordinateSpans = () => { + const index = allSpans.indexOf(span); + return allSpans.slice(0, index + 1); + }; + + const getSuperiorSpans = () => { + const index = allSpans.indexOf(span); + return allSpans.slice(index + 1); + }; + + const updateStyle = s => { + if (s.dataset.selected === 'yes') { + s.style.color = '#000000'; + } else { + s.style.color = '#777777'; + } + }; + + updateStyle(span); + + span.addEventListener('mouseenter', () => { + for (const s of getSubordinateSpans()) { + s.style.color = 'orange'; + } + }); + + span.addEventListener('mouseleave', () => { + for (const s of getSubordinateSpans()) { + updateStyle(s); + } + }); - const {hostname} = new URL(tab.url); - document.getElementById('hostname').appendChild(document.createTextNode(hostname)); + span.addEventListener('click', () => { + if (last(allSpans.filter(isSelected)) === span) { + toggleSelected(span); + } else { + setSelected(span, true); + } - const disableEverywhere = document.getElementById('disable-everywhere'); + for (const s of getSubordinateSpans()) { + setSelected(s, isSelected(span)); + updateStyle(s); + } - disableEverywhere.checked = disableEverywhereStatus; + for (const s of getSuperiorSpans()) { + setSelected(s, false); + updateStyle(s); + } - disableEverywhere.addEventListener('click', () => { - browser.storage.sync.set({disableEverywhere: disableEverywhere.checked}) - .then(() => browser.runtime.sendMessage({hostname})); + save(getSubordinateSpans().filter(isSelected).map(s => s.dataset.partValue)); + }); + }; + + const {hostnameMatch, pathnameMatch} = rule; + const {hostnameParts, pathnameParts} = getURLParts(rule.sourceURL); + + for (let i = 0; i < hostnameParts.length; i++) { + const part = hostnameParts[i]; + const span = document.createElement('span'); + span.appendChild(document.createTextNode(part)); + if (i < hostnameParts.length - 1) { + span.appendChild(document.createTextNode('.')); + } + span.dataset.partValue = part; + setSelected(span, hostnameMatch[hostnameParts.length - i - 1] === part); + hostnameSpans.unshift(span); + addPart(span, hostnameSpans, val => { + rule.hostnameMatch = val; + updateWildcardRepresentation(); + saveRule(); }); + } + + for (let i = 0; i < pathnameParts.length; i++) { + const part = pathnameParts[i]; + const span = document.createElement('span'); + span.appendChild(document.createTextNode('/')); + span.appendChild(document.createTextNode(part)); + span.dataset.partValue = part; + setSelected(span, pathnameMatch[i] === part); + pathnameSpans.push(span); + addPart(span, pathnameSpans, val => { + rule.pathnameMatch = val; + updateWildcardRepresentation(); + saveRule(); + }); + } + + updateWildcardRepresentation(); + + if (hostname) { + return loadTrackList({tab, rule, saveRule, deleteRule}) + .then(() => changeScreen('main-screen')); + } else { + changeScreen('invalid-host-screen'); + } +} + +function loadRuleList({tab, siteSettings, selectRule}) { + const createRuleItem = rule => { + const li = document.createElement('li'); + + li.classList.add('rule'); + li.dataset.wildcardRepresentation = getWildcardRepresentation(rule, rule.sourceURL || tab.url); + li.appendChild(document.createTextNode(li.dataset.wildcardRepresentation)); + li.title = li.dataset.wildcardRepresentation; + + li.addEventListener('click', () => { + selectRule(rule); + }); + + return li; + }; + + const allRulesList = document.getElementById('all-rules'); + clearChildren(allRulesList); + + const allRules = siteSettings; + + const allRuleItems = allRules.map(createRuleItem); + + allRuleItems.sort((a, b) => a.dataset.wildcardRepresentation < b.dataset.wildcardRepresentation ? -1 : 1); + + for (const item of allRuleItems) { + allRulesList.appendChild(item); + } + + const rulesOnThisPageList = document.getElementById('rules-on-this-page'); + clearChildren(rulesOnThisPageList); + + const createRuleLI = document.createElement('li'); + rulesOnThisPageList.appendChild(createRuleLI); + + const createRuleButton = document.createElement('button'); + createRuleLI.appendChild(createRuleButton); + + createRuleButton.appendChild(document.createTextNode('Create Rule')); + + createRuleButton.addEventListener('click', () => { + const {hostnameParts} = getURLParts(tab.url); + const rule = { + sourceURL: tab.url, + hostnameMatch: hostnameParts.slice().reverse(), + pathnameMatch: [], + music: [] + }; + selectRule(rule); + }); - return {tab, hostname}; - })(), - browser.storage.sync.get('siteSettings') - .then(({siteSettings = {}}) => siteSettings) -]) - .then(([{tab, hostname}, siteSettings]) => { - if (hostname) { - return loadTrackList({tab, hostname, siteSettings}) - .then(() => changeScreen('main-screen')); + const rulesOnThisPage = allRules.map(rule => ({ + rule, + score: getRuleScoreOnPage(rule, tab.url) + })).filter(obj => obj.score > 0); + + for (const obj of rulesOnThisPage) { + obj.item = createRuleItem(obj.rule); + } + + rulesOnThisPage.sort((a, b) => { + if (a.score === b.score) { + return a.item.dataset.wildcardRepresentation < b.item.dataset.wildcardRepresentation ? -1 : 1; } else { - changeScreen('invalid-host-screen'); + return b.score - a.score; } }); + + for (const {item} of rulesOnThisPage) { + rulesOnThisPageList.appendChild(item); + } +} + +Promise.all([ + browser.tabs.query({active: true, currentWindow: true}), + browser.storage.sync.get(['disableEverywhere', 'siteSettings']) +]).then(([[tab], {disableEverywhere: disableEverywhereStatus, siteSettings}]) => { + if (!Array.isArray(siteSettings)) { + siteSettings = []; + } + + console.log(siteSettings); + + const reloadRuleList = () => { + return browser.storage.sync.get('siteSettings').then(({siteSettings}) => loadRuleList({ + tab, siteSettings, + selectRule: rule => { + return loadRuleScreen({ + rule, tab, + saveRule: () => { + if (!siteSettings.includes(rule)) { + siteSettings.push(rule); + } + return browser.storage.sync.set({siteSettings}) + .then(() => browser.runtime.sendMessage({music: rule.music})); + }, + deleteRule: () => { + changeScreen('loading-screen'); + siteSettings = siteSettings.filter(r => r !== rule); + reloadRuleList(); + return browser.storage.sync.set({siteSettings}) + .then(() => reloadRuleList()) + .then(() => { + changeScreen('main-screen') + changeContentScreen('no-rule-content'); + }); + } + }).then(() => changeContentScreen('rule-content')); + } + })); + }; + + const disableEverywhere = document.getElementById('disable-everywhere'); + + disableEverywhere.checked = disableEverywhereStatus; + + disableEverywhere.addEventListener('click', () => { + browser.storage.sync.set({disableEverywhere: disableEverywhere.checked}) + .then(() => browser.runtime.sendMessage({urlString: tab.url})); + }); + + reloadRuleList().then(() => changeScreen('main-screen')); +}); diff --git a/extension/popup/style.css b/extension/popup/style.css index d4ac7b2..c05071f 100644 --- a/extension/popup/style.css +++ b/extension/popup/style.css @@ -4,7 +4,7 @@ body, html { padding: 0; margin: 0; min-width: 300px; - max-width: 400px; + max-width: 600px; } body { @@ -35,15 +35,20 @@ ul, p { margin: 4px 0; } -li { +ul { + list-style: none; + padding-left: 0; +} + +.content li { padding: 3px; } -li:nth-child(even) { +.content li:nth-child(even) { background-color: white; } -li:nth-child(odd) { +.content li:nth-child(odd) { background-color: #DDDDDD; } @@ -53,14 +58,14 @@ h1 { text-align: center; } -h2 { +.content h2 { font-size: 0.9em; margin: 2px 0; text-align: center; overflow: hidden; } -h2:before, h2:after { +.content h2:before, .content h2:after { background-color: black; content: ''; display: inline-block; @@ -70,12 +75,12 @@ h2:before, h2:after { width: 50%; } -h2:before { +.content h2:before { right: 0.5em; margin-left: -50%; } -h2:after { +.content h2:after { left: 0.5em; margin-right: -50%; } @@ -91,14 +96,59 @@ h2:after { display: none; } -#loading-screen p, #invalid-host-screen { - text-align: center; - font-style: oblique; +.screen.has-sidebar { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + padding: 0; + background-color: #CCD; } -#track-list { - list-style: none; - padding-left: 0; +.content { + min-width: 300px; + height: 100%; + padding: 5px; + box-sizing: border-box; + background-color: #EEE; +} + +.sidebar { + min-width: 200px; + height: 100%; + padding: 5px; + box-sizing: border-box; +} + +.sidebar h2 { + font-size: 0.9em; + margin: 0; + white-space: nowrap; +} + +.sidebar li { + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; + text-indent: 14px; +} + +.sidebar li.rule { + cursor: pointer; +} + +.content-screen { + width: 100%; + height: 100%; +} + +.content-screen:not(.visible) { + display: none; +} + +#loading-screen p, #invalid-host-screen, #no-rule-content { + text-align: center; + font-style: italic; } #track-list li.track button { -- cgit 1.3.0-6-gf8a5