From 9490da878222fde38ebf6377f5ff8d1e72336121 Mon Sep 17 00:00:00 2001 From: trickypr <23250792+trickypr@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:08:16 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Customizable=20UI=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .config/webpack.config.cjs | 1 + package.json | 1 + pnpm-lock.yaml | 9 + src/content/browser/Browser.svelte | 76 +---- src/content/browser/components/Browser.svelte | 1 + .../components/customizableUI/Block.svelte | 25 ++ .../customizableUI/BrowserView.svelte | 29 ++ .../customizableUI/CustomizableUI.svelte | 37 +++ .../customizableUI/IconButton.svelte | 32 ++ .../customizableUI/OmniboxContainer.svelte | 24 ++ .../components/customizableUI/Spacer.svelte | 17 + .../components/customizableUI/Tabs.svelte | 32 ++ .../customizableUI/UIItemBase.svelte | 34 ++ .../components/customizableUI/index.ts | 3 + .../components/keybindings/Keybinding.svelte | 2 +- .../BrowserContextMenu.svelte | 0 .../HamburgerMenu}/HamburgerMenu.svelte | 21 +- .../HamburgerMenu}/HamburgerMenuItem.svelte | 0 src/content/browser/components/menus/index.ts | 18 ++ .../{toolbar => }/omnibox/Bookmarks.svelte | 14 +- .../{toolbar => }/omnibox/Omnibox.svelte | 5 +- .../browser/components/toolbar/Toolbar.svelte | 54 ---- src/content/browser/lib/globalApi.ts | 6 +- src/content/settings/Settings.svelte | 16 +- .../pref/CustomizableUI/Component.svelte | 155 +++++++++ .../CustomizableUI/Components/Block.svelte | 28 ++ .../CustomizableUI/Components/Browser.svelte | 11 + .../Components/IconButton.svelte | 14 + .../CustomizableUI/Components/Omnibox.svelte | 15 + .../CustomizableUI/Components/Spacer.svelte | 32 ++ .../CustomizableUI/Components/Tabs.svelte | 25 ++ .../Components/TempDropTarget.svelte | 13 + .../pref/CustomizableUI/Components/index.ts | 13 + .../pref/CustomizableUI/Configure.svelte | 98 ++++++ .../CustomizableUI/CustomizableUIPref.svelte | 103 ++++++ .../components/pref/CustomizableUI/index.ts | 7 + src/content/settings/components/pref/index.ts | 6 +- src/content/settings/settings.ts | 2 + .../components}/ToolbarButton.svelte | 0 src/content/shared/components/index.ts | 5 +- src/content/shared/contextMenus/MenuItem.ts | 11 +- src/content/shared/customizableUI/helpers.ts | 295 ++++++++++++++++++ src/content/shared/customizableUI/index.ts | 8 + src/content/shared/customizableUI/items.ts | 95 ++++++ src/content/shared/customizableUI/style.ts | 108 +++++++ src/content/shared/customizableUI/types.ts | 96 ++++++ src/content/shared/svelteUtils.ts | 14 + .../{browser/lib => shared}/xul/observable.ts | 0 src/link.d.ts | 4 + src/prefs.js | 4 + tsconfig.json | 3 +- 51 files changed, 1461 insertions(+), 161 deletions(-) create mode 100644 src/content/browser/components/customizableUI/Block.svelte create mode 100644 src/content/browser/components/customizableUI/BrowserView.svelte create mode 100644 src/content/browser/components/customizableUI/CustomizableUI.svelte create mode 100644 src/content/browser/components/customizableUI/IconButton.svelte create mode 100644 src/content/browser/components/customizableUI/OmniboxContainer.svelte create mode 100644 src/content/browser/components/customizableUI/Spacer.svelte create mode 100644 src/content/browser/components/customizableUI/Tabs.svelte create mode 100644 src/content/browser/components/customizableUI/UIItemBase.svelte create mode 100644 src/content/browser/components/customizableUI/index.ts rename src/content/browser/components/{contextMenus => menus}/BrowserContextMenu.svelte (100%) rename src/content/browser/components/{toolbar => menus/HamburgerMenu}/HamburgerMenu.svelte (73%) rename src/content/browser/components/{toolbar => menus/HamburgerMenu}/HamburgerMenuItem.svelte (100%) create mode 100644 src/content/browser/components/menus/index.ts rename src/content/browser/components/{toolbar => }/omnibox/Bookmarks.svelte (91%) rename src/content/browser/components/{toolbar => }/omnibox/Omnibox.svelte (97%) delete mode 100644 src/content/browser/components/toolbar/Toolbar.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Component.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Components/Block.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Components/Browser.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Components/IconButton.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Components/Omnibox.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Components/Spacer.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Components/Tabs.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Components/TempDropTarget.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/Components/index.ts create mode 100644 src/content/settings/components/pref/CustomizableUI/Configure.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/CustomizableUIPref.svelte create mode 100644 src/content/settings/components/pref/CustomizableUI/index.ts rename src/content/{browser/components/toolbar => shared/components}/ToolbarButton.svelte (100%) create mode 100644 src/content/shared/customizableUI/helpers.ts create mode 100644 src/content/shared/customizableUI/index.ts create mode 100644 src/content/shared/customizableUI/items.ts create mode 100644 src/content/shared/customizableUI/style.ts create mode 100644 src/content/shared/customizableUI/types.ts rename src/content/{browser/lib => shared}/xul/observable.ts (100%) diff --git a/.config/webpack.config.cjs b/.config/webpack.config.cjs index fed87e2..8b15211 100644 --- a/.config/webpack.config.cjs +++ b/.config/webpack.config.cjs @@ -64,6 +64,7 @@ const sharedSettings = (contentFiles, dev) => { extensions: ['.ts', '.mjs', '.js', '.svelte'], alias: { '@shared': resolve('src/content/shared'), + '@browser': resolve('src/content/browser'), }, }, diff --git a/package.json b/package.json index aa959f2..9aad0f2 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@catppuccin/palette": "^0.2.0", "fnts": "^2.0.1", "mitt": "^3.0.1", + "nanoid": "^5.0.3", "remixicon": "^3.5.0", "snarkdown": "^2.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8e10a3..6cffd7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ dependencies: mitt: specifier: ^3.0.1 version: 3.0.1 + nanoid: + specifier: ^5.0.3 + version: 5.0.3 remixicon: specifier: ^3.5.0 version: 3.5.0 @@ -2833,6 +2836,12 @@ packages: hasBin: true dev: true + /nanoid@5.0.3: + resolution: {integrity: sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true diff --git a/src/content/browser/Browser.svelte b/src/content/browser/Browser.svelte index f882046..55bd3bf 100644 --- a/src/content/browser/Browser.svelte +++ b/src/content/browser/Browser.svelte @@ -3,81 +3,29 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- -
- {#each $tabs as tab (tab.getId())} - - {/each} - -
- - {#if currentTab} - - {/if} - -
- {#each sortedBrowers as tab (tab.getId())} - { - if ($selectedTab == -1) selectedTab.set(tab.getId()) - }} - /> - {/each} -
-
+{#if currentTab} + +{/if} + diff --git a/src/content/browser/components/Browser.svelte b/src/content/browser/components/Browser.svelte index 1ee7baf..13e5120 100644 --- a/src/content/browser/components/Browser.svelte +++ b/src/content/browser/components/Browser.svelte @@ -33,6 +33,7 @@ height: 100%; display: flex; flex-direction: column; + flex-grow: 1; } .browser-container[hidden] { diff --git a/src/content/browser/components/customizableUI/Block.svelte b/src/content/browser/components/customizableUI/Block.svelte new file mode 100644 index 0000000..53a81c3 --- /dev/null +++ b/src/content/browser/components/customizableUI/Block.svelte @@ -0,0 +1,25 @@ + + + + + + {#each component.content as child} + + {/each} + diff --git a/src/content/browser/components/customizableUI/BrowserView.svelte b/src/content/browser/components/customizableUI/BrowserView.svelte new file mode 100644 index 0000000..44841f3 --- /dev/null +++ b/src/content/browser/components/customizableUI/BrowserView.svelte @@ -0,0 +1,29 @@ + + + + + + {#each sortedBrowers as tab (tab.getId())} + + {/each} + diff --git a/src/content/browser/components/customizableUI/CustomizableUI.svelte b/src/content/browser/components/customizableUI/CustomizableUI.svelte new file mode 100644 index 0000000..ff434b5 --- /dev/null +++ b/src/content/browser/components/customizableUI/CustomizableUI.svelte @@ -0,0 +1,37 @@ + + + + +{#if component.type === 'block'} + +{:else if component.type === 'icon'} + +{:else if component.type === 'spacer'} + +{:else if component.type === 'browser'} + +{:else if component.type === 'omnibox'} + +{:else if component.type === 'tabs'} + +{/if} diff --git a/src/content/browser/components/customizableUI/IconButton.svelte b/src/content/browser/components/customizableUI/IconButton.svelte new file mode 100644 index 0000000..cfa252f --- /dev/null +++ b/src/content/browser/components/customizableUI/IconButton.svelte @@ -0,0 +1,32 @@ + + + + + + component.action(tab, button)} + > + + + diff --git a/src/content/browser/components/customizableUI/OmniboxContainer.svelte b/src/content/browser/components/customizableUI/OmniboxContainer.svelte new file mode 100644 index 0000000..fda18b3 --- /dev/null +++ b/src/content/browser/components/customizableUI/OmniboxContainer.svelte @@ -0,0 +1,24 @@ + + + + + + + diff --git a/src/content/browser/components/customizableUI/Spacer.svelte b/src/content/browser/components/customizableUI/Spacer.svelte new file mode 100644 index 0000000..61810fb --- /dev/null +++ b/src/content/browser/components/customizableUI/Spacer.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/content/browser/components/customizableUI/Tabs.svelte b/src/content/browser/components/customizableUI/Tabs.svelte new file mode 100644 index 0000000..02eba17 --- /dev/null +++ b/src/content/browser/components/customizableUI/Tabs.svelte @@ -0,0 +1,32 @@ + + + + + e.preventDefault()}> + {#each $tabs as tab} + + {/each} + + + diff --git a/src/content/browser/components/customizableUI/UIItemBase.svelte b/src/content/browser/components/customizableUI/UIItemBase.svelte new file mode 100644 index 0000000..72700a1 --- /dev/null +++ b/src/content/browser/components/customizableUI/UIItemBase.svelte @@ -0,0 +1,34 @@ + + + + + +
+ +
+ + diff --git a/src/content/browser/components/customizableUI/index.ts b/src/content/browser/components/customizableUI/index.ts new file mode 100644 index 0000000..e003224 --- /dev/null +++ b/src/content/browser/components/customizableUI/index.ts @@ -0,0 +1,3 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ diff --git a/src/content/browser/components/keybindings/Keybinding.svelte b/src/content/browser/components/keybindings/Keybinding.svelte index 2f51b01..0a1f8ea 100644 --- a/src/content/browser/components/keybindings/Keybinding.svelte +++ b/src/content/browser/components/keybindings/Keybinding.svelte @@ -5,7 +5,7 @@ - - panel.openPopup(button, 'bottomright topright')} - bind:button -> - - - - +
+ (document.getElementById('hamburgerMenu') as XULPanel | null)?.openPopup( + target, + anchor, + ) + +export { BrowserContextMenu, HamburgerMenu } diff --git a/src/content/browser/components/toolbar/omnibox/Bookmarks.svelte b/src/content/browser/components/omnibox/Bookmarks.svelte similarity index 91% rename from src/content/browser/components/toolbar/omnibox/Bookmarks.svelte rename to src/content/browser/components/omnibox/Bookmarks.svelte index 122be1f..9d1a238 100644 --- a/src/content/browser/components/toolbar/omnibox/Bookmarks.svelte +++ b/src/content/browser/components/omnibox/Bookmarks.svelte @@ -3,17 +3,19 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -
- tab.goBack()}> - - - tab.reload()}> - - - tab.goForward()}> - - - -
- - - -
- - -
- - diff --git a/src/content/browser/lib/globalApi.ts b/src/content/browser/lib/globalApi.ts index d895d0c..a0a5e60 100644 --- a/src/content/browser/lib/globalApi.ts +++ b/src/content/browser/lib/globalApi.ts @@ -24,7 +24,8 @@ const uriPref = (pref: string) => (): nsIURIType => const newTabUri = uriPref('browser.newtab.default') const newWindowUri = uriPref('browser.newwindow.default') -export const tabs = viewableWritable([new Tab(newWindowUri())]) +export const tabs = viewableWritable([]) +openTab(newWindowUri()) export function openTab(uri: nsIURIType = newTabUri()) { const newTab = new Tab(uri) @@ -126,6 +127,9 @@ export const windowApi = { windowTriggers: mitt(), closeTab, openTab, + get tabs() { + return tabs.readOnce() + }, setIcon: (browser: XULBrowserElement, iconURL: string) => tabs .readOnce() diff --git a/src/content/settings/Settings.svelte b/src/content/settings/Settings.svelte index ca8f4bf..9642bac 100644 --- a/src/content/settings/Settings.svelte +++ b/src/content/settings/Settings.svelte @@ -9,9 +9,12 @@ } from '../shared/search/constants' import Category from './components/Category.svelte' import SubCategory from './components/SubCategory.svelte' - import { ContextMenuPref } from './components/pref/ContextMenuPref' - import SelectPref from './components/pref/SelectPref.svelte' - import StringPref from './components/pref/StringPref.svelte' + import { + ContextMenuPref, + SelectPref, + StringPref, + CustomizableUiPref, + } from './components/pref' import { SidebarItemData, Sidebar } from './components/sidebar' let sidebarItems: SidebarItemData[] = [] @@ -46,8 +49,11 @@ - - + + + + + diff --git a/src/content/settings/components/pref/CustomizableUI/Component.svelte b/src/content/settings/components/pref/CustomizableUI/Component.svelte new file mode 100644 index 0000000..0f3eff5 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Component.svelte @@ -0,0 +1,155 @@ + + + + + + +{#if !isRoot} +
+{/if} + + + +
{ + if (!canDrag) return + e.stopPropagation() + if (hover) return + listener.emit('clearHover') + hover = true + }} + on:click={(e) => { + if (!draggable || component.type === 'temp-drop-target') return + e.stopPropagation() + selectedId = component.id + }} + on:dragstart={(e) => { + if (!canDrag) return + e.stopPropagation() + e.dataTransfer?.setData('component', JSON.stringify(component)) + if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move' + }} + on:dragover={(e) => { + e.stopPropagation() + e.preventDefault() + + const data = JSON.parse(e.dataTransfer?.getData('component') ?? 'false') + if (!data) return + if (data.id === component.id) return + + if (e.dataTransfer) e.dataTransfer.dropEffect = 'move' + dropTarget = calculateDropTargetRel(parentOrientation, e) + }} + on:dragleave|stopPropagation={() => (dropTarget = null)} + on:drop|preventDefault|stopPropagation={(e) => { + const data = JSON.parse(e.dataTransfer?.getData('component') ?? 'false') + if (!data || !dropTarget || isParent(data, component)) { + dropTarget = null + return + } + applyDrop(root, data, component, dropTarget) + + // Force an update + root.id = nanoid() + dropTarget = null + }} +> + {#if component.type === 'block'} + + {:else if component.type === 'icon'} + + {:else if component.type === 'spacer'} + + {:else if component.type === 'browser'} + + {:else if component.type === 'omnibox'} + + {:else if component.type === 'tabs'} + + {:else if component.type === 'temp-drop-target'} + + {/if} +
+ +{#if !isRoot} +
+{/if} + + diff --git a/src/content/settings/components/pref/CustomizableUI/Components/Block.svelte b/src/content/settings/components/pref/CustomizableUI/Components/Block.svelte new file mode 100644 index 0000000..c7efd76 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Components/Block.svelte @@ -0,0 +1,28 @@ + + + + +{#each component.content as child} + +{:else} + +{/each} diff --git a/src/content/settings/components/pref/CustomizableUI/Components/Browser.svelte b/src/content/settings/components/pref/CustomizableUI/Components/Browser.svelte new file mode 100644 index 0000000..34222da --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Components/Browser.svelte @@ -0,0 +1,11 @@ + + +
Browser Content
+ + diff --git a/src/content/settings/components/pref/CustomizableUI/Components/IconButton.svelte b/src/content/settings/components/pref/CustomizableUI/Components/IconButton.svelte new file mode 100644 index 0000000..f1215d4 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Components/IconButton.svelte @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/content/settings/components/pref/CustomizableUI/Components/Omnibox.svelte b/src/content/settings/components/pref/CustomizableUI/Components/Omnibox.svelte new file mode 100644 index 0000000..83a65c9 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Components/Omnibox.svelte @@ -0,0 +1,15 @@ + + +
Omnibox
+ + diff --git a/src/content/settings/components/pref/CustomizableUI/Components/Spacer.svelte b/src/content/settings/components/pref/CustomizableUI/Components/Spacer.svelte new file mode 100644 index 0000000..34f8f6d --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Components/Spacer.svelte @@ -0,0 +1,32 @@ + + + + +{#if verbose} + Spacer +{:else} +
+{/if} + + diff --git a/src/content/settings/components/pref/CustomizableUI/Components/Tabs.svelte b/src/content/settings/components/pref/CustomizableUI/Components/Tabs.svelte new file mode 100644 index 0000000..1152267 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Components/Tabs.svelte @@ -0,0 +1,25 @@ + + + + +{#if verbose} + Tabs +{:else} +
Tab 1
+
Tab 2
+{/if} + + diff --git a/src/content/settings/components/pref/CustomizableUI/Components/TempDropTarget.svelte b/src/content/settings/components/pref/CustomizableUI/Components/TempDropTarget.svelte new file mode 100644 index 0000000..02143e3 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Components/TempDropTarget.svelte @@ -0,0 +1,13 @@ + + + + +
+ {#if verbose} + Block + {/if} +
diff --git a/src/content/settings/components/pref/CustomizableUI/Components/index.ts b/src/content/settings/components/pref/CustomizableUI/Components/index.ts new file mode 100644 index 0000000..8e9cd19 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Components/index.ts @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Block from './Block.svelte' +import Browser from './Browser.svelte' +import IconButton from './IconButton.svelte' +import Omnibox from './Omnibox.svelte' +import Spacer from './Spacer.svelte' +import Tabs from './Tabs.svelte' +import TempDropTarget from './TempDropTarget.svelte' + +export { Block, Browser, IconButton, Omnibox, Spacer, Tabs, TempDropTarget } diff --git a/src/content/settings/components/pref/CustomizableUI/Configure.svelte b/src/content/settings/components/pref/CustomizableUI/Configure.svelte new file mode 100644 index 0000000..c26bb54 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/Configure.svelte @@ -0,0 +1,98 @@ + + + + +{#if selectedComponent && selectedPrefs} + {#each selectedPrefs as pref} +
+ + + {#if pref.type === 'string'} + {#if pref.options} + + {:else} + + (root = update((c) => { + c[pref.key] = e.target.value + return c + }))} + /> + {/if} + {/if} + + {#if pref.type === 'number'} + + (root = update((c) => { + c[pref.key] = Number(e.target.value) + return c + }))} + /> + {/if} + + {#if pref.type === 'block-size'} + + + {#if selectedComponent[pref.key].type !== 'content'} + + (root = update((c) => { + c[pref.key].value = Number(e.target.value) + return c + }))} + /> + {/if} + {/if} +
+ {:else} + Selected item does not have any prefs + {/each} +{/if} diff --git a/src/content/settings/components/pref/CustomizableUI/CustomizableUIPref.svelte b/src/content/settings/components/pref/CustomizableUI/CustomizableUIPref.svelte new file mode 100644 index 0000000..4a21fbb --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/CustomizableUIPref.svelte @@ -0,0 +1,103 @@ + + + + +

+ Warning: Editing this layout will likely unload all of your tabs! + Proceed with caution +

+ +
+
+

Items

+ + {#each cuiPreviewItems as item} +
+ + +
+
+ {/each} +
+ +
+ +
+ +
+

Configure item

+ + {#if selectedId} + + + {#if selectedId !== component.id} + + {/if} + {/if} +
+
+ + diff --git a/src/content/settings/components/pref/CustomizableUI/index.ts b/src/content/settings/components/pref/CustomizableUI/index.ts new file mode 100644 index 0000000..925ef56 --- /dev/null +++ b/src/content/settings/components/pref/CustomizableUI/index.ts @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import CustomizableUiPref from './CustomizableUIPref.svelte' + +export { CustomizableUiPref } diff --git a/src/content/settings/components/pref/index.ts b/src/content/settings/components/pref/index.ts index b490009..20e0020 100644 --- a/src/content/settings/components/pref/index.ts +++ b/src/content/settings/components/pref/index.ts @@ -1,6 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import SelectPref from './SelectPref.svelte' import StringPref from './StringPref.svelte' -export { StringPref } +export * from './ContextMenuPref' +export * from './CustomizableUI' + +export { StringPref, SelectPref } diff --git a/src/content/settings/settings.ts b/src/content/settings/settings.ts index eac4cdc..52bff47 100644 --- a/src/content/settings/settings.ts +++ b/src/content/settings/settings.ts @@ -1,6 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import 'remixicon/fonts/remixicon.css' + import '../global.css' import Settings from './Settings.svelte' import './settings.css' diff --git a/src/content/browser/components/toolbar/ToolbarButton.svelte b/src/content/shared/components/ToolbarButton.svelte similarity index 100% rename from src/content/browser/components/toolbar/ToolbarButton.svelte rename to src/content/shared/components/ToolbarButton.svelte diff --git a/src/content/shared/components/index.ts b/src/content/shared/components/index.ts index 775250a..2dd71b2 100644 --- a/src/content/shared/components/index.ts +++ b/src/content/shared/components/index.ts @@ -2,8 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Button from './Button.svelte' +import FancySelect from './FancySelect.svelte' +import Spinner from './Spinner.svelte' import TextInput from './TextInput.svelte' +import ToolbarButton from './ToolbarButton.svelte' export type ButtonKind = 'primary' | 'secondary' -export { Button, TextInput } +export { Button, FancySelect, Spinner, TextInput, ToolbarButton } diff --git a/src/content/shared/contextMenus/MenuItem.ts b/src/content/shared/contextMenus/MenuItem.ts index 811d993..9f0c0c4 100644 --- a/src/content/shared/contextMenus/MenuItem.ts +++ b/src/content/shared/contextMenus/MenuItem.ts @@ -1,10 +1,9 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { type Readable, readable } from 'svelte/store' +import { dynamicStringPref } from '@shared/svelteUtils' import type { ContextMenuInfo } from '../../../actors/ContextMenu.types' -import { observable } from '../../browser/lib/xul/observable' import { MENU_ITEM_ACTIONS } from './menuItems' /** @@ -59,10 +58,4 @@ export const fromIdString = (idString: string): MenuItem[] => export const getFromPref = (pref: string): MenuItem[] => fromIdString(Services.prefs.getStringPref(pref, '')) -export function getMenuItemsDynamicPref(pref: string): Readable { - return readable(getFromPref(pref), (set) => { - const observer = observable(() => set(getFromPref(pref))) - Services.prefs.addObserver(pref, observer) - return () => Services.prefs.removeObserver(pref, observer) - }) -} +export const getMenuItemsDynamicPref = dynamicStringPref(fromIdString) diff --git a/src/content/shared/customizableUI/helpers.ts b/src/content/shared/customizableUI/helpers.ts new file mode 100644 index 0000000..4cf0331 --- /dev/null +++ b/src/content/shared/customizableUI/helpers.ts @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import curry from 'fnts/curry' +import { nanoid } from 'nanoid' +import { type Readable, readable } from 'svelte/store' + +import { dynamicStringPref } from '@shared/svelteUtils' + +import { + type BlockComponent, + type BlockDirection, + type BlockSize, + type BrowserComponent, + type Component, + type ComponentId, + type ExportComponent, + type IconComponent, + type SpacerComponent, + type TempDropTargetComponent, + cuiPreviewItems, +} from '.' +import type { Tab } from '../../browser/components/tabs/tab' + +export const createBlock = ( + direction: BlockDirection = 'horizontal', + content: Component[] = [], + size: BlockSize = { type: 'content' }, +): ComponentId & BlockComponent => ({ + id: nanoid(), + type: 'block', + direction, + content, + size, + color: 'base', +}) + +export const createIcon = ( + icon: string, + action: (tab: Tab) => void = () => {}, + enabled: (tab: Tab) => Readable = () => readable(true), +): ComponentId & IconComponent => ({ + id: nanoid(), + type: 'icon', + icon, + enabled, + action, +}) + +export const tempDropTarget = ( + parent: string, +): ComponentId & TempDropTargetComponent => ({ + id: nanoid(), + type: 'temp-drop-target', + parent, +}) + +export const createSpacer = (grow = 1): ComponentId & SpacerComponent => ({ + id: nanoid(), + type: 'spacer', + grow, +}) + +export const createBrowser = (): ComponentId & BrowserComponent => ({ + id: nanoid(), + type: 'browser', +}) + +export function findChildWithId( + component: Component, + id: string, +): Component | null { + if (component.id === id) { + return component + } + + if (component.type === 'block') { + return component.content.reduce( + (acc, item) => acc ?? findChildWithId(item, id), + null, + ) + } + + return null +} + +export function getParent( + root: Component, + self: Component, +): BlockComponent | null { + if (root === self) { + return null + } + + if (root.type === 'block') { + if (self.type === 'temp-drop-target') + return findChildWithId(root, self.parent) as BlockComponent | null + + if (root.content.some((item) => item.id === self.id)) { + return root + } + + return root.content.reduce( + (acc, item) => acc ?? getParent(item, self), + null, + ) + } + + return null +} + +export function getParentOrientation( + root: Component, + self: Component, +): BlockDirection { + return getParent(root, self)?.direction ?? 'horizontal' +} + +export function calculateDropTargetRel( + parentOrientation: BlockDirection, + dragEvent: DragEvent & { currentTarget: HTMLDivElement & EventTarget }, +): 'before' | 'after' | null { + const boundingRect = dragEvent.currentTarget.getBoundingClientRect() + const x = dragEvent.x + const y = dragEvent.y + + if (parentOrientation === 'horizontal') { + const middle = boundingRect.x + boundingRect.width / 2 + return x < middle ? 'before' : 'after' + } + + const middle = boundingRect.y + boundingRect.height / 2 + return y < middle ? 'before' : 'after' +} + +export function applyDrop( + root: BlockComponent & ComponentId, + toMove: Component, + dropTarget: Component, + rel: 'before' | 'after', +) { + const isRootDrop = dropTarget.id === root.id + let dropTargetParent = getParent(root, dropTarget) + const toMoveParent = getParent(root, toMove) + + if (isRootDrop) dropTargetParent = root + + // Remote from old parent + if (toMoveParent && !isRootDrop) { + const toMoveIndex = toMoveParent.content.findIndex( + (item) => item.id === toMove.id, + ) + toMoveParent.content.splice(toMoveIndex, 1) + } + + // Add to new parent + if (dropTargetParent) { + let dropIndex = + dropTargetParent.content.findIndex((item) => item.id === dropTarget.id) + + (rel === 'before' ? 0 : 1) + if (dropIndex == -1) dropIndex = 0 // Void drop indexes + dropTargetParent.content.splice(dropIndex, 0, toMove) + } +} + +export function isParent(parent: Component, child: Component): boolean { + if (parent.id === child.id) { + return true + } + + if (parent.type === 'block') { + return parent.content.some((item) => isParent(item, child)) + } + + return false +} + +export const countChildrenWithType = curry( + (type: Component['type'], root: Component): number => { + let childrenCount = (root.type === 'block' ? root.content : []) + .map(countChildrenWithType(type)) + .reduce((acc, count) => acc + count, 0) + + if (root.type === type) childrenCount++ + + return childrenCount + }, +) + +export const updateComponentById = curry( + ( + root: Component, + id: string, + updater: (component: Component) => Component, + ): Component => { + if (root.id === id) { + return updater(root) + } + + if (root.type === 'block') { + return { + ...root, + content: root.content.map((item) => + updateComponentById(item, id, updater), + ), + } + } + + return root + }, +) + +export const removeChildById = curry( + (id: string, root: Component): Component => { + if (root.type === 'block') { + return { + ...root, + content: root.content + .filter((item) => item.id !== id) + .map(removeChildById(id)), + } + } + + return root + }, +) + +export function toExportType(component: Component): ExportComponent { + const output = { ...component } as ExportComponent & { id?: string } + delete output.id + + if (output.type === 'block') { + return { + ...output, + // @ts-expect-error The types here are not perfectly setup + content: output.content.map(toExportType), + } + } + + return output +} + +export function fromExportType(component: ExportComponent): Component { + if (!component.type) + return createBlock('vertical', [], { type: 'grow', value: 1 }) + + const output = { ...component, id: nanoid() } as Component + + if (output.type === 'block') + output.content = output.content.map(fromExportType) + + return output +} + +const fromExportTypeStableInternal = + (parentId: string) => + (component: ExportComponent, index: number): Component => { + if (!component.type) + return createBlock('vertical', [], { type: 'grow', value: 1 }) + + const id = `${parentId}-${index}` + const output = { ...component, id } as Component + + if (output.type === 'block') + output.content = output.content.map(fromExportTypeStableInternal(id)) + + // Icons have non-serializable properties that need to be loaded in + if (output.type === 'icon') { + const predefinedItem = cuiPreviewItems.find( + (existingItem) => + existingItem.component.type === 'icon' && + existingItem.component.icon === output.icon, + )?.component as IconComponent | undefined + + output.action = predefinedItem?.action ?? (() => {}) + output.enabled = predefinedItem?.enabled ?? (() => readable(false)) + } + + return output + } + +/** + * A variant of {@link fromExportType} that uses index based IDs instead of + * random ids + * + * It also performs extra initialization steps that are required for use in the + * browser + */ +export const fromExportTypeStable = (component: ExportComponent) => + fromExportTypeStableInternal('root')(component, 0) + +export const customizableUIDynamicPref = dynamicStringPref((json) => + fromExportTypeStable(JSON.parse(json)), +) diff --git a/src/content/shared/customizableUI/index.ts b/src/content/shared/customizableUI/index.ts new file mode 100644 index 0000000..a25e18b --- /dev/null +++ b/src/content/shared/customizableUI/index.ts @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export * from './helpers' +export * from './items' +export * from './style' +export * from './types' diff --git a/src/content/shared/customizableUI/items.ts b/src/content/shared/customizableUI/items.ts new file mode 100644 index 0000000..a2597b5 --- /dev/null +++ b/src/content/shared/customizableUI/items.ts @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { readable } from 'svelte/store' + +import type { Component, ExportComponent } from '.' +import { countChildrenWithType } from './helpers' + +export interface CUIPreviewItem { + component: ExportComponent + canAdd: (root: Component) => boolean +} + +const ALWAYS_ADD = () => true +const ALWAYS_ENABLE = () => readable(true) + +export const cuiPreviewItems: CUIPreviewItem[] = [ + { + canAdd: ALWAYS_ADD, + component: { + type: 'block', + direction: 'horizontal', + size: { type: 'content' }, + content: [], + color: 'base', + }, + }, + { + canAdd: ALWAYS_ADD, + component: { type: 'spacer', grow: 1 }, + }, + { + canAdd: (root) => countChildrenWithType('browser', root) === 0, + component: { type: 'browser' }, + }, + { + canAdd: ALWAYS_ADD, + component: { + type: 'icon', + icon: 'arrow-left-line', + enabled: (tab) => tab.canGoBack, + action: (tab) => tab.goBack(), + }, + }, + { + canAdd: ALWAYS_ADD, + component: { + type: 'icon', + icon: 'arrow-right-line', + enabled: (tab) => tab.canGoForward, + action: (tab) => tab.goForward(), + }, + }, + { + canAdd: ALWAYS_ADD, + component: { + type: 'icon', + icon: 'refresh-line', + enabled: ALWAYS_ENABLE, + action: (tab) => tab.reload(), + }, + }, + { + canAdd: ALWAYS_ADD, + component: { + type: 'icon', + icon: 'add-line', + enabled: ALWAYS_ENABLE, + action: () => window.windowApi.openTab(), + }, + }, + { + canAdd: ALWAYS_ADD, + component: { + type: 'icon', + icon: 'menu-line', + enabled: ALWAYS_ENABLE, + action: async (_, button) => { + const { openHamburgerMenu } = await import('@browser/components/menus') + openHamburgerMenu(button, 'after_start') + }, + }, + }, + { + canAdd: ALWAYS_ADD, + component: { + type: 'omnibox', + }, + }, + { + canAdd: ALWAYS_ADD, + component: { type: 'tabs' }, + }, +] diff --git a/src/content/shared/customizableUI/style.ts b/src/content/shared/customizableUI/style.ts new file mode 100644 index 0000000..0a64359 --- /dev/null +++ b/src/content/shared/customizableUI/style.ts @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import type { + BlockComponent, + BlockDirection, + ExportComponent, + SpacerComponent, +} from '.' + +function getBlockStyle(block: BlockComponent): string { + let style = ` + display: flex; + flex-direction: ${block.direction === 'horizontal' ? 'row' : 'column'}; + background-color: var(--${block.color}); + border-color: var(--${block.color}); + ` + + if (block.size.type === 'fixed') { + style += `${block.direction === 'horizontal' ? 'height' : 'width'}: ${ + block.size.value + }rem;` + } + + if (block.size.type === 'grow') { + style += `flex-grow: ${block.size.value};` + } + + return style +} + +function getBrowserStyle(): string { + return ` + flex-grow: 1; + + display: flex; + justify-content: center; + align-items: center; + ` +} + +function getSpacer( + spacer: SpacerComponent, + parentOrientation: BlockDirection, +): string { + return ` + flex-grow: ${spacer.grow}; + display: flex; + flex-direction: ${parentOrientation === 'horizontal' ? 'row' : 'column'}; + ` +} + +function getOmniboxStyle( + parentOrientation: BlockDirection, + preview: boolean, +): string { + return ` + flex-grow: ${parentOrientation === 'horizontal' ? 1 : 0}; + + ${ + preview + ? ` + display: flex; + justify-content: center; + align-items: center; + ` + : '' + } + ` +} + +function getTabsStyle(): string { + return ` + display:flex; + gap: 0.25rem; + ` +} + +function getIconButtonStyle(): string { + return ` + display: flex; + align-items: center; + ` +} + +export function getComponentStyle( + component: ExportComponent, + parentOrientation: BlockDirection, + preview = true, +): string { + switch (component.type) { + case 'block': + return getBlockStyle(component) + case 'browser': + return getBrowserStyle() + case 'spacer': + return getSpacer(component, parentOrientation) + case 'omnibox': + return getOmniboxStyle(parentOrientation, preview) + case 'tabs': + return getTabsStyle() + case 'icon': + return getIconButtonStyle() + default: + return '' + } +} diff --git a/src/content/shared/customizableUI/types.ts b/src/content/shared/customizableUI/types.ts new file mode 100644 index 0000000..a2e6ae4 --- /dev/null +++ b/src/content/shared/customizableUI/types.ts @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import type { Readable } from 'svelte/store' + +import type { Tab } from '../../browser/components/tabs/tab' + +export type BlockSize = + | { type: 'grow'; value: number } + | { type: 'fixed'; value: number } + | { type: 'content' } +export const surfaceColors = [ + 'crust', + 'mantle', + 'base', + 'surface-0', + 'surface-1', + 'surface-2', +] as const +export type SurfaceColors = (typeof surfaceColors)[number] +export type BlockDirection = 'horizontal' | 'vertical' +export interface BlockComponent { + type: 'block' + direction: BlockDirection + content: Component[] + size: BlockSize + color: SurfaceColors +} + +export interface IconComponent { + type: 'icon' + icon: string + enabled: (tab: Tab) => Readable + action: (tab: Tab, button: HTMLElement) => void +} + +export interface TempDropTargetComponent { + type: 'temp-drop-target' + parent: string +} + +export interface SpacerComponent { + type: 'spacer' + grow: number +} + +export interface BrowserComponent { + type: 'browser' +} + +export interface OmniboxComponent { + type: 'omnibox' +} + +export interface TabsComponent { + type: 'tabs' +} + +export interface ExportComponentMap { + block: BlockComponent + icon: IconComponent + 'temp-drop-target': TempDropTargetComponent + spacer: SpacerComponent + browser: BrowserComponent + omnibox: OmniboxComponent + tabs: TabsComponent +} + +export type ComponentMapKeys = keyof ExportComponentMap +export type ExportComponent = ExportComponentMap[ComponentMapKeys] + +export type ComponentId = { id: string } +export type Component = ComponentId & ExportComponentMap[ComponentMapKeys] + +export type PrefType = + | { type: 'string'; options?: readonly string[] } + | { type: 'number'; range?: [number, number]; steps?: number } + | { type: 'block-size' } +export const prefs: { + [k in ExportComponent['type']]: ({ + key: keyof Omit + } & PrefType)[] +} = { + block: [ + { key: 'direction', type: 'string', options: ['horizontal', 'vertical'] }, + { key: 'size', type: 'block-size' }, + { key: 'color', type: 'string', options: surfaceColors }, + ], + icon: [], + 'temp-drop-target': [], + spacer: [{ key: 'grow', type: 'number' }], + browser: [], + omnibox: [], + tabs: [], +} diff --git a/src/content/shared/svelteUtils.ts b/src/content/shared/svelteUtils.ts index 9a38866..8b4df70 100644 --- a/src/content/shared/svelteUtils.ts +++ b/src/content/shared/svelteUtils.ts @@ -13,6 +13,8 @@ import { readable, } from 'svelte/store' +import { observable } from './xul/observable' + type SubInvTuple = [Subscriber, Invalidator] // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -106,3 +108,15 @@ export function resolverStore( }) }) } + +// NOTE: Autocurrying doesn't infer T correctly +export const dynamicStringPref = + (processor: (value: string) => T) => + (pref: string): Readable => + readable(processor(Services.prefs.getStringPref(pref, '')), (set) => { + const observer = observable(() => + set(processor(Services.prefs.getStringPref(pref, ''))), + ) + Services.prefs.addObserver(pref, observer) + return () => Services.prefs.removeObserver(pref, observer) + }) diff --git a/src/content/browser/lib/xul/observable.ts b/src/content/shared/xul/observable.ts similarity index 100% rename from src/content/browser/lib/xul/observable.ts rename to src/content/shared/xul/observable.ts diff --git a/src/link.d.ts b/src/link.d.ts index 4c8748e..46420e4 100644 --- a/src/link.d.ts +++ b/src/link.d.ts @@ -70,6 +70,10 @@ declare interface XULFindBarElement extends HTMLElement { close() } +declare interface XULPanel extends HTMLElement { + openPopup(target: HTMLElement, anchor: string) +} + declare interface XULMenuPopup extends HTMLElement { openPopupAtScreen( x?: number, diff --git a/src/prefs.js b/src/prefs.js index de97f55..a5689d6 100644 --- a/src/prefs.js +++ b/src/prefs.js @@ -18,6 +18,10 @@ pref( 'browser.contextmenus.page', 'link__copy,link__new-tab,separator,selection__copy', ); +pref( + 'browser.uiCustomization.state', + '{"type":"block","direction":"vertical","content":[{"type":"block","direction":"horizontal","size":{"type":"content"},"content":[{"type":"tabs"},{"type":"icon","icon":"add-line"}],"color":"base"},{"type":"block","direction":"horizontal","size":{"type":"content"},"content":[{"type":"icon","icon":"arrow-left-line"},{"type":"icon","icon":"refresh-line"},{"type":"icon","icon":"arrow-right-line"},{"type":"spacer","grow":1},{"type":"omnibox"},{"type":"spacer","grow":1},{"type":"icon","icon":"menu-line"}],"color":"surface-0"},{"type":"browser"}],"size":{"type":"grow","value":1},"color":"base"}', +); // Default keybinds pref('browser.keybinds.toolbox', 'accel+alt+shift+I'); diff --git a/tsconfig.json b/tsconfig.json index 60550ba..62c7768 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "target": "es2022", "allowArbitraryExtensions": true, "paths": { - "@shared/*": ["./src/content/shared/*"] + "@shared/*": ["./src/content/shared/*"], + "@browser/*": ["./src/content/browser/*"] } }, "include": ["src/content/**/*"],