WIP: Drive sync flow

This commit is contained in:
I Made Setia Baruna 2020-11-05 13:29:58 +07:00
parent d850e0b921
commit 333a7b6928
10 changed files with 247 additions and 38 deletions

View file

@ -27,6 +27,7 @@
"@rollup/plugin-replace": "^2.3.4",
"@rollup/plugin-url": "^5.0.0",
"autoprefixer": "^10.0.1",
"dayjs": "^1.9.4",
"dotenv": "^8.2.0",
"postcss": "^8.1.2",
"postcss-load-config": "^3.0.0",
@ -37,6 +38,7 @@
"sapper": "^0.28.0",
"svelte": "^3.17.3",
"svelte-preprocess": "^4.5.1",
"svelte-simple-modal": "^0.6.1",
"tailwindcss": "^1.9.5"
}
}

View file

@ -1,12 +1,22 @@
<script>
import { onMount } from 'svelte';
import { driveSignedIn, driveLoading } from '../stores/dataSync';
// doc: /static/images.save_sync_flow.png
import dayjs from 'dayjs';
import { onMount, getContext } from 'svelte';
import { driveSignedIn, driveLoading, saveId } from '../stores/dataSync';
import { getLocalSaveJson, updateSave, updateTime, UPDATE_TIME_KEY } from '../stores/saveManager';
import SyncConflictModal from '../components/SyncConflictModal.svelte';
const { open: openModal } = getContext('simple-modal');
const CLIENT_ID = __paimon.env.GOOGLE_DRIVE_CLIENT_ID;
const API_KEY = __paimon.env.GOOGLE_DRIVE_API_KEY;
const DISCOVERY_DOCS = ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'];
const SCOPES = 'https://www.googleapis.com/auth/drive.appdata';
$: localSaveExists = $updateTime !== null;
onMount(() => {
const script = document.createElement('script');
script.onload = handleClientLoad;
@ -19,7 +29,7 @@
}
function updateSigninStatus(status) {
console.log('DRIVE signed in:', status);
console.log('update drive signed in status:', status);
driveSignedIn.set(status);
driveLoading.set(false);
@ -28,7 +38,46 @@
}
}
async function copyRemoteToLocal() {
try {
const remoteSaveData = await getData();
for (const k in remoteSaveData) {
updateSave(k, remoteSaveData[k], true);
}
} catch (err) {
console.error(err);
}
}
async function compareLocalSave() {
try {
const data = await getData();
const remoteTime = dayjs(data[UPDATE_TIME_KEY]);
if ($updateTime !== null && remoteTime.diff($updateTime) !== 0) {
console.log('DRIVE SYNC CONFLICT!');
openModal(
SyncConflictModal,
{
remoteTime: remoteTime,
localTime: $updateTime,
},
{
closeButton: false,
closeOnEsc: false,
closeOnOuterClick: false,
styleWindow: { background: '#25294A' },
},
);
}
} catch (err) {
console.error(err);
}
}
// check if remote save.json exists
async function getFiles() {
console.log('checking remote file');
try {
const { result } = await gapi.client.drive.files.list({
spaces: 'appDataFolder',
@ -36,14 +85,17 @@
});
console.log(result);
// create save.json on remote if not exists
if (result.files.length === 0) {
createFile();
await createFile();
} else {
const data = await gapi.client.drive.files.get({
fileId: result.files[0].id,
alt: 'media',
});
console.log(data);
saveId.set(result.files[0].id);
if (localSaveExists) {
await compareLocalSave();
} else {
await copyRemoteToLocal();
}
}
} catch (err) {
console.error(err);
@ -51,6 +103,8 @@
}
async function createFile() {
console.log('creating remote file');
try {
const { result } = await gapi.client.drive.files.create({
resource: {
@ -60,16 +114,45 @@
fields: 'id',
});
const data = await gapi.client.request({
path: `/upload/drive/v3/files/${result.id}`,
saveId.set(result.id);
if (localSaveExists) {
await saveData(getLocalSaveJson());
}
console.log(result);
} catch (err) {
console.error(err);
}
}
async function getData() {
console.log('reading remote file');
try {
const { result } = await gapi.client.drive.files.get({
fileId: $saveId,
alt: 'media',
});
return result;
} catch (err) {
console.error(err);
}
}
async function saveData(data) {
console.log('saving to remote file');
try {
await gapi.client.request({
path: `/upload/drive/v3/files/${$saveId}`,
method: 'PATCH',
params: {
uploadType: 'media',
},
body: JSON.stringify({ v: __paimon.env.CURRENT_VERSION }),
body: data,
});
console.log(data);
} catch (err) {
console.error(err);
}

View file

@ -0,0 +1,48 @@
<script>
import { mdiCloudAlert, mdiContentSave, mdiDownload, mdiFile, mdiGoogleDrive, mdiUpload } from '@mdi/js';
import Button from './Button.svelte';
import Icon from './Icon.svelte';
export let remoteTime;
export let localTime;
const remoteFormatted = remoteTime.format('dddd, MMMM D, YYYY h:mm A');
const localFormatted = localTime.format('dddd, MMMM D, YYYY h:mm A');
const remoteNewer = remoteTime.isAfter(localTime);
</script>
<div class="flex">
<Icon path={mdiCloudAlert} color="white" size={2} />
<p class="flex-1 text-white text-lg md:text-xl font-bold ml-4">
Your local data is conflicting with the ones stored in the Google Drive!
</p>
</div>
<div class="mt-4 p-4 bg-item rounded-xl text-white">
<p class="font-bold">
<Icon path={mdiGoogleDrive} className="mr-2" />
Google Drive Data -
{remoteNewer ? 'NEWER' : 'OLDER'}
</p>
<p class="text-gray-400 mt-1">Last modified: {remoteFormatted}</p>
<Button className="mt-2 w-full">
<Icon path={mdiDownload} className="mr-1" />Replace Local Data
</Button>
</div>
<p class="mt-2 text-white text-center">OR</p>
<div class="mt-2 p-4 bg-item rounded-xl text-white">
<p class="font-bold">
<Icon path={mdiFile} className="mr-2" />
Local Data -
{remoteNewer ? 'OLDER' : 'NEWER'}
</p>
<p class="text-gray-400 mt-1">Last modified: {localFormatted}</p>
<Button className="mt-2 w-full">
<Icon path={mdiUpload} className="mr-1" />Replace Google Drive Data
</Button>
</div>
<div class="flex mt-6 justify-end">
<Button className="w-full md:w-auto">
<Icon path={mdiContentSave} className="mr-1" />Download Both Data
</Button>
</div>

View file

@ -1,12 +1,21 @@
<script>
import { onMount } from 'svelte';
import Modal from 'svelte-simple-modal';
import Tailwind from '../components/Tailwindcss.svelte';
import Sidebar from '../components/Sidebar/Sidebar.svelte';
import Header from '../components/Header.svelte';
import DataSync from '../components/DataSync.svelte';
import { showSidebar } from '../stores/sidebar';
import { checkLocalSave } from '../stores/saveManager';
export let segment;
// check local storage save on load
onMount(() => {
checkLocalSave();
});
</script>
<svelte:head>
@ -20,12 +29,13 @@
{#if $showSidebar}
<Sidebar {segment} mobile />
{/if}
<main style="flex: 1 0 auto;">
<slot />
</main>
<Modal>
<main style="flex: 1 0 auto;">
<slot />
</main>
<DataSync />
</Modal>
<p class="lg:ml-64 px-8 py-4 text-gray-600">
Paimon.moe is not affiliated with miHoYo.<br />
Genshin Impact, game content and materials are trademarks and copyrights of miHoYo.
</p>
<DataSync />

View file

@ -1,9 +1,9 @@
<script>
import { mdiGoogleDrive, mdiLoading } from '@mdi/js';
import { mdiCheckCircleOutline, mdiGoogleDrive, mdiLoading } from '@mdi/js';
import Button from '../components/Button.svelte';
import Icon from '../components/Icon.svelte';
import { driveSignedIn, driveLoading } from '../stores/dataSync';
import { driveSignedIn, driveLoading, synced } from '../stores/dataSync';
function signIn() {
gapi.auth2.getAuthInstance().signIn();
@ -15,20 +15,32 @@
</script>
<div class="lg:ml-64 pt-20 px-8 lg:pt-8">
<p class="text-white mb-2">
Paimon.moe use Application Data Directory on your Google Drive to save and sync your wish counter and todo list.
</p>
<p class="text-white mb-4">Paimon.moe can only read and write file that this site create.</p>
{#if $driveLoading}
<Icon path={mdiLoading} color="white" spin />
{:else if !$driveSignedIn}
<Button on:click={signIn}>
<Icon path={mdiGoogleDrive} className="mr-2" />
Sign in to Google Drive
</Button>
{:else}
<p class="text-white">Drive signed in: {$driveSignedIn}</p>
{#if !$driveSignedIn}
<Button on:click={signIn}>
<Icon path={mdiGoogleDrive} className="mr-2" />
Sign in to Google Drive
</Button>
{:else}
<Button on:click={signOut}>
<Icon path={mdiGoogleDrive} className="mr-2" />
Sign out Google Drive
</Button>
{/if}
<Button on:click={signOut}>
<Icon path={mdiGoogleDrive} className="mr-2" />
Sign out Google Drive
</Button>
<p class="text-white mt-4">
Sync Status:
<span class={`font-bold ${$synced ? 'text-green-400' : 'text-yellow-400'}`}>
{$synced ? 'Synced' : 'Syncing...'}
{#if $synced}
<Icon path={mdiCheckCircleOutline} className="text-green-400" />
{:else}
<Icon path={mdiLoading} className="text-yellow-400" spin />
{/if}
</span>
</p>
{/if}
</div>

View file

@ -1,9 +1,11 @@
<script>
import { mdiStar } from '@mdi/js';
import { onMount } from 'svelte';
import { mdiStar } from '@mdi/js';
import debounce from 'lodash/debounce';
import Button from '../../components/Button.svelte';
import Icon from '../../components/Icon.svelte';
import { readSave, updateSave, fromRemote } from '../../stores/saveManager';
export let id = '';
export let name = '';
@ -12,13 +14,16 @@
let rare = 0;
$: path = `wish-counter-${id}`;
$: if ($fromRemote) {
readLocalData();
}
onMount(() => {
readLocalData();
});
function readLocalData() {
const data = localStorage.getItem(path);
const data = readSave(path);
if (data !== null) {
const counterData = JSON.parse(data);
total = counterData.total;
@ -27,14 +32,15 @@
}
}
function saveData() {
const saveData = debounce(() => {
const data = JSON.stringify({
total,
legendary,
rare,
});
localStorage.setItem(path, data);
}
updateSave(path, data);
}, 2000);
function add(val) {
if (total + val < 0) return;

View file

@ -2,3 +2,5 @@ import { writable } from 'svelte/store';
export const driveSignedIn = writable(false);
export const driveLoading = writable(true);
export const synced = writable(true);
export const saveId = writable('');

36
src/stores/saveManager.js Normal file
View file

@ -0,0 +1,36 @@
import dayjs from 'dayjs';
import { writable } from 'svelte/store';
export const updateTime = writable(null);
export const fromRemote = writable(false);
export const UPDATE_TIME_KEY = 'update-time';
export const checkLocalSave = () => {
const localUpdateTime = localStorage.getItem(UPDATE_TIME_KEY);
if (localUpdateTime !== null) {
updateTime.set(dayjs(localUpdateTime));
console.log('local save update time:', localUpdateTime);
}
};
export const getLocalSaveJson = () => {
return JSON.stringify(localStorage);
};
export const updateSave = (key, data, isFromRemote) => {
localStorage.setItem(key, data);
if (!isFromRemote) {
const currentTime = dayjs().toISOString();
updateTime.set(currentTime);
localStorage.setItem(UPDATE_TIME_KEY, currentTime);
} else {
fromRemote.set(true);
}
};
export const readSave = (key) => {
const data = localStorage.getItem(key);
return data;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -1278,6 +1278,11 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
dayjs@^1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.9.4.tgz#fcde984e227f4296f04e7b05720adad2e1071f1b"
integrity sha512-ABSF3alrldf7nM9sQ2U+Ln67NRwmzlLOqG7kK03kck0mw3wlSSEKv/XhKGGxUjQcS57QeiCyNdrFgtj9nWlrng==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -2386,6 +2391,11 @@ svelte-preprocess@^4.5.1:
detect-indent "^6.0.0"
strip-indent "^3.0.0"
svelte-simple-modal@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/svelte-simple-modal/-/svelte-simple-modal-0.6.1.tgz#5e984f384dda16bc50f00846314dc140ad89864b"
integrity sha512-GJGYj+jymzuar105fwkZ73dtcSFCordpbHqt53iE1N1GdqhvEmSs24idRzyIcO7TrTD/V/287X1icFXp88RQHQ==
svelte@^3.17.3:
version "3.29.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.29.0.tgz#80acac4254341ad8f3301e5ef03f4127ea967d96"