From d52caefe2cae6bb841a71c807715d9aaf63ba919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Sat, 10 Jan 2026 14:21:23 +0100 Subject: [PATCH] Add dev tool for quickly switching profile settings --- bookmarks/frontend/components/dev-tool.js | 257 ++++++++++++++++++++++ bookmarks/frontend/index.js | 1 + bookmarks/templates/shared/dev_tool.html | 9 + bookmarks/templates/shared/layout.html | 1 + bookmarks/templatetags/shared.py | 7 + rollup.config.mjs | 12 + 6 files changed, 287 insertions(+) create mode 100644 bookmarks/frontend/components/dev-tool.js create mode 100644 bookmarks/templates/shared/dev_tool.html diff --git a/bookmarks/frontend/components/dev-tool.js b/bookmarks/frontend/components/dev-tool.js new file mode 100644 index 0000000..3e51509 --- /dev/null +++ b/bookmarks/frontend/components/dev-tool.js @@ -0,0 +1,257 @@ +import { LitElement, html, css } from "lit"; + +class DevTool extends LitElement { + static properties = { + profile: { type: Object, state: true }, + formAction: { type: String, attribute: "data-form-action" }, + csrfToken: { type: String, attribute: "data-csrf-token" }, + isOpen: { type: Boolean, state: true }, + }; + + static styles = css` + :host { + position: fixed; + bottom: 1rem; + right: 1rem; + z-index: 10000; + } + + .button { + background: var(--btn-primary-bg-color); + color: var(--btn-primary-text-color); + border: none; + padding: var(--unit-2); + border-radius: var(--border-radius); + box-shadow: var(--btn-box-shadow); + cursor: pointer; + height: auto; + line-height: 0; + } + + .overlay { + display: none; + position: absolute; + bottom: 100%; + right: 0; + background: var(--body-color); + color: var(--text-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: var(--unit-2); + margin-bottom: var(--unit-2); + min-width: 220px; + box-shadow: var(--box-shadow-lg); + font-size: var(--font-size-sm); + } + + :host([open]) .overlay { + display: block; + } + + h3 { + margin: 0 0 var(--unit-2) 0; + } + + label { + display: flex; + align-items: center; + gap: var(--unit-1); + cursor: pointer; + } + + label:has(select) { + margin-bottom: var(--unit-1); + } + + label:has(select) span { + min-width: 100px; + } + + hr { + margin: var(--unit-2) 0; + border: none; + border-top: 1px solid var(--border-color); + } + `; + + static fields = [ + { + type: "select", + key: "theme", + label: "Theme", + options: [ + { value: "auto", label: "Auto" }, + { value: "light", label: "Light" }, + { value: "dark", label: "Dark" }, + ], + }, + { + type: "select", + key: "bookmark_date_display", + label: "Date", + options: [ + { value: "relative", label: "Relative" }, + { value: "absolute", label: "Absolute" }, + { value: "hidden", label: "Hidden" }, + ], + }, + { + type: "select", + key: "bookmark_description_display", + label: "Description", + options: [ + { value: "inline", label: "Inline" }, + { value: "separate", label: "Separate" }, + ], + }, + { type: "checkbox", key: "enable_favicons", label: "Favicons" }, + { type: "checkbox", key: "enable_preview_images", label: "Preview images" }, + { type: "checkbox", key: "display_url", label: "Display URL" }, + { type: "checkbox", key: "permanent_notes", label: "Permanent notes" }, + { type: "checkbox", key: "collapse_side_panel", label: "Collapse sidebar" }, + { type: "checkbox", key: "sticky_pagination", label: "Sticky pagination" }, + { type: "checkbox", key: "hide_bundles", label: "Hide bundles" }, + ]; + + constructor() { + super(); + this.isOpen = false; + this.profile = {}; + this._onOutsideClick = this._onOutsideClick.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + const profileData = document.getElementById("json_profile"); + this.profile = JSON.parse(profileData.textContent || "{}"); + document.addEventListener("click", this._onOutsideClick); + } + + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener("click", this._onOutsideClick); + } + + _onOutsideClick(e) { + if (!this.contains(e.target) && this.isOpen) { + this.isOpen = false; + this.removeAttribute("open"); + } + } + + _toggle() { + this.isOpen = !this.isOpen; + if (this.isOpen) { + this.setAttribute("open", ""); + } else { + this.removeAttribute("open"); + } + } + + _handleChange(key, value) { + this.profile = { ...this.profile, [key]: value }; + if (key === "theme") { + const themeLinks = document.head.querySelectorAll('link[href*="theme"]'); + themeLinks.forEach((link) => link.remove()); + } + this._submitForm(); + } + + _renderField(field) { + switch (field.type) { + case "checkbox": + return html` + + `; + case "select": + return html` + + `; + case "divider": + return html`
`; + default: + return null; + } + } + + async _submitForm() { + const formData = new FormData(); + formData.append("csrfmiddlewaretoken", this.csrfToken); + + // Profile fields + for (const [key, value] of Object.entries(this.profile)) { + if (typeof value === "boolean" && value) { + formData.append(key, "on"); + } else if (typeof value !== "boolean") { + formData.append(key, value); + } + } + + // Submit button name that settings.update expects + formData.append("update_profile", "1"); + + await fetch(this.formAction, { + method: "POST", + body: formData, + }); + + const url = new URL(window.location); + url.searchParams.set("ts", Date.now().toString()); + window.history.replaceState({}, "", url); + + Turbo.visit(url.toString()); + } + + render() { + return html` + +
+

Dev Tools

+ ${DevTool.fields.map((field) => this._renderField(field))} +
+ `; + } +} + +customElements.define("ld-dev-tool", DevTool); diff --git a/bookmarks/frontend/index.js b/bookmarks/frontend/index.js index 5639a52..3b5d3f8 100644 --- a/bookmarks/frontend/index.js +++ b/bookmarks/frontend/index.js @@ -3,6 +3,7 @@ import "./components/bookmark-page.js"; import "./components/clear-button.js"; import "./components/confirm-dropdown.js"; import "./components/details-modal.js"; +import "./components/dev-tool.js"; import "./components/dropdown.js"; import "./components/filter-drawer.js"; import "./components/form.js"; diff --git a/bookmarks/templates/shared/dev_tool.html b/bookmarks/templates/shared/dev_tool.html new file mode 100644 index 0000000..b932cda --- /dev/null +++ b/bookmarks/templates/shared/dev_tool.html @@ -0,0 +1,9 @@ +{% load shared %} +{% if debug and request.user.is_authenticated %} + {{ request.user.profile|model_to_dict|json_script:"json_profile" }} + + +{% endif %} diff --git a/bookmarks/templates/shared/layout.html b/bookmarks/templates/shared/layout.html index 3500694..0a44714 100644 --- a/bookmarks/templates/shared/layout.html +++ b/bookmarks/templates/shared/layout.html @@ -46,5 +46,6 @@
{% block overlays %}{% endblock %}
+ {% include 'shared/dev_tool.html' %} diff --git a/bookmarks/templatetags/shared.py b/bookmarks/templatetags/shared.py index 40676ab..04a3d8a 100644 --- a/bookmarks/templatetags/shared.py +++ b/bookmarks/templatetags/shared.py @@ -5,6 +5,7 @@ import markdown from bleach.linkifier import DEFAULT_CALLBACKS, Linker from bleach_allowlist import markdown_attrs, markdown_tags from django import template +from django.forms.models import model_to_dict from django.utils.safestring import mark_safe from bookmarks import utils @@ -60,6 +61,12 @@ def humanize_relative_date(value): return utils.humanize_relative_date(value) +@register.filter(name="model_to_dict") +def model_to_dict_filter(value): + result = model_to_dict(value) + return result + + @register.tag def htmlmin(parser, token): nodelist = parser.parse(("endhtmlmin",)) diff --git a/rollup.config.mjs b/rollup.config.mjs index 7394394..17e9b0e 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -3,6 +3,17 @@ import terser from '@rollup/plugin-terser'; const production = !process.env.ROLLUP_WATCH; +// Custom plugin to exclude dev-tool.js from production builds +const excludeDevTool = { + name: 'exclude-dev-tool', + load(id) { + if (production && id.endsWith('dev-tool.js')) { + return ''; + } + return null; + }, +}; + export default { input: 'bookmarks/frontend/index.js', output: { @@ -13,6 +24,7 @@ export default { file: 'bookmarks/static/bundle.js', }, plugins: [ + excludeDevTool, // If you have external dependencies installed from // npm, you'll most likely need these plugins. In // some cases you'll need additional configuration —