« get me outta code hell

Rule Scope - interactive-bgm - Browser extension that adds background music based on the site you're browsing
about summary refs log tree commit diff
path: root/extension
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2019-03-23 19:25:26 -0300
committerFlorrie <towerofnix@gmail.com>2019-03-23 19:25:26 -0300
commita55a4500ee8262d4c9c50d7c769ebc8cb742bb93 (patch)
treec9811aa397282453888e1e747c3c028c5262c6af /extension
parente03b677b1d92ee7a0b139c23cb8076c44770e903 (diff)
Rule Scope
Diffstat (limited to 'extension')
-rw-r--r--extension/background.html7
-rw-r--r--extension/background.js28
-rw-r--r--extension/interactive-bgm.js10
-rw-r--r--extension/lib.js34
-rw-r--r--extension/manifest.json2
-rw-r--r--extension/popup/index.html31
-rw-r--r--extension/popup/main.js374
-rw-r--r--extension/popup/style.css78
8 files changed, 476 insertions, 88 deletions
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 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <script type="module" src="background.js"></script>
+    </head>
+</html>
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 @@
         <div class="screen visible" id="loading-screen">
             <p>Loading...</p>
         </div>
-        <div class="screen" id="main-screen">
-            <h1 id="hostname"></h1>
-            <h2>Tracks</h2>
-            <ul id="track-list"></ul>
-            <h1>General Settings</h1>
-            <p><label><input id="disable-everywhere" type="checkbox"> Disable everywhere for now?</label></p>
+        <div class="screen has-sidebar" id="main-screen">
+            <div class="sidebar">
+                <h1>Rules</h1>
+                <h2>Applies on this page:</h2>
+                <ul id="rules-on-this-page"></ul>
+                <h2>All rules:</h2>
+                <ul id="all-rules"></ul>
+            </div>
+            <div class="content">
+                <div class="content-screen" id="rule-content">
+                    <h1 id="hostname"></h1>
+                    <p>(Applies to: <span id="wildcard-representation">*</span>)</p>
+                    <h2>Tracks</h2>
+                    <ul id="track-list"></ul>
+                    <h1>General Settings</h1>
+                    <p><label><input id="disable-everywhere" type="checkbox"> Disable everywhere for now?</label></p>
+                </div>
+                <div class="content-screen visible" id="no-rule-content">
+                    <p>No rule selected. Please select or create one!</p>
+                </div>
+            </div>
         </div>
         <div class="screen" id="invalid-host-screen">
-            <p>Sorry, this page doesn't appear to have a hostname. We can't configure music here.</p>
+            <p>Sorry, this page doesn't appear to have a hostname.<br>We can't configure music here.</p>
         </div>
-        <script src="main.js"></script>
+        <script type="module" src="main.js"></script>
     </body>
 </html>
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 {