Compare commits

..

No commits in common. "974d8f22fdabfeedc56c7f85be107827db17d8ad" and "0a2891d919512e45307f2cc4cbc8a10b62d7e6dc" have entirely different histories.

15 changed files with 523 additions and 3 deletions

View File

@ -34,9 +34,9 @@ const test = process.env.TEST === "true";
const watch = process.env.WATCH === "true";
// Create absolute paths to various directories.
const buildDir = toAbsolutePath("../build");
const buildDir = toAbsolutePath("build");
const outDir = path.join(buildDir, browser);
const sourceDir = toAbsolutePath("../source");
const sourceDir = toAbsolutePath("source");
// Ensure that the output directory exists.
await fsp.mkdir(outDir, {recursive: true});

View File

@ -0,0 +1,3 @@
export * from './page-footer.js';
export * from './page-header.js';
export * from './page-main.js';

View File

@ -0,0 +1,39 @@
import {PrivacyLink} from '@holllo/preact-components';
import {html} from 'htm/preact';
import {Component} from 'preact';
import type {Settings} from '../../settings/settings.js';
type Props = {
settings: Settings;
};
export class PageFooter extends Component<Props> {
render() {
const {settings} = this.props;
const version = settings.manifest.version;
const donateAttributes = {
href: 'https://liberapay.com/Holllo',
};
const donateLink = html`
<${PrivacyLink} attributes="${donateAttributes}">Donate<//>
`;
const versionAttributes = {
href: `https://git.bauke.xyz/Holllo/queue/releases/tag/${version}`,
};
const versionLink = html`
<${PrivacyLink} attributes="${versionAttributes}">v${version}<//>
`;
return html`
<footer class="page-footer">
<p>
${donateLink} 💖 ${versionLink} © Holllo Free and open-source,
forever.
</p>
</footer>
`;
}
}

View File

@ -0,0 +1,15 @@
import {html} from 'htm/preact';
import {Component} from 'preact';
export class PageHeader extends Component {
render() {
return html`
<header class="page-header">
<h1>
<span class="icon"></span>
Queue
</h1>
</header>
`;
}
}

View File

@ -0,0 +1,189 @@
import {ConfirmButton, PrivacyLink} from '@holllo/preact-components';
import {Component, html} from 'htm/preact';
import type {Settings} from '../../settings/settings.js';
import {updateBadge} from '../../utilities/badge.js';
import type {History} from '../../utilities/history.js';
type Props = {
history: History;
settings: Settings;
};
type State = {
queue: Queue.Item[];
};
export class PageMain extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
queue: props.settings.queue,
};
}
moveItem = async (id: number, direction: Queue.MoveDirection) => {
const {settings} = this.props;
await settings.moveQueueItem(id, direction);
this.setState({queue: this.props.settings.queue});
};
removeItem = async (id: number) => {
const {settings} = this.props;
await settings.removeQueueItem(id);
await updateBadge(settings);
this.setState({queue: this.props.settings.queue});
};
render() {
const isFirefox = import.meta.env.VITE_BROWSER === 'firefox';
const queueItems = this.state.queue
.sort((a, b) => a.sortIndex - b.sortIndex)
.map(
(item) =>
html`
<${queueItem}
item=${item}
move=${this.moveItem}
remove=${this.removeItem}
/>
`,
);
if (queueItems.length === 0) {
queueItems.push(html`<li>No items queued. 🤷</li>`);
}
const historyItems = this.props.history.queue
.sort((a, b) => b.added.getTime() - a.added.getTime())
.map((item) => html`<${queueItem} item=${item} />`);
let history: HtmComponent | undefined;
if (historyItems.length > 0) {
history = html`
<details class="history">
<summary>Queue history</summary>
<ul class="q-list">
${historyItems}
</ul>
</details>
`;
}
return html`
<main class="page-main">
<ul class="q-list">
${queueItems}
</ul>
${history}
<details class="usage">
<summary>How do I use Queue?</summary>
<p>Adding links to your queue:</p>
<ul>
<li>
Right-click any link ${isFirefox ? 'or tab' : ''} and click "Add
to Queue".
</li>
</ul>
<p>Opening the next link from your queue:</p>
<ul>
<li>Click on the extension icon to open it in the current tab.</li>
<li>
Right-click the extension icon and click "Open next link in new
tab".
</li>
</ul>
<p>Opening the extension page:</p>
<ul>
${isFirefox
? html`<li>Double-click the extension icon.</li>`
: undefined}
<li>
Right-click the extension icon and click "Open the extension
page".
</li>
</ul>
<p>Deleting queue items:</p>
<ul>
<li>
Click the red button with the and then confirm it by clicking
again.
</li>
</ul>
</details>
</main>
`;
}
}
type ItemProps = {
item: Queue.Item;
move?: (id: number, direction: Queue.MoveDirection) => Promise<void>;
remove?: (id: number) => Promise<void>;
};
function queueItem(props: ItemProps): HtmComponent {
const added = props.item.added.toLocaleString();
const {id, text, url} = props.item;
const move = [];
if (props.move !== undefined) {
const moveButtons: Array<[string, Queue.MoveDirection]> = [
['↑', 'up'],
['↓', 'down'],
];
move.push(
...moveButtons.map(
([text, direction]) =>
html`
<button
title="Move item ${direction}"
onClick=${async () => props.move!(id, direction)}
>
${text}
</button>
`,
),
);
}
let remove;
if (props.remove !== undefined) {
remove = html`
<${ConfirmButton}
class="confirm-button"
click=${async () => props.remove!(id)}
confirmClass="confirm"
confirmText="✓"
extraAttributes=${{title: 'Remove'}}
text="✗"
timeout=${5 * 1000}
/>
`;
}
return html`
<li class="q-item">
<p class="title">
<${PrivacyLink} attributes=${{href: url}}>${text ?? url}<//>
</p>
<div class="buttons">${move}${remove}</div>
<p>
<time datetime=${added} title="Link queued on ${added}.">
${added}
</time>
</p>
</li>
`;
}

View File

@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Queue</title>
<link rel="shortcut icon" href="/queue.png" type="image/png">
<link rel="stylesheet" href="./index.scss">
</head>
<body class="love">
@ -14,7 +15,7 @@
This WebExtension doesn't work without JavaScript enabled, sorry! 😭
</noscript>
<script type="module" src="./setup.js"></script>
<script type="module" src="./index.ts"></script>
</body>
</html>

28
source/options/index.scss Normal file
View File

@ -0,0 +1,28 @@
@use '../../node_modules/modern-normalize/modern-normalize.css';
@use 'scss/reset';
@use 'scss/mixins';
@use 'scss/love';
// Component styles
@use 'scss/components/page-header';
@use 'scss/components/page-main';
@use 'scss/components/page-footer';
html {
font-size: 62.5%;
}
body {
background-color: var(--db-1);
color: var(--df-1);
font-size: 1.5rem;
padding: 16px;
}
a {
color: var(--da-3);
&:hover {
color: var(--df-2);
}
}

35
source/options/index.ts Normal file
View File

@ -0,0 +1,35 @@
import {html} from 'htm/preact';
import {Component, render} from 'preact';
import {Settings} from '../settings/settings.js';
import {updateBadge} from '../utilities/badge.js';
import {History} from '../utilities/history.js';
import {PageFooter, PageHeader, PageMain} from './components/components.js';
window.addEventListener('DOMContentLoaded', async () => {
const history = await History.fromLocalStorage();
const settings = await Settings.fromSyncStorage();
await updateBadge(settings);
render(
html`<${OptionsPage} history=${history} settings=${settings} />`,
document.body,
);
});
type Props = {
history: Queue.Item[];
settings: Settings;
};
class OptionsPage extends Component<Props> {
render() {
const {history, settings} = this.props;
return html`
<${PageHeader} />
<${PageMain} history=${history} settings=${settings} />
<${PageFooter} settings=${settings} />
`;
}
}

View File

@ -0,0 +1,9 @@
@use '../mixins';
.page-footer {
@include mixins.responsive-container;
border: 1px solid var(--df-2);
margin-bottom: 16px;
padding: 16px;
}

View File

@ -0,0 +1,23 @@
@use '../mixins';
.page-header {
@include mixins.responsive-container;
border: 1px solid var(--df-2);
margin-bottom: 16px;
h1 {
font-size: 2.5rem;
}
.icon {
align-items: center;
background-color: var(--df-2);
color: var(--db-1);
display: inline-flex;
height: 4.5rem;
justify-content: center;
margin-right: 8px;
width: 4.5rem;
}
}

View File

@ -0,0 +1,106 @@
@use '../mixins';
.page-main {
@include mixins.responsive-container;
margin-bottom: 16px;
}
.q-list {
border: 1px solid var(--df-2);
list-style: none;
margin-bottom: 16px;
padding: 16px;
> li:not(:last-child) {
margin-bottom: 16px;
}
}
.q-item {
background-color: var(--db-2);
display: grid;
grid-template-columns: auto min-content;
padding: 8px;
.title {
display: inline-block;
font-size: 2rem;
font-weight: bold;
margin-bottom: 8px;
}
.buttons {
display: grid;
gap: 4px;
grid-template-columns: repeat(3, 1fr);
button {
align-items: center;
background-color: var(--da-3);
border: none;
color: var(--db-1);
cursor: pointer;
display: flex;
flex-direction: column;
font-weight: bold;
height: 2.5rem;
justify-content: center;
padding: 0;
width: 2.5rem;
&.confirm-button {
background-color: var(--la-1);
color: var(--df-1);
&.confirm {
background-color: var(--df-1);
color: var(--la-1);
}
}
}
}
}
.history,
.usage {
border: 1px solid var(--df-2);
&[open] {
summary {
background-color: var(--df-2);
color: var(--db-1);
margin-bottom: 16px;
}
> :not(summary) {
padding: 0 16px;
}
}
summary {
cursor: pointer;
font-weight: bold;
padding: 16px;
&:hover {
background-color: var(--df-1);
color: var(--db-1);
}
}
}
.history {
margin-bottom: 16px;
.q-list {
border: none;
}
}
.usage {
ul {
list-style: square;
margin: 4px 0 2rem 16px;
}
}

View File

@ -0,0 +1,45 @@
/*
The Love Theme CSS Custom Properties
https://love.holllo.cc - version 0.1.0
MIT license
*/
.love {
/* Love Dark */
--df-1: #f2efff;
--df-2: #e6deff;
--db-1: #1f1731;
--db-2: #2a2041;
--da-1: #f99fb1;
--da-2: #faa56c;
--da-3: #d2b83a;
--da-4: #96c839;
--da-5: #3bd18a;
--da-6: #3ecdbf;
--da-7: #41c8e5;
--da-8: #98b9f8;
--da-9: #d5a6f8;
--da-10: #f99add;
--dg-1: #e2e2e2;
--dg-2: #c6c6c6;
--dg-3: #ababab;
/* Love Light */
--lf-1: #1f1731;
--lf-2: #2a2041;
--lb-1: #f2efff;
--lb-2: #e6deff;
--la-1: #8b123c;
--la-2: #6a3b11;
--la-3: #514610;
--la-4: #384d10;
--la-5: #115133;
--la-6: #124f49;
--la-7: #144d5a;
--la-8: #17477e;
--la-9: #6f1995;
--la-10: #81156a;
--lg-1: #1b1b1b;
--lg-2: #303030;
--lg-3: #474747;
}

View File

@ -0,0 +1,11 @@
@use 'variables';
@mixin responsive-container {
margin-left: auto;
margin-right: auto;
width: variables.$large-breakpoint;
@media (max-width: variables.$large-breakpoint) {
width: 100%;
}
}

View File

@ -0,0 +1,12 @@
h1,
h2,
h3,
h4,
h5,
ol,
ul,
li,
p {
margin: 0;
padding: 0;
}

View File

@ -0,0 +1,4 @@
$small-breakpoint: 600px;
$medium-breakpoint: 900px;
$large-breakpoint: 1200px;
$extra-large-breakpoint: 1800px;