From 708055da3375ffed348ff67772cb6ed24b8fd167 Mon Sep 17 00:00:00 2001 From: dietrich ayala Date: Thu, 30 May 2024 12:47:49 +0200 Subject: [PATCH] checkypointy --- README.md | 32 +++++ api.js | 5 + background.js | 108 ++++++++++++++++ content-script.js | 12 ++ home.css | 93 ++++++++++++++ home.html | 34 +++++ home.js | 314 ++++++++++++++++++++++++++++++++++++++++++++++ icons/icon.png | Bin 0 -> 3342 bytes icons/icon@2x.png | Bin 0 -> 8382 bytes manifest.json | 53 ++++++++ popup.css | 15 +++ popup.html | 18 +++ popup.js | 54 ++++++++ sessions.js | 71 +++++++++++ 14 files changed, 809 insertions(+) create mode 100644 README.md create mode 100644 api.js create mode 100644 background.js create mode 100644 content-script.js create mode 100644 home.css create mode 100644 home.html create mode 100644 home.js create mode 100644 icons/icon.png create mode 100644 icons/icon@2x.png create mode 100644 manifest.json create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js create mode 100644 sessions.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba08794 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ + +High level: +* Tabs are hidden +* Escape shows tabs page +* Escape again shows groups page +* Escape view hides url bar (true fullscreen)? + +Implementation +* Content-script for * catches ESC, sends to background +* Background catches ESC, switches views (or does nothing, or help page?) +* Content page displays a grid of tabs, or of groups + +TODO +* groups page shows windows as groups? + +Handling home page +* Create new tab for it +* OR use it like about:home, navigate away from it on url click +* Hide from visible tabs in home page +* ESC always goes back to it +* Always hide it, in whatever group it is + +What happens when... + +* User closes a tab +* User closes the window +* User goes fullscreen (from Firefox) +* User goes fullscreen (from web API) + +How to... + +* Open a new tab from home page? Have a URL entry bar there? diff --git a/api.js b/api.js new file mode 100644 index 0000000..e6a0997 --- /dev/null +++ b/api.js @@ -0,0 +1,5 @@ +// browser (Firefox) vs chrome (Chromium) +const isChrome = typeof chrome != 'undefined'; +const api = isChrome ? chrome : browser; + +export default api; diff --git a/background.js b/background.js new file mode 100644 index 0000000..a9c5ef8 --- /dev/null +++ b/background.js @@ -0,0 +1,108 @@ +console.log('background:start'); + +import api from './api.js'; + +const homeURL = api.runtime.getURL('home.html'); +const homeTitle = 'Extension: (Escape) - ESCAPE'; + +/* +"normal" +"popup" +"panel" +"detached_panel" +*/ +//const windowType = 'normal'; +//const windowType = 'popup'; +//const windowType = 'panel'; +const windowType = 'detached_panel'; + +let homeTabId = null; +let port = null; + +// home page first load opens the connection +api.runtime.onConnect.addListener(p => { + console.log('port connected'); + port = p; +}); + +// Listen for ESC from web content +function escapeHandler(message) { + if (message.ESC === 1) { + api.tabs.query({url: homeURL}).then(tabs => { + if (tabs.length) { + api.tabs.update(tabs[0].id, { active: true }); + port.postMessage({show: 'tabs'}); + } + // Otherwise open in a new tab + else { + api.tabs.create({url:homeURL}).then((tab) => { + // We don't want to sync this URL ever nor clutter the users history + api.history.deleteUrl({url:homeURL}); + }).catch((e) => { throw e }); + } + }); + } +} +api.runtime.onMessage.addListener(escapeHandler); + +api.commands.onCommand.addListener((command) => { + console.log('onCommand', command); + if (command === 'open-escape') { + openEscapeWindow(); + } +}); + +const escapeWindowIsOpen = async () => { + if (homeTabId != null) { + return true; + } + else { + const ew = await findEscapeWindow(); + return ew || false; + } +}; + +const findEscapeWindow = async () => { + const ws = await api.windows.getAll({ populate: false, windowTypes: ['popup'] }); + return ws.find(wi => wi.title == homeTitle); +}; + +const createEscapeWindow = async () => { + const opts = { + url: homeURL, + type: windowType + }; + + const onLoadingComplete = async (tabId) => { + homeTabId = tabId; + }; + + api.tabs.onUpdated.addListener((tabId, changeInfo, tabInfo) => { + if (changeInfo.status === 'complete') { + onLoadingComplete(tabInfo.id); + } + }, { properties: ["status"] }); + + await api.windows.create(opts); +}; + +const openEscapeWindow = async () => { + if (await escapeWindowIsOpen()) { + return; + } + createEscapeWindow(); +}; + +await openEscapeWindow(); + +/* +setTimeout(async () => { + const isOpen = await escapeWindowIsOpen(); + console.log('OPEN?', isOpen); + + const w = await findEscapeWindow(); + console.log('W?', w); +}, 1000); +*/ + +console.log('background:end'); diff --git a/content-script.js b/content-script.js new file mode 100644 index 0000000..3650d9f --- /dev/null +++ b/content-script.js @@ -0,0 +1,12 @@ +document.onkeydown = function(evt) { + evt = evt || window.event; + var isEscape = false; + if ("key" in evt) { + isEscape = (evt.key == "Escape" || evt.key == "Esc"); + } else { + isEscape = (evt.keyCode == 27); + } + if (isEscape) { + browser.runtime.sendMessage({"ESC": 1}); + } +}; diff --git a/home.css b/home.css new file mode 100644 index 0000000..81f6bd6 --- /dev/null +++ b/home.css @@ -0,0 +1,93 @@ +.controls { +} + +.controls.tabsview { + display: none; +} + +html { + background: #f5f7f8; + font-family: 'Roboto', sans-serif; + -webkit-font-smoothing: antialiased; + padding: 20px 0; +} + +.cards { + width: 90%; + max-width: 1240px; + margin: 0 auto; + + display: grid; + + grid-template-columns: 1fr; + grid-template-rows: auto; + grid-gap: 20px; +} + +@media only screen and (min-width: 500px) { + .cards { + grid-template-columns: 1fr 1fr; + } +} + +@media only screen and (min-width: 850px) { + .cards { + grid-template-columns: 1fr 1fr 1fr 1fr; + } +} + +/* card */ + +.card { + min-height: 100%; + background: white; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + display: flex; + flex-direction: column; + text-decoration: none; + color: #444; + position: relative; + top: 0; + transition: all .1s ease-in; +} + +.card:hover { + top: -2px; + box-shadow: 0 4px 5px rgba(0,0,0,0.2); +} + +.card article { + padding: 20px; + display: flex; + + flex: 1; + justify-content: space-between; + flex-direction: column; + +} +.card .thumb { + padding-bottom: 60%; + background-size: cover; + background-position: center center; +} + +.card p { + flex: 1; /* make p grow to fill available space*/ + line-height: 1.4; +} + +/* typography */ +h1 { + font-size: 20px; + margin: 0; + color: #333; +} + +.card span { + font-size: 12px; + font-weight: bold; + color: #999; + text-transform: uppercase; + letter-spacing: .05em; + margin: 2em 0 0 0; +} diff --git a/home.html b/home.html new file mode 100644 index 0000000..5c93cbf --- /dev/null +++ b/home.html @@ -0,0 +1,34 @@ + + + + + + + ESCAPE + + + + + + + + +
+ +
+ +
+
+ + + + + diff --git a/home.js b/home.js new file mode 100644 index 0000000..79f8fe6 --- /dev/null +++ b/home.js @@ -0,0 +1,314 @@ +/* + + api.sessions.setTabValue(currentTabId, key, value); + await api.sessions.getTabValue(currentTabId, key); + + init groups + * there's always a default group (no group property on the tab) + * (OR should default group be all tabs) + * otherwise get group property from the tab + + managing storage + * scaling to lots of tabs/groups + * collect all on startup + * manage cache + * what can change? + * new tab in group + * tab changes groups + * tab closed (no action necessary) + * undo close tab to a group that no longer exists (use current group) + + relevant tab events + * new + * set to current group + * activate + * close + +*/ + +// browser (Firefox) vs chrome (Chromium) +//const isChrome = typeof chrome != 'undefined'; +//const api = isChrome ? chrome : browser; +import api from './api.js'; + +const VIEW_GROUPS = 1; +const VIEW_TABS = 2; + +// keys for extension-level data +const EXT_DATA_CONFIG_KEY = 'config'; + +// keys for per-tab data +const TAB_DATA_GROUP_KEY = 'groupId'; + +// default strings +// TODO: move to i18n +const DEFAULT_GROUP_TITLE = 'Default Group'; +const DEFAULT_NEW_GROUP_TITLE = 'New Group'; + +// Handle ESC +document.onkeydown = function(evt) { + evt = evt || window.event; + var isEscape = evt.key == 'Escape'; + if (isEscape && currentView == VIEW_TABS) { + showGroups(); + } +}; + +var currentView = null; + +var config = null; + +//await api.storage.local.remove(EXT_DATA_CONFIG_KEY); + +// Open port to background, so we can listen +// for when we need to show tabs (user hits ESC on web page) +var port = api.runtime.connect({name: 'background'}); +port.onMessage.addListener(function(msg) { + if (msg.show && msg.show == 'tabs') { + showTabs(); + } +}); + +// Kick out the jams +document.addEventListener('DOMContentLoaded', async () => { + // Initialize storage - this loads groups, everything else + await loadStorage(); + + // New group click handler + document.querySelector('.newgroup').addEventListener('click', async () => { + await newGroup(); + }); +}); + +const loadStorage = async () => { + + const data = await api.storage.local.get(EXT_DATA_CONFIG_KEY); + console.log('storage', data); + + // Not first run! + if (data.config) { + config = data.config; + } + else { + // First run! + console.log('first run'); + + config = {}; + + // Set up group storage + config.groups = {}; + + // Create default group + var id = await newGroup(DEFAULT_GROUP_TITLE); + + // Storage default group id as last active group + config.lastGroupId = id; + + // + await updateStorage(); + } + + // Pull data from each tab about which group they belong to + initializeGroupData().then(function() { + // Data loaded, start building UI + populateUI(); + }); +} + +// Save changes to config to persistent storage +// TODO: Temporary hack. Replace with proper evented solution. +const updateStorage = async () => { + await api.storage.local.set({ config }); + console.log('wrote', config); + const data = await api.storage.local.get(EXT_DATA_CONFIG_KEY); + console.log('data', data); +} + +// On DOM +function populateUI() { + // Populate groups with their tabs + initializeGroupData().then(function() { + showTabs(); + }); + + // Listen for things that'll change our state + initTabEventListeners(); +} + +function initTabEventListeners() { + // add new tabs in this window to current group + api.tabs.onCreated.addListener(tab => { + addTabToGroup(tab.id, config.lastGroupId); + }); + + // remove detached tabs from their group + api.tabs.onDetached.addListener(tab => { + removeTabFromGroup(tab.id, config.lastGroupId); + }); + + // remove removed tabs from their group + api.tabs.onRemoved.addListener(tab => { + removeTabFromGroup(tab.id, config.lastGroupId); + }); +} + +function addTabToGroup(tabId, groupId) { + var index = config.groups[groupId].tabs.indexOf(tabId); + if (index == -1) { + config.groups[groupId].tabs.push(tabId); + api.sessions.setTabValue(tabId, TAB_DATA_GROUP_KEY, groupId); + } +} + +function removeTabFromGroup(tabId, groupId) { + var index = config.groups[groupId].tabs.indexOf(tabId); + if (index > -1) { + config.groups[groupId].tabs.splice(index, 1); + api.sessions.remove(tab.id, TAB_DATA_GROUP_KEY); + } +} + +function getGroupById(id) { + return config.groups[id]; +} + +function activateGroup(groupId) { + const group = getGroupById(groupId); + const lastGroup = getGroupById(config.lastGroupId); + + /* + + currentWindowTabs = api.tabs.query({currentWindow: true}); + newGroupTabCount + currentWindowTabCount + const tabCountDifference = Math.abs(newGroupTabCount - currentWindowTabCount); + if (tabCountDifference > 0) { + // close last + } + + */ + + if (group.tabs.length === 0) { + // New group? + api.tabs.create({}).then(tab => { + addTabToGroup(tab.id, groupId); + //api.tabshideshow.show(group.tabs); + //api.tabshideshow.hide(lastGroup.tabs); + setLastActiveGroupId(groupId); + }); + } + else { + //api.tabshideshow.show(group.tabs); + //api.tabshideshow.hide(lastGroup.tabs); + setLastActiveGroupId(groupId); + } +} + +function getLastActiveGroupId() { + return config.lastGroupId; +} + +function setLastActiveGroupId(id) { + config.lastGroupId = id; +} + +function initializeGroupData() { + return new Promise(function(resolve, reject) { + // Clear out old tab ids + for (let id in config.groups) { + config.groups[id].tabs = []; + } + + api.tabs.query({currentWindow: true}).then(tabs => { + tabs.forEach(tab => { + api.sessions.getTabValue(tab.id, TAB_DATA_GROUP_KEY).then(groupId => { + if (groupId && config.groups[groupId]) { + config.groups[groupId].tabs.push(tab.id); + } + else { + // This should only happen on first run. + // Add all default tabs to default group. + var groupId = getLastActiveGroupId(); + addTabToGroup(tab.id, groupId); + } + }); + }); + resolve(); + }); + }); +} + +const newGroup = async (title) => { + console.log('newGroup', title); + var id = window.crypto.getRandomValues(new Uint32Array(1))[0]; + var group = { + id: id, + title: title || 'New Group', + tabs: [] + }; + config.groups[id] = group; + showGroups(); + await updateStorage(); + return id; +} + +function showGroups() { + clearCards(); + for (let id in config.groups) { + var group = config.groups[id]; + var card = addCard(); + card.querySelector('h1').innerText = group.title; + card.dataset.id = group.id; + card.addEventListener('click', e => { + var groupId = parseInt(card.dataset.id); + if (groupId != config.lastGroupId) { + activateGroup(groupId); + } + }); + card.classList.add('group'); + } + currentView = VIEW_GROUPS; + document.querySelector('.controls').classList.add('groupsview'); + document.querySelector('.controls').classList.remove('tabsview'); +} + +function showTabs() { + var group = getGroupById(config.lastGroupId); + clearCards(); + api.tabs.query({currentWindow: true}).then(function(tabs) { + tabs.forEach(tab => { + if (group.tabs.indexOf(tab.id) == -1) { + return; + } + var card = addCard(); + card.querySelector('h1').innerText = tab.title; + card.dataset.id = tab.id; + card.addEventListener('click', e => { + var tabId = parseInt(card.dataset.id); + api.tabs.update(tabId, { active: true }); + }); + }); + }); + currentView = VIEW_TABS; + document.querySelector('.controls').classList.add('tabsview'); + document.querySelector('.controls').classList.remove('groupsview'); +} + +function clearCards() { + var container = document.querySelector('.cards'); + Array.prototype.slice.call(container.children).forEach(child => { + child.parentNode.removeChild(child); + }); +} + +function addCard() { + var container = document.querySelector('.cards'); + + var cardTpl = document.querySelector('.tpl-card') + var cardClone = document.importNode(cardTpl.content, true); + container.appendChild(cardClone); + var card = container.lastElementChild; + card.classList.add('card'); + + return card; +} diff --git a/icons/icon.png b/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b35102aa985feeb50878177c7cc0467c5eaaa86c GIT binary patch literal 3342 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 literal 0 HcmV?d00001 diff --git a/icons/icon@2x.png b/icons/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a5acdb195e5cc2e16ef7172052effbd65333b603 GIT binary patch literal 8382 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"], + "js": ["content-script.js"] + } + ], + + "commands": { + "open-escape": { + "suggested_key": { + "default": "Command+Period" + }, + "description": "Open Escape" + } + }, + + "permissions": [ + "history", + "storage", + "sessions", + "tabs" + ] +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..1ba1c01 --- /dev/null +++ b/popup.css @@ -0,0 +1,15 @@ +html, body { + width: 200px; +} + +.button { + margin: 3% auto; + padding: 4px; + text-align: center; + font-size: 1.5em; + cursor: pointer; +} + +.button:hover { + background-color: #CFF2F2; +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..1107104 --- /dev/null +++ b/popup.html @@ -0,0 +1,18 @@ + + + + + + + + + +
Toggle Tab Bar
+
Show Some
+
Show All
+
Hide Some
+
Hide All
+ + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..d081ca1 --- /dev/null +++ b/popup.js @@ -0,0 +1,54 @@ + +document.addEventListener("click", (e) => { + if (true || e.target.classList.contains("toggletabbar")) { + browser.tabshideshow.getTabBarVisible().then(isVisible => { + browser.tabshideshow.setTabBarVisible(!isVisible); + window.close(); + }); + } + else { + // Get all tabs + browser.tabs.query({currentWindow: true}).then(tabs => { + // Get all tab ids as array + var ids = tabs.map(tab => tab.id); + + // Show some tabs, and hide the rest + if (e.target.classList.contains("showsome")) { + var [showIds, hideIds] = randomSetFromArray(ids); + browser.tabshideshow.show(showIds); + } + // Hide some tabs, show the rest + if (e.target.classList.contains("hidesome")) { + var [showIds, hideIds] = randomSetFromArray(ids); + browser.tabshideshow.hide(hideIds); + } + // Show all + else if (e.target.classList.contains("showall")) { + browser.tabshideshow.show(ids); + } + // Hide all (should fail unless there are pinned tabs) + else if (e.target.classList.contains("hideall")) { + browser.tabshideshow.hide(ids); + } + }); + } +}); + +function randomIntInRange(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +// Unique random set from array +function randomSetFromArray(arr) { + var newLength = randomIntInRange(1, arr.length - 1); + var matched = []; + while (matched.length < newLength) { + var randomIndex = randomIntInRange(0, arr.length - 1); + var randomVal = arr[randomIndex]; + if (matched.indexOf(randomVal) == -1) { + matched.push(randomVal); + } + } + var notmatched = arr.filter(val => matched.indexOf(val) == -1) + return [matched, notmatched]; +} diff --git a/sessions.js b/sessions.js new file mode 100644 index 0000000..47bb203 --- /dev/null +++ b/sessions.js @@ -0,0 +1,71 @@ +// Get current active tab +export async function getCurrentTab() { + return (await getWindowTabs({ active: true }))[0]; +} + +// Get all tabs of current window +export async function getWindowTabs( + optionalQuery: QueryInfo = {} +): Promise { + return getTabs({ ...optionalQuery, currentWindow: true }); +} + +// Get all tabs that match a query obj - TODO: (Firefox) => compress favicon string even further? +export async function getTabs( + queryInfo: QueryInfo = {}, + options?: compressOptions +): Promise { + const tabs = await browser?.tabs?.query(queryInfo); + + for (const tab of tabs) { + if (!tab.url) continue; + + if (tab.url.startsWith(getExtensionURL('discarded'))) + tab.url = replaceDiscardedURL(tab.url); + + if (tab.favIconUrl) { + if (compress) + tab.favIconUrl = await compress.icon(tab.favIconUrl, options); + + tab.favIconUrl = compressLZ(tab.favIconUrl); + } + + for (const prop in tab) { + if (!tabAttr.includes(prop)) delete tab[prop as keyof ETab]; + } + } + + return tabs; +} + +// Get current session - TODO: arg: options?: compressOptions goes undefined after 1st call +export async function getSession(queryInfo?: QueryInfo) { + const session: ESession = { + title: 'Current Session', + windows: [], + id: 'current', + dateSaved: undefined, + dateModified: undefined, + tabsNumber: 0 + }; + + if (!queryInfo) queryInfo = {}; + + session.windows = await browser?.windows?.getAll(); + + for (const window of session.windows) { + queryInfo.windowId = window.id; + + window.tabs = await getTabs(queryInfo, compress_options); + + session.tabsNumber += window.tabs.length; + } + + return session; +} + +function replaceDiscardedURL(url: string) { + const searchParams = new URLSearchParams(new URL(url).search); + + return decodeURIComponent(searchParams.get('url')!); +} -- 2.51.2