Initial commit

This commit is contained in:
Yuki Joou 2024-11-24 15:35:17 +01:00
commit 0c80bcfb58
8 changed files with 288 additions and 0 deletions

9
build-package.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
set -ex;
pushd src/
zip ../tabunny.zip *
popd

18
src/manifest.json Normal file
View file

@ -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"
]
}

68
src/script.js Normal file
View file

@ -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'});
};

22
src/sessions-browser.html Normal file
View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tabunny sessions</title>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<main>
<h1>Tabunny sessions</h1>
<h2>Past sessions</h2>
<div id="sessions">
</div>
</main>
<script src="viewer.js"></script>
</body>
</html>

41
src/style.css Normal file
View file

@ -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;
}

28
src/tabunny.html Normal file
View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tabunny</title>
<link href="style.css" rel="stylesheet" />
</head>
<body>
<main>
<button id="open-sessions-viewer">View past sessions</button>
<div>
<label for="session-name">Session name:</label>
<input autofocus type="text" id="session-name" name="session-name" placeholder="Session name" />
</div>
<button id="save-session">Save</button>
<button id="save-quit-session">Save and end session</button>
<output id="save-status"></output>
<script src="script.js"></script>
</main>
</body>
</html>

102
src/viewer.js Normal file
View file

@ -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();

BIN
tabunny.zip Normal file

Binary file not shown.