From 0c80bcfb58feaa6c0b5683296620b186a437740e Mon Sep 17 00:00:00 2001 From: Yuki Joou Date: Sun, 24 Nov 2024 15:35:17 +0100 Subject: [PATCH] Initial commit --- build-package.sh | 9 ++++ src/manifest.json | 18 +++++++ src/script.js | 68 +++++++++++++++++++++++++ src/sessions-browser.html | 22 ++++++++ src/style.css | 41 +++++++++++++++ src/tabunny.html | 28 +++++++++++ src/viewer.js | 102 ++++++++++++++++++++++++++++++++++++++ tabunny.zip | Bin 0 -> 3708 bytes 8 files changed, 288 insertions(+) create mode 100755 build-package.sh create mode 100644 src/manifest.json create mode 100644 src/script.js create mode 100644 src/sessions-browser.html create mode 100644 src/style.css create mode 100644 src/tabunny.html create mode 100644 src/viewer.js create mode 100644 tabunny.zip diff --git a/build-package.sh b/build-package.sh new file mode 100755 index 0000000..8bb80ec --- /dev/null +++ b/build-package.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -ex; + +pushd src/ + +zip ../tabunny.zip * + +popd diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..37e69c6 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,18 @@ +{ + "manifest_version": 2, + "name": "Tabunny", + "version": "0.3", + + "description": "manages tabs for bunnies", + + "browser_action": { + "browser_style": false, + "default_title": "Tabunny", + "default_popup": "tabunny.html" + }, + + "permissions": [ + "tabs", + "storage" + ] +} diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..5acc7c4 --- /dev/null +++ b/src/script.js @@ -0,0 +1,68 @@ +const $ = (q) => document.querySelector(q); +const sessionName = $("#session-name"); +const saveSession = $("#save-session"); +const saveQuitSession = $("#save-quit-session"); +const saveStatus = $("#save-status"); + +const openSessionsViewer = $("#open-sessions-viewer"); + +sessionName.value = new Date().toJSON(); +sessionName.select(); + +async function doSaveSession() { + const tabs = await browser.tabs.query({}); + const localStorage = browser.storage.local; + const currentSessionName = sessionName.value; + + const sessionID = crypto.randomUUID(); + const storedSessions = await localStorage.get("sessions"); + + let sessions = storedSessions["sessions"] ?? []; + sessions.push({ + id: sessionID, + name: currentSessionName, + time: new Date(), + }); + + await localStorage.set({ sessions }); + + let sessionData = { + tabs: [] + }; + + for (const tab of tabs) { + const tabData = { + id: tab.id, + url: tab.url, + title: tab.title, + window: tab.windowId, + favicon: tab.favIconUrl, + } + sessionData["tabs"].push(tabData); + } + + const dataToSave = {}; + dataToSave["session." + sessionID] = sessionData; + await localStorage.set(dataToSave); + return true; +} + +async function endSession() { + const tabs = await browser.tabs.query({}); + browser.tabs.create({}); + for (const tab of tabs) { + await browser.tabs.remove(tab.id); + } +} + +saveSession.onclick = () => { + doSaveSession().then(saveStatus.innerText = "Session saved!"); +}; + +saveQuitSession.onclick = () => { + doSaveSession().then(endSession()); +} + +openSessionsViewer.onclick = () => { + browser.tabs.create({url: 'sessions-browser.html'}); +}; diff --git a/src/sessions-browser.html b/src/sessions-browser.html new file mode 100644 index 0000000..ba97795 --- /dev/null +++ b/src/sessions-browser.html @@ -0,0 +1,22 @@ + + + + + + + Tabunny sessions + + + +
+

Tabunny sessions

+ +

Past sessions

+ +
+ +
+
+ + + diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..899f715 --- /dev/null +++ b/src/style.css @@ -0,0 +1,41 @@ +:root { + color-scheme: light dark; +} + +h1 { + text-align: center; +} + +html, body { + --background: light-dark(#eff1f5, #1e1e2e); + --text: light-dark(#4c4f69, #cdd6f4); + --link: light-dark(#8839ef, #cba6f7) +} + +body { + background: var(--background); + color: var(--text); + + display: flex; + flex-direction: column; + align-items: center; +} + +main { + width: 95%; + max-width: 50rem; +} + +a { + color: var(--link); +} + +img.favicon { + height: 12pt; + margin: 1pt; + vertical-align: middle; +} + +a.action-link { + margin-right: 3pt; +} diff --git a/src/tabunny.html b/src/tabunny.html new file mode 100644 index 0000000..39a059e --- /dev/null +++ b/src/tabunny.html @@ -0,0 +1,28 @@ + + + + + + + Tabunny + + + +
+ + +
+ + +
+ + + + + + + + +
+ + diff --git a/src/viewer.js b/src/viewer.js new file mode 100644 index 0000000..d5ae530 --- /dev/null +++ b/src/viewer.js @@ -0,0 +1,102 @@ +const $ = (q) => document.querySelector(q); + +const sessionsDisplay = $("#sessions"); + +const localStorage = browser.storage.local; + +async function loadSessions() { + const storedSessions = await localStorage.get("sessions"); + const sessions = storedSessions["sessions"] ?? []; + + for (const session of sessions) { + const sessionDisplay = document.createElement("details"); + const sessionSummary = document.createElement("summary"); + + sessionSummary.innerText = session.name; + sessionDisplay.appendChild(sessionSummary); + + const deleteAction = document.createElement("a"); + deleteAction.href = "#"; + deleteAction.innerText = "Delete"; + deleteAction.classList.add("action-link"); + deleteAction.onclick = async () => { + if (!confirm("Do you really want to delete " + session.name + "?")) return; + // Let's avoid issues if the tab was left running for a while! + const currentStoredSessions = await localStorage.get("sessions"); + const currentSessions = storedSessions["sessions"] ?? []; + let newSessions = currentSessions.filter(theSession => theSession !== session); + await localStorage.set({sessions: newSessions}); + await localStorage.remove("session." + session.id); + sessionDisplay.remove(); + }; + sessionDisplay.appendChild(deleteAction); + + const renameAction = document.createElement("a"); + renameAction.href = "#"; + renameAction.innerText = "Rename"; + renameAction.classList.add("action-link"); + renameAction.onclick = async () => { + // Let's avoid issues if the tab was left running for a while! + const currentStoredSessions = await localStorage.get("sessions"); + const currentSessions = storedSessions["sessions"] ?? []; + const newName = prompt("Enter the new name", session.name); + let newSessions = currentSessions.map( + theSession => (theSession === session ? { ...theSession, name: newName } : theSession) + ); + await localStorage.set({sessions: newSessions}); + sessionSummary.innerText = newName; + }; + sessionDisplay.appendChild(renameAction); + + const fullSessionID = "session." + session.id; + const storedSessionData = await localStorage.get(fullSessionID); + const sessionData = storedSessionData[fullSessionID]; + if (sessionData == undefined) { + const errorMessage = document.createElement("p"); + errorMessage.innerText = "No session data available. Save probably failed, and data was lost :("; + sessionDisplay.appendChild(errorMessage); + continue; + } + + const tabsData = sessionData["tabs"]; + if (tabsData == undefined) { + const errorMessage = document.createElement("p"); + errorMessage.innerText = "No tabs data available. Save probably failed, and data was lost :("; + sessionDisplay.appendChild(errorMessage); + continue; + } + + let tabsListCSV = "session,window,name,url,favicon\n"; + + const tabsList = document.createElement("ul"); + for (const tab of tabsData) { + const tabDisplay = document.createElement("li"); + tabDisplay.innerText = "Window " + tab.window + ": "; + if (tab.favicon) { + const faviconDisplay = document.createElement("img"); + faviconDisplay.src = tab.favicon; + faviconDisplay.classList.add("favicon"); + tabDisplay.appendChild(faviconDisplay); + } + const tabLink = document.createElement("a"); + tabLink.href = tab.url; + tabLink.innerText = tab.title; + tabDisplay.appendChild(tabLink); + tabsList.appendChild(tabDisplay); + + tabsListCSV += `"${session.name}",${tab.window},"${tab.title}",${tab.url},${tab.favicon}\n`; + } + sessionDisplay.appendChild(tabsList); + + const exportAction = document.createElement("a"); + exportAction.href = "data:text/csv;charset=utf-8," + encodeURIComponent(tabsListCSV); + exportAction.innerText = "Export (CSV)"; + exportAction.classList.add("action-link"); + exportAction.download = session.name + ".csv"; + sessionDisplay.appendChild(exportAction); + + sessionsDisplay.appendChild(sessionDisplay); + } +} + +loadSessions(); diff --git a/tabunny.zip b/tabunny.zip new file mode 100644 index 0000000000000000000000000000000000000000..7d8e77527caedef1db6e89a39f5b64f0bdc95438 GIT binary patch literal 3708 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 m{8bHpCZ3=N0DjU2GVWL#e!