commit 0c80bcfb58feaa6c0b5683296620b186a437740e Author: Yuki Joou Date: Sun Nov 24 15:35:17 2024 +0100 Initial commit 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 0000000..7d8e775 Binary files /dev/null and b/tabunny.zip differ