Last active
July 23, 2019 18:38
-
-
Save jacekkopecky/8b78dd004d7ce9ca53a51a4da892ca2d to your computer and use it in GitHub Desktop.
Google Drive: remember which folders were open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| Google Drive Folder Tree State Saver | |
| (c) 2019 Jacek Kopecky <[email protected]> | |
| License: Creative Commons Attribution-ShareAlike | |
| This script can be added as custom JS in drive.google.com so the browser remembers which folders you had open. | |
| Tested in Google Chrome with the extension "Custom JavaScript for Websites 2" (id ddbjnfjiigjmcpcpkmhogomapikjbjdk) | |
| analysis of how Drive the webpage works (in case they change it): | |
| element with role=tree contains the tree | |
| tree item container elements have role="treeitem" with aria-expanded=(true or false) | |
| element with data-target="expander" is the expander icon | |
| element with data-name="name" is a label, inside it something with data-tooltip has the text of the label | |
| */ | |
| (() => { | |
| setTimeout(setup, 100); | |
| function findTreeItemEl(el) { | |
| while (el && !isTreeItem(el)) { | |
| el = el.parentElement; | |
| } | |
| return el; | |
| } | |
| function isTreeItem(el) { | |
| return el.getAttribute("role") === 'treeitem'; | |
| } | |
| function getItemName(item) { | |
| return item.querySelector('[data-name=name] [data-tooltip]').textContent; | |
| } | |
| function findTreePath(el) { | |
| const item = findTreeItemEl(el); | |
| if (!item) return []; | |
| else return [...findTreePath(item.parentElement), getItemName(item)]; | |
| } | |
| function getTreeItem(path) { | |
| const stringified = JSON.stringify(path); | |
| for (const item of document.querySelectorAll('[role=treeitem]')) { | |
| if (JSON.stringify(findTreePath(item)) === stringified) return item; | |
| } | |
| return null; | |
| } | |
| function listExpandedTreeItems() { | |
| const retval = []; | |
| for (const item of document.querySelectorAll('[role=treeitem]')) { | |
| if (isExpanded(item)) { | |
| retval.push(findTreePath(item)); | |
| } | |
| } | |
| return retval; | |
| } | |
| function isExpanded(item) { | |
| return item.getAttribute('aria-expanded') === 'true'; | |
| } | |
| function saveExpandedState() { | |
| localStorage.driveFolderExpansionState=JSON.stringify(listExpandedTreeItems()); | |
| console.log('google drive folder tree state saved'); | |
| } | |
| function setupMutationObserver() { | |
| const ob = new MutationObserver(debounce(saveExpandedState, 500)); | |
| const options = { | |
| subtree: true, | |
| attributeFilter: ["aria-expanded"], | |
| }; | |
| ob.observe(document.querySelector('[role=tree]'), options); | |
| } | |
| function restoreExpandedState(state) { | |
| let expanded = 0; | |
| let toExpand = null; | |
| const missing = []; | |
| for (const folder of state) { | |
| const item = getTreeItem(folder); | |
| // item may be missing if it's in a subtree that's not expanded yet | |
| if (item) { | |
| if (isExpanded(item)) { | |
| expanded += 1; | |
| } else { | |
| toExpand = item; | |
| } | |
| } else { | |
| missing.push(folder); | |
| } | |
| } | |
| if (toExpand) toExpand.querySelector('[data-target=expander]').click(); | |
| return { | |
| done: expanded, | |
| total: state.length, | |
| missing, | |
| }; | |
| } | |
| async function setup() { | |
| try { | |
| if (window.googleDriveFolderTreeStateSaverSetupDone) { | |
| console.log('ignoring reload'); | |
| return; | |
| } | |
| let state; | |
| try { | |
| state = JSON.parse(localStorage.driveFolderExpansionState); | |
| } catch (e) { /* do nothing */ } | |
| if (!Array.isArray(state) || state.length === 0) { | |
| showMessage('installing google drive folder tree state saver'); | |
| } else { | |
| msg = 'reopening folders'; | |
| showMessage(msg); | |
| // we expand one item at a time | |
| // some folders may take time to load, this gives them about 4s extra | |
| let tries = state.length + 20; | |
| do { | |
| let {done, total, missing} = restoreExpandedState(state); | |
| showMessage(msg + ` (${done}/${total})`); | |
| if (tries <= 1 && missing.length > 0) { | |
| showMessage('some folder cannot be reopened, see console log'); | |
| for (const folder of missing) { | |
| console.log('could not reopen folder', folder.join(' / ')); | |
| } | |
| } | |
| if (done === total) break; | |
| // give it time to expand and load the item | |
| await delay(200); | |
| tries -= 1; | |
| } while (tries > 0); | |
| } | |
| setupMutationObserver(); | |
| window.googleDriveTreeStateSaverSetupDone = false; | |
| console.log('google drive folder tree state saver enabled'); | |
| } catch (e) { | |
| console.error(e); | |
| showMessage('error ' + e.message); | |
| } | |
| } | |
| // helpful messages in the corner | |
| let hideMessageTimeout; | |
| let msgEl; | |
| function showMessage(msg) { | |
| setupMsgEl(); | |
| msgEl.textContent = msg; | |
| msgEl.style.display = 'block'; | |
| if (hideMessageTimeout) clearTimeout(hideMessageTimeout); | |
| hideMessageTimeout = setTimeout(clearMessage, 2000); | |
| } | |
| function clearMessage() { | |
| msgEl.textContent = 'no message'; | |
| msgEl.style.display = 'none'; | |
| hideMessageTimeout = null; | |
| } | |
| function setupMsgEl() { | |
| if (!msgEl) { | |
| msgEl = document.createElement('div'); | |
| document.body.appendChild(msgEl); | |
| msgEl.style.cssText = ` | |
| display: block; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| font-size: .9rem; | |
| padding: .3em .5em; | |
| background-color: yellow; | |
| color: black; | |
| border: solid black; | |
| border-width: 0 1px 1px 0; | |
| border-radius: 0 0 .3em 0; | |
| z-index: 999; | |
| `; | |
| } | |
| } | |
| function delay(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| // from https://davidwalsh.name/javascript-debounce-function | |
| function debounce(func, wait, immediate) { | |
| let timeout; | |
| return function() { | |
| const context = this, args = arguments; | |
| const later = function() { | |
| timeout = null; | |
| if (!immediate) func.apply(context, args); | |
| }; | |
| const callNow = immediate && !timeout; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| if (callNow) func.apply(context, args); | |
| }; | |
| } | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
extension Custom Javascript: ddbjnfjiigjmcpcpkmhogomapikjbjdk