This repository has been archived by the owner on Feb 24, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a75d0cf
commit 477691a
Showing
13 changed files
with
295 additions
and
304 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { DOCUMENT } from '../lib/globals'; | ||
import { loadAllTabGroups } from './tabs_store'; | ||
import { ingestTabs } from './tabs_ui'; | ||
|
||
export class Debugger { | ||
async double(): Promise<void> { | ||
const groups = await loadAllTabGroups(); | ||
let earliest = Math.min(...groups.map(g => g.date)); | ||
const groupDiv: HTMLDivElement = DOCUMENT.get().querySelector('#groups'); | ||
const promises: Promise<void>[] = []; | ||
for (const group of groups) { | ||
earliest -= 1000; | ||
await ingestTabs(<any>group.tabs, groupDiv, () => earliest); | ||
} | ||
await Promise.all(promises); | ||
} | ||
} | ||
|
||
declare global { | ||
interface Window { | ||
gt: Debugger; | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { DOCUMENT } from '../lib/globals'; | ||
|
||
export function makeElement( | ||
type: string, | ||
attrs: { [key: string]: string } = {}, | ||
children?: string | Element[], | ||
): Element { | ||
const elem = DOCUMENT.get().createElement(type); | ||
for (const key in attrs) { | ||
elem.setAttribute(key, attrs[key]); | ||
} | ||
|
||
if (children === undefined) return elem; | ||
|
||
if (typeof children === 'string') { | ||
elem.innerText = children; | ||
} else { | ||
children.map(c => elem.appendChild(c)); | ||
} | ||
return elem; | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* User interface code for graytabby. | ||
* | ||
* Anything responsible for responding to user input or for displaying data to the user. | ||
*/ | ||
|
||
import { DOCUMENT } from '../lib/globals'; | ||
import { Debugger } from './debugger'; | ||
import { bindOptions } from './options_ui'; | ||
import { bindTabs } from './tabs_ui'; | ||
|
||
/** | ||
* The main entry point for GrayTabby. | ||
*/ | ||
export async function graytabby(): Promise<void> { | ||
DOCUMENT.get().title = 'GrayTabby'; | ||
await bindOptions(); | ||
await bindTabs(); | ||
DOCUMENT.get().defaultView.gt = new Debugger(); | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { DOCUMENT } from '../lib/globals'; | ||
|
||
let totalTabs = 0; | ||
|
||
export function updateInfo(delta: number): void { | ||
const infoNode = <HTMLParagraphElement>DOCUMENT.get().querySelector('#info'); | ||
totalTabs += delta; | ||
infoNode.innerText = 'Total tabs: ' + totalTabs.toString(); | ||
} | ||
|
||
export function getTotalTabs(): number { | ||
return totalTabs; | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { getOptions, setOptions } from '../lib/options'; | ||
import { DOCUMENT } from '../lib/globals'; | ||
|
||
export async function bindOptions(): Promise<void> { | ||
const document = DOCUMENT.get(); | ||
const modal = <HTMLDivElement>document.querySelector('#optionsModal'); | ||
const logo = <HTMLImageElement>document.querySelector('#logo'); | ||
const content = <HTMLDivElement>document.querySelector('#optionsModal .content'); | ||
logo.onclick = () => (modal.style.display = 'block'); | ||
modal.onclick = event => { | ||
if (!content.contains(<HTMLElement>event.target)) modal.style.display = 'none'; | ||
}; | ||
|
||
const checkboxes: HTMLInputElement[] = Array.from( | ||
content.querySelectorAll('label input[type="checkbox"]'), | ||
); | ||
for (const checkbox of checkboxes) { | ||
const label = <HTMLLabelElement>checkbox.parentElement; | ||
const span = <HTMLSpanElement>label.querySelector('span'); | ||
span.onclick = () => (checkbox.checked = !checkbox.checked); | ||
} | ||
|
||
const optionsLimitNode = <HTMLInputElement>document.querySelector('#optionsLimit'); | ||
const optionsDupesNode = <HTMLInputElement>document.querySelector('#optionsDupes'); | ||
const optionsAtLoad = await getOptions(); | ||
|
||
optionsLimitNode.value = optionsAtLoad.tabLimit.toString(); | ||
optionsDupesNode.checked = optionsAtLoad.archiveDupes; | ||
|
||
optionsDupesNode.onchange = async () => { | ||
await setOptions({ | ||
archiveDupes: optionsDupesNode.checked, | ||
}); | ||
}; | ||
|
||
// From https://stackoverflow.com/questions/469357/html-text-input-allow-only-numeric-input | ||
// HTML5 validators have poor support at the moment. | ||
optionsLimitNode.onkeydown = (e): boolean => { | ||
return ( | ||
e.ctrlKey || | ||
e.altKey || | ||
(47 < e.keyCode && e.keyCode < 58 && e.shiftKey == false) || | ||
(95 < e.keyCode && e.keyCode < 106) || | ||
e.keyCode == 8 || | ||
e.keyCode == 9 || | ||
(e.keyCode > 34 && e.keyCode < 40) || | ||
e.keyCode == 46 | ||
); | ||
}; | ||
|
||
optionsLimitNode.onkeyup = async () => { | ||
const newLimit = Number(optionsLimitNode.value); | ||
if (newLimit != NaN) { | ||
await setOptions({ | ||
tabLimit: newLimit, | ||
}); | ||
} | ||
}; | ||
} |
File renamed without changes.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import { BROWSER, DOCUMENT, ARCHIVAL } from '../lib/globals'; | ||
import { updateInfo, getTotalTabs } from './info_ui'; | ||
import { | ||
keyFromDate, | ||
dateFromKey, | ||
GrayTabGroup, | ||
GrayTab, | ||
eraseTabGroup, | ||
saveTabGroup, | ||
keyFromGroup, | ||
loadAllTabGroups, | ||
} from './tabs_store'; | ||
import { makeElement } from './dom'; | ||
import { getOptions } from '../lib/options'; | ||
import { BrowserTab } from '../lib/types'; | ||
|
||
function prependInsideContainer(parent: Element, child: Element): Element { | ||
if (parent.firstChild == null) parent.appendChild(child); | ||
else parent.insertBefore(child, parent.firstChild); | ||
return child; | ||
} | ||
|
||
function countGroups(): number { | ||
return DOCUMENT.get().querySelectorAll('#groups > div').length; | ||
} | ||
|
||
function faviconLocation(url: string): string { | ||
const domain = new URL(url).hostname; | ||
if (domain) return `https://www.google.com/s2/favicons?domain=${domain}`; | ||
return ''; | ||
} | ||
|
||
function renderFavicon(url: string): HTMLImageElement { | ||
const loc = faviconLocation(url); | ||
return <HTMLImageElement>makeElement('img', { | ||
src: loc, | ||
width: '16', | ||
height: '16', | ||
}); | ||
} | ||
|
||
function groupFromDiv(target: number | HTMLDivElement): GrayTabGroup { | ||
let div: HTMLDivElement; | ||
let date: number; | ||
if (typeof target === 'number') { | ||
date = target; | ||
div = DOCUMENT.get().querySelector(`#${keyFromDate(date)}`); | ||
} else { | ||
div = target; | ||
date = dateFromKey(div.id); | ||
} | ||
const group: GrayTabGroup = { | ||
date: date, | ||
tabs: [], | ||
}; | ||
if (div == null) return group; | ||
const lis = div.querySelectorAll('li'); | ||
lis.forEach(li => { | ||
const a: HTMLAnchorElement = li.querySelector('a'); | ||
const tab: GrayTab = { | ||
key: Number(a.attributes.getNamedItem('key').value), | ||
url: a.href, | ||
title: a.innerText, | ||
}; | ||
group.tabs.push(tab); | ||
}); | ||
return group; | ||
} | ||
|
||
/** | ||
* Updates the backend to match the DOM for the tab group with the given date. | ||
*/ | ||
async function syncGroupFromDOM(target: number | HTMLDivElement): Promise<void> { | ||
const group = groupFromDiv(target); | ||
if (group.tabs.length == 0) { | ||
return eraseTabGroup(group.date); | ||
} | ||
return saveTabGroup(group); | ||
} | ||
|
||
async function linkRowClickhandler(event: MouseEvent): Promise<void> { | ||
const a = <HTMLAnchorElement>event.target; | ||
const row = <HTMLDivElement>a.parentElement; | ||
const li = <HTMLDivElement>row.parentElement; | ||
const ul = <HTMLUListElement>li.parentElement; | ||
const groupDiv = <HTMLDivElement>ul.parentElement; | ||
|
||
event.preventDefault(); | ||
await BROWSER.get().tabs.create({ url: a.href, active: false }); | ||
li.remove(); | ||
updateInfo(-1); | ||
if (groupDiv.querySelector('li') == null) groupDiv.remove(); | ||
await syncGroupFromDOM(dateFromKey(groupDiv.id)); | ||
} | ||
|
||
function renderLinkRow(group: GrayTabGroup, tab: GrayTab): HTMLLIElement { | ||
const li = <HTMLLIElement>makeElement('li'); | ||
const row = <HTMLDivElement>li.appendChild(makeElement('div')); | ||
row.appendChild(renderFavicon(tab.url)); | ||
const a = <HTMLAnchorElement>( | ||
row.appendChild(makeElement('a', { href: tab.url, key: tab.key.toString() }, tab.title)) | ||
); | ||
a.onclick = linkRowClickhandler; | ||
return li; | ||
} | ||
|
||
function renderGroup(group: GrayTabGroup): HTMLDivElement { | ||
const div = <HTMLDivElement>makeElement('div', { id: keyFromGroup(group) }); | ||
div.appendChild(makeElement('span', {}, new Date(group.date).toLocaleString())); | ||
const ul = div.appendChild(makeElement('ul')); | ||
for (const tab of group.tabs) { | ||
ul.appendChild(renderLinkRow(group, tab)); | ||
} | ||
return div; | ||
} | ||
|
||
export async function ingestTabs( | ||
tabSummaries: BrowserTab[], | ||
groupsNode: HTMLDivElement, | ||
now = () => new Date().getTime(), | ||
): Promise<void> { | ||
if (tabSummaries.length == 0) return; | ||
let counter = 0; | ||
const group: GrayTabGroup = { | ||
tabs: tabSummaries.map(ts => { | ||
return { ...ts, key: counter++ }; | ||
}), | ||
date: now(), | ||
}; | ||
prependInsideContainer(groupsNode, renderGroup(group)); | ||
await syncGroupFromDOM(group.date); | ||
updateInfo(group.tabs.length); | ||
|
||
const tabLimit = (await getOptions()).tabLimit; | ||
while (getTotalTabs() > tabLimit && countGroups() > 1) { | ||
const victim = <HTMLDivElement>DOCUMENT.get().querySelector('#groups>div:last-child'); | ||
const removed = victim.querySelectorAll('li').length; | ||
updateInfo(-removed); | ||
await eraseTabGroup(dateFromKey(victim.id)); | ||
victim.remove(); | ||
} | ||
} | ||
|
||
export async function bindTabs(): Promise<void> { | ||
const groupsNode = <HTMLDivElement>DOCUMENT.get().querySelector('#groups'); | ||
let counter = 0; | ||
for (const group of await loadAllTabGroups()) { | ||
counter += group.tabs.length; | ||
groupsNode.appendChild(renderGroup(group)); | ||
} | ||
ARCHIVAL.get().sub(summaries => ingestTabs(summaries, groupsNode)); | ||
updateInfo(counter); | ||
} |
Oops, something went wrong.