diff --git a/index.html b/index.html index 94f4b56..5283998 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,13 @@
edit
+Ctrl+Escape opens a dialog that will allow you to remove all your text from the page.
+ + + diff --git a/script.js b/script.js index 82e6f50..6568e08 100644 --- a/script.js +++ b/script.js @@ -7,7 +7,7 @@ function toggleItem(event) { event.preventDefault(); const gridItem = event.target.closest('.grid-item'); if (event.metaKey || event.shiftKey) { - gridItem?.classList.toggle('lo'); + gridItem?.classList.toggle('lo'); } const editableChild = gridItem.querySelector('[contenteditable]'); @@ -25,7 +25,7 @@ function updateURL() { } } let fragment = '' - if (inverseIds.length > 0 && inverseIds.length < 9 ) { + if (inverseIds.length > 0 && inverseIds.length < 9) { fragment = inverseIds.join('-'); } setFragment(fragment); @@ -37,6 +37,7 @@ function setFragment(fragment) { } function saveContent() { + // save time mmc.current = []; for (const element of el.editableElements) { const parentWithId = element.closest('[id]'); @@ -69,6 +70,10 @@ function loadContent() { handleFragment(); } +function isThereContent() { + return mmc.current.length > 0; +} + function handleFragment() { const fragment = window.location.hash.slice(1); if (fragment) { @@ -89,7 +94,7 @@ function handleFragment() { function keyboardHandler(event) { if (event.ctrlKey && event.key === 'Escape') { - const confirmed = confirm('Are you sure you want to remove all text from this page?'); + const confirmed = confirm('Are you sure you want to remove all text from this page?\nTHIS WILL ALSO DELETE YOUR SYNCED DATA FROM GOOGLE DRIVE!'); if (confirmed) { for (const element of el.editableElements) { element.innerHTML = ''; @@ -127,7 +132,18 @@ function prep() { el.help.addEventListener('click', openUsageDialog); - + el.googleLogin = document.querySelector('#sign-in'); + el.googleLogout = document.querySelector('#sign-out'); + el.googleSyncLoad = document.querySelector('#sync-load'); + el.googleSyncSave = document.querySelector('#sync-save'); + el.googleLogin.style.visibility = 'hidden'; + el.googleLogout.style.visibility = 'hidden'; + el.googleSyncLoad.style.visibility = 'hidden'; + el.googleSyncSave.style.visibility = 'hidden'; + el.googleLogin.addEventListener('click', handleAuthClick); + el.googleLogout.addEventListener('click', handleSignoutClick); + el.googleSyncLoad.addEventListener('click', loadFromDrive); + el.googleSyncSave.addEventListener('click', saveToDrive); } function openUsageDialog() { @@ -138,5 +154,237 @@ function openUsageDialog() { }); } +/** GOOGLE DRIVE STUFFS ** +* What can happen with the sync: + * User has data in local storage, but not in google drive + * User clicks sync + * User has data in google drive, but not in local storage + * google drive data is loaded into local storage + * User has data in both local storage and google drive + * User is prompted to choose which to keep + * User has no data in either local storage or google drive + * User starts a new localStorage session which is synced to google drive + * User has data in both local storage and google drive, but they are the same + * Nothing happens +**/ +const CLIENT_ID = '18857136519-p50s7t4q7os98eijoo94g37mcdl9oj66.apps.googleusercontent.com'; +const API_KEY = 'AIzaSyC6BHVwaIAMnP5itX3frfPJLgR10s2S-3w'; +const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'; +const SCOPES = 'https://www.googleapis.com/auth/drive' + +let tokenClient; +let gapiInited = false; +let gisInited = false; + +/** +* Callback after api.js is loaded. +*/ +function gapiLoaded() { + gapi.load('client', initializeGapiClient); +} + +/** + * Callback after the API client is loaded. Loads the + * discovery doc to initialize the API. + */ +async function initializeGapiClient() { + await gapi.client.init({ + apiKey: API_KEY, + discoveryDocs: [DISCOVERY_DOC], + }); + gapiInited = true; + maybeEnableButtons(); +} + +/** + * Callback after Google Identity Services are loaded. + */ +function gisLoaded() { + tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: CLIENT_ID, + scope: SCOPES, + callback: '', // defined later + }); + gisInited = true; +} +maybeEnableButtons(); + +/** + * Enables user interaction after all libraries are loaded. + */ +function maybeEnableButtons() { + if (gapiInited && gisInited) { + el.googleLogin.style.visibility = 'visible'; + } +} + +/** + * Sign in the user upon button click. + */ +function handleAuthClick() { + tokenClient.callback = async (resp) => { + if (resp.error !== undefined) { + throw (resp); + } + el.googleSyncLoad.style.visibility = 'visible'; + el.googleSyncSave.style.visibility = 'visible'; + el.googleLogout.style.visibility = 'visible'; + el.googleLogin.style.visibility = 'hidden'; + await loadFromDrive(); + }; + + if (gapi.client.getToken() === null) { + // Prompt the user to select a Google Account and ask for consent to share their data + // when establishing a new session. + tokenClient.requestAccessToken({ prompt: 'consent' }); + } else { + // Skip display of account chooser and consent dialog for an existing session. + tokenClient.requestAccessToken({ prompt: '' }); + } +} + +/** + * Sign out the user upon button click. + */ +function handleSignoutClick() { + const token = gapi.client.getToken(); + if (token !== null) { + google.accounts.oauth2.revoke(token.access_token); + gapi.client.setToken(''); + document.getElementById('content').innerText = ''; + el.googleSyncLoad.style.visibility = 'hidden'; + el.googleSyncSave.style.visibility = 'hidden'; + el.googleLogin.style.visibility = 'visible'; + el.googleLogout.style.visibility = 'hidden'; + } +} + +/** + * Discover files + */ +async function loadFromDrive() { + // load .mmc file and prompt user to choose which to keep + // check & load .mmc file exists in drive + let response; + try { + response = await gapi.client.drive.files.list({ + 'fields': 'files(id, name)', + }); + } catch (err) { + console.log(err.message); + return; + } + const files = response.result.files; + if (!files || files.length == 0) { + console.log('No files found.'); + return; + } + + const mmcFiles = files.filter(file => file.name === '.mmc'); + // there should only be one .mmc file + // so we'll just ignore any others for now :) + if (mmcFiles.length == 0) { + newToDrive(); + return; + } + + const mmcFile = mmcFiles[0]; + + // load .mmc file + try { + response = await gapi.client.drive.files.get({ + 'fileId': mmcFile.id, + 'alt': 'media', + }); + } catch (err) { + console.log(err.message); + return; + } + const content = response.body; + const confirmed = confirm('Would you like to load your synced data from Google Drive?\nTHIS WILL OVERWRITE YOUR LOCAL DATA!'); + if (confirmed) { + console.log(content); + localStorage.setItem('mmc', content); + Object.assign(mmc, JSON.parse(content)); + loadContent(); + } +} + +async function saveToDrive() { + // save .mmc file and prompt user to choose which to keep + let response; + try { + response = await gapi.client.drive.files.list({ + 'fields': 'files(id, name)', + }); + } catch (err) { + console.log(err.message); + return; + } + const files = response.result.files; + if (!files || files.length == 0) { + console.log('No files found.'); + return; + } + + const mmcFiles = files.filter(file => file.name === '.mmc'); + // there should only be one .mmc file + // so we'll just ignore any others for now :) + if (mmcFiles.length == 0) { + newToDrive(); + return; + } + + const mmcFile = mmcFiles[0]; + const confirmed = confirm('Would you like to save your data to Google Drive?\nTHIS WILL OVERWRITE YOUR SYNCED DATA!'); + if (confirmed) { + saveContent(); + // save .mmc file + try { + // we have to do this manually because the GAPI does not support BLOB upload + response = await gapi.client.request({ + 'path': `/upload/drive/v3/files/${mmcFile.id}`, + 'method': 'PATCH', + 'params': { + 'uploadType': 'media', + }, + 'body': localStorage.getItem('mmc'), + }); + } catch (err) { + console.log(err.message); + return; + } + } +} + +async function newToDrive() { + // save new .mmc file to drive + let response; + try { + response = await gapi.client.drive.files.create({ + 'name': '.mmc', + 'mimeType': 'text/plain', + }); + } catch (err) { + console.log(err.message); + return; + } + mmc.syncid = response.result.id; +} + +async function deleteFromDrive() { + // delete .mmc file from drive + let response; + try { + response = await gapi.client.drive.files.delete({ + 'fileId': mmc.syncid, + }); + } catch (err) { + console.log(err.message); + return; + } + +} + window.addEventListener('load', prep); diff --git a/styles.css b/styles.css index 2282a7c..e95e31b 100644 --- a/styles.css +++ b/styles.css @@ -246,4 +246,24 @@ dialog button { strong { color: var(--superblack); -} \ No newline at end of file +} + +.google-button { + background: var(--white); + border: 1px solid var(--darkgrey); + border-radius: 0.2em; + padding: 0.1em; + margin: 0.1em; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 0.2em; + cursor: pointer; + transition: all 0.2s linear; +} + +.google-button:hover { + transform: scale(1.1); +} +