From 6b852e135be2ce1924f2f2c62c264ea0491facd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Polakovi=C4=8D?= Date: Tue, 16 Mar 2021 18:44:28 +0100 Subject: [PATCH 1/3] Implement ConnectionStatus class. Fix online / offline mode issues. --- public/index.html | 5 +- public/styles.css | 53 ++++++--- src/index.js | 59 +++++++--- src/js/components/OfflineToggleButton.js | 79 ++++++++++++++ src/js/components/ToggleButton.js | 3 +- src/js/components/VideoCard.js | 59 +++++----- src/js/components/VideoDownloader.js | 27 +++-- src/js/constants.js | 5 + src/js/modules/ConnectionStatus.module.js | 103 ++++++++++++++++++ .../modules/VideoDownloaderRegistry.module.js | 5 +- src/js/pages/Downloads.js | 20 ++-- src/js/pages/Settings.js | 25 ++--- src/js/utils/appendVideoToGallery.js | 22 ++-- src/js/utils/getDownloaderElement.module.js | 20 ++++ src/js/utils/initializeGlobalToggle.js | 32 ------ src/js/utils/settings.js | 9 ++ src/js/utils/updateOnlineStatus.js | 14 --- 17 files changed, 388 insertions(+), 152 deletions(-) create mode 100644 src/js/components/OfflineToggleButton.js create mode 100644 src/js/modules/ConnectionStatus.module.js create mode 100644 src/js/utils/getDownloaderElement.module.js delete mode 100644 src/js/utils/initializeGlobalToggle.js delete mode 100644 src/js/utils/updateOnlineStatus.js diff --git a/public/index.html b/public/index.html index 94ba9e4..ee1e1a5 100644 --- a/public/index.html +++ b/public/index.html @@ -13,6 +13,7 @@ +
No internet connection

KINO

@@ -25,7 +26,7 @@

KINO

Manage Downloads Settings - Offline Online + Offline Mode
@@ -40,8 +41,6 @@

KINO

-
- diff --git a/public/styles.css b/public/styles.css index 872674a..49bf784 100644 --- a/public/styles.css +++ b/public/styles.css @@ -50,6 +50,27 @@ table { box-sizing: border-box; } +/** + * Animations. + */ +@keyframes shake { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } +} + /* App Styles */ html, body { margin: 0; @@ -69,22 +90,26 @@ body { .tip { padding: 2rem; } -#connection-status { - position: fixed; - bottom: 0; - right: 0; - width: auto; +#offline-banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 100; font: normal 0.8em sans-serif; + line-height: 2em; + height: 0; + overflow: hidden; color: #fff; - padding: 0.5em; + background: #fa4659; text-transform: uppercase; - border-top-left-radius: 5px; -} -.online { - background: #2eb872; + text-align: center; + transition: height 100ms ease-in-out; } -.offline { - background: #fa4659; +#offline-banner.active { + height: 2em; + transform: translate3d(0, 0, 0); + animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; } h3 { @@ -216,8 +241,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; diff --git a/src/index.js b/src/index.js index a681560..8e86c07 100644 --- a/src/index.js +++ b/src/index.js @@ -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. @@ -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'; /** @@ -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. */ @@ -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('active'); + setTimeout(() => offlineBanner.classList.remove('active'), 3000); + } + }, +); + /** * 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); @@ -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(); diff --git a/src/js/components/OfflineToggleButton.js b/src/js/components/OfflineToggleButton.js new file mode 100644 index 0000000..a628066 --- /dev/null +++ b/src/js/components/OfflineToggleButton.js @@ -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; + } +} diff --git a/src/js/components/ToggleButton.js b/src/js/components/ToggleButton.js index a0f7a13..704223a 100644 --- a/src/js/components/ToggleButton.js +++ b/src/js/components/ToggleButton.js @@ -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 } }), ); @@ -71,7 +72,7 @@ export default class ToggleButton extends HTMLElement { } get checked() { - return this.getAttribute('checked'); + return this.getAttribute('checked') === 'true'; } set checked(value) { diff --git a/src/js/components/VideoCard.js b/src/js/components/VideoCard.js index c0db371..2f63d86 100644 --- a/src/js/components/VideoCard.js +++ b/src/js/components/VideoCard.js @@ -1,5 +1,3 @@ -import { loadSetting } from '../utils/settings'; - const 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, true)); - window.addEventListener('offline', this.updateOnlineStatus.bind(this)); - window.addEventListener('offline-mock', this.updateOnlineStatus.bind(this, false)); - } - - updateOnlineStatus(mock) { - const isOnline = mock !== undefined ? mock : navigator.onLine; - const offlineContentOnly = loadSetting('offline-content-only'); - const downloader = this._root.querySelector('video-downloader'); - const isDownloaded = downloader.state === 'done'; - if (((!isOnline || offlineContentOnly) && !isDownloaded)) { - this.classList.add('disabled'); - } else { - this.classList.remove('disabled'); - } - } - - attachDownloader(downloader) { - downloader.onStatusUpdate = this.updateOnlineStatus.bind(this); - 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; @@ -109,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); } } diff --git a/src/js/components/VideoDownloader.js b/src/js/components/VideoDownloader.js index 5991e2a..3ded5ea 100644 --- a/src/js/components/VideoDownloader.js +++ b/src/js/components/VideoDownloader.js @@ -149,12 +149,14 @@ export default class extends HTMLElement { return ['state', 'progress', 'downloading']; } - constructor() { + constructor({ connectionStatus }) { super(); - // Attach Shadow DOM. - this.internal = {}; - this.internal.root = this.attachShadow({ mode: 'open' }); + this.internal = { + connectionStatus, + changeCallbacks: [], + root: this.attachShadow({ mode: 'open' }), + }; } /** @@ -174,10 +176,21 @@ export default class extends HTMLElement { } set state(state) { + const oldState = this.state; this.setAttribute('state', state); - if (this.onStatusUpdate) { - this.onStatusUpdate(state); - } + + this.internal.changeCallbacks.forEach( + (callback) => callback(oldState, state), + ); + } + + /** + * Subscribe to state changes. + * + * @param {Function} callback Callback function to run when the component's state changes. + */ + subscribe(callback) { + this.internal.changeCallbacks.push(callback); } get downloading() { diff --git a/src/js/constants.js b/src/js/constants.js index 383dd82..8b88fb0 100644 --- a/src/js/constants.js +++ b/src/js/constants.js @@ -90,3 +90,8 @@ export const DEFAULT_AUDIO_PRIORITIES = [ * These are all the types the Streamer has support for. */ export const ALL_STREAM_TYPES = ['audio', 'video']; + +/** + * Settings key names. + */ +export const SETTING_KEY_TOGGLE_OFFLINE = 'toggle-offline'; diff --git a/src/js/modules/ConnectionStatus.module.js b/src/js/modules/ConnectionStatus.module.js new file mode 100644 index 0000000..d48f8ab --- /dev/null +++ b/src/js/modules/ConnectionStatus.module.js @@ -0,0 +1,103 @@ +export default class ConnectionStatus { + constructor(offlineForced = false) { + this.internal = { + status: navigator.onLine ? 'online' : 'offline', + offlineForced, + changeCallbacks: [], + }; + + window.addEventListener('online', () => { + const oldStatus = this.status; + this.internal.status = 'online'; + + if (this.status !== oldStatus) this.broadcast(); + }); + + window.addEventListener('offline', () => { + const oldStatus = this.status; + this.internal.status = 'offline'; + + if (this.status !== oldStatus) this.broadcast(); + }); + } + + /** + * Returns the connection status, optionally overriden by the + * offline status being forced. + * + * @returns {string} Connection status. + */ + get status() { + return this.internal.offlineForced ? 'offline' : this.internal.status; + } + + /** + * Toggle forced offline mode on. + */ + forceOffline() { + this.internal.offlineForced = true; + this.broadcast(); + } + + /** + * Toggle forced offline mode off. + */ + unforceOffline() { + this.internal.offlineForced = false; + this.broadcast(); + } + + /** + * Returns detailed information about the current status. + * + * @returns {object} Detailed information about the status. + */ + getStatusDetail() { + return { + status: this.status, + navigatorStatus: this.internal.status, + forcedOffline: this.internal.offlineForced, + }; + } + + /** + * Subscribe to connection status changes. + * + * @param {Function} callback Callback function to run when connection status changes. + */ + subscribe(callback) { + const detail = this.getStatusDetail(); + + this.internal.changeCallbacks.push(callback); + callback(detail); + } + + /** + * Broadcast the status to all subscribers and emits a global event + * signalling the change. + * + * @param {object} detail Detail object to be broadcasted. + */ + broadcast(detail = null) { + if (!detail) detail = this.getStatusDetail(); + + this.internal.changeCallbacks.forEach( + (callback) => callback(detail), + ); + + window.dispatchEvent( + new CustomEvent('connection-change', { detail }), + ); + } + + /** + * Broadcast the detail information with alert flag set to true in order + * to indicate user action that couldn't be finished because the client is offline. + */ + alert() { + const detail = this.getStatusDetail(); + detail.alert = true; + + this.broadcast(detail); + } +} diff --git a/src/js/modules/VideoDownloaderRegistry.module.js b/src/js/modules/VideoDownloaderRegistry.module.js index 90507e9..4b8202f 100644 --- a/src/js/modules/VideoDownloaderRegistry.module.js +++ b/src/js/modules/VideoDownloaderRegistry.module.js @@ -8,8 +8,9 @@ import VideoDownloader from '../components/VideoDownloader'; * This helps maintain the component's state even across page loads. */ export default class VideoDownloaderRegistry { - constructor() { + constructor({ connectionStatus }) { this.instances = {}; + this.connectionStatus = connectionStatus; } /** @@ -20,7 +21,7 @@ export default class VideoDownloaderRegistry { * @returns {VideoDownloader} Instantiated VideoDownloader. */ create(videoId) { - this.instances[videoId] = new VideoDownloader(); + this.instances[videoId] = new VideoDownloader({ connectionStatus: this.connectionStatus }); return this.instances[videoId]; } diff --git a/src/js/pages/Downloads.js b/src/js/pages/Downloads.js index f63b2e6..d3be3f0 100644 --- a/src/js/pages/Downloads.js +++ b/src/js/pages/Downloads.js @@ -1,5 +1,5 @@ import getIDBConnection from '../modules/IDBConnection.module'; -import { SW_CACHE_NAME } from '../constants'; +import getDownloaderElement from '../utils/getDownloaderElement.module'; /** * @param {RouterContext} routerContext Context object passed by the Router. @@ -9,6 +9,7 @@ export default async (routerContext) => { mainContent, apiData, navigate, + connectionStatus, videoDownloaderRegistry, } = routerContext; mainContent.innerHTML = ` @@ -53,14 +54,15 @@ export default async (routerContext) => { allMeta.forEach((meta) => { const videoData = apiData.find((vd) => vd.id === meta.videoId); const card = document.createElement('video-card'); - let downloader = videoDownloaderRegistry.get(videoData.id); - if (!downloader) { - downloader = videoDownloaderRegistry.create(videoData.id); - downloader.init(videoData, SW_CACHE_NAME); - } - downloader.setAttribute('expanded', 'false'); - card.render(videoData, navigate); - card.attachDownloader(downloader); + const downloader = getDownloaderElement(videoDownloaderRegistry, videoData); + + card.render({ + videoData, + navigate, + connectionStatus, + downloader, + }); + grid.appendChild(card); }); diff --git a/src/js/pages/Settings.js b/src/js/pages/Settings.js index 11d0e70..828da3a 100644 --- a/src/js/pages/Settings.js +++ b/src/js/pages/Settings.js @@ -1,14 +1,11 @@ -import { saveSetting, loadSetting } from '../utils/settings'; - -const onChange = (key) => ({ detail }) => { - saveSetting(key, detail.value); -}; - /** * @param {RouterContext} routerContext Context object passed by the Router. */ export default (routerContext) => { - const { mainContent } = routerContext; + const { + mainContent, + connectionStatus, + } = routerContext; mainContent.innerHTML = `

Settings

@@ -16,7 +13,7 @@ export default (routerContext) => {
- +

Show offline content only

When enabled, you will only be shown content that is available offline.

@@ -31,14 +28,8 @@ export default (routerContext) => {
`; - const toggleButtonOffline = mainContent.querySelector('toggle-button#offline-content-only'); - toggleButtonOffline.addEventListener('change', onChange('offline-content-only')); - // TODO: Listen for global toggle change and sync this local setting? - // Should we enable and gray-out that option while offline? - // +auto enable when going offline on this page? - const isOffline = !navigator.onLine; - if (loadSetting('offline-content-only') || isOffline) { - toggleButtonOffline.checked = true; - } + mainContent + .querySelector('offline-toggle-button') + .assignConnectionStatus(connectionStatus); }; diff --git a/src/js/utils/appendVideoToGallery.js b/src/js/utils/appendVideoToGallery.js index a834f86..984507a 100644 --- a/src/js/utils/appendVideoToGallery.js +++ b/src/js/utils/appendVideoToGallery.js @@ -1,4 +1,4 @@ -import { SW_CACHE_NAME } from '../constants'; +import getDownloaderElement from './getDownloaderElement.module'; /** * @param {RouterContext} routerContext Context passed through by Router. @@ -10,6 +10,7 @@ function appendVideoToGallery(routerContext, localContext) { apiData, navigate, mainContent, + connectionStatus, } = routerContext; const category = localContext.category || ''; @@ -23,18 +24,15 @@ function appendVideoToGallery(routerContext, localContext) { apiData.forEach((videoData) => { const card = document.createElement('video-card'); + const downloader = getDownloaderElement(videoDownloaderRegistry, videoData); + + card.render({ + videoData, + navigate, + connectionStatus, + downloader, + }); - let downloader = videoDownloaderRegistry.get(videoData.id); - if (!downloader) { - downloader = videoDownloaderRegistry.create(videoData.id); - downloader.init(videoData, SW_CACHE_NAME); - } - downloader.setAttribute('expanded', 'false'); - - const player = document.createElement('video-player'); - card.render(videoData, navigate); - card.attachDownloader(downloader); - player.render(videoData); videoGallery.appendChild(card); }); diff --git a/src/js/utils/getDownloaderElement.module.js b/src/js/utils/getDownloaderElement.module.js new file mode 100644 index 0000000..ee79026 --- /dev/null +++ b/src/js/utils/getDownloaderElement.module.js @@ -0,0 +1,20 @@ +import { SW_CACHE_NAME } from '../constants'; + +/** + * Returns a `VideoDownloader` object for a given video ID. + * + * @param {VideoDownloaderRegistry} videoDownloaderRegistry Registry. + * @param {object} videoData Video data. + * + * @returns {VideoDownloader} `VideoDownloader` instance. + */ +export default (videoDownloaderRegistry, videoData) => { + let downloader = videoDownloaderRegistry.get(videoData.id); + if (!downloader) { + downloader = videoDownloaderRegistry.create(videoData.id); + downloader.init(videoData, SW_CACHE_NAME); + } + downloader.setAttribute('expanded', 'false'); + + return downloader; +}; diff --git a/src/js/utils/initializeGlobalToggle.js b/src/js/utils/initializeGlobalToggle.js deleted file mode 100644 index 4fc2fbc..0000000 --- a/src/js/utils/initializeGlobalToggle.js +++ /dev/null @@ -1,32 +0,0 @@ -import { loadSetting, saveSetting } from './settings'; - -const onChange = (key) => ({ detail }) => { - saveSetting(key, !detail.value); - window.dispatchEvent(new CustomEvent(`${!detail.value ? 'offline' : 'online'}-mock`)); -}; - -/** - * Update online status for header toggle - */ -function updateOnlineStatus() { - const toggleButtonOffline = document.querySelector('header toggle-button#offline-content-only'); - toggleButtonOffline.checked = navigator.onLine; -} - -/** - * Initialize the offline/online toggle from the header. - */ -export default function initializeGlobalToggle() { - const toggleButtonOffline = document.querySelector('header toggle-button#offline-content-only'); - toggleButtonOffline.addEventListener('change', onChange('offline-content-only')); - - window.addEventListener('online', updateOnlineStatus); - window.addEventListener('offline', updateOnlineStatus); - - // Should we enable and gray-out that option while offline? - // +auto enable when going offline on this page? - const isOnline = navigator.onLine; - if (!loadSetting('offline-content-only') || isOnline) { - toggleButtonOffline.checked = true; - } -} diff --git a/src/js/utils/settings.js b/src/js/utils/settings.js index a39f62a..cd81b80 100644 --- a/src/js/utils/settings.js +++ b/src/js/utils/settings.js @@ -13,3 +13,12 @@ export function saveSetting(key, value) { export function loadSetting(key) { return JSON.parse(localStorage.getItem(key)); } + +/** + * Removes a settings entry. + * + * @param {string} key Setting key. + */ +export function removeSetting(key) { + localStorage.removeItem(key); +} diff --git a/src/js/utils/updateOnlineStatus.js b/src/js/utils/updateOnlineStatus.js deleted file mode 100644 index ae700b3..0000000 --- a/src/js/utils/updateOnlineStatus.js +++ /dev/null @@ -1,14 +0,0 @@ -import { saveSetting } from './settings'; - -/** - * Update online status helper. - */ -export default function updateOnlineStatus() { - const status = document.getElementById('connection-status'); - const condition = navigator.onLine ? 'online' : 'offline'; - status.className = condition; - status.innerHTML = condition; - - // If we want to sync the setting with actual connection state - saveSetting('offline-content-only', condition === 'offline'); -} From d539c6e6c36d1d32b4b9aadb5ebbb9752bc86905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Polakovi=C4=8D?= Date: Wed, 17 Mar 2021 10:04:30 +0100 Subject: [PATCH 2/3] Show banner when offline. Alert when unable to exit offline mode. --- public/index.html | 3 --- public/styles.css | 36 ++++++++++++++++++++---------------- src/index.js | 4 ++-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/public/index.html b/public/index.html index ee1e1a5..cc0b06c 100644 --- a/public/index.html +++ b/public/index.html @@ -25,9 +25,6 @@

KINO

Home Manage Downloads Settings - - Offline Mode -
diff --git a/public/styles.css b/public/styles.css index 49bf784..a25701a 100644 --- a/public/styles.css +++ b/public/styles.css @@ -55,19 +55,19 @@ table { */ @keyframes shake { 10%, 90% { - transform: translate3d(-1px, 0, 0); + transform: translateX(-50.5%); } 20%, 80% { - transform: translate3d(2px, 0, 0); + transform: translateX(-49%); } 30%, 50%, 70% { - transform: translate3d(-4px, 0, 0); + transform: translateX(-48%); } 40%, 60% { - transform: translate3d(4px, 0, 0); + transform: translateX(-52%); } } @@ -91,25 +91,29 @@ body { padding: 2rem; } #offline-banner { - position: absolute; - top: 0; - left: 0; - width: 100%; + position: fixed; + opacity: 0; + display: none; + bottom: 1em; + left: 50%; + transform: translateX(-50%); z-index: 100; font: normal 0.8em sans-serif; - line-height: 2em; - height: 0; + padding: 0.5em 1em; + border-radius: 4px; overflow: hidden; color: #fff; background: #fa4659; text-transform: uppercase; - text-align: center; - transition: height 100ms ease-in-out; + transition: opacity 200ms ease-in-out; + white-space: pre; +} +[data-connection="offline"] #offline-banner { + display: inline-block; + opacity: 1; } -#offline-banner.active { - height: 2em; - transform: translate3d(0, 0, 0); - animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; +#offline-banner.alert { + animation: shake 0.6s cubic-bezier(.36,.07,.19,.97) both; } h3 { diff --git a/src/index.js b/src/index.js index 8e86c07..748dddf 100644 --- a/src/index.js +++ b/src/index.js @@ -61,8 +61,8 @@ connectionStatus.subscribe( document.body.dataset.connection = navigatorStatus; if (alert && navigatorStatus === 'offline') { - offlineBanner.classList.add('active'); - setTimeout(() => offlineBanner.classList.remove('active'), 3000); + offlineBanner.classList.add('alert'); + setTimeout(() => offlineBanner.classList.remove('alert'), 600); } }, ); From e3283e7aba0bf27e6e4734e040fe4ab4b505beb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Polakovi=C4=8D?= Date: Wed, 17 Mar 2021 10:17:53 +0100 Subject: [PATCH 3/3] Broadcast all underlying connection changes. --- src/js/modules/ConnectionStatus.module.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/js/modules/ConnectionStatus.module.js b/src/js/modules/ConnectionStatus.module.js index d48f8ab..b0be150 100644 --- a/src/js/modules/ConnectionStatus.module.js +++ b/src/js/modules/ConnectionStatus.module.js @@ -7,17 +7,13 @@ export default class ConnectionStatus { }; window.addEventListener('online', () => { - const oldStatus = this.status; this.internal.status = 'online'; - - if (this.status !== oldStatus) this.broadcast(); + this.broadcast(); }); window.addEventListener('offline', () => { - const oldStatus = this.status; this.internal.status = 'offline'; - - if (this.status !== oldStatus) this.broadcast(); + this.broadcast(); }); }