+
+
Using Pin-It
Edit the list of URLs below to setup which tabs you'd like to be pinned in
your browser. Please make sure they are valid URLs, starting with
http://
or https://
.
-
Once set-up, just click the extension icon and the tabs will open.
+
+
+ Once set-up, just click the extension icon and the tabs will open, Learn more here .
+
-
- Pinned Tabs
-
- {#each urls as url, i (i)}
-
- {/each}
- Add URL
-
-
+
+
+
+
+
diff --git a/src/frontend/apps/options/components/AutoOpenTabs.svelte b/src/frontend/apps/options/components/AutoOpenTabs.svelte
new file mode 100644
index 0000000..30f99f5
--- /dev/null
+++ b/src/frontend/apps/options/components/AutoOpenTabs.svelte
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/src/frontend/apps/options/components/LayoutOptionsSection.svelte b/src/frontend/apps/options/components/LayoutOptionsSection.svelte
new file mode 100644
index 0000000..65af922
--- /dev/null
+++ b/src/frontend/apps/options/components/LayoutOptionsSection.svelte
@@ -0,0 +1,35 @@
+
+
+
+
+ {title}{#if isExperimental}
+ Experimental
+ {/if}
+
+
+
+
+
diff --git a/src/frontend/apps/options/components/PinnedTabs.svelte b/src/frontend/apps/options/components/PinnedTabs.svelte
new file mode 100644
index 0000000..4ab17ea
--- /dev/null
+++ b/src/frontend/apps/options/components/PinnedTabs.svelte
@@ -0,0 +1,51 @@
+
+
+
+
+ {#each urls as url, i (i)}
+
+ {/each}
+ Add URL
+
+
+
+
diff --git a/src/frontend/apps/options/options.css b/src/frontend/apps/options/options.css
index 35bb2d5..53d28eb 100644
--- a/src/frontend/apps/options/options.css
+++ b/src/frontend/apps/options/options.css
@@ -1,4 +1,4 @@
-@import '../../common/styles/_reset.css';
+@import "../../common/styles/_reset.css";
#app {
display: flex;
@@ -7,43 +7,8 @@
align-items: center;
}
-.l-settings {
- display: grid;
- grid-template-rows: auto auto auto 1fr auto;
- width: 100%;
- max-width: 680px;
- flex: 1;
- padding: var(--p-8);
- container-type: inline-size;
-}
-
-.l-settings-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.l-settings-section {
- display: grid;
- grid-template-columns: 1fr;
- gap: var(--p-8);
-
- margin-block: var(--p-8);
- margin-bottom: var(--p-16);
-}
-
-.l-settings-section__title {
- font-weight: 600;
-}
-
-.l-settings-section__body-list {
- display: inline-flex;
- flex-direction: column;
- gap: var(--p-4);
-}
-
-.l-settings-section__body-list button {
- align-self: center;
+a {
+ color: var(--highlight);
}
.l-settings-bmc {
@@ -51,12 +16,7 @@
}
@container (width > 440px) {
- .l-settings-section {
- grid-template-columns: auto 1fr;
- }
-
.l-settings-bmc {
text-align: right;
}
-
}
diff --git a/src/frontend/components/checkbox/Checkbox.svelte b/src/frontend/components/checkbox/Checkbox.svelte
new file mode 100644
index 0000000..3a64273
--- /dev/null
+++ b/src/frontend/components/checkbox/Checkbox.svelte
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/src/frontend/components/loader/Loader.svelte b/src/frontend/components/loader/Loader.svelte
index c37c39b..d30f2ad 100644
--- a/src/frontend/components/loader/Loader.svelte
+++ b/src/frontend/components/loader/Loader.svelte
@@ -1,3 +1,42 @@
-
Loading
+
-
+
Loading
+
+
diff --git a/src/frontend/components/loader/c-loader.css b/src/frontend/components/loader/c-loader.css
deleted file mode 100644
index 6be6640..0000000
--- a/src/frontend/components/loader/c-loader.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.c-loader,
-.c-loader:after {
- border-radius: 50%;
- width: var(--p-8);
- aspect-ratio: 1;
- overflow: hidden;
-}
-
-.c-loader {
- font-size: var(--p-4);
- position: relative;
- text-indent: -9999em;
- border-width: var(--p-2);
- border-style: solid;
- border-color: var(--primary-grey);
- border-left-color: var(--primary-light);
- transform: translateZ(0);
- animation: loaderKeyFrames 1.1s infinite linear;
-}
-
-@keyframes loaderKeyFrames {
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(360deg);
- }
-}
diff --git a/src/libs/controllers/_open-tabs.ts b/src/libs/controllers/_open-tabs.ts
index f9a2f50..92c0b72 100644
--- a/src/libs/controllers/_open-tabs.ts
+++ b/src/libs/controllers/_open-tabs.ts
@@ -1,6 +1,6 @@
import * as browser from "webextension-polyfill";
import { logger } from "../utils/_logger";
-import { getUrlsToPin } from "../../../src/libs/models/_pinned-tabs";
+import { getUrlsToPin } from "../models/_pinned-tabs-browser-storage";
/**
* @param {number} windowID
diff --git a/src/libs/models/_auto-open-tabs-browser-storage.ts b/src/libs/models/_auto-open-tabs-browser-storage.ts
new file mode 100644
index 0000000..97ad9d5
--- /dev/null
+++ b/src/libs/models/_auto-open-tabs-browser-storage.ts
@@ -0,0 +1,24 @@
+import * as browser from "webextension-polyfill";
+
+export const AUTO_OPEN_STORAGE_KEY = "auto-open-tabs";
+
+export async function getAutoOpenTabs(): Promise
{
+ const result = await browser.storage.sync.get(AUTO_OPEN_STORAGE_KEY);
+ if (result[AUTO_OPEN_STORAGE_KEY]) {
+ return result[AUTO_OPEN_STORAGE_KEY];
+ }
+
+ return {
+ autoOpenTabsNewWindow: false,
+ };
+}
+
+export async function setAutoOpenTabs(autoOpen: AutoOpenTabs): Promise {
+ await browser.storage.sync.set({
+ [AUTO_OPEN_STORAGE_KEY]: autoOpen,
+ });
+}
+
+interface AutoOpenTabs {
+ autoOpenTabsNewWindow: boolean;
+}
diff --git a/src/libs/models/_auto-open-tabs-store.ts b/src/libs/models/_auto-open-tabs-store.ts
new file mode 100644
index 0000000..e1ec579
--- /dev/null
+++ b/src/libs/models/_auto-open-tabs-store.ts
@@ -0,0 +1,74 @@
+import {
+ type Readable,
+ type Subscriber,
+ type Unsubscriber,
+} from "svelte/store";
+import debounce, { type DebouncedFunc } from "lodash-es/debounce";
+import {
+ getAutoOpenTabs,
+ setAutoOpenTabs,
+} from "./_auto-open-tabs-browser-storage";
+
+export const PINNED_TAB_STORE_ID = "pinned-tabs";
+
+export class AutoOpenTabsStore implements Readable {
+ private _subscribers = new Set>();
+ private _autoOpenTabsNewWindow: boolean = false;
+ private _debounceCallCount = 0;
+ private _pendingChanges: boolean = false;
+ private _debouncedAutoOpen: DebouncedFunc<() => void>;
+
+ constructor() {
+ this._debouncedAutoOpen = debounce(async () => {
+ const thisCallCount = this._debounceCallCount;
+ await setAutoOpenTabs({
+ autoOpenTabsNewWindow: this._autoOpenTabsNewWindow,
+ });
+ this._pendingChanges = this._debounceCallCount !== thisCallCount;
+ this.notifySubscribers();
+ }, 1000);
+ this.initialize();
+ }
+
+ // Get the current state of this store
+ private get state(): AutoOpenTabs {
+ return {
+ autoOpenTabsNewWindow: this._autoOpenTabsNewWindow,
+ pendingChanges: this._pendingChanges,
+ };
+ }
+
+ // Initialize the values of this store
+ private async initialize() {
+ const opts = await getAutoOpenTabs();
+ this._autoOpenTabsNewWindow = opts.autoOpenTabsNewWindow;
+ this.notifySubscribers();
+ }
+
+ // Subscribe to this store (Part of Readable interface)
+ subscribe(sub: Subscriber): Unsubscriber {
+ this._subscribers.add(sub);
+ sub(this.state);
+ return () => {
+ this._subscribers.delete(sub);
+ };
+ }
+
+ // Notify subscribers of a change in state
+ private notifySubscribers = () => {
+ this._subscribers.forEach((sub) => sub(this.state));
+ };
+
+ setAutoOpenTabsNewWindow(open: boolean) {
+ this._autoOpenTabsNewWindow = open;
+ this._pendingChanges = true;
+ this.notifySubscribers();
+ this._debounceCallCount++;
+ this._debouncedAutoOpen();
+ }
+}
+
+interface AutoOpenTabs {
+ autoOpenTabsNewWindow: boolean;
+ pendingChanges: boolean;
+}
diff --git a/src/libs/models/_debounce-work.ts b/src/libs/models/_debounce-work.ts
deleted file mode 100644
index 074911b..0000000
--- a/src/libs/models/_debounce-work.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { debounce } from "lodash";
-
-export class DebounceWork {
- private debouncedFn: () => void;
- private pendingCalls: number = 0;
- private chain: Promise = Promise.resolve();
-
- constructor(public fn: () => Promise) {
- this.debouncedFn = debounce(() => {
- // Reset pending calls
- this.pendingCalls = 0;
- // Append to queue of work
- this.chain = this.chain.then(() => fn());
- }, 1000);
- }
-
- run() {
- this.pendingCalls++;
- this.debouncedFn();
- }
-
- isComplete() {
- return this.pendingCalls === 0;
- }
-}
diff --git a/src/libs/models/_pinned-tabs-browser-storage.test.ts b/src/libs/models/_pinned-tabs-browser-storage.test.ts
new file mode 100644
index 0000000..e5f5732
--- /dev/null
+++ b/src/libs/models/_pinned-tabs-browser-storage.test.ts
@@ -0,0 +1,77 @@
+import { expect, describe, test, beforeEach, afterEach, vi } from "vitest";
+import { getUrlsToPin, setUrlsToPin } from "./_pinned-tabs-browser-storage";
+import { storage } from "webextension-polyfill";
+
+vi.mock("webextension-polyfill", () => {
+ return {
+ storage: {
+ sync: {
+ get: vi.fn().mockResolvedValue({}),
+ set: vi.fn().mockResolvedValue(undefined),
+ },
+ },
+ };
+});
+
+beforeEach(() => {});
+
+afterEach(() => {
+ vi.clearAllMocks();
+});
+
+describe("getUrlsToPin", () => {
+ test("returns empty array for new storage", async () => {
+ const urls = await getUrlsToPin();
+ expect(urls).toEqual([]);
+ });
+
+ test("returns empty array from storage", async () => {
+ storage.sync.get = vi.fn().mockResolvedValue({
+ "pinned-tabs": [],
+ });
+
+ const urls = await getUrlsToPin();
+ expect(urls).toEqual([]);
+ });
+
+ test("returns single url from storage", async () => {
+ storage.sync.get = vi.fn().mockResolvedValue({
+ "pinned-tabs": ["https://www.gaunt.dev"],
+ });
+
+ const urls = await getUrlsToPin();
+ expect(urls).toEqual(["https://www.gaunt.dev"]);
+ });
+
+ test("returns multiple urls from storage", async () => {
+ storage.sync.get = vi.fn().mockResolvedValue({
+ "pinned-tabs": ["https://www.gaunt.dev", "http://www.gaunt.dev"],
+ });
+
+ const urls = await getUrlsToPin();
+ expect(urls).toEqual(["https://www.gaunt.dev", "http://www.gaunt.dev"]);
+ });
+});
+
+describe("setUrlsToPin", () => {
+ test("saves empty array", async () => {
+ await setUrlsToPin([]);
+ expect(storage.sync.set).toHaveBeenCalledWith({
+ "pinned-tabs": [],
+ });
+ });
+
+ test("saves empty array for empty strings and invalid urls", async () => {
+ await setUrlsToPin(["", "this is not a URL"]);
+ expect(storage.sync.set).toHaveBeenCalledWith({
+ "pinned-tabs": [],
+ });
+ });
+
+ test("saves valid and normalized urls", async () => {
+ await setUrlsToPin(["http://www.gaunt.dev", "https://gaunt.dev/projects"]);
+ expect(storage.sync.set).toHaveBeenCalledWith({
+ "pinned-tabs": ["http://www.gaunt.dev/", "https://gaunt.dev/projects"],
+ });
+ });
+});
diff --git a/src/libs/models/_pinned-tabs.ts b/src/libs/models/_pinned-tabs-browser-storage.ts
similarity index 76%
rename from src/libs/models/_pinned-tabs.ts
rename to src/libs/models/_pinned-tabs-browser-storage.ts
index 12538d3..beeb256 100644
--- a/src/libs/models/_pinned-tabs.ts
+++ b/src/libs/models/_pinned-tabs-browser-storage.ts
@@ -1,9 +1,9 @@
-import * as browser from "webextension-polyfill";
+import { storage } from "webextension-polyfill";
export const URLS_TO_PIN_STORAGE_KEY = "pinned-tabs";
export async function getUrlsToPin(): Promise {
- const result = await browser.storage.sync.get(URLS_TO_PIN_STORAGE_KEY);
+ const result = await storage.sync.get(URLS_TO_PIN_STORAGE_KEY);
if (result[URLS_TO_PIN_STORAGE_KEY]) {
return result[URLS_TO_PIN_STORAGE_KEY];
}
@@ -21,7 +21,7 @@ export async function setUrlsToPin(urls: string[]): Promise {
}
})
.filter((u) => u.trim().length > 0);
- await browser.storage.sync.set({
+ await storage.sync.set({
[URLS_TO_PIN_STORAGE_KEY]: urls,
});
}
diff --git a/src/libs/models/_pinned-tabs-store.ts b/src/libs/models/_pinned-tabs-store.ts
new file mode 100644
index 0000000..58b97c0
--- /dev/null
+++ b/src/libs/models/_pinned-tabs-store.ts
@@ -0,0 +1,69 @@
+import {
+ type Readable,
+ type Subscriber,
+ type Unsubscriber,
+} from "svelte/store";
+import { getUrlsToPin, setUrlsToPin } from "./_pinned-tabs-browser-storage";
+import debounce, { type DebouncedFunc } from "lodash-es/debounce";
+
+export class PinnedTabsStore implements Readable {
+ private _subscribers = new Set>();
+ private _urls: string[] = [];
+ private _debounceCallCount = 0;
+ private _pendingChanges: boolean = false;
+ private _debouncedSetURLs: DebouncedFunc<() => void>;
+
+ constructor() {
+ this._debouncedSetURLs = debounce(async () => {
+ const thisCallCount = this._debounceCallCount;
+ await setUrlsToPin(this._urls);
+ this._pendingChanges = this._debounceCallCount !== thisCallCount;
+ this.notifySubscribers();
+ }, 1000);
+ this.initialize();
+ }
+
+ // Get the current state of this store
+ private get state(): PinnedTabs {
+ return {
+ urls: this._urls,
+ pendingChanges: this._pendingChanges,
+ };
+ }
+
+ // Initialize the values of this store
+ private async initialize() {
+ this._urls = await getUrlsToPin();
+ if (this._urls.length === 0) {
+ this._urls = [""];
+ }
+ this.notifySubscribers();
+ }
+
+ // Subscribe to this store (Part of Readable interface)
+ subscribe(sub: Subscriber): Unsubscriber {
+ this._subscribers.add(sub);
+ sub(this.state);
+ return () => {
+ this._subscribers.delete(sub);
+ };
+ }
+
+ // Notify subscribers of a change in state
+ private notifySubscribers = () => {
+ this._subscribers.forEach((sub) => sub(this.state));
+ };
+
+ saveURLs(urls: string[]) {
+ this._urls = urls;
+ this._pendingChanges = true;
+ this.notifySubscribers();
+ this._debounceCallCount++;
+ this._debouncedSetURLs();
+ }
+}
+
+interface PinnedTabs {
+ urls: string[];
+ pendingChanges: boolean;
+}
diff --git a/src/libs/utils/_sleep.test.ts b/src/libs/utils/_sleep.test.ts
new file mode 100644
index 0000000..7c11e80
--- /dev/null
+++ b/src/libs/utils/_sleep.test.ts
@@ -0,0 +1,16 @@
+import { expect, test, beforeEach, afterEach, vi } from "vitest";
+import { sleep } from "./_sleep";
+
+beforeEach(() => {
+ vi.useFakeTimers();
+});
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+test("sleep by set amount", () => {
+ const promise = sleep(100);
+ vi.advanceTimersByTime(100 + 1);
+ expect(promise).resolves.toBeUndefined();
+});
diff --git a/src/libs/utils/_sleep.ts b/src/libs/utils/_sleep.ts
new file mode 100644
index 0000000..0f532df
--- /dev/null
+++ b/src/libs/utils/_sleep.ts
@@ -0,0 +1,3 @@
+export function sleep(delay: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, delay));
+}
diff --git a/vite.config.ts b/vite.config.ts
index be3b485..ffcce93 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -59,7 +59,7 @@ async function bundleExtension() {
const output = createWriteStream(zipPath);
// eslint-disable-next-line new-cap
- const archive = new archiver("zip", {
+ const archive = archiver("zip", {
zlib: {
// Sets the compression level
level: 9,