Skip to content

Commit

Permalink
Local Backup and Restore #60
Browse files Browse the repository at this point in the history
  • Loading branch information
AguzzTN54 committed Dec 1, 2023
1 parent 2c9caf1 commit 5775131
Show file tree
Hide file tree
Showing 17 changed files with 962 additions and 252 deletions.
9 changes: 6 additions & 3 deletions src/lib/components/ModalTpl.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
export let blank = false;
export let disabled = false;
export let confirmOnly = false;
export let noDimiss = false;
let content;
onMount(() =>
Expand All @@ -21,8 +22,10 @@
const dispatch = createEventDispatcher();
const confirmClick = () => dispatch('confirm');
const cancelClik = () => {
if (confirmOnly) return;
const cancelClik = () => dispatch('cancel');
const dimiss = () => {
if (confirmOnly || noDimiss) return;
dispatch('cancel');
};
Expand All @@ -48,7 +51,7 @@
class:dark
in:fade={{ duration: 200 }}
out:fade={{ duration: 80 }}
on:mousedown|self={cancelClik}
on:mousedown|self={dimiss}
>
<div
class="modal-content"
Expand Down
185 changes: 185 additions & 0 deletions src/lib/helpers/dataAPI/data-merger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { locale } from 'svelte-i18n';
import { initialAmount } from '$lib/data/wish-setup.json';
import { BannerManager, HistoryManager } from '$lib/store/IDB-manager';
import * as stores from '$lib/store/app-stores';
import { cookie } from '$lib/store/cookie';
import {
fatepointManager,
localBalance,
owneditem,
storageLocal,
rollCounter as rq,
ownedOutfits as costumeManager
} from '$lib/store/localstore-manager';
import { adKey } from '../accessKey';
import { onlineBanner } from '../custom-banner';

export const placeDataToAppDB = async (parsedFile, action) => {
if (action === 'replace') await replaceData(parsedFile);
if (action === 'merge') await mergeData(parsedFile);

await setAccessKey(parsedFile.accessKey);
};

const setAccessKey = (key) => {
const hasKey = cookie.get('accessKey');
if (hasKey) return;
return adKey.verify(key);
};

const { clearIDB: clearHistories, addHistory, getAllHistories } = HistoryManager;
const { clear: clearCustomBanner, put: addCustomBanner, get: getCustomBanner } = BannerManager;

const replaceData = async ({ settings, histories, banners } = {}) => {
// replace custom banner
await clearCustomBanner();
for (const banner of banners) {
await addCustomBanner(banner);
}

// Replace histories;
await clearHistories();
for (const history of histories) {
await addHistory(history);
}

// Reset Setting and Localstorage
const { date } = storageLocal.get('export');
settings.data.export.date = date;
const settingString = JSON.stringify(settings);
localStorage.setItem('WishSimulator.App', settingString);
updateSiteSettings(settings.data);
};

const mergeData = async ({ settings, histories, banners } = {}) => {
await mergeCustomBanner(banners);
await mergeHistories(histories);
const updatedSettings = mergeSettings(settings.data);
updateSiteSettings(updatedSettings);
};

const updateSiteSettings = (settings = {}) => {
const { balance = {}, config = {}, rollcounter = {} } = settings;

// Balance Update
const balanceKey = ['acquaint', 'intertwined', 'genesis', 'primogem', 'stardust', 'starglitter'];
balanceKey.forEach((key) => {
if (key in balance) return stores[key].set(balance[key]); // restore values
if (key.match('primo')) return stores[key].set(initialAmount['primogem']); // set default value
if (!key.match(/(acquaint|intertwined)/)) return stores[key].set(0);
return stores[key].set(initialAmount['fates']); // default values for fates stone
});

// Beginner Banner
const { beginner = 0 } = rollcounter;
const starterRemain = 20 - beginner;
stores.beginnerRemaining.set(starterRemain < 0 ? 0 : starterRemain);
stores.showBeginner.set(beginner < 20);

// other store setting
const { locale: lang, autoskip, wishAmount, multipull } = config;
locale.set(lang);
stores.autoskip.set(autoskip);
stores.wishAmount.set(wishAmount);
stores.multipull.set(multipull);
};

const mergeCustomBanner = async (banners = []) => {
const shareIDs = [];
for (const banner of banners) {
const { itemID: idFromImport, shareID: shareIDFormImport, dateFromImport } = banner;
const checkDB = await getCustomBanner(idFromImport);
const isNewerData = new Date(dateFromImport) > new Date(checkDB?.lastModified);

// collect shareID to verify banner is available Online
if (shareIDFormImport && !checkDB?.shareID) {
shareIDs.push({ id: shareIDFormImport, itemID: idFromImport });
}

// add newer modified item
if (!checkDB || isNewerData) {
const bannerData = isNewerData ? { ...banner, isChanged: true } : banner;
await addCustomBanner(bannerData);
continue;
}
}

if (shareIDs.length < 1) return;
// Renew if banner not detected in cloud
const ids = shareIDs.map(({ id }) => id).join(',');
const { success, data = [] } = (await onlineBanner.getData(ids, 'multi')) || {};
if (!success) return;

const cloudBannerIDs = data.map(({ id }) => id);
const unAvailableBanner = shareIDs.filter(({ id }) => !cloudBannerIDs.includes(id));

for (let i = 0; i < unAvailableBanner.length; i++) {
const { itemID } = unAvailableBanner[i] || {};
const oldData = await getCustomBanner(itemID);
delete oldData.hostedImages;
delete oldData.imageHash;
delete oldData.shareID;
oldData.imgChanged = { artURL: true, faceURL: true, thumbnail: true };
oldData.lastModified = new Date().toISOString();
oldData.isChanged = true;
await addCustomBanner(oldData);
}
};

const mergeHistories = async (histories) => {
const currentHistories = await getAllHistories();
const compareArray = currentHistories.map(({ itemID, bannerName, pity, time }) => {
const dataString = `${itemID}/${bannerName}/${pity}/${time}`;
return dataString;
});

for (let i = 0; i < histories.length; i++) {
const newData = histories[i];
const { itemID, bannerName, pity, time, banner } = newData;
const newDataString = `${itemID}/${bannerName}/${pity}/${time}`;
const isDuplicated = compareArray.includes(newDataString);

if (isDuplicated) continue;
owneditem.put({ itemID });
await addHistory(newData);
rq.put(banner);
}
};

const mergeSettings = (settings = {}) => {
const { balance = {}, fatepoint = [], ownedOutfits = [] } = settings;
Object.keys(balance).forEach((key) => {
const before = localBalance.get(key);
const after = before + balance[key];
localBalance.set(key, after);
});

// Merge Fatepoint
const currentFatePoint = fatepointManager.getAll();
const comparePoint = currentFatePoint.map(({ version, phase }) => `${version}-${phase}`);

for (let i = 0; i < fatepoint.length; i++) {
const newPoint = fatepoint[i] || {};
const newDataString = `${newPoint.version}-${newPoint.phase}`;
const isDuplicated = comparePoint.includes(newDataString);

if (isDuplicated) continue;
fatepointManager.restore(newPoint);
}

// Merge outfit
const currentOutfit = costumeManager.getAll();
const compareOutfit = currentOutfit.map(({ name }) => name).filter((n) => n);

for (let i = 0; i < ownedOutfits.length; i++) {
const newOutfit = ownedOutfits[i];
const isDuplicated = compareOutfit.includes(newOutfit.name);
if (isDuplicated) continue;
newOutfit.outfitName = newOutfit.name;
costumeManager.set(newOutfit);
}

const { data: finalSettings } = storageLocal.getData();
return finalSettings;
};

48 changes: 46 additions & 2 deletions src/lib/helpers/dataAPI/export-import.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import { BannerManager, HistoryManager } from '$lib/store/IDB-manager';
import { cookie } from '$lib/store/cookie';
import { storageLocal } from '$lib/store/localstore-manager';
import { randomNumber } from '../gacha/itemdrop-base';

const generateExportID = () => {
const { id } = storageLocal.get('export');
const exportID = id || `GI${randomNumber(111111111, 999999999)}`;
const date = new Date();
storageLocal.set('export', { id: exportID, date });
};

export const generateFileString = async () => {
const banners = await BannerManager.getAll();
const histories = await HistoryManager.getAllHistories();
const settings = storageLocal.getData();
const accessKey = cookie.get('accessKey');

generateExportID();
const settings = storageLocal.getData();
const dataToExport = { banners, histories, settings, accessKey };
return JSON.stringify(dataToExport);
};

export const generateExport = async () => {
export const exportFileLegacy = async () => {
const text = await generateFileString();
const blob = new Blob([text], { type: 'text/plain' });
const anchor = document.createElement('a');
Expand All @@ -24,3 +33,38 @@ export const generateExport = async () => {
anchor.click();
};

export const allowedType = 'application/octet-stream, application/json, text/plain';
const isValidBackupFile = (file) => {
const checkType = allowedType.match(file.type);
const checkExt = file.name.match(/.(bin|json|txt)$/);
const isValidFile = checkType && checkExt;
return isValidFile;
};

const verifyBackupFile = async (file) => {
try {
const fileContent = await file.text();
const parsed = JSON.parse(fileContent);
const { id } = parsed?.settings?.data?.export || {};
if (!id) return null;
return parsed;
} catch (e) {
return null;
}
};

export const parseFileObj = async (file) => {
const isValidFile = isValidBackupFile(file);
if (!isValidFile) throw new Error('Not a valid Backup File');

const parsedFile = await verifyBackupFile(file);
if (!parsedFile) throw new Error('Failed to parse imported file');
return parsedFile;
};

export const importFileLegacy = async (files) => {
const file = files[0];
const parsedFile = await parseFileObj(file);
return { file, parsedFile };
};

74 changes: 65 additions & 9 deletions src/lib/helpers/dataAPI/filesystem.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { AssetManager } from '$lib/store/IDB-manager';
import { autoExport, fileData, fileHandle, savingToSystem } from '$lib/store/filesystem-store';
import {
autoExport,
fileData,
fileHandle as storeHandle,
savingToSystem
} from '$lib/store/filesystem-store';
import { browserDetect } from '../mobileDetect';
import { generateFileString } from './export-import';
import { generateFileString, parseFileObj } from './export-import';

export const FSSupported = () => {
const { isSupported } = browserDetect();
return isSupported && ('chooseFileSystemEntries' in window || 'showOpenFilePicker' in window);
const { isSupported: browserSupported } = browserDetect();
const oldMethodSuppported = 'chooseFileSystemEntries' in window;
const newMethodSupported = 'showSaveFilePicker' in window;
return browserSupported && (oldMethodSuppported || newMethodSupported);
};

export const calculateByteSize = (size) => {
if (!size || isNaN(size)) return '...B';
const mb = (size / (1024 * 1024)).toFixed(2);
return `${mb}MB`;
};

const setFileStore = async (fileHandle) => {
Expand All @@ -21,10 +34,10 @@ const saveHandle = async (fileHandle) => {
};

export const readFileHandle = async () => {
const { fileHandle: file } = (await AssetManager.get('savedFile')) || {};
if (!file) return null;
fileData.set({ name: file.name });
fileHandle.set(file);
const { fileHandle } = (await AssetManager.get('savedFile')) || {};
if (!fileHandle) return null;
fileData.set({ name: fileHandle.name });
storeHandle.set(fileHandle);
};

const clearLocalFile = async () => {
Expand Down Expand Up @@ -106,6 +119,7 @@ export const saveExport = async () => {
await saveHandle(fileHandle);

setFileStore(fileHandle);
storeHandle.set(fileHandle);
autoExport.set(true);
savingToSystem.set(false);
return fileHandle;
Expand All @@ -122,7 +136,9 @@ export const renewSavedFile = async () => {
if (!fileHandle) return savingToSystem.set(false); //no filehandle detected

const isExist = await checkFileExist(fileHandle);
if (!isExist) throw new Error('Target File is not exist, Auto Export will be turned off');
if (!isExist) {
throw new Error('Destination File does not exist, Auto Export will be turned off');
}

const fileString = await generateFileString();
await writeFile(fileHandle, fileString);
Expand All @@ -134,3 +150,43 @@ export const renewSavedFile = async () => {
}
};

// Read Import File
const getFileData = async (handle) => {
const { fileHandle: currentHandle } = await AssetManager.get('savedFile');
const isSameHandle = await handle.isSameEntry(currentHandle);
if (isSameHandle) throw new Error('You cannot import the currently exported file.');

const file = await handle.getFile();
const parsedFile = await parseFileObj(file);
return { handle, file, parsedFile };
};

export const readDropedFile = async (items) => {
const fileHandlesPromises = [...items]
.filter((item) => item.kind === 'file')
.map((item) => item.getAsFileSystemHandle());

for await (const handle of fileHandlesPromises) {
if (handle.kind !== 'file') continue;
return getFileData(handle);
}
};

// export const showFilePicker = async () => {
// const pickerOptions = {
// types: [
// {
// accept: {
// 'application/octet-stream': ['.bin'],
// 'application/json': ['.json'],
// 'text/plain': ['.txt']
// }
// }
// ],
// excludeAcceptAllOption: true,
// multiple: false
// };
// const [handle] = await window.showOpenFilePicker(pickerOptions);
// return getFileData(handle);
// };

Loading

0 comments on commit 5775131

Please sign in to comment.