391 lines
10 KiB
JavaScript
391 lines
10 KiB
JavaScript
const el = {};
|
|
const mmc = {
|
|
current: []
|
|
};
|
|
|
|
function toggleItem(event) {
|
|
event.preventDefault();
|
|
const gridItem = event.target.closest('.grid-item');
|
|
if (event.metaKey || event.shiftKey) {
|
|
gridItem?.classList.toggle('lo');
|
|
}
|
|
|
|
const editableChild = gridItem.querySelector('[contenteditable]');
|
|
if (editableChild) {
|
|
editableChild.focus();
|
|
}
|
|
updateURL()
|
|
}
|
|
|
|
function updateURL() {
|
|
const inverseIds = [];
|
|
for (const element of document.querySelectorAll('.grid-item')) {
|
|
if (!element.classList.contains('lo')) {
|
|
inverseIds.push(element.id);
|
|
}
|
|
}
|
|
let fragment = ''
|
|
if (inverseIds.length > 0 && inverseIds.length < 9) {
|
|
fragment = inverseIds.join('-');
|
|
}
|
|
setFragment(fragment);
|
|
}
|
|
|
|
function setFragment(fragment) {
|
|
const url = `${window.location.pathname}${window.location.search}#${fragment}`;
|
|
window.history.replaceState(null, null, url);
|
|
}
|
|
|
|
function saveContent() {
|
|
// save time
|
|
mmc.current = [];
|
|
for (const element of el.editableElements) {
|
|
const parentWithId = element.closest('[id]');
|
|
if (parentWithId) {
|
|
mmc.current.push({
|
|
id: parentWithId.id,
|
|
content: element.innerHTML
|
|
});
|
|
}
|
|
}
|
|
localStorage.setItem('mmc', JSON.stringify(mmc));
|
|
}
|
|
|
|
|
|
function loadContent() {
|
|
const mmcString = localStorage.getItem('mmc');
|
|
if (mmcString) {
|
|
Object.assign(mmc, JSON.parse(mmcString));
|
|
for (const element of el.editableElements) {
|
|
const parentWithId = element.closest('[id]');
|
|
if (parentWithId) {
|
|
const item = mmc.current.find(item => item.id === parentWithId.id);
|
|
if (item) {
|
|
element.innerHTML = item.content;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
handleFragment();
|
|
}
|
|
|
|
function isThereContent() {
|
|
return mmc.current.length > 0;
|
|
}
|
|
|
|
function handleFragment() {
|
|
const fragment = window.location.hash.slice(1);
|
|
if (fragment) {
|
|
const ids = fragment.split('-');
|
|
for (const id of ids) {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.classList.remove('lo');
|
|
}
|
|
}
|
|
for (const element of document.querySelectorAll('.grid-item')) {
|
|
if (!ids.includes(element.id)) {
|
|
element.classList.add('lo');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function keyboardHandler(event) {
|
|
if (event.ctrlKey && event.key === 'Escape') {
|
|
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 = '';
|
|
}
|
|
saveContent();
|
|
location.reload();
|
|
}
|
|
} else if (event.key === 'Escape') {
|
|
document.activeElement.blur();
|
|
}
|
|
const gridItem = event.target.closest('.grid-item');
|
|
if (gridItem) {
|
|
if (gridItem.classList.contains('lo')) {
|
|
gridItem.classList.remove('lo');
|
|
}
|
|
}
|
|
}
|
|
|
|
function prep() {
|
|
el.gridItems = document.querySelectorAll('.grid-item');
|
|
el.editableElements = document.querySelectorAll('[contenteditable]');
|
|
el.help = document.querySelector('#help');
|
|
|
|
loadContent();
|
|
|
|
document.addEventListener('keydown', keyboardHandler);
|
|
|
|
for (const item of el.gridItems) {
|
|
item.addEventListener('click', toggleItem);
|
|
}
|
|
|
|
for (const element of el.editableElements) {
|
|
element.addEventListener('input', saveContent);
|
|
}
|
|
|
|
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() {
|
|
const dialog = document.querySelector('#usage');
|
|
dialog.showModal();
|
|
dialog.addEventListener('click', () => {
|
|
dialog.close();
|
|
});
|
|
}
|
|
|
|
/** 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);
|
|
|