From 60429b280ad53fab4fd6012d311938fe1345006f Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Wed, 19 Sep 2018 21:43:55 -0700 Subject: [PATCH] Serialize state to indexdb --- js/actionTypes.ts | 1 + js/index.js | 10 ++++++--- js/indexdb.js | 29 ++++++++++++++++++++++++ js/reducers/display.ts | 48 +++++++++++++++++++++++++++++++++++++++- js/reducers/equalizer.ts | 17 +++++++++++++- js/selectors.ts | 11 ++++++++- js/types.ts | 21 ++++++++++++++++-- js/utils.ts | 16 ++++++++++++++ js/webampLazy.js | 15 ++++++++++++- package.json | 1 + yarn.lock | 11 +++++++++ 11 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 js/indexdb.js diff --git a/js/actionTypes.ts b/js/actionTypes.ts index ec1ce8f5..8586771c 100644 --- a/js/actionTypes.ts +++ b/js/actionTypes.ts @@ -71,3 +71,4 @@ export const SET_DUMMY_VIZ_DATA = "SET_DUMMY_VIZ_DATA"; export const SET_WINDOW_VISIBILITY = "SET_WINDOW_VISIBILITY"; export const LOADING = "LOADING"; export const CLOSE_REQUESTED = "CLOSE_REQUESTED"; +export const LOAD_SERIALIZED_STATE = "LOAD_SERIALIZED_STATE"; diff --git a/js/index.js b/js/index.js index 9bdb6420..f5d90d1e 100644 --- a/js/index.js +++ b/js/index.js @@ -36,6 +36,8 @@ import { disableMarquee } from "./config"; +import { bindToIndexDB } from "./indexdb"; + const requireJSZip = () => { return new Promise((resolve, reject) => { require.ensure( @@ -153,7 +155,7 @@ function genAudioFileUrlsFromDropbox() { }); } -Raven.context(() => { +Raven.context(async () => { window.Raven = Raven; if (screenshot) { document.getElementsByClassName("about")[0].style.visibility = "hidden"; @@ -291,8 +293,10 @@ Raven.context(() => { }); document.body.appendChild(fileInput); - webamp.renderWhenReady(document.getElementById("app")); - // Expose webamp instance for debugging and integration tests. window.__webamp = webamp; + + await bindToIndexDB(webamp); + + await webamp.renderWhenReady(document.getElementById("app")); }); diff --git a/js/indexdb.js b/js/indexdb.js new file mode 100644 index 00000000..36563817 --- /dev/null +++ b/js/indexdb.js @@ -0,0 +1,29 @@ +import IdbKvStore from "idb-kv-store"; +import { throttle } from "./utils"; +const LOCAL_STORAGE_KEY = "webamp_state"; + +export async function bindToIndexDB(webamp) { + const localStore = new IdbKvStore("webamp_state_database"); + + let previousSerializedState = null; + try { + previousSerializedState = await localStore.get(LOCAL_STORAGE_KEY); + } catch (e) { + console.error("Failed to load the saves state from IndexDB", e); + } + + if (previousSerializedState != null) { + webamp.loadSerializedState(previousSerializedState); + } + + async function persist() { + const serializedState = webamp.getSerializedState(); + try { + await localStore.set(LOCAL_STORAGE_KEY, serializedState); + } catch (e) { + console.log("Failed to save our state to IndexDB", e); + } + } + + webamp.onStateChange(throttle(persist, 1000)); +} diff --git a/js/reducers/display.ts b/js/reducers/display.ts index 7d3982d5..b3a0e16d 100644 --- a/js/reducers/display.ts +++ b/js/reducers/display.ts @@ -23,7 +23,8 @@ import { SET_Z_INDEX, DISABLE_MARQUEE, SET_DUMMY_VIZ_DATA, - LOADING + LOADING, + LOAD_SERIALIZED_STATE } from "../actionTypes"; import { DEFAULT_SKIN, VISUALIZER_ORDER } from "../constants"; @@ -48,6 +49,19 @@ export interface DisplayState { dummyVizData: null; // TODO: Figure out what kind of data this actually is. } +export interface DisplaySerializedStateV1 { + visualizerStyle: number; + doubled: boolean; + llama: boolean; + marqueeStep: number; + skinImages: SkinImages; + skinCursors: Cursors | null; + skinRegion: SkinRegion; + skinGenLetterWidths: GenLetterWidths | null; + skinColors: string[]; // Theoretically this could be a tuple of a specific length + skinPlaylistStyle: PlaylistStyle | null; +} + const defaultDisplayState = { doubled: false, marqueeStep: 0, @@ -123,12 +137,44 @@ const display = ( return { ...state, zIndex: action.zIndex }; case SET_DUMMY_VIZ_DATA: return { ...state, dummyVizData: action.data }; + case LOAD_SERIALIZED_STATE: + return { ...state, ...action.serializedState.display }; default: return state; } }; export default display; +export const getSerializedState = ( + state: DisplayState +): DisplaySerializedStateV1 => { + // My kingdom for a type-safe `_.pick`. + const { + visualizerStyle, + doubled, + llama, + marqueeStep, + skinImages, + skinCursors, + skinRegion, + skinGenLetterWidths, + skinColors, + skinPlaylistStyle + } = state; + return { + visualizerStyle, + doubled, + llama, + marqueeStep, + skinImages, + skinCursors, + skinRegion, + skinGenLetterWidths, + skinColors, + skinPlaylistStyle + }; +}; + export const getVisualizationOrder = (state: DisplayState): Array => { return [...state.additionalVisualizers, ...VISUALIZER_ORDER]; }; diff --git a/js/reducers/equalizer.ts b/js/reducers/equalizer.ts index ab8d08a9..bc7d8f47 100644 --- a/js/reducers/equalizer.ts +++ b/js/reducers/equalizer.ts @@ -4,7 +4,8 @@ import { SET_BAND_VALUE, SET_EQ_AUTO, SET_EQ_ON, - SET_EQ_OFF + SET_EQ_OFF, + LOAD_SERIALIZED_STATE } from "../actionTypes"; export interface EqualizerState { @@ -13,6 +14,12 @@ export interface EqualizerState { sliders: Record; } +export interface EqualizerSerializedStateV1 { + on: boolean; + auto: boolean; + sliders: Record; +} + const defaultState = { on: true, auto: false, @@ -45,9 +52,17 @@ const equalizer = ( return { ...state, on: false }; case SET_EQ_AUTO: return { ...state, auto: action.value }; + case LOAD_SERIALIZED_STATE: + return action.serializedState.equalizer || state; default: return state; } }; +export function getSerializedState( + state: EqualizerState +): EqualizerSerializedStateV1 { + return state; +} + export default equalizer; diff --git a/js/selectors.ts b/js/selectors.ts index 6bd12dca..59ab3bd5 100644 --- a/js/selectors.ts +++ b/js/selectors.ts @@ -4,7 +4,8 @@ import { WebampWindow, WindowId, WindowInfo, - LoadedURLTrack + LoadedURLTrack, + SerializedStateV1 } from "./types"; import { createSelector } from "reselect"; import { @@ -26,6 +27,7 @@ import { import { createPlaylistURL } from "./playlistHtml"; import * as fromPlaylist from "./reducers/playlist"; import * as fromDisplay from "./reducers/display"; +import * as fromEqualizer from "./reducers/equalizer"; import { generateGraph } from "./resizeUtils"; export const getSliders = (state: AppState) => state.equalizer.sliders; @@ -403,3 +405,10 @@ export const getVolume = (state: AppState) => state.media.volume; export const getBalance = (state: AppState) => state.media.balance; export const getChannels = (state: AppState) => state.media.channels; +export function getSerlializedState(state: AppState): SerializedStateV1 { + return { + version: 1, + equalizer: fromEqualizer.getSerializedState(state.equalizer), + display: fromDisplay.getSerializedState(state.display) + }; +} diff --git a/js/types.ts b/js/types.ts index a8b21c84..49391db9 100644 --- a/js/types.ts +++ b/js/types.ts @@ -2,9 +2,12 @@ import { PlaylistState } from "./reducers/playlist"; import { SettingsState } from "./reducers/settings"; import { UserInputState } from "./reducers/userInput"; import { MediaState } from "./reducers/media"; -import { DisplayState } from "./reducers/display"; +import { DisplayState, DisplaySerializedStateV1 } from "./reducers/display"; import { WindowState } from "./reducers/windows"; -import { EqualizerState } from "./reducers/equalizer"; +import { + EqualizerState, + EqualizerSerializedStateV1 +} from "./reducers/equalizer"; import { NetworkState } from "./reducers/network"; export type Skin = { @@ -341,6 +344,10 @@ export type Action = | { type: "CLOSE_REQUESTED"; cancel: () => void; + } + | { + type: "LOAD_SERIALIZED_STATE"; + serializedState: SerializedStateV1; }; export interface WebampWindow { @@ -491,3 +498,13 @@ export interface MiddlewareStore { dispatch: Dispatch; getState: GetState; } + +export interface SerializedStateV1 { + version: 1; + /* + windows: WindowSerializedState; + settings: SettingsSerializedState; + */ + display: DisplaySerializedStateV1; + equalizer: EqualizerSerializedStateV1; +} diff --git a/js/utils.ts b/js/utils.ts index 5e4ceb39..bebf80b5 100644 --- a/js/utils.ts +++ b/js/utils.ts @@ -268,6 +268,22 @@ export function debounce(func: F, delay: number): F { } as any; } +// Trailing edge only throttle +export function throttle(func: F, delay: number): F { + let timeout: NodeJS.Timer | null = null; + let callbackArgs: any[] | null = null; + + return function(this: any, ...args: any[]): void { + callbackArgs = args; + if (!timeout) { + timeout = setTimeout(() => { + func.apply(this, callbackArgs); + timeout = null; + }, delay); + } + } as any; +} + let counter = 0; export function uniqueId() { return counter++; diff --git a/js/webampLazy.js b/js/webampLazy.js index 87042525..93e24673 100644 --- a/js/webampLazy.js +++ b/js/webampLazy.js @@ -32,7 +32,8 @@ import { LOADED, REGISTER_VISUALIZER, SET_Z_INDEX, - CLOSE_REQUESTED + CLOSE_REQUESTED, + LOAD_SERIALIZED_STATE } from "./actionTypes"; import Emitter from "./emitter"; @@ -231,6 +232,18 @@ class Winamp { return storeHas(this.store, state => !state.display.loading); } + loadSerializedState(serializedState) { + this.store.dispatch({ type: LOAD_SERIALIZED_STATE, serializedState }); + } + + getSerializedState() { + return Selectors.getSerlializedState(this.store.getState()); + } + + onStateChange(cb) { + return this.store.subscribe(cb); + } + async renderWhenReady(node) { await this.skinIsLoaded(); const genWindowComponents = {}; diff --git a/package.json b/package.json index acabe5f9..6f150376 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "html-webpack-inline-svg-plugin": "^1.2.4", "html-webpack-plugin": "^3.0.6", "http-server": "^0.11.1", + "idb-kv-store": "^4.4.0", "invariant": "^2.2.3", "jest": "^23.5.0", "jest-image-snapshot": "^2.4.1", diff --git a/yarn.lock b/yarn.lock index 220c3ca2..385e290d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4150,6 +4150,13 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" +idb-kv-store@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/idb-kv-store/-/idb-kv-store-4.4.0.tgz#22c56a8d5f90bd88f818a859db9c58627de278be" + dependencies: + inherits "^2.0.3" + promisize "^1.1.2" + ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" @@ -6720,6 +6727,10 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +promisize@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/promisize/-/promisize-1.1.2.tgz#9b47e2cb2ae497eb1ebadc2c4191d64d15c949d1" + prompts@^0.1.9: version "0.1.14" resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2"