mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 00:05:09 +01:00
hope it works: Merge pull request #40 from mrjvs/better-localstorage
Versioning for localstorage items
This commit is contained in:
commit
9448b990e3
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { TypeSelector } from './TypeSelector';
|
||||
import { NumberSelector } from './NumberSelector';
|
||||
import { VideoProgressStore } from '../lib/storage/VideoProgress'
|
||||
import './EpisodeSelector.css'
|
||||
|
||||
export function EpisodeSelector({ setSelectedSeason, selectedSeason, setEpisode, seasons, episodes, currentSeason, currentEpisode, streamData }) {
|
||||
const choices = episodes ? episodes.map(v => {
|
||||
let progressData = JSON.parse(localStorage.getItem('video-progress') || "{}")
|
||||
const progressData = VideoProgressStore.get();
|
||||
|
||||
let currentlyAt = 0;
|
||||
let totalDuration = 0;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
import { Arrow } from './Arrow'
|
||||
// import { Cross } from './Crosss'
|
||||
import { PercentageOverlay } from './PercentageOverlay'
|
||||
import { VideoProgressStore } from '../lib/storage/VideoProgress'
|
||||
import './MovieRow.css'
|
||||
|
||||
// title: string
|
||||
// onClick: () => void
|
||||
export function MovieRow(props) {
|
||||
const progressData = JSON.parse(localStorage.getItem("video-progress") || "{}")
|
||||
const progressData = VideoProgressStore.get();
|
||||
let progress;
|
||||
let percentage = null;
|
||||
|
||||
|
43
src/lib/storage/VideoProgress.js
Normal file
43
src/lib/storage/VideoProgress.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { versionedStoreBuilder } from './base.js';
|
||||
|
||||
/*
|
||||
version 0
|
||||
{
|
||||
[{scraperid}]: {
|
||||
movie: {
|
||||
[{movie-id}]: {
|
||||
full: {
|
||||
currentlyAt: number,
|
||||
totalDuration: number,
|
||||
updatedAt: number, // unix timestamp in ms
|
||||
meta: FullMetaObject, // no idea whats in here
|
||||
}
|
||||
}
|
||||
},
|
||||
show: {
|
||||
[{show-id}]: {
|
||||
[{season}-{episode}]: {
|
||||
currentlyAt: number,
|
||||
totalDuration: number,
|
||||
updatedAt: number, // unix timestamp in ms
|
||||
show: {
|
||||
episode: string,
|
||||
season: string,
|
||||
},
|
||||
meta: FullMetaObject, // no idea whats in here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
export const VideoProgressStore = versionedStoreBuilder()
|
||||
.setKey('video-progress')
|
||||
.addVersion({
|
||||
version: 0,
|
||||
create() {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
.build()
|
230
src/lib/storage/base.js
Normal file
230
src/lib/storage/base.js
Normal file
@ -0,0 +1,230 @@
|
||||
function buildStoreObject(d) {
|
||||
const data = {
|
||||
versions: d.versions,
|
||||
currentVersion: d.maxVersion,
|
||||
id: d.storageString,
|
||||
}
|
||||
|
||||
function update(obj) {
|
||||
if (!obj)
|
||||
throw new Error("object to update is not an object");
|
||||
|
||||
// repeat until object fully updated
|
||||
if (obj["--version"] === undefined)
|
||||
obj["--version"] = 0;
|
||||
while (obj["--version"] !== this.currentVersion) {
|
||||
// get version
|
||||
let version = obj["--version"] || 0;
|
||||
if (version.constructor !== Number || version < 0)
|
||||
version = -42; // invalid on purpose so it will reset
|
||||
else {
|
||||
version = (version+1).toString()
|
||||
}
|
||||
|
||||
// check if version exists
|
||||
if (!this.versions[version]) {
|
||||
console.error(`Version not found for storage item in store ${this.id}, resetting`);
|
||||
obj = null;
|
||||
break;
|
||||
}
|
||||
|
||||
// update object
|
||||
obj = this.versions[version].update(obj);
|
||||
}
|
||||
|
||||
// if resulting obj is null, use latest version as init object
|
||||
if (obj === null) {
|
||||
console.error(`Storage item for store ${this.id} has been reset due to faulty updates`);
|
||||
return this.versions[this.currentVersion.toString()].init();
|
||||
}
|
||||
|
||||
// updates succesful, return
|
||||
return obj;
|
||||
}
|
||||
|
||||
function get() {
|
||||
// get from storage api
|
||||
const store = this;
|
||||
let data = localStorage.getItem(this.id);
|
||||
|
||||
// parse json if item exists
|
||||
if (data) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
if (!data.constructor) {
|
||||
console.error(`Storage item for store ${this.id} has not constructor`)
|
||||
throw new Error("storage item has no constructor")
|
||||
}
|
||||
if (data.constructor !== Object) {
|
||||
console.error(`Storage item for store ${this.id} is not an object`)
|
||||
throw new Error("storage item is not an object")
|
||||
}
|
||||
} catch (_) {
|
||||
// if errored, set to null so it generates new one, see below
|
||||
console.error(`Failed to parse storage item for store ${this.id}`)
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
|
||||
// if item doesnt exist, generate from version init
|
||||
if (!data) {
|
||||
data = this.versions[this.currentVersion.toString()].init();
|
||||
}
|
||||
|
||||
// update the data if needed
|
||||
data = this.update(data);
|
||||
|
||||
// add a save object to return value
|
||||
data.save = function save() {
|
||||
localStorage.setItem(store.id, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// add instance helpers
|
||||
Object.entries(d.instanceHelpers).forEach(([name, helper]) => {
|
||||
if (data[name] !== undefined)
|
||||
throw new Error(`helper name: ${name} on instance of store ${this.id} is reserved`)
|
||||
data[name] = helper.bind(data);
|
||||
})
|
||||
|
||||
// return data
|
||||
return data;
|
||||
}
|
||||
|
||||
// add functions to store
|
||||
data.get = get.bind(data);
|
||||
data.update = update.bind(data);
|
||||
|
||||
// add static helpers
|
||||
Object.entries(d.staticHelpers).forEach(([name, helper]) => {
|
||||
if (data[name] !== undefined)
|
||||
throw new Error(`helper name: ${name} on store ${data.id} is reserved`)
|
||||
data[name] = helper.bind({});
|
||||
})
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Builds a versioned store
|
||||
*
|
||||
* manages versioning of localstorage items
|
||||
*/
|
||||
export function versionedStoreBuilder() {
|
||||
return {
|
||||
_data: {
|
||||
versionList: [],
|
||||
maxVersion: 0,
|
||||
versions: {},
|
||||
storageString: null,
|
||||
instanceHelpers: {},
|
||||
staticHelpers: {},
|
||||
},
|
||||
|
||||
/*
|
||||
* set key of localstorage item, must be unique
|
||||
*/
|
||||
setKey(str) {
|
||||
this._data.storageString = str;
|
||||
return this;
|
||||
},
|
||||
|
||||
/*
|
||||
* add a version to the store
|
||||
*
|
||||
* version: version number
|
||||
* migrate: function to update from previous version to this version
|
||||
* create: function to return an empty storage item from this version (in correct syntax)
|
||||
*/
|
||||
addVersion({ version, migrate, create }) {
|
||||
// input checking
|
||||
if (version < 0)
|
||||
throw new Error("Cannot add version below 0 in store");
|
||||
if (version > 0 && !migrate)
|
||||
throw new Error(`Missing migration on version ${version} (needed for any version above 0)`);
|
||||
|
||||
// update max version list
|
||||
if (version > this._data.maxVersion)
|
||||
this._data.maxVersion = version;
|
||||
// add to version list
|
||||
this._data.versionList.push(version);
|
||||
|
||||
|
||||
// register version
|
||||
this._data.versions[version.toString()] = {
|
||||
version: version, // version number
|
||||
update: migrate ? (data) => { // update function, and increment version
|
||||
migrate(data);
|
||||
data["--version"] = version;
|
||||
return data;
|
||||
} : null,
|
||||
init: create ? () => { // return an initial object
|
||||
const data = create();
|
||||
data["--version"] = version;
|
||||
return data;
|
||||
} : null
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/*
|
||||
* register a instance or static helper to the store
|
||||
*
|
||||
* name: name of the helper function
|
||||
* helper: function to execute, the 'this' context is the current storage item (type is instance)
|
||||
* type: "instance" or "static". instance is put on the storage item when you store.get() it, static is on the store
|
||||
*/
|
||||
registerHelper({ name, helper, type }) {
|
||||
// type
|
||||
if (!type)
|
||||
type = "instance"
|
||||
|
||||
// input checking
|
||||
if (!name || name.constructor !== String) {
|
||||
throw new Error("helper name is not a string")
|
||||
}
|
||||
if (!helper || helper.constructor !== Function) {
|
||||
throw new Error("helper function is not a function")
|
||||
}
|
||||
if (!["instance", "static"].includes(type)) {
|
||||
throw new Error("helper type must be either 'instance' or 'static'")
|
||||
}
|
||||
|
||||
// register helper
|
||||
if (type === "instance")
|
||||
this._data.instanceHelpers[name] = helper
|
||||
else if (type === "static")
|
||||
this._data.staticHelpers[name] = helper
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/*
|
||||
* returns function store based on what has been set
|
||||
*/
|
||||
build() {
|
||||
// check if version list doesnt skip versions
|
||||
const versionListSorted = this._data.versionList.sort((a,b)=>a-b);
|
||||
versionListSorted.forEach((v, i, arr) => {
|
||||
if (i === 0)
|
||||
return;
|
||||
if (v !== arr[i-1]+1)
|
||||
throw new Error("Version list of store is not incremental");
|
||||
})
|
||||
|
||||
// version zero must exist
|
||||
if (versionListSorted[0] !== 0)
|
||||
throw new Error("Version 0 doesn't exist in version list of store");
|
||||
|
||||
// max version must have init function
|
||||
if (!this._data.versions[this._data.maxVersion.toString()].init)
|
||||
throw new Error(`Missing create function on version ${this._data.maxVersion} (needed for latest version of store)`);
|
||||
|
||||
// check storage string
|
||||
if (!this._data.storageString)
|
||||
throw new Error("storage key not set in store");
|
||||
|
||||
// build versioned store
|
||||
return buildStoreObject(this._data);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import { useMovie } from '../hooks/useMovie'
|
||||
import { VideoElement } from '../components/VideoElement'
|
||||
import { EpisodeSelector } from '../components/EpisodeSelector'
|
||||
import { getStreamUrl } from '../lib/index'
|
||||
import { VideoProgressStore } from '../lib/storage/VideoProgress'
|
||||
|
||||
import './Movie.css'
|
||||
|
||||
@ -81,26 +82,26 @@ export function MovieView(props) {
|
||||
}, [streamData.seasons, streamData.episodes, streamData.type, selectedSeason])
|
||||
|
||||
React.useEffect(() => {
|
||||
let ls = JSON.parse(localStorage.getItem("video-progress") || "{}")
|
||||
const progressData = VideoProgressStore.get();
|
||||
let key = streamData.type === "show" ? `${season}-${episode}` : "full"
|
||||
let time = ls?.[streamData.source]?.[streamData.type]?.[streamData.slug]?.[key]?.currentlyAt;
|
||||
let time = progressData?.[streamData.source]?.[streamData.type]?.[streamData.slug]?.[key]?.currentlyAt;
|
||||
setStartTime(time);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [baseRouteMatch, showRouteMatch]);
|
||||
|
||||
const setProgress = (evt) => {
|
||||
let ls = JSON.parse(localStorage.getItem("video-progress") || "{}")
|
||||
let progressSave = VideoProgressStore.get();
|
||||
|
||||
if (!ls[streamData.source])
|
||||
ls[streamData.source] = {}
|
||||
if (!ls[streamData.source][streamData.type])
|
||||
ls[streamData.source][streamData.type] = {}
|
||||
if (!ls[streamData.source][streamData.type][streamData.slug])
|
||||
ls[streamData.source][streamData.type][streamData.slug] = {}
|
||||
if (!progressSave[streamData.source])
|
||||
progressSave[streamData.source] = {}
|
||||
if (!progressSave[streamData.source][streamData.type])
|
||||
progressSave[streamData.source][streamData.type] = {}
|
||||
if (!progressSave[streamData.source][streamData.type][streamData.slug])
|
||||
progressSave[streamData.source][streamData.type][streamData.slug] = {}
|
||||
|
||||
// Store real data
|
||||
let key = streamData.type === "show" ? `${season}-${episode}` : "full"
|
||||
ls[streamData.source][streamData.type][streamData.slug][key] = {
|
||||
progressSave[streamData.source][streamData.type][streamData.slug][key] = {
|
||||
currentlyAt: Math.floor(evt.currentTarget.currentTime),
|
||||
totalDuration: Math.floor(evt.currentTarget.duration),
|
||||
updatedAt: Date.now(),
|
||||
@ -108,13 +109,13 @@ export function MovieView(props) {
|
||||
}
|
||||
|
||||
if(streamData.type === "show") {
|
||||
ls[streamData.source][streamData.type][streamData.slug][key].show = {
|
||||
progressSave[streamData.source][streamData.type][streamData.slug][key].show = {
|
||||
season,
|
||||
episode
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem("video-progress", JSON.stringify(ls))
|
||||
progressSave.save();
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -12,16 +12,16 @@
|
||||
width: 100%;
|
||||
max-width: 624px;
|
||||
}
|
||||
.cardView nav a {
|
||||
.cardView nav span {
|
||||
padding: 8px 16px;
|
||||
margin-right: 10px;
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
}
|
||||
.cardView nav a:not(.selected-link) {
|
||||
.cardView nav span:not(.selected-link) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.cardView nav a.selected-link {
|
||||
.cardView nav span.selected-link {
|
||||
background: var(--card);
|
||||
color: var(--button-text);
|
||||
font-weight: bold;
|
||||
|
@ -11,6 +11,7 @@ import { Title } from '../components/Title';
|
||||
import { TypeSelector } from '../components/TypeSelector';
|
||||
import { useMovie } from '../hooks/useMovie';
|
||||
import { findContent, getEpisodes, getStreamUrl } from '../lib/index';
|
||||
import { VideoProgressStore } from '../lib/storage/VideoProgress'
|
||||
|
||||
import './Search.css';
|
||||
|
||||
@ -134,7 +135,7 @@ export function SearchView() {
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const progressData = JSON.parse(localStorage.getItem('video-progress') || "{}")
|
||||
const progressData = VideoProgressStore.get();
|
||||
let newContinueWatching = []
|
||||
|
||||
Object.keys(progressData).forEach((source) => {
|
||||
@ -214,9 +215,9 @@ export function SearchView() {
|
||||
|
||||
{/* Nav */}
|
||||
<nav>
|
||||
<a className={page === 'search' ? 'selected-link' : ''} onClick={() => setPage('search')} href>Search</a>
|
||||
<span className={page === 'search' ? 'selected-link' : ''} onClick={() => setPage('search')}>Search</span>
|
||||
{continueWatching.length > 0 ?
|
||||
<a className={page === 'watching' ? 'selected-link' : ''} onClick={() => setPage('watching')} href>Continue watching</a>
|
||||
<span className={page === 'watching' ? 'selected-link' : ''} onClick={() => setPage('watching')}>Continue watching</span>
|
||||
: ''}
|
||||
</nav>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user