Compare commits
6 Commits
ab6ef97d91
...
913e8f0da9
Author | SHA1 | Date |
---|---|---|
Bauke | 913e8f0da9 | |
Bauke | ee012aea1d | |
Bauke | f6571b895b | |
Bauke | e591db7bcf | |
Bauke | d4400c5c31 | |
Bauke | c72959841f |
|
@ -6,7 +6,7 @@
|
||||||
[![Get Queue for Chrome](./images/chrome-web-store.png)](https://chrome.google.com/webstore/detail/queue/epnbikemcmienphlfmidkimpjnmohcbl)
|
[![Get Queue for Chrome](./images/chrome-web-store.png)](https://chrome.google.com/webstore/detail/queue/epnbikemcmienphlfmidkimpjnmohcbl)
|
||||||
[![Get Queue for Edge](./images/microsoft.png)](https://microsoftedge.microsoft.com/addons/detail/queue/aanjampfdpcnhoeglmfefmmegdbifaak)
|
[![Get Queue for Edge](./images/microsoft.png)](https://microsoftedge.microsoft.com/addons/detail/queue/aanjampfdpcnhoeglmfefmmegdbifaak)
|
||||||
|
|
||||||
![Latest Queue screenshot](./images/queue-version-0-2-2.png)
|
![Latest Queue screenshot](./images/queue-version-0-3-0.png)
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 103 KiB |
|
@ -6,7 +6,7 @@ export default function createManifest(
|
||||||
const manifest: Record<string, unknown> = {
|
const manifest: Record<string, unknown> = {
|
||||||
name: 'Queue',
|
name: 'Queue',
|
||||||
description: 'A WebExtension for queueing links.',
|
description: 'A WebExtension for queueing links.',
|
||||||
version: '0.2.6',
|
version: '0.3.0',
|
||||||
permissions: ['contextMenus', 'storage'],
|
permissions: ['contextMenus', 'storage'],
|
||||||
options_ui: {
|
options_ui: {
|
||||||
page: 'options/index.html',
|
page: 'options/index.html',
|
||||||
|
|
|
@ -23,6 +23,12 @@ export class PageMain extends Component<Props, State> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
removeItem = async (id: number) => {
|
||||||
const {settings} = this.props;
|
const {settings} = this.props;
|
||||||
await settings.removeQueueItem(id);
|
await settings.removeQueueItem(id);
|
||||||
|
@ -34,9 +40,16 @@ export class PageMain extends Component<Props, State> {
|
||||||
const isFirefox = import.meta.env.VITE_BROWSER === 'firefox';
|
const isFirefox = import.meta.env.VITE_BROWSER === 'firefox';
|
||||||
|
|
||||||
const queueItems = this.state.queue
|
const queueItems = this.state.queue
|
||||||
.sort((a, b) => a.added.getTime() - b.added.getTime())
|
.sort((a, b) => a.sortIndex - b.sortIndex)
|
||||||
.map(
|
.map(
|
||||||
(item) => html`<${queueItem} item=${item} remove=${this.removeItem} />`,
|
(item) =>
|
||||||
|
html`
|
||||||
|
<${queueItem}
|
||||||
|
item=${item}
|
||||||
|
move=${this.moveItem}
|
||||||
|
remove=${this.removeItem}
|
||||||
|
/>
|
||||||
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (queueItems.length === 0) {
|
if (queueItems.length === 0) {
|
||||||
|
@ -114,12 +127,35 @@ export class PageMain extends Component<Props, State> {
|
||||||
|
|
||||||
type ItemProps = {
|
type ItemProps = {
|
||||||
item: Queue.Item;
|
item: Queue.Item;
|
||||||
|
move?: (id: number, direction: Queue.MoveDirection) => Promise<void>;
|
||||||
remove?: (id: number) => Promise<void>;
|
remove?: (id: number) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function queueItem(props: ItemProps): HtmComponent {
|
function queueItem(props: ItemProps): HtmComponent {
|
||||||
const added = props.item.added.toLocaleString();
|
const added = props.item.added.toLocaleString();
|
||||||
const {id, text, url} = props.item;
|
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;
|
let remove;
|
||||||
if (props.remove !== undefined) {
|
if (props.remove !== undefined) {
|
||||||
remove = html`
|
remove = html`
|
||||||
|
@ -141,7 +177,7 @@ function queueItem(props: ItemProps): HtmComponent {
|
||||||
<${PrivacyLink} attributes=${{href: url}}>${text ?? url}<//>
|
<${PrivacyLink} attributes=${{href: url}}>${text ?? url}<//>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="buttons">${remove}</div>
|
<div class="buttons">${move}${remove}</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<time datetime=${added} title="Link queued on ${added}.">
|
<time datetime=${added} title="Link queued on ${added}.">
|
||||||
|
|
|
@ -30,23 +30,34 @@
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-button {
|
.buttons {
|
||||||
align-items: center;
|
display: grid;
|
||||||
background-color: var(--la-1);
|
gap: 4px;
|
||||||
border: none;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
color: var(--df-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(--df-1);
|
align-items: center;
|
||||||
color: var(--la-1);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,23 @@ export const dataMigrations: Array<Migration<string>> = [
|
||||||
migrated[key] = item;
|
migrated[key] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return migrated;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.3.0',
|
||||||
|
async migrate(data: Record<string, any>) {
|
||||||
|
const migrated: Record<string, any> = {
|
||||||
|
version: '0.3.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries<Queue.Item>(data)) {
|
||||||
|
if (key.startsWith('qi')) {
|
||||||
|
migrated[key] = value;
|
||||||
|
migrated[key].sortIndex = value.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return migrated;
|
return migrated;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -42,6 +42,7 @@ export class Settings {
|
||||||
const item: Queue.Item = {
|
const item: Queue.Item = {
|
||||||
added: new Date(),
|
added: new Date(),
|
||||||
id,
|
id,
|
||||||
|
sortIndex: id,
|
||||||
text,
|
text,
|
||||||
url,
|
url,
|
||||||
};
|
};
|
||||||
|
@ -51,12 +52,40 @@ export class Settings {
|
||||||
[`qi${id}`]: {
|
[`qi${id}`]: {
|
||||||
added: item.added.toISOString(),
|
added: item.added.toISOString(),
|
||||||
id,
|
id,
|
||||||
|
sortIndex: id,
|
||||||
text,
|
text,
|
||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async moveQueueItem(
|
||||||
|
id: number,
|
||||||
|
direction: Queue.MoveDirection,
|
||||||
|
): Promise<void> {
|
||||||
|
const targetItem = this.queue.find((item) => item.id === id);
|
||||||
|
if (targetItem === undefined) {
|
||||||
|
throw new Error(`Failed to move item with ID: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousIndex = targetItem.sortIndex;
|
||||||
|
let targetIndex = previousIndex;
|
||||||
|
if (direction === 'down') {
|
||||||
|
targetIndex += 1;
|
||||||
|
} else if (direction === 'up') {
|
||||||
|
targetIndex -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingItem = this.queue.find(
|
||||||
|
(item) => item.sortIndex === targetIndex,
|
||||||
|
);
|
||||||
|
if (existingItem !== undefined) {
|
||||||
|
existingItem.sortIndex = previousIndex;
|
||||||
|
targetItem.sortIndex = targetIndex;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public newQueueItemId(): number {
|
public newQueueItemId(): number {
|
||||||
const item = this.queue.sort((a, b) => b.id - a.id)[0];
|
const item = this.queue.sort((a, b) => b.id - a.id)[0];
|
||||||
return item === undefined ? 1 : item.id + 1;
|
return item === undefined ? 1 : item.id + 1;
|
||||||
|
|
|
@ -22,8 +22,11 @@ declare global {
|
||||||
type Item = {
|
type Item = {
|
||||||
added: Date;
|
added: Date;
|
||||||
id: number;
|
id: number;
|
||||||
|
sortIndex: number;
|
||||||
text: string;
|
text: string;
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MoveDirection = 'up' | 'down';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ const queueItemSample: Queue.Item = {
|
||||||
id: 1,
|
id: 1,
|
||||||
text: 'Sample',
|
text: 'Sample',
|
||||||
url: 'https://example.org',
|
url: 'https://example.org',
|
||||||
};
|
} as unknown as Queue.Item;
|
||||||
|
|
||||||
test('dataMigrations happy path', async (t) => {
|
test('dataMigrations happy path', async (t) => {
|
||||||
let data: Record<string, any> = {
|
let data: Record<string, any> = {
|
||||||
|
@ -37,7 +37,14 @@ test('dataMigrations unhappy path', async (t) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Serializing & Deserializing Queue', (t) => {
|
test('Serializing & Deserializing Queue', (t) => {
|
||||||
const serialized = serializeQueue([queueItemSample]);
|
const sample: Queue.Item = {
|
||||||
|
added: queueItemSample.added,
|
||||||
|
id: queueItemSample.id,
|
||||||
|
sortIndex: queueItemSample.id,
|
||||||
|
text: queueItemSample.text,
|
||||||
|
url: queueItemSample.url,
|
||||||
|
};
|
||||||
|
const serialized = serializeQueue([sample]);
|
||||||
t.snapshot(serialized, 'Serialized');
|
t.snapshot(serialized, 'Serialized');
|
||||||
|
|
||||||
serialized.extra = 'Extra';
|
serialized.extra = 'Extra';
|
||||||
|
|
|
@ -18,6 +18,19 @@ Generated by [AVA](https://avajs.dev).
|
||||||
version: '0.1.7',
|
version: '0.1.7',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> Migration 0.3.0
|
||||||
|
|
||||||
|
{
|
||||||
|
qi1: {
|
||||||
|
added: Date 2022-03-02 16:00:00 UTC {},
|
||||||
|
id: 1,
|
||||||
|
sortIndex: 1,
|
||||||
|
text: 'Sample',
|
||||||
|
url: 'https://example.org',
|
||||||
|
},
|
||||||
|
version: '0.3.0',
|
||||||
|
}
|
||||||
|
|
||||||
## dataMigrations unhappy path
|
## dataMigrations unhappy path
|
||||||
|
|
||||||
> Migration 0.1.7
|
> Migration 0.1.7
|
||||||
|
@ -26,6 +39,12 @@ Generated by [AVA](https://avajs.dev).
|
||||||
version: '0.1.7',
|
version: '0.1.7',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> Migration 0.3.0
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.3.0',
|
||||||
|
}
|
||||||
|
|
||||||
## Serializing & Deserializing Queue
|
## Serializing & Deserializing Queue
|
||||||
|
|
||||||
> Serialized
|
> Serialized
|
||||||
|
@ -34,6 +53,7 @@ Generated by [AVA](https://avajs.dev).
|
||||||
qi1: {
|
qi1: {
|
||||||
added: '2022-03-02T16:00:00.000Z',
|
added: '2022-03-02T16:00:00.000Z',
|
||||||
id: 1,
|
id: 1,
|
||||||
|
sortIndex: 1,
|
||||||
text: 'Sample',
|
text: 'Sample',
|
||||||
url: 'https://example.org',
|
url: 'https://example.org',
|
||||||
},
|
},
|
||||||
|
@ -45,6 +65,7 @@ Generated by [AVA](https://avajs.dev).
|
||||||
{
|
{
|
||||||
added: Date 2022-03-02 16:00:00 UTC {},
|
added: Date 2022-03-02 16:00:00 UTC {},
|
||||||
id: 1,
|
id: 1,
|
||||||
|
sortIndex: 1,
|
||||||
text: 'Sample',
|
text: 'Sample',
|
||||||
url: 'https://example.org',
|
url: 'https://example.org',
|
||||||
},
|
},
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue