Skip to content

Commit

Permalink
Merge branch 'update/connection-status' into update/undo-removals
Browse files Browse the repository at this point in the history
  • Loading branch information
dero committed Mar 17, 2021
2 parents db4d4cb + e3283e7 commit ab6262c
Show file tree
Hide file tree
Showing 17 changed files with 385 additions and 151 deletions.
6 changes: 1 addition & 5 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="offline-banner">No internet connection</div>
<header class="_with-video">
<div class="container">
<h1><a data-use-router href="/">KINO</a></h1>
Expand All @@ -24,9 +25,6 @@ <h1><a data-use-router href="/">KINO</a></h1>
<a data-use-router href="/">Home</a>
<a data-use-router href="/downloads">Manage Downloads</a>
<a data-use-router href="/settings">Settings</a>
<span>
Offline <toggle-button id="offline-content-only"></toggle-button> Online
</span>
</div>
</div>
</header>
Expand All @@ -40,8 +38,6 @@ <h1><a data-use-router href="/">KINO</a></h1>
</div>
</footer>

<div id="connection-status"></div>

<script type="module" src="/dist/js/index.js"></script>

</body>
Expand Down
53 changes: 41 additions & 12 deletions public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,27 @@ table {
box-sizing: border-box;
}

/**
* Animations.
*/
@keyframes shake {
10%, 90% {
transform: translateX(-50.5%);
}

20%, 80% {
transform: translateX(-49%);
}

30%, 50%, 70% {
transform: translateX(-48%);
}

40%, 60% {
transform: translateX(-52%);
}
}

/* App Styles */
html, body {
margin: 0;
Expand All @@ -69,22 +90,30 @@ body {
.tip {
padding: 2rem;
}
#connection-status {
#offline-banner {
position: fixed;
bottom: 0;
right: 0;
width: auto;
opacity: 0;
display: none;
bottom: 1em;
left: 50%;
transform: translateX(-50%);
z-index: 100;
font: normal 0.8em sans-serif;
padding: 0.5em 1em;
border-radius: 4px;
overflow: hidden;
color: #fff;
padding: 0.5em;
background: #fa4659;
text-transform: uppercase;
border-top-left-radius: 5px;
transition: opacity 200ms ease-in-out;
white-space: pre;
}
.online {
background: #2eb872;
[data-connection="offline"] #offline-banner {
display: inline-block;
opacity: 1;
}
.offline {
background: #fa4659;
#offline-banner.alert {
animation: shake 0.6s cubic-bezier(.36,.07,.19,.97) both;
}

h3 {
Expand Down Expand Up @@ -216,8 +245,8 @@ header .menu span {
border-radius: 2rem;
padding: 0.5rem 1rem;
}
header .menu toggle-button {
padding: 0 1rem;
header .menu offline-toggle-button {
padding-right: 1rem;
}
header .container {
padding: 2rem 0;
Expand Down
59 changes: 46 additions & 13 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
* Router, Connection utils.
*/
import Router from './js/modules/Router.module';
import updateOnlineStatus from './js/utils/updateOnlineStatus';
import initializeGlobalToggle from './js/utils/initializeGlobalToggle';
import VideoDownloaderRegistry from './js/modules/VideoDownloaderRegistry.module';
import ConnectionStatus from './js/modules/ConnectionStatus.module';

/**
* Web Components implementation.
Expand All @@ -14,6 +13,7 @@ import VideoCardComponent from './js/components/VideoCard';
import VideoDownloaderComponent from './js/components/VideoDownloader';
import VideoGrid from './js/components/VideoGrid';
import ToggleButton from './js/components/ToggleButton';
import OfflineToggleButton from './js/components/OfflineToggleButton';
import ProgressRing from './js/components/ProgressRing';

/**
Expand All @@ -25,6 +25,12 @@ import CategoryPage from './js/pages/Category';
import DownloadsPage from './js/pages/Downloads';
import SettingsPage from './js/pages/Settings';

/**
* Settings
*/
import { loadSetting } from './js/utils/settings';
import { SETTING_KEY_TOGGLE_OFFLINE } from './js/constants';

/**
* Custom Elements definition.
*/
Expand All @@ -33,19 +39,55 @@ customElements.define('video-card', VideoCardComponent);
customElements.define('video-downloader', VideoDownloaderComponent);
customElements.define('video-grid', VideoGrid);
customElements.define('toggle-button', ToggleButton);
customElements.define('offline-toggle-button', OfflineToggleButton);
customElements.define('progress-ring', ProgressRing);

/**
* Tracks the connection status of the application and broadcasts
* when the connections status changes.
*/
const offlineForced = loadSetting(SETTING_KEY_TOGGLE_OFFLINE) || false;
const connectionStatus = new ConnectionStatus(offlineForced);
const offlineBanner = document.querySelector('#offline-banner');

/**
* Allow the page styling to respond to the global connection status.
*
* If an alert is emitted, slide in the "Not connected" message to inform
* the user the action they attempted can't be performed right now.
*/
connectionStatus.subscribe(
({ navigatorStatus, alert }) => {
document.body.dataset.connection = navigatorStatus;

if (alert && navigatorStatus === 'offline') {
offlineBanner.classList.add('alert');
setTimeout(() => offlineBanner.classList.remove('alert'), 600);
}
},
);

/**
* Initialize a registry holding instances of the `VideoDownload` web components.
*
* This is to allow us to share these instances between pages.
*/
const videoDownloaderRegistry = new VideoDownloaderRegistry();
const videoDownloaderRegistry = new VideoDownloaderRegistry({ connectionStatus });

/**
* Bind the offline toggle(s) to the `ConnectionStatus` instance.
*/
[...document.querySelectorAll('offline-toggle-button')].forEach(
(button) => button.assignConnectionStatus(connectionStatus),
);

/**
* Router setup.
*/
const router = new Router({ videoDownloaderRegistry });
const router = new Router({
videoDownloaderRegistry,
connectionStatus,
});
router.route('/', HomePage);
router.route('/settings', SettingsPage);
router.route('/downloads', DownloadsPage);
Expand All @@ -60,12 +102,3 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
});
}

/**
* Connection status.
*/
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
updateOnlineStatus();

initializeGlobalToggle();
79 changes: 79 additions & 0 deletions src/js/components/OfflineToggleButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { loadSetting, saveSetting, removeSetting } from '../utils/settings';
import { SETTING_KEY_TOGGLE_OFFLINE } from '../constants';

import ToggleButton from './ToggleButton';

/**
* Respond to button interaction.
*
* @param {boolean} forceOffline Force the offline state.
* @param {ConnectionStatus} connectionStatus ConnectionStatus instance.
*/
const buttonInteractionHandler = (forceOffline, connectionStatus) => {
if (forceOffline) {
saveSetting(SETTING_KEY_TOGGLE_OFFLINE, true);
connectionStatus.forceOffline();
} else if (connectionStatus.getStatusDetail().navigatorStatus === 'offline') {
/**
* If we want to leave offline mode, but we're not online, let's
* prevent the action and broadcast a "Not connected" alert instead.
*/
connectionStatus.alert();
} else {
removeSetting(SETTING_KEY_TOGGLE_OFFLINE);
connectionStatus.unforceOffline();
}
};

/**
* Respond to connection status changes.
*
* @param {HTMLElement} button The toggle button element.
* @param {ConnectionStatus} connectionStatus ConnectionStatus instance.
*/
const networkChangeHandler = (button, connectionStatus) => {
const offlineModeEnabled = loadSetting(SETTING_KEY_TOGGLE_OFFLINE);

if (offlineModeEnabled) {
button.checked = true;
} else {
button.checked = (connectionStatus.status === 'offline');
}
};

/**
* The offline toggle button is a special case of a toggle button
* in a sense that it accepts and uses a `connectionStatus`
* instance to help drive its logic.
*/
export default class OfflineToggleButton extends ToggleButton {
constructor() {
super();
this.initialized = false;
}

/**
* @param {ConnectionStatus} connectionStatus ConnectionStatus instance.
*/
assignConnectionStatus(connectionStatus) {
if (this.initialized) return;

/**
* On button interaction, we want to persist the state if it's
* based on user gesture.
*/
this.$checkbox.addEventListener('change', (e) => {
const forceOffline = (e.target.checked === true);
buttonInteractionHandler(forceOffline, connectionStatus);
});

/**
* Respond to network changes.
*/
connectionStatus.subscribe(
() => networkChangeHandler(this.$checkbox, connectionStatus),
);

this.initialized = true;
}
}
3 changes: 2 additions & 1 deletion src/js/components/ToggleButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default class ToggleButton extends HTMLElement {
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.$checkbox = this._root.querySelector('input');
this.$checkbox.addEventListener('change', (e) => {
this.checked = e.target.checked;
this.dispatchEvent(
new CustomEvent('change', { detail: { value: e.target.checked } }),
);
Expand All @@ -71,7 +72,7 @@ export default class ToggleButton extends HTMLElement {
}

get checked() {
return this.getAttribute('checked');
return this.getAttribute('checked') === 'true';
}

set checked(value) {
Expand Down
58 changes: 31 additions & 27 deletions src/js/components/VideoCard.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { loadSetting } from '../utils/settings';

const style = `
<style>
:host {
Expand Down Expand Up @@ -54,36 +52,32 @@ const style = `
}
</style>`;

export default class extends HTMLElement {
/**
* When the connection status changes, enable or disable the card
* with respect to the download state.
*
* @param {ConnectionStatus} connectionStatus ConnectionStatus instance.
* @param {VideoDownloader} downloader `VideoDownloader` instance.
*/
function connectionStatusChangeHandler(connectionStatus, downloader) {
if (connectionStatus.status === 'offline' && downloader.state !== 'done') {
this.classList.add('disabled');
} else {
this.classList.remove('disabled');
}
}

export default class VideoCard extends HTMLElement {
constructor() {
super();
this._root = this.attachShadow({ mode: 'open' });

window.addEventListener('online', this.updateOnlineStatus.bind(this));
window.addEventListener('online-mock', this.updateOnlineStatus.bind(this, { mock: true }));
window.addEventListener('offline', this.updateOnlineStatus.bind(this));
window.addEventListener('offline-mock', this.updateOnlineStatus.bind(this, { mock: false }));
}

updateOnlineStatus(opts = {}) {
const isOnline = opts.mock !== undefined ? opts.mock : navigator.onLine;
const offlineContentOnly = loadSetting('offline-content-only');
const isDownloaded = opts.downloader && (opts.downloader.state === 'done');
if (((!isOnline || offlineContentOnly) && !isDownloaded)) {
this.classList.add('disabled');
} else {
this.classList.remove('disabled');
}
}

attachDownloader(downloader) {
downloader.onStatusUpdate = this.updateOnlineStatus.bind(this, { downloader });
this._root.querySelector('.downloader').appendChild(downloader);
this.updateOnlineStatus();
}

render(videoData, navigate) {
this.navigate = navigate;
render({
videoData,
connectionStatus,
downloader,
}) {
const templateElement = document.createElement('template');
let posterImage = videoData.thumbnail;

Expand All @@ -108,5 +102,15 @@ export default class extends HTMLElement {

const ui = templateElement.content.cloneNode(true);
this._root.appendChild(ui);
this._root.querySelector('.downloader').appendChild(downloader);

const boundHandler = connectionStatusChangeHandler.bind(this, connectionStatus, downloader);

/**
* Whenever connection status or downloader state changes,
* maybe disable / enable the card.
*/
connectionStatus.subscribe(boundHandler);
downloader.subscribe(boundHandler);
}
}
Loading

0 comments on commit ab6262c

Please sign in to comment.