@@ -417,7 +447,24 @@ export default class extends HTMLElement {
*/
clickHandler(e) {
if (this.state === 'done') {
- this.removeFromIDB();
+ this.willremove = true;
+ this.state = 'ready';
+
+ window.addEventListener('beforeunload', this.unloadHandler);
+ this.removalTimeout = setTimeout(async () => {
+ this.willremove = false;
+ await this.removeFromIDB();
+ window.removeEventListener('beforeunload', this.unloadHandler);
+ }, 5000);
+ } else if (e.target.className === 'undo-remove') {
+ if (this.willremove === true) {
+ if (this.removalTimeout) {
+ this.state = 'done';
+ this.willremove = false;
+ clearTimeout(this.removalTimeout);
+ window.removeEventListener('beforeunload', this.unloadHandler);
+ }
+ }
} else if (e.target.className === 'cancel') {
this.removeFromIDB();
} else if (this.downloading === false) {
@@ -438,6 +485,16 @@ export default class extends HTMLElement {
this.downloading = false;
}
+ /**
+ * Page `beforeunload` event handler.
+ *
+ * @param {Event} unloadEvent Unload event.
+ */
+ unloadHandler(unloadEvent) {
+ unloadEvent.returnValue = '';
+ unloadEvent.preventDefault();
+ }
+
/**
* @returns {VideoMeta} Video meta value.
*/
diff --git a/src/js/constants.js b/src/js/constants.js
index 7e54e5f..648bc16 100644
--- a/src/js/constants.js
+++ b/src/js/constants.js
@@ -97,3 +97,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..b0be150
--- /dev/null
+++ b/src/js/modules/ConnectionStatus.module.js
@@ -0,0 +1,99 @@
+export default class ConnectionStatus {
+ constructor(offlineForced = false) {
+ this.internal = {
+ status: navigator.onLine ? 'online' : 'offline',
+ offlineForced,
+ changeCallbacks: [],
+ };
+
+ window.addEventListener('online', () => {
+ this.internal.status = 'online';
+ this.broadcast();
+ });
+
+ window.addEventListener('offline', () => {
+ this.internal.status = 'offline';
+ 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 ec6c341..4994c96 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 = new Map();
+ this.connectionStatus = connectionStatus;
}
/**
@@ -20,7 +21,7 @@ export default class VideoDownloaderRegistry {
* @returns {VideoDownloader} Instantiated VideoDownloader.
*/
create(videoId) {
- this.instances.set(videoId, new VideoDownloader());
+ this.instances.set(videoId, new VideoDownloader({ connectionStatus: this.connectionStatus }));
return this.instances.get(videoId);
}
diff --git a/src/js/pages/Downloads.js b/src/js/pages/Downloads.js
index dcf7668..54c357d 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 = `
@@ -18,6 +19,7 @@ export default async (routerContext) => {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
grid-gap: 2rem;
+ max-width: 1200px;
}
.clearing {
opacity: 0.3;
@@ -27,8 +29,8 @@ export default async (routerContext) => {
Manage your downloads
-