Template improvements

This commit is contained in:
Sascha Ißbrücker
2026-01-01 13:40:57 +01:00
parent 38d450a916
commit ffc1a69085
69 changed files with 1847 additions and 1605 deletions

View File

@@ -16,6 +16,7 @@ test:
format: format:
uv run black bookmarks uv run black bookmarks
uv run djlint bookmarks/templates --reformat --quiet
npx prettier bookmarks/frontend --write npx prettier bookmarks/frontend --write
npx prettier bookmarks/styles --write npx prettier bookmarks/styles --write

View File

@@ -98,7 +98,7 @@ make test
### Formatting ### Formatting
Format Python code with black, and JavaScript code with prettier: Format Python code with black, Django templates with djlint, and JavaScript code with prettier:
``` ```
make format make format
``` ```

View File

@@ -1,5 +1,4 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% block content %} {% block content %}
<table style="width: 100%"> <table style="width: 100%">
<thead> <thead>

View File

@@ -1,12 +1,9 @@
{% extends "bookmarks/layout.html" %} {% extends "shared/layout.html" %}
{% load static %} {% load static %}
{% load shared %} {% load shared %}
{% load bookmarks %} {% load bookmarks %}
{% block content %} {% block content %}
<ld-bookmark-page <ld-bookmark-page class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #} {# Bookmark list #}
<main class="main col-2" aria-labelledby="main-heading"> <main class="main col-2" aria-labelledby="main-heading">
<div class="section-header mb-0"> <div class="section-header mb-0">
@@ -19,19 +16,15 @@
</ld-filter-drawer-trigger> </ld-filter-drawer-trigger>
</div> </div>
</div> </div>
<form class="bookmark-actions" <form class="bookmark-actions"
action="{{ bookmark_list.action_url|safe }}" action="{{ bookmark_list.action_url|safe }}"
method="post" autocomplete="off"> method="post"
autocomplete="off">
{% csrf_token %} {% csrf_token %}
{% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_archive' %} {% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_archive' %}
<div id="bookmark-list-container">{% include 'bookmarks/bookmark_list.html' %}</div>
<div id="bookmark-list-container">
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form> </form>
</main> </main>
{# Filters #} {# Filters #}
<div class="side-panel col-1 hide-md"> <div class="side-panel col-1 hide-md">
{% include 'bookmarks/bundle_section.html' %} {% include 'bookmarks/bundle_section.html' %}
@@ -39,12 +32,6 @@
</div> </div>
</ld-bookmark-page> </ld-bookmark-page>
{% endblock %} {% endblock %}
{% block overlays %} {% block overlays %}
{# Bookmark details #}
<turbo-frame id="details-modal" target="_top">
{% if details %}
{% include 'bookmarks/details/modal.html' %} {% include 'bookmarks/details/modal.html' %}
{% endif %}
</turbo-frame>
{% endblock %} {% endblock %}

View File

@@ -1,17 +1,18 @@
{% load static %} {% load static %}
{% load shared %} {% load shared %}
{% load pagination %} {% load pagination %}
{% if bookmark_list.is_empty %} {% if bookmark_list.is_empty %}
{% include 'bookmarks/empty_bookmarks.html' %} {% include 'bookmarks/empty_bookmarks.html' %}
{% else %} {% else %}
<section aria-label="Bookmark list"> <section aria-label="Bookmark list">
<ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}" <ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}"
role="list" tabindex="-1" role="list"
style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }};" tabindex="-1"
style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }}"
data-bookmarks-total="{{ bookmark_list.bookmarks_total }}"> data-bookmarks-total="{{ bookmark_list.bookmarks_total }}">
{% for bookmark_item in bookmark_list.items %} {% for bookmark_item in bookmark_list.items %}
<li data-bookmark-id="{{ bookmark_item.id }}" role="listitem" <li data-bookmark-id="{{ bookmark_item.id }}"
role="listitem"
{% if bookmark_item.css_classes %}class="{{ bookmark_item.css_classes }}"{% endif %}> {% if bookmark_item.css_classes %}class="{{ bookmark_item.css_classes }}"{% endif %}>
<div class="content"> <div class="content">
<div class="title"> <div class="title">
@@ -24,41 +25,35 @@
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %} {% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
<img class="favicon" src="{% static bookmark_item.favicon_file %}" alt=""> <img class="favicon" src="{% static bookmark_item.favicon_file %}" alt="">
{% endif %} {% endif %}
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"> <a href="{{ bookmark_item.url }}"
target="{{ bookmark_list.link_target }}"
rel="noopener">
<span>{{ bookmark_item.title }}</span> <span>{{ bookmark_item.title }}</span>
</a> </a>
</div> </div>
{% if bookmark_list.show_url %} {% if bookmark_list.show_url %}
<div class="url-path truncate"> <div class="url-path truncate">
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener" <a href="{{ bookmark_item.url }}"
class="url-display"> target="{{ bookmark_list.link_target }}"
{{ bookmark_item.url }} rel="noopener"
</a> class="url-display">{{ bookmark_item.url }}</a>
</div> </div>
{% endif %} {% endif %}
{% if bookmark_list.description_display == 'inline' %} {% if bookmark_list.description_display == 'inline' %}
<div class="description inline truncate"> <div class="description inline truncate">
{% if bookmark_item.tags %} {% if bookmark_item.tags %}
<span class="tags"> <span class="tags">
{% for tag in bookmark_item.tags %} {% for tag in bookmark_item.tags %}<a href="?{{ tag.query_string }}">#{{ tag.name }}</a>{% endfor %}
<a href="?{{ tag.query_string }}">#{{ tag.name }}</a>
{% endfor %}
</span> </span>
{% endif %} {% endif %}
{% if bookmark_item.tags and bookmark_item.description %} | {% endif %} {% if bookmark_item.tags and bookmark_item.description %}|{% endif %}
{% if bookmark_item.description %} {% if bookmark_item.description %}<span>{{ bookmark_item.description }}</span>{% endif %}
<span>{{ bookmark_item.description }}</span>
{% endif %}
</div> </div>
{% else %} {% else %}
{% if bookmark_item.description %} {% if bookmark_item.description %}<div class="description separate">{{ bookmark_item.description }}</div>{% endif %}
<div class="description separate">{{ bookmark_item.description }}</div>
{% endif %}
{% if bookmark_item.tags %} {% if bookmark_item.tags %}
<div class="tags"> <div class="tags">
{% for tag in bookmark_item.tags %} {% for tag in bookmark_item.tags %}<a href="?{{ tag.query_string }}">#{{ tag.name }}</a>{% endfor %}
<a href="?{{ tag.query_string }}">#{{ tag.name }}</a>
{% endfor %}
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
@@ -73,21 +68,19 @@
<a href="{{ bookmark_item.snapshot_url }}" <a href="{{ bookmark_item.snapshot_url }}"
title="{{ bookmark_item.snapshot_title }}" title="{{ bookmark_item.snapshot_title }}"
target="{{ bookmark_list.link_target }}" target="{{ bookmark_list.link_target }}"
rel="noopener"> rel="noopener">{{ bookmark_item.display_date }}</a>
{{ bookmark_item.display_date }}
</a>
{% else %} {% else %}
<span>{{ bookmark_item.display_date }}</span> <span>{{ bookmark_item.display_date }}</span>
{% endif %} {% endif %}
{% if not bookmark_list.is_preview %} {% if not bookmark_list.is_preview %}<span>|</span>{% endif %}
<span>|</span>
{% endif %}
{% endif %} {% endif %}
{% if not bookmark_list.is_preview %} {% if not bookmark_list.is_preview %}
{# View link is visible for both owned and shared bookmarks #} {# View link is visible for both owned and shared bookmarks #}
{% if bookmark_list.show_view_action %} {% if bookmark_list.show_view_action %}
<a href="{{ bookmark_item.details_url }}" class="view-action" <a href="{{ bookmark_item.details_url }}"
data-turbo-action="replace" data-turbo-frame="details-modal">View</a> class="view-action"
data-turbo-action="replace"
data-turbo-frame="details-modal">View</a>
{% endif %} {% endif %}
{% if bookmark_item.is_editable %} {% if bookmark_item.is_editable %}
{# Bookmark owner actions #} {# Bookmark owner actions #}
@@ -96,19 +89,23 @@
{% endif %} {% endif %}
{% if bookmark_list.show_archive_action %} {% if bookmark_list.show_archive_action %}
{% if bookmark_item.is_archived %} {% if bookmark_item.is_archived %}
<button type="submit" name="unarchive" value="{{ bookmark_item.id }}" <button type="submit"
class="btn btn-link btn-sm">Unarchive name="unarchive"
</button> value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Unarchive</button>
{% else %} {% else %}
<button type="submit" name="archive" value="{{ bookmark_item.id }}" <button type="submit"
class="btn btn-link btn-sm">Archive name="archive"
</button> value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Archive</button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if bookmark_list.show_remove_action %} {% if bookmark_list.show_remove_action %}
<button data-confirm type="submit" name="remove" value="{{ bookmark_item.id }}" <button data-confirm
class="btn btn-link btn-sm">Remove type="submit"
</button> name="remove"
value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm">Remove</button>
{% endif %} {% endif %}
{% else %} {% else %}
{# Shared bookmark actions #} {# Shared bookmark actions #}
@@ -120,9 +117,12 @@
<div class="extra-actions"> <div class="extra-actions">
<span class="hide-sm">|</span> <span class="hide-sm">|</span>
{% if bookmark_item.show_mark_as_read %} {% if bookmark_item.show_mark_as_read %}
<button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}" <button type="submit"
name="mark_as_read"
value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm btn-icon" class="btn btn-link btn-sm btn-icon"
data-confirm data-confirm-question="Mark as read?"> data-confirm
data-confirm-question="Mark as read?">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-unread"></use> <use xlink:href="#ld-icon-unread"></use>
</svg> </svg>
@@ -130,9 +130,12 @@
</button> </button>
{% endif %} {% endif %}
{% if bookmark_item.show_unshare %} {% if bookmark_item.show_unshare %}
<button type="submit" name="unshare" value="{{ bookmark_item.id }}" <button type="submit"
name="unshare"
value="{{ bookmark_item.id }}"
class="btn btn-link btn-sm btn-icon" class="btn btn-link btn-sm btn-icon"
data-confirm data-confirm-question="Unshare?"> data-confirm
data-confirm-question="Unshare?">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<use xlink:href="#ld-icon-share"></use> <use xlink:href="#ld-icon-share"></use>
</svg> </svg>
@@ -154,19 +157,22 @@
</div> </div>
{% if bookmark_list.show_preview_images %} {% if bookmark_list.show_preview_images %}
{% if bookmark_item.preview_image_file %} {% if bookmark_item.preview_image_file %}
<img class="preview-image" src="{% static bookmark_item.preview_image_file %}" loading="lazy"/> <img class="preview-image"
src="{% static bookmark_item.preview_image_file %}"
loading="lazy" />
{% else %} {% else %}
<div class="preview-image placeholder"> <div class="preview-image placeholder">
<div class="img"/> <div class="img" /></div>
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}"> <div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}">
{% pagination bookmark_list.bookmarks_page %} {% pagination bookmark_list.bookmarks_page %}
</div> </div>
</section> </section>
{% endif %} {% endif %}
<script>
document.dispatchEvent(new CustomEvent('bookmark-list-updated'));
</script>

View File

@@ -1,18 +1,14 @@
{% load shared %} {% load shared %}
{% htmlmin %} {% htmlmin %}
<div class="bulk-edit-bar"> <div class="bulk-edit-bar">
<div class="bulk-edit-actions"> <div class="bulk-edit-actions">
<label class="form-checkbox bulk-edit-checkbox all"> <label class="form-checkbox bulk-edit-checkbox all">
<input type="checkbox"> <input type="checkbox">
<i class="form-icon"></i> <i class="form-icon"></i>
</label> </label>
<select name="bulk_action" class="form-select select-sm"> <select name="bulk_action" class="form-select select-sm">
{% if not 'bulk_archive' in disable_actions %} {% if not 'bulk_archive' in disable_actions %}<option value="bulk_archive">Archive</option>{% endif %}
<option value="bulk_archive">Archive</option> {% if not 'bulk_unarchive' in disable_actions %}<option value="bulk_unarchive">Unarchive</option>{% endif %}
{% endif %}
{% if not 'bulk_unarchive' in disable_actions %}
<option value="bulk_unarchive">Unarchive</option>
{% endif %}
<option value="bulk_delete">Delete</option> <option value="bulk_delete">Delete</option>
<option value="bulk_tag">Add tags</option> <option value="bulk_tag">Add tags</option>
<option value="bulk_untag">Remove tags</option> <option value="bulk_untag">Remove tags</option>
@@ -23,20 +19,23 @@
<option value="bulk_unshare">Unshare</option> <option value="bulk_unshare">Unshare</option>
{% endif %} {% endif %}
<option value="bulk_refresh">Refresh from website</option> <option value="bulk_refresh">Refresh from website</option>
{% if bookmark_list.snapshot_feature_enabled %} {% if bookmark_list.snapshot_feature_enabled %}<option value="bulk_snapshot">Create HTML snapshot</option>{% endif %}
<option value="bulk_snapshot">Create HTML snapshot</option>
{% endif %}
</select> </select>
<ld-tag-autocomplete input-name="bulk_tag_string" input-placeholder="Tag names..." variant="small"></ld-tag-autocomplete> <ld-tag-autocomplete input-name="bulk_tag_string"
<button data-confirm type="submit" name="bulk_execute" class="btn btn-link btn-sm"> input-placeholder="Tag names..."
variant="small">
</ld-tag-autocomplete>
<button data-confirm
type="submit"
name="bulk_execute"
class="btn btn-link btn-sm">
<span>Execute</span> <span>Execute</span>
</button> </button>
<label class="form-checkbox select-across d-none"> <label class="form-checkbox select-across d-none">
<input type="checkbox" name="bulk_select_across"> <input type="checkbox" name="bulk_select_across">
<i class="form-icon"></i> <i class="form-icon"></i>
All pages (<span class="total">{{ bookmark_list.bookmarks_total }}</span> bookmarks) All pages (<span class="total">{{ bookmark_list.bookmarks_total }}</span> bookmarks)
</label> </label>
</div> </div>
</div> </div>
{% endhtmlmin %} {% endhtmlmin %}

View File

@@ -1,9 +1,16 @@
<button class="btn hide-sm ml-2 bulk-edit-active-toggle" title="Bulk edit"> <button class="btn hide-sm ml-2 bulk-edit-active-toggle" title="Bulk edit">
<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> width="20px"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> height="20px"
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"/> viewBox="0 0 24 24"
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"/> fill="none"
<path d="M16 5l3 3"/> stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
<path d="M16 5l3 3" />
</svg> </svg>
</button> </button>

View File

@@ -4,12 +4,19 @@
<h2 id="bundles-heading">Bundles</h2> <h2 id="bundles-heading">Bundles</h2>
<ld-dropdown class="dropdown dropdown-right ml-auto"> <ld-dropdown class="dropdown dropdown-right ml-auto">
<button class="btn btn-noborder dropdown-toggle" aria-label="Bundles menu"> <button class="btn btn-noborder dropdown-toggle" aria-label="Bundles menu">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> width="20"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> height="20"
<path d="M4 6l16 0"/> viewBox="0 0 24 24"
<path d="M4 12l16 0"/> fill="none"
<path d="M4 18l16 0"/> stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 6l16 0" />
<path d="M4 12l16 0" />
<path d="M4 18l16 0" />
</svg> </svg>
</button> </button>
<ul class="menu" role="list" tabindex="-1"> <ul class="menu" role="list" tabindex="-1">
@@ -18,7 +25,8 @@
</li> </li>
{% if bookmark_list.search.q %} {% if bookmark_list.search.q %}
<li class="menu-item"> <li class="menu-item">
<a href="{% url 'linkding:bundles.new' %}?q={{ bookmark_list.search.q|urlencode }}" class="menu-link">Create <a href="{% url 'linkding:bundles.new' %}?q={{ bookmark_list.search.q|urlencode }}"
class="menu-link">Create
bundle from search</a> bundle from search</a>
</li> </li>
{% endif %} {% endif %}

View File

@@ -1,9 +1,7 @@
{% extends "bookmarks/layout.html" %} {% extends "shared/layout.html" %}
{% block content %} {% block content %}
<script type="application/javascript"> <script type="application/javascript">
window.close() window.close()
</script> </script>
<p>You can now close this window.</p> <p>You can now close this window.</p>
{% endblock %} {% endblock %}

View File

@@ -1,42 +1,70 @@
{% if asset.content_type == 'text/html' %} {% if asset.content_type == 'text/html' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> width="24"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> height="24"
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> viewBox="0 0 24 24"
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/> fill="none"
<path d="M2 21v-6"/> stroke="currentColor"
<path d="M5 15v6"/> stroke-width="2"
<path d="M2 18h3"/> stroke-linecap="round"
<path d="M20 15v6h2"/> stroke-linejoin="round">
<path d="M13 21v-6l2 3l2 -3v6"/> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7.5 15h3"/> <path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M9 15v6"/> <path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" />
<path d="M2 21v-6" />
<path d="M5 15v6" />
<path d="M2 18h3" />
<path d="M20 15v6h2" />
<path d="M13 21v-6l2 3l2 -3v6" />
<path d="M7.5 15h3" />
<path d="M9 15v6" />
</svg> </svg>
{% elif asset.content_type == 'application/pdf' %} {% elif asset.content_type == 'application/pdf' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> width="24"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> height="24"
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> viewBox="0 0 24 24"
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/> fill="none"
<path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"/> stroke="currentColor"
<path d="M17 18h2"/> stroke-width="2"
<path d="M20 15h-3v6"/> stroke-linecap="round"
<path d="M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z"/> stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" />
<path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6" />
<path d="M17 18h2" />
<path d="M20 15h-3v6" />
<path d="M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z" />
</svg> </svg>
{% elif asset.content_type == 'image/png' or asset.content_type == 'image/jpeg' or asset.content_type == 'image.gif' %} {% elif asset.content_type == 'image/png' or asset.content_type == 'image/jpeg' or asset.content_type == 'image.gif' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> width="24"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> height="24"
<path d="M15 8h.01"/> viewBox="0 0 24 24"
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z"/> fill="none"
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5"/> stroke="currentColor"
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"/> stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 8h.01" />
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z" />
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5" />
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3" />
</svg> </svg>
{% else %} {% else %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg xmlns="http://www.w3.org/2000/svg"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> width="24"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> height="24"
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> viewBox="0 0 24 24"
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/> fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" />
</svg> </svg>
{% endif %} {% endif %}

View File

@@ -3,47 +3,53 @@
<div class="item-list assets"> <div class="item-list assets">
{% for asset in details.assets %} {% for asset in details.assets %}
<div class="list-item" data-asset-id="{{ asset.id }}"> <div class="list-item" data-asset-id="{{ asset.id }}">
<div class="list-item-icon {{ asset.icon_classes }}"> <div class="list-item-icon {{ asset.icon_classes }}">{% include 'bookmarks/details/asset_icon.html' %}</div>
{% include 'bookmarks/details/asset_icon.html' %}
</div>
<div class="list-item-text {{ asset.text_classes }}"> <div class="list-item-text {{ asset.text_classes }}">
<span class="truncate"> <span class="truncate">
{{ asset.display_name }} {{ asset.display_name }}
{% if asset.status == 'pending' %}(queued){% endif %} {% if asset.status == 'pending' %}(queued){% endif %}
{% if asset.status == 'failure' %}(failed){% endif %} {% if asset.status == 'failure' %}(failed){% endif %}
</span> </span>
{% if asset.file_size %} {% if asset.file_size %}<span class="filesize">{{ asset.file_size|filesizeformat }}</span>{% endif %}
<span class="filesize">{{ asset.file_size|filesizeformat }}</span>
{% endif %}
</div> </div>
<div class="list-item-actions"> <div class="list-item-actions">
{% if asset.file %} {% if asset.file %}
<a class="btn btn-link" href="{% url 'linkding:assets.view' asset.id %}" target="_blank">View</a> <a class="btn btn-link"
href="{% url 'linkding:assets.view' asset.id %}"
target="_blank">View</a>
{% endif %} {% endif %}
{% if details.is_editable %} {% if details.is_editable %}
<button data-confirm type="submit" name="remove_asset" value="{{ asset.id }}" class="btn btn-link"> <button data-confirm
Remove type="submit"
</button> name="remove_asset"
value="{{ asset.id }}"
class="btn btn-link">Remove</button>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if details.is_editable %} {% if details.is_editable %}
<div class="assets-actions"> <div class="assets-actions">
{% if details.snapshots_enabled %} {% if details.snapshots_enabled %}
<button type="submit" name="create_html_snapshot" value="{{ details.bookmark.id }}" class="btn btn-sm" <button type="submit"
{% if details.has_pending_assets %}disabled{% endif %}>Create HTML snapshot name="create_html_snapshot"
</button> value="{{ details.bookmark.id }}"
class="btn btn-sm"
{% if details.has_pending_assets %}disabled{% endif %}>Create HTML snapshot</button>
{% endif %} {% endif %}
{% if details.uploads_enabled %} {% if details.uploads_enabled %}
<ld-upload-button> <ld-upload-button>
<button id="upload-asset" name="upload_asset" value="{{ details.bookmark.id }}" type="submit" <button id="upload-asset"
class="btn btn-sm">Upload file name="upload_asset"
</button> value="{{ details.bookmark.id }}"
<input id="upload-asset-file" name="upload_asset_file" type="file" class="d-hide"> type="submit"
class="btn btn-sm">Upload file</button>
<input id="upload-asset-file"
name="upload_asset_file"
type="file"
class="d-hide">
</ld-upload-button> </ld-upload-button>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,21 +1,26 @@
{% load static %} {% load static %}
{% load shared %} {% load shared %}
<ld-form> <ld-form>
<form action="{{ details.action_url }}" method="post" enctype="multipart/form-data"> <form action="{{ details.action_url }}"
method="post"
enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="update_state" value="{{ details.bookmark.id }}"> <input type="hidden" name="update_state" value="{{ details.bookmark.id }}">
<div class="weblinks"> <div class="weblinks">
<a class="weblink" href="{{ details.bookmark.url }}" rel="noopener" <a class="weblink"
href="{{ details.bookmark.url }}"
rel="noopener"
target="{{ details.profile.bookmark_link_target }}"> target="{{ details.profile.bookmark_link_target }}">
{% if details.show_link_icons %} {% if details.show_link_icons %}
<img class="favicon" src="{% static details.bookmark.favicon_file %}" alt=""> <img class="favicon"
src="{% static details.bookmark.favicon_file %}"
alt="">
{% endif %} {% endif %}
<span>{{ details.bookmark.url }}</span> <span>{{ details.bookmark.url }}</span>
</a> </a>
{% if details.latest_snapshot %} {% if details.latest_snapshot %}
<a class="weblink" href="{% url 'linkding:assets.read' details.latest_snapshot.id %}" <a class="weblink"
href="{% url 'linkding:assets.read' details.latest_snapshot.id %}"
target="{{ details.profile.bookmark_link_target }}"> target="{{ details.profile.bookmark_link_target }}">
{% if details.show_link_icons %} {% if details.show_link_icons %}
<svg class="favicon" xmlns="http://www.w3.org/2000/svg"> <svg class="favicon" xmlns="http://www.w3.org/2000/svg">
@@ -26,13 +31,14 @@
</a> </a>
{% endif %} {% endif %}
{% if details.web_archive_snapshot_url %} {% if details.web_archive_snapshot_url %}
<a class="weblink" href="{{ details.web_archive_snapshot_url }}" <a class="weblink"
href="{{ details.web_archive_snapshot_url }}"
target="{{ details.profile.bookmark_link_target }}"> target="{{ details.profile.bookmark_link_target }}">
{% if details.show_link_icons %} {% if details.show_link_icons %}
<svg class="favicon" viewBox="0 0 76 86" xmlns="http://www.w3.org/2000/svg"> <svg class="favicon"
<path viewBox="0 0 76 86"
d="m76 82v4h-76l.00080851-4zm-3-6v5h-70v-5zm-62.6696277-54 .8344146.4217275.4176066 6.7436084.4176065 10.9576581v10.5383496l-.4176065 13.1364492-.0694681 8.8498268-1.1825531.3523804h-4.17367003l-1.25202116-.3523804-.48627608-8.8498268-.41840503-13.0662957v-10.5375432l.41840503-11.028618.38167482-6.7798947.87034634-.3854412zm60.0004653 0 .8353798.4217275.4168913 6.7436084.4168913 10.9576581v10.5383496l-.4168913 13.1364492-.0686832 8.8498268-1.1835879.3523804h-4.1737047l-1.2522712-.3523804-.4879704-8.8498268-.4168913-13.0662957v-10.5375432l.4168913-11.028618.3833483-6.7798947.8697215-.3854412zm-42.000632 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2529447-.3523804-.4863246-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8688361-.3854412zm23 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2521462-.3523804-.4871231-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8696347-.3854412zm21.6697944-9v7h-70v-7zm-35.7200748-13 36.7200748 8.4088317-1.4720205 2.5911683h-70.32799254l-2.19998696-2.10140371z" xmlns="http://www.w3.org/2000/svg">
fill="currentColor" fill-rule="evenodd"/> <path d="m76 82v4h-76l.00080851-4zm-3-6v5h-70v-5zm-62.6696277-54 .8344146.4217275.4176066 6.7436084.4176065 10.9576581v10.5383496l-.4176065 13.1364492-.0694681 8.8498268-1.1825531.3523804h-4.17367003l-1.25202116-.3523804-.48627608-8.8498268-.41840503-13.0662957v-10.5375432l.41840503-11.028618.38167482-6.7798947.87034634-.3854412zm60.0004653 0 .8353798.4217275.4168913 6.7436084.4168913 10.9576581v10.5383496l-.4168913 13.1364492-.0686832 8.8498268-1.1835879.3523804h-4.1737047l-1.2522712-.3523804-.4879704-8.8498268-.4168913-13.0662957v-10.5375432l.4168913-11.028618.3833483-6.7798947.8697215-.3854412zm-42.000632 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2529447-.3523804-.4863246-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8688361-.3854412zm23 0 .8344979.4217275.4176483 6.7436084.4176482 10.9576581v10.5383496l-.4176482 13.1364492-.0686764 8.8498268-1.1834698.3523804h-4.1740866l-1.2521462-.3523804-.4871231-8.8498268-.4168497-13.0662957v-10.5375432l.4168497-11.028618.38331-6.7798947.8696347-.3854412zm21.6697944-9v7h-70v-7zm-35.7200748-13 36.7200748 8.4088317-1.4720205 2.5911683h-70.32799254l-2.19998696-2.10140371z" fill="currentColor" fill-rule="evenodd" />
</svg> </svg>
{% endif %} {% endif %}
<span>Internet Archive</span> <span>Internet Archive</span>
@@ -41,7 +47,7 @@
</div> </div>
{% if details.preview_image_enabled and details.bookmark.preview_image_file %} {% if details.preview_image_enabled and details.bookmark.preview_image_file %}
<div class="preview-image"> <div class="preview-image">
<img src="{% static details.bookmark.preview_image_file %}" alt=""/> <img src="{% static details.bookmark.preview_image_file %}" alt="" />
</div> </div>
{% endif %} {% endif %}
<div class="sections grid columns-2 columns-sm-1 gap-0"> <div class="sections grid columns-2 columns-sm-1 gap-0">
@@ -51,14 +57,18 @@
<div class="d-flex" style="gap: .8rem"> <div class="d-flex" style="gap: .8rem">
<div class="form-group"> <div class="form-group">
<label class="form-switch"> <label class="form-switch">
<input data-submit-on-change type="checkbox" name="is_archived" <input data-submit-on-change
type="checkbox"
name="is_archived"
{% if details.bookmark.is_archived %}checked{% endif %}> {% if details.bookmark.is_archived %}checked{% endif %}>
<i class="form-icon"></i> Archived <i class="form-icon"></i> Archived
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-switch"> <label class="form-switch">
<input data-submit-on-change type="checkbox" name="unread" <input data-submit-on-change
type="checkbox"
name="unread"
{% if details.bookmark.unread %}checked{% endif %}> {% if details.bookmark.unread %}checked{% endif %}>
<i class="form-icon"></i> Unread <i class="form-icon"></i> Unread
</label> </label>
@@ -66,7 +76,9 @@
{% if details.profile.enable_sharing %} {% if details.profile.enable_sharing %}
<div class="form-group"> <div class="form-group">
<label class="form-switch"> <label class="form-switch">
<input data-submit-on-change type="checkbox" name="shared" <input data-submit-on-change
type="checkbox"
name="shared"
{% if details.bookmark.shared %}checked{% endif %}> {% if details.bookmark.shared %}checked{% endif %}>
<i class="form-icon"></i> Shared <i class="form-icon"></i> Shared
</label> </label>
@@ -77,9 +89,7 @@
{% endif %} {% endif %}
<section class="files col-2"> <section class="files col-2">
<h3>Files</h3> <h3>Files</h3>
<div> <div>{% include 'bookmarks/details/assets.html' %}</div>
{% include 'bookmarks/details/assets.html' %}
</div>
</section> </section>
{% if details.bookmark.tag_names %} {% if details.bookmark.tag_names %}
<section class="tags col-1"> <section class="tags col-1">

View File

@@ -1,12 +1,13 @@
<ld-details-modal class="modal active bookmark-details" <turbo-frame id="details-modal" target="_top">
data-bookmark-id="{{ details.bookmark.id }}" data-close-url="{{ details.close_url }}" {% if details %}
<ld-details-modal class="modal active bookmark-details"
data-bookmark-id="{{ details.bookmark.id }}"
data-close-url="{{ details.close_url }}"
data-turbo-frame="details-modal"> data-turbo-frame="details-modal">
<div class="modal-overlay" data-close-modal></div> <div class="modal-overlay" data-close-modal></div>
<div class="modal-container" role="dialog" aria-modal="true"> <div class="modal-container" role="dialog" aria-modal="true">
{% include 'shared/modal_header.html' with title=details.bookmark.resolved_title %} {% include 'shared/modal_header.html' with title=details.bookmark.resolved_title %}
<div class="modal-body"> <div class="modal-body">{% include 'bookmarks/details/form.html' %}</div>
{% include 'bookmarks/details/form.html' %}
</div>
{% if details.is_editable %} {% if details.is_editable %}
<div class="modal-footer"> <div class="modal-footer">
<div class="actions"> <div class="actions">
@@ -15,17 +16,22 @@
href="{% url 'linkding:bookmarks.edit' details.bookmark.id %}?return_url={{ details.edit_return_url|urlencode }}">Edit</a> href="{% url 'linkding:bookmarks.edit' details.bookmark.id %}?return_url={{ details.edit_return_url|urlencode }}">Edit</a>
</div> </div>
<div class="right-actions"> <div class="right-actions">
<form action="{{ details.delete_url }}" method="post" data-turbo-action="replace"> <form action="{{ details.delete_url }}"
method="post"
data-turbo-action="replace">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="disable_turbo" value="true"> <input type="hidden" name="disable_turbo" value="true">
<button data-confirm class="btn btn-error btn-wide" <button data-confirm
type="submit" name="remove" value="{{ details.bookmark.id }}"> class="btn btn-error btn-wide"
Delete type="submit"
</button> name="remove"
value="{{ details.bookmark.id }}">Delete</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</ld-details-modal> </ld-details-modal>
{% endif %}
</turbo-frame>

View File

@@ -1,11 +1,7 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% block head %} {% block head %}
{% with page_title="Edit bookmark - Linkding" %} {% with page_title="Edit bookmark - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="bookmarks-form-page"> <div class="bookmarks-form-page">
<main aria-labelledby="main-heading"> <main aria-labelledby="main-heading">
@@ -13,7 +9,8 @@
<h1 id="main-heading">Edit bookmark</h1> <h1 id="main-heading">Edit bookmark</h1>
</div> </div>
<ld-form data-submit-on-ctrl-enter> <ld-form data-submit-on-ctrl-enter>
<form action="{% url 'linkding:bookmarks.edit' bookmark_id %}?return_url={{ return_url|urlencode }}" method="post" <form action="{% url 'linkding:bookmarks.edit' bookmark_id %}?return_url={{ return_url|urlencode }}"
method="post"
novalidate> novalidate>
{% include 'bookmarks/form.html' %} {% include 'bookmarks/form.html' %}
</form> </form>

View File

@@ -10,8 +10,7 @@
<p class="empty-subtitle"> <p class="empty-subtitle">
You can get started by <a href="{% url 'linkding:bookmarks.new' %}">adding</a> bookmarks, You can get started by <a href="{% url 'linkding:bookmarks.new' %}">adding</a> bookmarks,
<a href="{% url 'linkding:settings.general' %}">importing</a> your existing bookmarks or configuring the <a href="{% url 'linkding:settings.general' %}">importing</a> your existing bookmarks or configuring the
<a href="{% url 'linkding:settings.integrations' %}">browser extension</a> or the <a <a href="{% url 'linkding:settings.integrations' %}">browser extension</a> or the <a href="{% url 'linkding:settings.integrations' %}">bookmarklet</a>.
href="{% url 'linkding:settings.integrations' %}">bookmarklet</a>.
</p> </p>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,7 +1,6 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %} {% load static %}
{% load shared %} {% load shared %}
<div class="bookmarks-form"> <div class="bookmarks-form">
{% csrf_token %} {% csrf_token %}
{{ form.auto_close|attr:"type:hidden" }} {{ form.auto_close|attr:"type:hidden" }}
@@ -11,11 +10,7 @@
{{ form.url|form_field:"validation"|add_class:"form-input"|attr:"autofocus" }} {{ form.url|form_field:"validation"|add_class:"form-input"|attr:"autofocus" }}
<i class="form-icon loading"></i> <i class="form-icon loading"></i>
</div> </div>
{% if form.url.errors %} {% if form.url.errors %}<div class="form-input-hint">{{ form.url.errors }}</div>{% endif %}
<div class="form-input-hint">
{{ form.url.errors }}
</div>
{% endif %}
<div class="form-input-hint bookmark-exists"> <div class="form-input-hint bookmark-exists">
This URL is already bookmarked. This URL is already bookmarked.
The form has been pre-filled with the existing bookmark, and saving the form will update the existing bookmark. The form has been pre-filled with the existing bookmark, and saving the form will update the existing bookmark.
@@ -23,7 +18,9 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.tag_string.id_for_label }}" class="form-label">Tags</label> <label for="{{ form.tag_string.id_for_label }}" class="form-label">Tags</label>
<ld-tag-autocomplete input-id="{{ form.tag_string.auto_id }}" input-name="{{ form.tag_string.html_name }}" input-value="{{ form.tag_string.value|default_if_none:'' }}" <ld-tag-autocomplete input-id="{{ form.tag_string.auto_id }}"
input-name="{{ form.tag_string.html_name }}"
input-value="{{ form.tag_string.value|default_if_none:'' }}"
input-aria-describedby="{{ form.tag_string.auto_id }}_help"> input-aria-describedby="{{ form.tag_string.auto_id }}_help">
</ld-tag-autocomplete> </ld-tag-autocomplete>
<div id="{{ form.tag_string.auto_id }}_help" class="form-input-hint"> <div id="{{ form.tag_string.auto_id }}_help" class="form-input-hint">
@@ -39,9 +36,7 @@
<div class="flex"> <div class="flex">
<button id="refresh-button" class="btn btn-link suffix-button" type="button">Refresh from website</button> <button id="refresh-button" class="btn btn-link suffix-button" type="button">Refresh from website</button>
<ld-clear-button data-for="{{ form.title.id_for_label }}"> <ld-clear-button data-for="{{ form.title.id_for_label }}">
<button class="ml-2 btn btn-link suffix-button" type="button"> <button class="ml-2 btn btn-link suffix-button" type="button">Clear</button>
Clear
</button>
</ld-clear-button> </ld-clear-button>
</div> </div>
</div> </div>
@@ -52,9 +47,7 @@
<div class="d-flex justify-between align-baseline"> <div class="d-flex justify-between align-baseline">
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label> <label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
<ld-clear-button data-for="{{ form.description.id_for_label }}"> <ld-clear-button data-for="{{ form.description.id_for_label }}">
<button class="btn btn-link suffix-button" type="button"> <button class="btn btn-link suffix-button" type="button">Clear</button>
Clear
</button>
</ld-clear-button> </ld-clear-button>
</div> </div>
{{ form.description|add_class:"form-input"|attr:"rows:3" }} {{ form.description|add_class:"form-input"|attr:"rows:3" }}
@@ -67,9 +60,7 @@
</summary> </summary>
<label for="{{ form.notes.id_for_label }}" class="text-assistive">Notes</label> <label for="{{ form.notes.id_for_label }}" class="text-assistive">Notes</label>
{{ form.notes|form_field:"help"|add_class:"form-input"|attr:"rows:8" }} {{ form.notes|form_field:"help"|add_class:"form-input"|attr:"rows:8" }}
<div id="{{ form.notes.auto_id }}_help" class="form-input-hint"> <div id="{{ form.notes.auto_id }}_help" class="form-input-hint">Additional notes, supports Markdown.</div>
Additional notes, supports Markdown.
</div>
</details> </details>
{{ form.notes.errors }} {{ form.notes.errors }}
</div> </div>
@@ -104,7 +95,9 @@
{% if form.is_auto_close %} {% if form.is_auto_close %}
<input type="submit" value="Save and close" class="btn btn-primary btn-wide"> <input type="submit" value="Save and close" class="btn btn-primary btn-wide">
{% else %} {% else %}
<input type="submit" value="Save" class="btn btn-primary btn btn-primary btn-wide"> <input type="submit"
value="Save"
class="btn btn-primary btn btn-primary btn-wide">
{% endif %} {% endif %}
<a href="{{ return_url }}" class="btn">Cancel</a> <a href="{{ return_url }}" class="btn">Cancel</a>
</div> </div>
@@ -123,7 +116,7 @@
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}'); const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
const refreshButton = document.getElementById('refresh-button'); const refreshButton = document.getElementById('refresh-button');
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists'); const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
const editedBookmarkId = {{ form.instance.id|default:0 }}; const editedBookmarkId = parseInt('{{ form.instance.id|default:0 }}');
let isTitleModified = !!titleInput.value; let isTitleModified = !!titleInput.value;
let isDescriptionModified = !!descriptionInput.value; let isDescriptionModified = !!descriptionInput.value;

View File

@@ -1,44 +0,0 @@
{% load static %}
<head>
<meta charset="UTF-8">
<link rel="icon" href="{% static 'favicon.ico' %}" sizes="48x48">
<link rel="icon" href="{% static 'favicon.svg' %}" sizes="any" type="image/svg+xml">
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'apple-touch-icon.png' %}">
<link rel="mask-icon" href="{% static 'safari-pinned-tab.svg' %}" color="#5856e0">
<link rel="manifest" href="{% url 'linkding:manifest' %}">
<link rel="search" type="application/opensearchdescription+xml" title="Linkding" href="{% url 'linkding:opensearch' %}"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="description" content="Self-hosted bookmark service">
<meta name="robots" content="index,follow">
<meta name="author" content="Sascha Ißbrücker">
<title>{{ page_title|default:'Linkding' }}</title>
{# Include specific theme variant based on user profile setting #}
{% if request.user_profile.theme == 'light' %}
<link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
<meta name="theme-color" content="#5856e0">
{% elif request.user_profile.theme == 'dark' %}
<link href="{% static 'theme-dark.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
<meta name="theme-color" content="#161822">
{% else %}
{# Use auto theme as fallback #}
<link href="{% static 'theme-dark.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"
media="(prefers-color-scheme: dark)"/>
<link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"
media="(prefers-color-scheme: light)"/>
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#161822">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0">
{% endif %}
{% if request.user_profile.custom_css %}
<link href="{% url 'linkding:custom_css' %}?hash={{ request.user_profile.custom_css_hash }}" rel="stylesheet" type="text/css"/>
{% endif %}
<meta name="turbo-cache-control" content="no-preview">
{% if not request.global_settings.enable_link_prefetch %}
<meta name="turbo-prefetch" content="false">
{% endif %}
{% if rss_feed_url %}
<link rel="alternate" type="application/rss+xml" href="{{ rss_feed_url }}" />
{% endif %}
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
</head>

View File

@@ -1,14 +1,10 @@
{% extends "bookmarks/layout.html" %} {% extends "shared/layout.html" %}
{% load static %} {% load static %}
{% load shared %} {% load shared %}
{% load bookmarks %} {% load bookmarks %}
{% block title %}Bookmarks - Linkding{% endblock %} {% block title %}Bookmarks - Linkding{% endblock %}
{% block content %} {% block content %}
<ld-bookmark-page <ld-bookmark-page class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #} {# Bookmark list #}
<main class="main col-2" aria-labelledby="main-heading"> <main class="main col-2" aria-labelledby="main-heading">
<div class="section-header mb-0"> <div class="section-header mb-0">
@@ -21,19 +17,15 @@
</ld-filter-drawer-trigger> </ld-filter-drawer-trigger>
</div> </div>
</div> </div>
<form class="bookmark-actions" <form class="bookmark-actions"
action="{{ bookmark_list.action_url|safe }}" action="{{ bookmark_list.action_url|safe }}"
method="post" autocomplete="off"> method="post"
autocomplete="off">
{% csrf_token %} {% csrf_token %}
{% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_unarchive' %} {% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_unarchive' %}
<div id="bookmark-list-container">{% include 'bookmarks/bookmark_list.html' %}</div>
<div id="bookmark-list-container">
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form> </form>
</main> </main>
{# Filters #} {# Filters #}
<div class="side-panel col-1 hide-md"> <div class="side-panel col-1 hide-md">
{% include 'bookmarks/bundle_section.html' %} {% include 'bookmarks/bundle_section.html' %}
@@ -41,12 +33,6 @@
</div> </div>
</ld-bookmark-page> </ld-bookmark-page>
{% endblock %} {% endblock %}
{% block overlays %} {% block overlays %}
{# Bookmark details #}
<turbo-frame id="details-modal" target="_top">
{% if details %}
{% include 'bookmarks/details/modal.html' %} {% include 'bookmarks/details/modal.html' %}
{% endif %}
</turbo-frame>
{% endblock %} {% endblock %}

View File

@@ -1,83 +0,0 @@
{% load static %}
<!DOCTYPE html>
{# Use data attributes as storage for access in static scripts #}
<html lang="en" data-api-base-url="{% url 'linkding:api-root' %}">
{% block head %}{% include 'bookmarks/head.html' %}{% endblock %}
<body>
<div class="d-none">
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="ld-icon-unread" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0"></path>
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0"></path>
<path d="M3 6l0 13"></path>
<path d="M12 6l0 13"></path>
<path d="M21 6l0 13"></path>
</symbol>
</svg>
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="ld-icon-share" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M18 6m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M18 18m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M8.7 10.7l6.6 -3.4"></path>
<path d="M8.7 13.3l6.6 3.4"></path>
</symbol>
</svg>
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="ld-icon-note" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path>
<path d="M9 7l6 0"></path>
<path d="M9 11l6 0"></path>
<path d="M9 15l4 0"></path>
</symbol>
</svg>
</div>
<header class="container">
{% if has_toasts %}
<div class="message-list">
<form action="{% url 'linkding:toasts.acknowledge' %}?return_url={{ request.path | urlencode }}" method="post">
{% csrf_token %}
{% for toast in toast_messages %}
<div class="toast d-flex">
{{ toast.message }}
<button type="submit" name="toast" value="{{ toast.id }}" class="btn btn-clear"></button>
</div>
{% endfor %}
</form>
</div>
{% endif %}
<div class="d-flex justify-between">
<a href="{% url 'linkding:root' %}" class="app-link d-flex align-center">
<img class="app-logo" src="{% static 'logo.png' %}" alt="Application logo">
<span class="app-name">LINKDING</span>
</a>
<nav>
{% if request.user.is_authenticated %}
{# Only show nav items menu when logged in #}
{% include 'bookmarks/nav_menu.html' %}
{% else %}
{# Otherwise show login link #}
<a href="{% url 'login' %}" class="btn btn-link">Login</a>
{% endif %}
</nav>
</div>
</header>
<div class="content container">
{% block content %}
{% endblock %}
</div>
<div class="modals">
{% block overlays %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -1,109 +0,0 @@
{% load shared %}
{% htmlmin %}
{# Basic menu list #}
<div class="hide-md">
<a href="{% url 'linkding:bookmarks.new' %}" class="btn btn-primary mr-2">Add bookmark</a>
<ld-dropdown class="dropdown">
<button class="btn btn-link dropdown-toggle" tabindex="0">
Bookmarks
</button>
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}" class="menu-link">Active</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.archived' %}" class="menu-link">Archived</a>
</li>
{% if request.user_profile.enable_sharing %}
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.shared' %}" class="menu-link">Shared</a>
</li>
{% endif %}
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}?unread=yes" class="menu-link">Unread</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}?q=!untagged" class="menu-link">Untagged</a>
</li>
</ul>
</ld-dropdown>
<ld-dropdown class="dropdown">
<button class="btn btn-link dropdown-toggle" tabindex="0">
Settings
</button>
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'linkding:settings.general' %}" class="menu-link">General</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:settings.integrations' %}" class="menu-link">Integrations</a>
</li>
{% if request.user.is_superuser %}
<li class="menu-item">
<a href="{% url 'admin:index' %}" class="menu-link" data-turbo="false">Admin</a>
</li>
{% endif %}
</ul>
</ld-dropdown>
<form class="d-inline" action="{% url 'logout' %}" method="post" data-turbo="false">
{% csrf_token %}
<button type="submit" class="btn btn-link">Logout</button>
</form>
</div>
{# Menu drop-down for smaller devices #}
<div class="show-md">
<a href="{% url 'linkding:bookmarks.new' %}" aria-label="Add bookmark" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
style="width: 24px; height: 24px">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
</a>
<ld-dropdown class="dropdown dropdown-right">
<button class="btn btn-link dropdown-toggle" aria-label="Navigation menu" tabindex="0">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
style="width: 24px; height: 24px">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<!-- menu component -->
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}" class="menu-link">Bookmarks</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.archived' %}" class="menu-link">Archived bookmarks</a>
</li>
{% if request.user_profile.enable_sharing %}
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.shared' %}" class="menu-link">Shared bookmarks</a>
</li>
{% endif %}
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}?unread=yes" class="menu-link">Unread</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}?q=!untagged" class="menu-link">Untagged</a>
</li>
<div class="divider"></div>
<li class="menu-item">
<a href="{% url 'linkding:settings.general' %}" class="menu-link">Settings</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:settings.integrations' %}" class="menu-link">Integrations</a>
</li>
{% if request.user.is_superuser %}
<li class="menu-item">
<a href="{% url 'admin:index' %}" class="menu-link" data-turbo="false">Admin</a>
</li>
{% endif %}
<div class="divider"></div>
<li class="menu-item">
<form class="d-inline" action="{% url 'logout' %}" method="post" data-turbo="false">
{% csrf_token %}
<button type="submit" class="btn btn-link menu-link">Logout</button>
</form>
</li>
</ul>
</ld-dropdown>
</div>
{% endhtmlmin %}

View File

@@ -1,11 +1,7 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% block head %} {% block head %}
{% with page_title="New bookmark - Linkding" %} {% with page_title="New bookmark - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="bookmarks-form-page"> <div class="bookmarks-form-page">
<main aria-labelledby="main-heading"> <main aria-labelledby="main-heading">

View File

@@ -1,34 +1,50 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="reader-mode"> <html lang="en" class="reader-mode">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Reader view</title> <title>Reader view</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui"> <meta name="viewport"
content="width=device-width, initial-scale=1.0, minimal-ui">
{# Include specific theme variant based on user profile setting #} {# Include specific theme variant based on user profile setting #}
{% if request.user_profile.theme == 'light' %} {% if request.user_profile.theme == 'light' %}
<link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/> <link href="{% static 'theme-light.css' %}?v={{ app_version }}"
rel="stylesheet"
type="text/css" />
<meta name="theme-color" content="#5856e0"> <meta name="theme-color" content="#5856e0">
{% elif request.user_profile.theme == 'dark' %} {% elif request.user_profile.theme == 'dark' %}
<link href="{% static 'theme-dark.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/> <link href="{% static 'theme-dark.css' %}?v={{ app_version }}"
rel="stylesheet"
type="text/css" />
<meta name="theme-color" content="#161822"> <meta name="theme-color" content="#161822">
{% else %} {% else %}
{# Use auto theme as fallback #} {# Use auto theme as fallback #}
<link href="{% static 'theme-dark.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css" <link href="{% static 'theme-dark.css' %}?v={{ app_version }}"
media="(prefers-color-scheme: dark)"/> rel="stylesheet"
<link href="{% static 'theme-light.css' %}?v={{ app_version }}" rel="stylesheet" type="text/css" type="text/css"
media="(prefers-color-scheme: light)"/> media="(prefers-color-scheme: dark)" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#161822"> <link href="{% static 'theme-light.css' %}?v={{ app_version }}"
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0"> rel="stylesheet"
type="text/css"
media="(prefers-color-scheme: light)" />
<meta name="theme-color"
media="(prefers-color-scheme: dark)"
content="#161822">
<meta name="theme-color"
media="(prefers-color-scheme: light)"
content="#5856e0">
{% endif %} {% endif %}
{% if request.user_profile.custom_css %} {% if request.user_profile.custom_css %}
<link href="{% url 'linkding:custom_css' %}?hash={{ request.user_profile.custom_css_hash }}" rel="stylesheet" type="text/css"/> <link href="{% url 'linkding:custom_css' %}?hash={{ request.user_profile.custom_css_hash }}"
rel="stylesheet"
type="text/css" />
{% endif %} {% endif %}
</head> </head>
<body> <body>
<template id="content">{{ content|safe }}</template> <template id="content">{{ content|safe }}</template>
<script src="{% static 'vendor/Readability.js' %}" type="application/javascript"></script> <script src="{% static 'vendor/Readability.js' %}"
<script type="application/javascript"> type="application/javascript"></script>
<script type="application/javascript">
function estimateReadingTime(charCount, wordsPerMinute) { function estimateReadingTime(charCount, wordsPerMinute) {
const avgWordLength = 5; const avgWordLength = 5;
const totalWords = charCount / avgWordLength; const totalWords = charCount / avgWordLength;
@@ -64,7 +80,7 @@
container.append(articleByline); container.append(articleByline);
} }
if(article.length) { if (article.length) {
const minTime = estimateReadingTime(article.length, 225); const minTime = estimateReadingTime(article.length, 225);
const maxTime = estimateReadingTime(article.length, 175); const maxTime = estimateReadingTime(article.length, 175);
@@ -85,6 +101,6 @@
content.replaceWith(container); content.replaceWith(container);
} }
makeReadable(); makeReadable();
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,9 +1,7 @@
{% load widget_tweaks %} {% load widget_tweaks %}
<div class="search-container"> <div class="search-container">
<form id="search" action="" method="get" role="search"> <form id="search" action="" method="get" role="search">
<ld-search-autocomplete <ld-search-autocomplete input-name="q"
input-name="q"
input-placeholder="Search for words or #tags" input-placeholder="Search for words or #tags"
input-value="{{ search.q|default_if_none:'' }}" input-value="{{ search.q|default_if_none:'' }}"
target="{{ request.user_profile.bookmark_link_target }}" target="{{ request.user_profile.bookmark_link_target }}"
@@ -13,15 +11,21 @@
unread="{{ search.unread }}"> unread="{{ search.unread }}">
</ld-search-autocomplete> </ld-search-autocomplete>
<input type="submit" value="Search" class="d-none"> <input type="submit" value="Search" class="d-none">
{% for hidden_field in search_form.hidden_fields %} {% for hidden_field in search_form.hidden_fields %}{{ hidden_field }}{% endfor %}
{{ hidden_field }}
{% endfor %}
</form> </form>
<ld-dropdown class="search-options dropdown dropdown-right"> <ld-dropdown class="search-options dropdown dropdown-right">
<button type="button" aria-label="Search preferences" <button type="button"
aria-label="Search preferences"
class="btn dropdown-toggle{% if search.has_modified_preferences %} badge{% endif %}"> class="btn dropdown-toggle{% if search.has_modified_preferences %} badge{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> width="20"
height="20"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path> <path d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
<path d="M6 4v4"></path> <path d="M6 4v4"></path>
@@ -45,7 +49,9 @@
</div> </div>
{% endif %} {% endif %}
{% if 'shared' in preferences_form.editable_fields %} {% if 'shared' in preferences_form.editable_fields %}
<div class="form-group radio-group" role="radiogroup" aria-labelledby="search-shared-label"> <div class="form-group radio-group"
role="radiogroup"
aria-labelledby="search-shared-label">
<label id="search-shared-label" <label id="search-shared-label"
class="form-label{% if 'shared' in search.modified_params %} text-bold{% endif %}"> class="form-label{% if 'shared' in search.modified_params %} text-bold{% endif %}">
Shared filter Shared filter
@@ -60,7 +66,9 @@
</div> </div>
{% endif %} {% endif %}
{% if 'unread' in preferences_form.editable_fields %} {% if 'unread' in preferences_form.editable_fields %}
<div class="form-group radio-group" role="radiogroup" aria-labelledby="search-unread-label"> <div class="form-group radio-group"
role="radiogroup"
aria-labelledby="search-unread-label">
<label id="search-unread-label" <label id="search-unread-label"
class="form-label{% if 'unread' in search.modified_params %} text-bold{% endif %}"> class="form-label{% if 'unread' in search.modified_params %} text-bold{% endif %}">
Unread filter Unread filter
@@ -80,10 +88,7 @@
<button type="submit" class="btn btn-sm" name="save">Save as default</button> <button type="submit" class="btn btn-sm" name="save">Save as default</button>
{% endif %} {% endif %}
</div> </div>
{% for hidden_field in preferences_form.hidden_fields %}{{ hidden_field }}{% endfor %}
{% for hidden_field in preferences_form.hidden_fields %}
{{ hidden_field }}
{% endfor %}
</form> </form>
</div> </div>
</ld-dropdown> </ld-dropdown>

View File

@@ -1,12 +1,10 @@
{% extends "bookmarks/layout.html" %} {% extends "shared/layout.html" %}
{% load static %} {% load static %}
{% load shared %} {% load shared %}
{% load bookmarks %} {% load bookmarks %}
{% block content %} {% block content %}
<ld-bookmark-page no-bulk-edit <ld-bookmark-page no-bulk-edit
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}"> class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
{# Bookmark list #} {# Bookmark list #}
<main class="main col-2" aria-labelledby="main-heading"> <main class="main col-2" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
@@ -18,17 +16,14 @@
</ld-filter-drawer-trigger> </ld-filter-drawer-trigger>
</div> </div>
</div> </div>
<form class="bookmark-actions" <form class="bookmark-actions"
action="{{ bookmark_list.action_url|safe }}" action="{{ bookmark_list.action_url|safe }}"
method="post" autocomplete="off"> method="post"
autocomplete="off">
{% csrf_token %} {% csrf_token %}
<div id="bookmark-list-container"> <div id="bookmark-list-container">{% include 'bookmarks/bookmark_list.html' %}</div>
{% include 'bookmarks/bookmark_list.html' %}
</div>
</form> </form>
</main> </main>
{# Filters #} {# Filters #}
<div class="side-panel col-1 hide-md"> <div class="side-panel col-1 hide-md">
<section aria-labelledby="user-heading"> <section aria-labelledby="user-heading">
@@ -44,12 +39,6 @@
</div> </div>
</ld-bookmark-page> </ld-bookmark-page>
{% endblock %} {% endblock %}
{% block overlays %} {% block overlays %}
{# Bookmark details #}
<turbo-frame id="details-modal" target="_top">
{% if details %}
{% include 'bookmarks/details/modal.html' %} {% include 'bookmarks/details/modal.html' %}
{% endif %}
</turbo-frame>
{% endblock %} {% endblock %}

View File

@@ -1,11 +1,10 @@
{% load shared %} {% load shared %}
{% htmlmin %} {% htmlmin %}
<div class="tag-cloud"> <div class="tag-cloud">
{% if tag_cloud.has_selected_tags %} {% if tag_cloud.has_selected_tags %}
<p class="selected-tags"> <p class="selected-tags">
{% for tag in tag_cloud.selected_tags %} {% for tag in tag_cloud.selected_tags %}
<a href="?{{ tag.query_string }}" <a href="?{{ tag.query_string }}" class="text-bold mr-2">
class="text-bold mr-2">
<span>-{{ tag.name }}</span> <span>-{{ tag.name }}</span>
</a> </a>
{% endfor %} {% endfor %}
@@ -17,15 +16,12 @@
{% for tag in group.tags %} {% for tag in group.tags %}
{# Highlight first char of first tag in group if grouping is enabled #} {# Highlight first char of first tag in group if grouping is enabled #}
{% if group.highlight_first_char and forloop.counter == 1 %} {% if group.highlight_first_char and forloop.counter == 1 %}
<a href="?{{ tag.query_string }}" <a href="?{{ tag.query_string }}" class="mr-2" data-is-tag-item>
class="mr-2" data-is-tag-item> <span class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
<span
class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
</a> </a>
{% else %} {% else %}
{# Render tags normally #} {# Render tags normally #}
<a href="?{{ tag.query_string }}" <a href="?{{ tag.query_string }}" class="mr-2" data-is-tag-item>
class="mr-2" data-is-tag-item>
<span>{{ tag.name }}</span> <span>{{ tag.name }}</span>
</a> </a>
{% endif %} {% endif %}
@@ -33,5 +29,5 @@
</p> </p>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endhtmlmin %} {% endhtmlmin %}

View File

@@ -4,12 +4,19 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<ld-dropdown class="dropdown dropdown-right ml-auto"> <ld-dropdown class="dropdown dropdown-right ml-auto">
<button class="btn btn-noborder dropdown-toggle" aria-label="Tags menu"> <button class="btn btn-noborder dropdown-toggle" aria-label="Tags menu">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> width="20"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> height="20"
<path d="M4 6l16 0"/> viewBox="0 0 24 24"
<path d="M4 12l16 0"/> fill="none"
<path d="M4 18l16 0"/> stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 6l16 0" />
<path d="M4 12l16 0" />
<path d="M4 18l16 0" />
</svg> </svg>
</button> </button>
<ul class="menu" role="list" tabindex="-1"> <ul class="menu" role="list" tabindex="-1">
@@ -20,7 +27,5 @@
</ld-dropdown> </ld-dropdown>
{% endif %} {% endif %}
</div> </div>
<div id="tag-cloud-container"> <div id="tag-cloud-container">{% include 'bookmarks/tag_cloud.html' %}</div>
{% include 'bookmarks/tag_cloud.html' %}
</div>
</section> </section>

View File

@@ -1,21 +0,0 @@
<turbo-stream action="update" target="bookmark-list-container">
<template>
{% include 'bookmarks/bookmark_list.html' %}
<script>
document.dispatchEvent(new CustomEvent('bookmark-list-updated'));
</script>
</template>
</turbo-stream>
<turbo-stream action="update" target="tag-cloud-container">
<template>
{% include 'bookmarks/tag_cloud.html' %}
</template>
</turbo-stream>
<turbo-stream action="update" method="morph" target="details-modal">
<template>
{% if details %}
{% include 'bookmarks/details/modal.html' %}
{% endif %}
</template>
</turbo-stream>

View File

@@ -1,10 +0,0 @@
<html>
{% include 'bookmarks/head.html' %}
<body>
<turbo-frame id="details-modal">
{% if details %}
{% include 'bookmarks/details/modal.html' %}
{% endif %}
</turbo-frame>
</body>
</html>

View File

@@ -1,10 +1,7 @@
{% load widget_tweaks %} {% load widget_tweaks %}
<ld-form data-form-reset> <ld-form data-form-reset>
<form id="user-select" action="" method="get"> <form id="user-select" action="" method="get">
{% for hidden_field in form.hidden_fields %} {% for hidden_field in form.hidden_fields %}{{ hidden_field }}{% endfor %}
{{ hidden_field }}
{% endfor %}
<div class="form-group"> <div class="form-group">
<div class="d-flex"> <div class="d-flex">
{% render_field form.user class+="form-select" data-submit-on-change="" %} {% render_field form.user class+="form-select" data-submit-on-change="" %}

View File

@@ -1,32 +1,27 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block head %} {% block head %}
{% with page_title="Edit bundle - Linkding" %} {% with page_title="Edit bundle - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="bundles-editor-page grid columns-md-1"> <div class="bundles-editor-page grid columns-md-1">
<main aria-labelledby="main-heading"> <main aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
<h1 id="main-heading">Edit bundle</h1> <h1 id="main-heading">Edit bundle</h1>
</div> </div>
{% include 'shared/messages.html' %} {% include 'shared/messages.html' %}
<form id="bundle-form"
<form id="bundle-form" action="{% url 'linkding:bundles.edit' bundle.id %}" method="post" novalidate> action="{% url 'linkding:bundles.edit' bundle.id %}"
method="post"
novalidate>
{% csrf_token %} {% csrf_token %}
{% include 'bundles/form.html' %} {% include 'bundles/form.html' %}
</form> </form>
</main> </main>
<aside class="col-2" aria-labelledby="preview-heading"> <aside class="col-2" aria-labelledby="preview-heading">
<div class="section-header"> <div class="section-header">
<h2 id="preview-heading">Preview</h2> <h2 id="preview-heading">Preview</h2>
</div> </div>
{% include 'bundles/preview.html' %} {% include 'bundles/preview.html' %}
</aside> </aside>
</div> </div>

View File

@@ -1,64 +1,51 @@
{% load widget_tweaks %} {% load widget_tweaks %}
<div class="form-group {% if form.name.errors %}has-error{% endif %}"> <div class="form-group {% if form.name.errors %}has-error{% endif %}">
<label for="{{ form.name.id_for_label }}" class="form-label">Name</label> <label for="{{ form.name.id_for_label }}" class="form-label">Name</label>
{{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }} {{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }}
{% if form.name.errors %} {% if form.name.errors %}<div class="form-input-hint">{{ form.name.errors }}</div>{% endif %}
<div class="form-input-hint">
{{ form.name.errors }}
</div>
{% endif %}
</div> </div>
<div class="form-group {% if form.search.errors %}has-error{% endif %}"> <div class="form-group {% if form.search.errors %}has-error{% endif %}">
<label for="{{ form.search.id_for_label }}" class="form-label">Search terms</label> <label for="{{ form.search.id_for_label }}" class="form-label">Search terms</label>
{{ form.search|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }} {{ form.search|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }}
{% if form.search.errors %} {% if form.search.errors %}<div class="form-input-hint">{{ form.search.errors }}</div>{% endif %}
<div class="form-input-hint"> <div class="form-input-hint">All of these search terms must be present in a bookmark to match.</div>
{{ form.search.errors }}
</div>
{% endif %}
<div class="form-input-hint">
All of these search terms must be present in a bookmark to match.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.any_tags.id_for_label }}" class="form-label">Tags</label> <label for="{{ form.any_tags.id_for_label }}" class="form-label">Tags</label>
<ld-tag-autocomplete input-id="{{ form.any_tags.auto_id }}" input-name="{{ form.any_tags.html_name }}" <ld-tag-autocomplete input-id="{{ form.any_tags.auto_id }}"
input-name="{{ form.any_tags.html_name }}"
input-value="{{ form.any_tags.value|default_if_none:'' }}"> input-value="{{ form.any_tags.value|default_if_none:'' }}">
</ld-tag-autocomplete> </ld-tag-autocomplete>
<div class="form-input-hint"> <div class="form-input-hint">At least one of these tags must be present in a bookmark to match.</div>
At least one of these tags must be present in a bookmark to match.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.all_tags.id_for_label }}" class="form-label">Required tags</label> <label for="{{ form.all_tags.id_for_label }}" class="form-label">Required tags</label>
<ld-tag-autocomplete input-id="{{ form.all_tags.auto_id }}" input-name="{{ form.all_tags.html_name }}" <ld-tag-autocomplete input-id="{{ form.all_tags.auto_id }}"
input-name="{{ form.all_tags.html_name }}"
input-value="{{ form.all_tags.value|default_if_none:'' }}"> input-value="{{ form.all_tags.value|default_if_none:'' }}">
</ld-tag-autocomplete> </ld-tag-autocomplete>
<div class="form-input-hint"> <div class="form-input-hint">All of these tags must be present in a bookmark to match.</div>
All of these tags must be present in a bookmark to match.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.excluded_tags.id_for_label }}" class="form-label">Excluded tags</label> <label for="{{ form.excluded_tags.id_for_label }}" class="form-label">Excluded tags</label>
<ld-tag-autocomplete input-id="{{ form.excluded_tags.auto_id }}" input-name="{{ form.excluded_tags.html_name }}" <ld-tag-autocomplete input-id="{{ form.excluded_tags.auto_id }}"
input-name="{{ form.excluded_tags.html_name }}"
input-value="{{ form.excluded_tags.value|default_if_none:'' }}"> input-value="{{ form.excluded_tags.value|default_if_none:'' }}">
</ld-tag-autocomplete> </ld-tag-autocomplete>
<div class="form-input-hint"> <div class="form-input-hint">None of these tags must be present in a bookmark to match.</div>
None of these tags must be present in a bookmark to match.
</div>
</div> </div>
<div class="form-footer d-flex mt-4"> <div class="form-footer d-flex mt-4">
<input type="submit" name="save" value="Save" class="btn btn-primary btn-wide"> <input type="submit"
<a href="{% url 'linkding:bundles.index' %}" class="btn btn-wide ml-auto">Cancel</a> name="save"
<a href="{% url 'linkding:bundles.preview' %}" data-turbo-frame="preview" class="d-none" id="preview-link"></a> value="Save"
class="btn btn-primary btn-wide">
<a href="{% url 'linkding:bundles.index' %}"
class="btn btn-wide ml-auto">Cancel</a>
<a href="{% url 'linkding:bundles.preview' %}"
data-turbo-frame="preview"
class="d-none"
id="preview-link"></a>
</div> </div>
<script> <script>
(function init() { (function init() {
const bundleForm = document.getElementById('bundle-form'); const bundleForm = document.getElementById('bundle-form');

View File

@@ -1,20 +1,14 @@
{% extends "bookmarks/layout.html" %} {% extends "shared/layout.html" %}
{% block head %} {% block head %}
{% with page_title="Bundles - Linkding" %} {% with page_title="Bundles - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="bundles-page crud-page" aria-labelledby="main-heading"> <main class="bundles-page crud-page" aria-labelledby="main-heading">
<div class="crud-header"> <div class="crud-header">
<h1 id="main-heading">Bundles</h1> <h1 id="main-heading">Bundles</h1>
<a href="{% url 'linkding:bundles.new' %}" class="btn">Add bundle</a> <a href="{% url 'linkding:bundles.new' %}" class="btn">Add bundle</a>
</div> </div>
{% include 'shared/messages.html' %} {% include 'shared/messages.html' %}
{% if bundles %} {% if bundles %}
<form action="{% url 'linkding:bundles.action' %}" method="post"> <form action="{% url 'linkding:bundles.action' %}" method="post">
{% csrf_token %} {% csrf_token %}
@@ -32,30 +26,40 @@
<tr data-bundle-id="{{ bundle.id }}" draggable="true"> <tr data-bundle-id="{{ bundle.id }}" draggable="true">
<td> <td>
<div class="d-flex align-center"> <div class="d-flex align-center">
<svg xmlns="http://www.w3.org/2000/svg" class="text-secondary mr-1" width="16" height="16" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> class="text-secondary mr-1"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> width="16"
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/> height="16"
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/> viewBox="0 0 24 24"
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/> fill="none"
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/> stroke="currentColor"
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/> stroke-width="2"
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/> stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
</svg> </svg>
<span>{{ bundle.name }}</span> <span>{{ bundle.name }}</span>
</div> </div>
</td> </td>
<td class="actions"> <td class="actions">
<a class="btn btn-link" href="{% url 'linkding:bundles.edit' bundle.id %}">Edit</a> <a class="btn btn-link"
<button data-confirm type="submit" name="remove_bundle" value="{{ bundle.id }}" href="{% url 'linkding:bundles.edit' bundle.id %}">Edit</a>
class="btn btn-link">Remove <button data-confirm
</button> type="submit"
name="remove_bundle"
value="{{ bundle.id }}"
class="btn btn-link">Remove</button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<input type="submit" name="move_bundle" value="" class="d-none"> <input type="submit" name="move_bundle" value="" class="d-none">
<input type="hidden" name="move_position" value=""> <input type="hidden" name="move_position" value="">
</form> </form>
@@ -66,7 +70,6 @@
</div> </div>
{% endif %} {% endif %}
</main> </main>
<script> <script>
(function init() { (function init() {
const tableBody = document.querySelector(".crud-table tbody"); const tableBody = document.querySelector(".crud-table tbody");

View File

@@ -1,32 +1,27 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block head %} {% block head %}
{% with page_title="New bundle - Linkding" %} {% with page_title="New bundle - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="bundles-editor-page grid columns-md-1"> <div class="bundles-editor-page grid columns-md-1">
<main aria-labelledby="main-heading"> <main aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
<h1 id="main-heading">New bundle</h1> <h1 id="main-heading">New bundle</h1>
</div> </div>
{% include 'shared/messages.html' %} {% include 'shared/messages.html' %}
<form id="bundle-form"
<form id="bundle-form" action="{% url 'linkding:bundles.new' %}" method="post" novalidate> action="{% url 'linkding:bundles.new' %}"
method="post"
novalidate>
{% csrf_token %} {% csrf_token %}
{% include 'bundles/form.html' %} {% include 'bundles/form.html' %}
</form> </form>
</main> </main>
<aside class="col-2" aria-labelledby="preview-heading"> <aside class="col-2" aria-labelledby="preview-heading">
<div class="section-header"> <div class="section-header">
<h2 id="preview-heading">Preview</h2> <h2 id="preview-heading">Preview</h2>
</div> </div>
{% include 'bundles/preview.html' %} {% include 'bundles/preview.html' %}
</aside> </aside>
</div> </div>

View File

@@ -1,12 +1,8 @@
<turbo-frame id="preview"> <turbo-frame id="preview">
{% if bookmark_list.is_empty %} {% if bookmark_list.is_empty %}
<div> <div>No bookmarks match the current bundle.</div>
No bookmarks match the current bundle. {% else %}
</div> <div class="mb-4">Found {{ bookmark_list.bookmarks_total }} bookmarks matching this bundle.</div>
{% else %}
<div class="mb-4">
Found {{ bookmark_list.bookmarks_total }} bookmarks matching this bundle.
</div>
{% include 'bookmarks/bookmark_list.html' %} {% include 'bookmarks/bookmark_list.html' %}
{% endif %} {% endif %}
</turbo-frame> </turbo-frame>

View File

@@ -1,19 +1,13 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block head %} {% block head %}
{% with page_title="Registration complete - Linkding" %} {% with page_title="Registration complete - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading"> <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
<h1 id="main-heading">Registration complete</h1> <h1 id="main-heading">Registration complete</h1>
</div> </div>
<p class="text-success"> <p class="text-success">You can now use the application.</p>
You can now use the application.
</p>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,18 +1,16 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block head %} {% block head %}
{% with page_title="Registration - Linkding" %} {% with page_title="Registration - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading"> <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
<h1 id="main-heading">Register</h1> <h1 id="main-heading">Register</h1>
</div> </div>
<form method="post" action="{% url 'django_registration_register' %}" novalidate> <form method="post"
action="{% url 'django_registration_register' %}"
novalidate>
{% csrf_token %} {% csrf_token %}
<div class="form-group {% if form.errors.username %}has-error{% endif %}"> <div class="form-group {% if form.errors.username %}has-error{% endif %}">
<label class="form-label" for="{{ form.username.id_for_label }}">Username</label> <label class="form-label" for="{{ form.username.id_for_label }}">Username</label>
@@ -34,7 +32,7 @@
{{ form.password2|add_class:'form-input'|attr:"placeholder: " }} {{ form.password2|add_class:'form-input'|attr:"placeholder: " }}
<div class="form-input-hint">{{ form.errors.password2 }}</div> <div class="form-input-hint">{{ form.errors.password2 }}</div>
</div> </div>
<br/> <br />
<input type="submit" value="Register" class="btn btn-primary btn-wide"> <input type="submit" value="Register" class="btn btn-primary btn-wide">
<input type="hidden" name="next" value="{{ next }}"> <input type="hidden" name="next" value="{{ next }}">
</form> </form>

View File

@@ -1,12 +1,8 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block head %} {% block head %}
{% with page_title="Login - Linkding" %} {% with page_title="Login - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading"> <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
@@ -27,13 +23,14 @@
<label class="form-label" for="{{ form.password.id_for_label }}">Password</label> <label class="form-label" for="{{ form.password.id_for_label }}">Password</label>
{{ form.password|add_class:'form-input'|attr:'placeholder: ' }} {{ form.password|add_class:'form-input'|attr:'placeholder: ' }}
</div> </div>
<br />
<br/>
<div class="d-flex justify-between"> <div class="d-flex justify-between">
<input type="submit" value="Login" class="btn btn-primary btn-wide"/> <input type="submit" value="Login" class="btn btn-primary btn-wide" />
<input type="hidden" name="next" value="{{ next }}"/> <input type="hidden" name="next" value="{{ next }}" />
{% if enable_oidc %} {% if enable_oidc %}
<a class="btn btn-link" href="{% url 'oidc_authentication_init' %}" data-turbo="false">Login with OIDC</a> <a class="btn btn-link"
href="{% url 'oidc_authentication_init' %}"
data-turbo="false">Login with OIDC</a>
{% endif %} {% endif %}
{% if allow_registration %} {% if allow_registration %}
<a href="{% url 'django_registration_register' %}" class="btn btn-link">Register</a> <a href="{% url 'django_registration_register' %}" class="btn btn-link">Register</a>

View File

@@ -1,19 +1,13 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block head %} {% block head %}
{% with page_title="Password changed - Linkding" %} {% with page_title="Password changed - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading"> <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
<h1 id="main-heading">Password Changed</h1> <h1 id="main-heading">Password Changed</h1>
</div> </div>
<p class="text-success"> <p class="text-success">Your password was changed successfully.</p>
Your password was changed successfully.
</p>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,12 +1,8 @@
{% extends 'bookmarks/layout.html' %} {% extends 'shared/layout.html' %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block head %} {% block head %}
{% with page_title="Change password - Linkding" %} {% with page_title="Change password - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading"> <main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
<div class="section-header"> <div class="section-header">
@@ -17,33 +13,22 @@
<div class="form-group {% if form.old_password.errors %}has-error{% endif %}"> <div class="form-group {% if form.old_password.errors %}has-error{% endif %}">
<label class="form-label" for="{{ form.old_password.id_for_label }}">Old password</label> <label class="form-label" for="{{ form.old_password.id_for_label }}">Old password</label>
{{ form.old_password|add_class:'form-input'|attr:"placeholder: " }} {{ form.old_password|add_class:'form-input'|attr:"placeholder: " }}
{% if form.old_password.errors %} {% if form.old_password.errors %}<div class="form-input-hint">{{ form.old_password.errors }}</div>{% endif %}
<div class="form-input-hint">
{{ form.old_password.errors }}
</div>
{% endif %}
</div> </div>
<div class="form-group {% if form.new_password1.errors %}has-error{% endif %}"> <div class="form-group {% if form.new_password1.errors %}has-error{% endif %}">
<label class="form-label" for="{{ form.new_password1.id_for_label }}">New password</label> <label class="form-label" for="{{ form.new_password1.id_for_label }}">New password</label>
{{ form.new_password1|add_class:'form-input'|attr:"placeholder: " }} {{ form.new_password1|add_class:'form-input'|attr:"placeholder: " }}
{% if form.new_password1.errors %} {% if form.new_password1.errors %}<div class="form-input-hint">{{ form.new_password1.errors }}</div>{% endif %}
<div class="form-input-hint">
{{ form.new_password1.errors }}
</div>
{% endif %}
</div> </div>
<div class="form-group {% if form.new_password2.errors %}has-error{% endif %}"> <div class="form-group {% if form.new_password2.errors %}has-error{% endif %}">
<label class="form-label" for="{{ form.new_password2.id_for_label }}">Confirm new password</label> <label class="form-label" for="{{ form.new_password2.id_for_label }}">Confirm new password</label>
{{ form.new_password2|add_class:'form-input'|attr:"placeholder: " }} {{ form.new_password2|add_class:'form-input'|attr:"placeholder: " }}
{% if form.new_password2.errors %} {% if form.new_password2.errors %}<div class="form-input-hint">{{ form.new_password2.errors }}</div>{% endif %}
<div class="form-input-hint">
{{ form.new_password2.errors }}
</div> </div>
{% endif %} <br />
</div> <input type="submit"
value="Change Password"
<br/> class="btn btn-primary btn-wide">
<input type="submit" value="Change Password" class="btn btn-primary btn-wide">
</form> </form>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -1,8 +1,10 @@
<turbo-frame id="api-modal"> <turbo-frame id="api-modal">
<form method="post" action="{% url 'linkding:settings.integrations.create_api_token' %}" <form method="post"
action="{% url 'linkding:settings.integrations.create_api_token' %}"
data-turbo-frame="api-section"> data-turbo-frame="api-section">
{% csrf_token %} {% csrf_token %}
<ld-modal class="modal active" data-close-url="{% url 'linkding:settings.integrations' %}" <ld-modal class="modal active"
data-close-url="{% url 'linkding:settings.integrations' %}"
data-turbo-frame="api-modal"> data-turbo-frame="api-modal">
<div class="modal-overlay" data-close-modal></div> <div class="modal-overlay" data-close-modal></div>
<div class="modal-container" role="dialog" aria-modal="true"> <div class="modal-container" role="dialog" aria-modal="true">
@@ -26,5 +28,5 @@
</div> </div>
</div> </div>
</ld-modal> </ld-modal>
</form> </form>
</turbo-frame> </turbo-frame>

View File

@@ -1,30 +1,23 @@
{% extends "bookmarks/layout.html" %} {% extends "shared/layout.html" %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block head %} {% block head %}
{% with page_title="Settings - Linkding" %} {% with page_title="Settings - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="settings-page" aria-labelledby="main-heading"> <main class="settings-page" aria-labelledby="main-heading">
<h1 id="main-heading">Settings</h1> <h1 id="main-heading">Settings</h1>
{# Profile section #} {# Profile section #}
{% if success_message %} {% if success_message %}<div class="toast toast-success mb-4">{{ success_message }}</div>{% endif %}
<div class="toast toast-success mb-4">{{ success_message }}</div> {% if error_message %}<div class="toast toast-error mb-4">{{ error_message }}</div>{% endif %}
{% endif %}
{% if error_message %}
<div class="toast toast-error mb-4">{{ error_message }}</div>
{% endif %}
<section aria-labelledby="profile-heading"> <section aria-labelledby="profile-heading">
<h2 id="profile-heading">Profile</h2> <h2 id="profile-heading">Profile</h2>
<p> <p>
<a href="{% url 'change_password' %}">Change password</a> <a href="{% url 'change_password' %}">Change password</a>
</p> </p>
<form action="{% url 'linkding:settings.update' %}" method="post" novalidate data-turbo="false"> <form action="{% url 'linkding:settings.update' %}"
method="post"
novalidate
data-turbo="false">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label> <label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
@@ -34,7 +27,8 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.bookmark_date_display.id_for_label }}" class="form-label">Bookmark date format</label> <label for="{{ form.bookmark_date_display.id_for_label }}"
class="form-label">Bookmark date format</label>
{{ form.bookmark_date_display|add_class:"form-select width-25 width-sm-100" }} {{ form.bookmark_date_display|add_class:"form-select width-25 width-sm-100" }}
<div class="form-input-hint"> <div class="form-input-hint">
Whether to show bookmark dates as relative (how long ago), or as absolute dates. Alternatively the date can Whether to show bookmark dates as relative (how long ago), or as absolute dates. Alternatively the date can
@@ -42,30 +36,31 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.bookmark_description_display.id_for_label }}" class="form-label">Bookmark <label for="{{ form.bookmark_description_display.id_for_label }}"
description</label> class="form-label">
Bookmark
description
</label>
{{ form.bookmark_description_display|add_class:"form-select width-25 width-sm-100" }} {{ form.bookmark_description_display|add_class:"form-select width-25 width-sm-100" }}
<div class="form-input-hint"> <div class="form-input-hint">
Whether to show bookmark descriptions and tags in the same line, or as separate blocks. Whether to show bookmark descriptions and tags in the same line, or as separate blocks.
</div> </div>
</div> </div>
<div <div class="form-group {% if request.user_profile.bookmark_description_display == 'inline' %}d-hide{% endif %}">
class="form-group {% if request.user_profile.bookmark_description_display == 'inline' %}d-hide{% endif %}"> <label for="{{ form.bookmark_description_max_lines.id_for_label }}"
<label for="{{ form.bookmark_description_max_lines.id_for_label }}" class="form-label">Bookmark description class="form-label">
max lines</label> Bookmark description
max lines
</label>
{{ form.bookmark_description_max_lines|add_class:"form-input width-25 width-sm-100" }} {{ form.bookmark_description_max_lines|add_class:"form-input width-25 width-sm-100" }}
<div class="form-input-hint"> <div class="form-input-hint">Limits the number of lines that are displayed for the bookmark description.</div>
Limits the number of lines that are displayed for the bookmark description.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.display_url.id_for_label }}" class="form-checkbox"> <label for="{{ form.display_url.id_for_label }}" class="form-checkbox">
{{ form.display_url }} {{ form.display_url }}
<i class="form-icon"></i> Show bookmark URL <i class="form-icon"></i> Show bookmark URL
</label> </label>
<div class="form-input-hint"> <div class="form-input-hint">When enabled, this setting displays the bookmark URL below the title.</div>
When enabled, this setting displays the bookmark URL below the title.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.permanent_notes.id_for_label }}" class="form-checkbox"> <label for="{{ form.permanent_notes.id_for_label }}" class="form-checkbox">
@@ -79,45 +74,41 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Bookmark actions</label> <label class="form-label">Bookmark actions</label>
<label for="{{ form.display_view_bookmark_action.id_for_label }}" class="form-checkbox"> <label for="{{ form.display_view_bookmark_action.id_for_label }}"
class="form-checkbox">
{{ form.display_view_bookmark_action }} {{ form.display_view_bookmark_action }}
<i class="form-icon"></i> View <i class="form-icon"></i> View
</label> </label>
<label for="{{ form.display_edit_bookmark_action.id_for_label }}" class="form-checkbox"> <label for="{{ form.display_edit_bookmark_action.id_for_label }}"
class="form-checkbox">
{{ form.display_edit_bookmark_action }} {{ form.display_edit_bookmark_action }}
<i class="form-icon"></i> Edit <i class="form-icon"></i> Edit
</label> </label>
<label for="{{ form.display_archive_bookmark_action.id_for_label }}" class="form-checkbox"> <label for="{{ form.display_archive_bookmark_action.id_for_label }}"
class="form-checkbox">
{{ form.display_archive_bookmark_action }} {{ form.display_archive_bookmark_action }}
<i class="form-icon"></i> Archive <i class="form-icon"></i> Archive
</label> </label>
<label for="{{ form.display_remove_bookmark_action.id_for_label }}" class="form-checkbox"> <label for="{{ form.display_remove_bookmark_action.id_for_label }}"
class="form-checkbox">
{{ form.display_remove_bookmark_action }} {{ form.display_remove_bookmark_action }}
<i class="form-icon"></i> Remove <i class="form-icon"></i> Remove
</label> </label>
<div class="form-input-hint"> <div class="form-input-hint">Which actions to display for each bookmark.</div>
Which actions to display for each bookmark.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.bookmark_link_target.id_for_label }}" class="form-label">Open bookmarks in</label> <label for="{{ form.bookmark_link_target.id_for_label }}" class="form-label">Open bookmarks in</label>
{{ form.bookmark_link_target|add_class:"form-select width-25 width-sm-100" }} {{ form.bookmark_link_target|add_class:"form-select width-25 width-sm-100" }}
<div class="form-input-hint"> <div class="form-input-hint">Whether to open bookmarks a new page or in the same page.</div>
Whether to open bookmarks a new page or in the same page.
</div>
</div> </div>
<div class="form-group{% if form.items_per_page.errors %} has-error{% endif %}"> <div class="form-group{% if form.items_per_page.errors %} has-error{% endif %}">
<label for="{{ form.items_per_page.id_for_label }}" class="form-label">Items per page</label> <label for="{{ form.items_per_page.id_for_label }}" class="form-label">Items per page</label>
{{ form.items_per_page|add_class:"form-input width-25 width-sm-100"|attr:"min:10" }} {{ form.items_per_page|add_class:"form-input width-25 width-sm-100"|attr:"min:10" }}
{% if form.items_per_page.errors %} {% if form.items_per_page.errors %}
<div class="form-input-hint is-error"> <div class="form-input-hint is-error">{{ form.items_per_page.errors }}</div>
{{ form.items_per_page.errors }}
</div>
{% else %} {% else %}
{% endif %} {% endif %}
<div class="form-input-hint"> <div class="form-input-hint">The number of bookmarks to display per page.</div>
The number of bookmarks to display per page.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.sticky_pagination.id_for_label }}" class="form-checkbox"> <label for="{{ form.sticky_pagination.id_for_label }}" class="form-checkbox">
@@ -130,7 +121,8 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.collapse_side_panel.id_for_label }}" class="form-checkbox"> <label for="{{ form.collapse_side_panel.id_for_label }}"
class="form-checkbox">
{{ form.collapse_side_panel }} {{ form.collapse_side_panel }}
<i class="form-icon"></i> Collapse side panel <i class="form-icon"></i> Collapse side panel
</label> </label>
@@ -144,9 +136,7 @@
{{ form.hide_bundles }} {{ form.hide_bundles }}
<i class="form-icon"></i> Hide bundles <i class="form-icon"></i> Hide bundles
</label> </label>
<div class="form-input-hint"> <div class="form-input-hint">Allows to hide the bundles in the side panel if you don't intend to use them.</div>
Allows to hide the bundles in the side panel if you don't intend to use them.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.tag_search.id_for_label }}" class="form-label">Tag search</label> <label for="{{ form.tag_search.id_for_label }}" class="form-label">Tag search</label>
@@ -166,7 +156,8 @@
<div class="form-input-hint"> <div class="form-input-hint">
Since version 1.44.0, linkding has a new search engine that supports logical expressions (and, or, not). Since version 1.44.0, linkding has a new search engine that supports logical expressions (and, or, not).
If you run into any issues with the new search, you can enable this option to temporarily switch back to the old search. If you run into any issues with the new search, you can enable this option to temporarily switch back to the old search.
Please report any issues you encounter with the new search on <a href="https://github.com/sissbruecker/linkding/issues" target="_blank">GitHub</a> so they can be addressed. Please report any issues you encounter with the new search on <a href="https://github.com/sissbruecker/linkding/issues"
target="_blank">GitHub</a> so they can be addressed.
This option will be removed in a future version. This option will be removed in a future version.
</div> </div>
</div> </div>
@@ -183,10 +174,9 @@
<summary> <summary>
<span class="form-label d-inline-block">Auto Tagging</span> <span class="form-label d-inline-block">Auto Tagging</span>
</summary> </summary>
<label for="{{ form.auto_tagging_rules.id_for_label }}" class="text-assistive">Auto Tagging</label> <label for="{{ form.auto_tagging_rules.id_for_label }}"
<div> class="text-assistive">Auto Tagging</label>
{{ form.auto_tagging_rules|add_class:"form-input monospace"|attr:"rows:6" }} <div>{{ form.auto_tagging_rules|add_class:"form-input monospace"|attr:"rows:6" }}</div>
</div>
</details> </details>
<div class="form-input-hint"> <div class="form-input-hint">
Automatically adds tags to bookmarks based on predefined rules. Automatically adds tags to bookmarks based on predefined rules.
@@ -204,8 +194,8 @@ reddit.com/r/Music music reddit</pre>
Automatically loads favicons for bookmarked websites and displays them next to each bookmark. Automatically loads favicons for bookmarked websites and displays them next to each bookmark.
Enabling this feature automatically downloads all missing favicons. Enabling this feature automatically downloads all missing favicons.
By default, this feature uses a <b>Google service</b> to download favicons. By default, this feature uses a <b>Google service</b> to download favicons.
If you don't want to use this service, check the <a If you don't want to use this service, check the
href="https://linkding.link/options/#ld_favicon_provider" <a href="https://linkding.link/options/#ld_favicon_provider"
target="_blank">options documentation</a> on how to configure a custom favicon provider. target="_blank">options documentation</a> on how to configure a custom favicon provider.
Icons are downloaded in the background, and it may take a while for them to show up. Icons are downloaded in the background, and it may take a while for them to show up.
</div> </div>
@@ -214,7 +204,8 @@ reddit.com/r/Music music reddit</pre>
{% endif %} {% endif %}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.enable_preview_images.id_for_label }}" class="form-checkbox"> <label for="{{ form.enable_preview_images.id_for_label }}"
class="form-checkbox">
{{ form.enable_preview_images }} {{ form.enable_preview_images }}
<i class="form-icon"></i> Enable Preview Images <i class="form-icon"></i> Enable Preview Images
</label> </label>
@@ -224,17 +215,18 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.web_archive_integration.id_for_label }}" class="form-label">Internet Archive <label for="{{ form.web_archive_integration.id_for_label }}"
integration</label> class="form-label">
Internet Archive
integration
</label>
{{ form.web_archive_integration|add_class:"form-select width-25 width-sm-100" }} {{ form.web_archive_integration|add_class:"form-select width-25 width-sm-100" }}
<div class="form-input-hint"> <div class="form-input-hint">
Enabling this feature will automatically create snapshots of bookmarked websites on the <a Enabling this feature will automatically create snapshots of bookmarked websites on the
href="https://web.archive.org/" target="_blank" rel="noopener">Internet Archive Wayback <a href="https://web.archive.org/" target="_blank" rel="noopener">Internet Archive Wayback Machine</a>.
Machine</a>.
This allows to preserve, and later access the website as it was at the point in time it was bookmarked, in This allows to preserve, and later access the website as it was at the point in time it was bookmarked, in
case it goes offline or its content is modified. case it goes offline or its content is modified.
Please consider donating to the <a href="https://archive.org/donate" target="_blank" Please consider donating to the <a href="https://archive.org/donate" target="_blank" rel="noopener">Internet Archive</a> if you make use of this feature.
rel="noopener">Internet Archive</a> if you make use of this feature.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -248,19 +240,20 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.enable_public_sharing.id_for_label }}" class="form-checkbox"> <label for="{{ form.enable_public_sharing.id_for_label }}"
class="form-checkbox">
{{ form.enable_public_sharing }} {{ form.enable_public_sharing }}
<i class="form-icon"></i> Enable public bookmark sharing <i class="form-icon"></i> Enable public bookmark sharing
</label> </label>
<div class="form-input-hint"> <div class="form-input-hint">
Makes shared bookmarks publicly accessible, without requiring a login. Makes shared bookmarks publicly accessible, without requiring a login.
That means that anyone with a link to this instance can view shared bookmarks via the <a That means that anyone with a link to this instance can view shared bookmarks via the <a href="{% url 'linkding:bookmarks.shared' %}">shared bookmarks page</a>.
href="{% url 'linkding:bookmarks.shared' %}">shared bookmarks page</a>.
</div> </div>
</div> </div>
{% if has_snapshot_support %} {% if has_snapshot_support %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.enable_automatic_html_snapshots.id_for_label }}" class="form-checkbox"> <label for="{{ form.enable_automatic_html_snapshots.id_for_label }}"
class="form-checkbox">
{{ form.enable_automatic_html_snapshots }} {{ form.enable_automatic_html_snapshots }}
<i class="form-icon"></i> Automatically create HTML snapshots <i class="form-icon"></i> Automatically create HTML snapshots
</label> </label>
@@ -272,7 +265,8 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<label for="{{ form.default_mark_unread.id_for_label }}" class="form-checkbox"> <label for="{{ form.default_mark_unread.id_for_label }}"
class="form-checkbox">
{{ form.default_mark_unread }} {{ form.default_mark_unread }}
<i class="form-icon"></i> Create bookmarks as unread by default <i class="form-icon"></i> Create bookmarks as unread by default
</label> </label>
@@ -283,7 +277,8 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.default_mark_shared.id_for_label }}" class="form-checkbox"> <label for="{{ form.default_mark_shared.id_for_label }}"
class="form-checkbox">
{{ form.default_mark_shared }} {{ form.default_mark_shared }}
<i class="form-icon"></i> Create bookmarks as shared by default <i class="form-icon"></i> Create bookmarks as shared by default
</label> </label>
@@ -299,36 +294,39 @@ reddit.com/r/Music music reddit</pre>
<span class="form-label d-inline-block">Custom CSS</span> <span class="form-label d-inline-block">Custom CSS</span>
</summary> </summary>
<label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label> <label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label>
<div> <div>{{ form.custom_css|add_class:"form-input monospace"|attr:"rows:6" }}</div>
{{ form.custom_css|add_class:"form-input monospace"|attr:"rows:6" }}
</div>
</details> </details>
<div class="form-input-hint"> <div class="form-input-hint">Allows to add custom CSS to the page.</div>
Allows to add custom CSS to the page.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="submit" name="update_profile" value="Save" class="btn btn-primary btn-wide mt-2"> <input type="submit"
name="update_profile"
value="Save"
class="btn btn-primary btn-wide mt-2">
</div> </div>
</form> </form>
</section> </section>
{# Global settings section #} {# Global settings section #}
{% if global_settings_form %} {% if global_settings_form %}
<section aria-labelledby="global-settings-heading"> <section aria-labelledby="global-settings-heading">
<h2 id="global-settings-heading">Global settings</h2> <h2 id="global-settings-heading">Global settings</h2>
<form action="{% url 'linkding:settings.update' %}" method="post" novalidate data-turbo="false"> <form action="{% url 'linkding:settings.update' %}"
method="post"
novalidate
data-turbo="false">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="{{ global_settings_form.landing_page.id_for_label }}" class="form-label">Landing page</label> <label for="{{ global_settings_form.landing_page.id_for_label }}"
class="form-label">Landing page</label>
{{ global_settings_form.landing_page|add_class:"form-select width-25 width-sm-100" }} {{ global_settings_form.landing_page|add_class:"form-select width-25 width-sm-100" }}
<div class="form-input-hint"> <div class="form-input-hint">The page that unauthenticated users are redirected to when accessing the root URL.</div>
The page that unauthenticated users are redirected to when accessing the root URL.
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ global_settings_form.guest_profile_user.id_for_label }}" class="form-label">Guest user <label for="{{ global_settings_form.guest_profile_user.id_for_label }}"
profile</label> class="form-label">
Guest user
profile
</label>
{{ global_settings_form.guest_profile_user|add_class:"form-select width-25 width-sm-100" }} {{ global_settings_form.guest_profile_user|add_class:"form-select width-25 width-sm-100" }}
<div class="form-input-hint"> <div class="form-input-hint">
The user profile to use for users that are not logged in. This will affect how publicly shared bookmarks The user profile to use for users that are not logged in. This will affect how publicly shared bookmarks
@@ -337,7 +335,8 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ global_settings_form.enable_link_prefetch.id_for_label }}" class="form-checkbox"> <label for="{{ global_settings_form.enable_link_prefetch.id_for_label }}"
class="form-checkbox">
{{ global_settings_form.enable_link_prefetch }} {{ global_settings_form.enable_link_prefetch }}
<i class="form-icon"></i> Enable prefetching links on hover <i class="form-icon"></i> Enable prefetching links on hover
</label> </label>
@@ -346,20 +345,25 @@ reddit.com/r/Music music reddit</pre>
navigating application, but also increases the load on the server as well as bandwidth usage. navigating application, but also increases the load on the server as well as bandwidth usage.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="submit" name="update_global_settings" value="Save" class="btn btn-primary btn-wide mt-2"> <input type="submit"
name="update_global_settings"
value="Save"
class="btn btn-primary btn-wide mt-2">
</div> </div>
</form> </form>
</section> </section>
{% endif %} {% endif %}
{# Import section #} {# Import section #}
<section aria-labelledby="import-heading"> <section aria-labelledby="import-heading">
<h2 id="import-heading">Import</h2> <h2 id="import-heading">Import</h2>
<p>Import bookmarks and tags in the Netscape HTML format. This will execute a sync where new bookmarks are <p>
added and existing ones are updated.</p> Import bookmarks and tags in the Netscape HTML format. This will execute a sync where new bookmarks are
<form method="post" enctype="multipart/form-data" action="{% url 'linkding:settings.import' %}"> added and existing ones are updated.
</p>
<form method="post"
enctype="multipart/form-data"
action="{% url 'linkding:settings.import' %}">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="import_map_private_flag" class="form-checkbox"> <label for="import_map_private_flag" class="form-checkbox">
@@ -381,21 +385,19 @@ reddit.com/r/Music music reddit</pre>
</div> </div>
</form> </form>
</section> </section>
{# Export section #} {# Export section #}
<section aria-labelledby="export-heading"> <section aria-labelledby="export-heading">
<h2 id="export-heading">Export</h2> <h2 id="export-heading">Export</h2>
<p>Export all bookmarks in Netscape HTML format.</p> <p>Export all bookmarks in Netscape HTML format.</p>
<a class="btn btn-primary" target="_blank" href="{% url 'linkding:settings.export' %}">Download (.html)</a> <a class="btn btn-primary"
target="_blank"
href="{% url 'linkding:settings.export' %}">Download (.html)</a>
{% if export_error %} {% if export_error %}
<div class="has-error"> <div class="has-error">
<p class="form-input-hint"> <p class="form-input-hint">{{ export_error }}</p>
{{ export_error }}
</p>
</div> </div>
{% endif %} {% endif %}
</section> </section>
{# About section #} {# About section #}
<section class="about" aria-labelledby="about-heading"> <section class="about" aria-labelledby="about-heading">
<h2 id="about-heading">About</h2> <h2 id="about-heading">About</h2>
@@ -409,10 +411,8 @@ reddit.com/r/Music music reddit</pre>
<td style="vertical-align: top">Links</td> <td style="vertical-align: top">Links</td>
<td> <td>
<div class="d-flex flex-column gap-2"> <div class="d-flex flex-column gap-2">
<a href="https://github.com/sissbruecker/linkding/" <a href="https://github.com/sissbruecker/linkding/" target="_blank">GitHub</a>
target="_blank">GitHub</a> <a href="https://linkding.link/" target="_blank">Documentation</a>
<a href="https://linkding.link/"
target="_blank">Documentation</a>
<a href="https://github.com/sissbruecker/linkding/blob/master/CHANGELOG.md" <a href="https://github.com/sissbruecker/linkding/blob/master/CHANGELOG.md"
target="_blank">Changelog</a> target="_blank">Changelog</a>
</div> </div>
@@ -422,7 +422,6 @@ reddit.com/r/Music music reddit</pre>
</table> </table>
</section> </section>
</main> </main>
<script> <script>
(function init() { (function init() {
const enableSharing = document.getElementById("{{ form.enable_sharing.id_for_label }}"); const enableSharing = document.getElementById("{{ form.enable_sharing.id_for_label }}");
@@ -460,5 +459,4 @@ reddit.com/r/Music music reddit</pre>
bookmarkDescriptionDisplay.addEventListener("change", updateBookmarkDescriptionMaxLines); bookmarkDescriptionDisplay.addEventListener("change", updateBookmarkDescriptionMaxLines);
})(); })();
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,32 +1,39 @@
{% extends "bookmarks/layout.html" %} {% extends "shared/layout.html" %}
{% block head %} {% block head %}
{% with page_title="Integrations - Linkding" %} {% with page_title="Integrations - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="settings-page" aria-labelledby="main-heading"> <main class="settings-page" aria-labelledby="main-heading">
<h1 id="main-heading">Integrations</h1> <h1 id="main-heading">Integrations</h1>
<section aria-labelledby="browser-extension-heading"> <section aria-labelledby="browser-extension-heading">
<h2 id="browser-extension-heading">Browser Extension</h2> <h2 id="browser-extension-heading">Browser Extension</h2>
<p>The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The <p>
extension is available in the official extension stores for:</p> The browser extension allows you to quickly add new bookmarks without leaving the page that you are on. The
extension is available in the official extension stores for:
</p>
<ul> <ul>
<li><a href="https://addons.mozilla.org/firefox/addon/linkding-extension/" target="_blank">Firefox</a></li> <li>
<li><a href="https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe" <a href="https://addons.mozilla.org/firefox/addon/linkding-extension/"
target="_blank">Chrome</a></li> target="_blank">Firefox</a>
</li>
<li>
<a href="https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe"
target="_blank">Chrome</a>
</li>
</ul> </ul>
<p>The extension is <a href="https://github.com/sissbruecker/linkding-extension" target="_blank">open source</a> <p>
as well, which enables you to build and manually load it into any browser that supports Chrome extensions.</p> The extension is <a href="https://github.com/sissbruecker/linkding-extension"
target="_blank">open source</a>
as well, which enables you to build and manually load it into any browser that supports Chrome extensions.
</p>
<h2>Bookmarklet</h2> <h2>Bookmarklet</h2>
<p>The bookmarklet is an alternative, cross-browser way to quickly add new bookmarks without opening the linkding <p>
application first. Here's how it works:</p> The bookmarklet is an alternative, cross-browser way to quickly add new bookmarks without opening the linkding
application first. Here's how it works:
</p>
<ul> <ul>
<li>Choose your preferred method for detecting website titles and descriptions below (<a <li>
href="https://linkding.link/troubleshooting/#automatically-detected-title-and-description-are-incorrect" Choose your preferred method for detecting website titles and descriptions below (<a href="https://linkding.link/troubleshooting/#automatically-detected-title-and-description-are-incorrect"
target="_blank">Help</a>) target="_blank">Help</a>)
</li> </li>
<li>Drag the bookmarklet below into your browser's bookmark bar / toolbar</li> <li>Drag the bookmarklet below into your browser's bookmark bar / toolbar</li>
@@ -35,27 +42,39 @@
<li>linkding opens in a new window or tab and allows you to add a bookmark for the site</li> <li>linkding opens in a new window or tab and allows you to add a bookmark for the site</li>
<li>After saving the bookmark, the linkding window closes, and you are back on your website</li> <li>After saving the bookmark, the linkding window closes, and you are back on your website</li>
</ul> </ul>
<div class="form-group radio-group"
<div class="form-group radio-group" role="radiogroup" aria-labelledby="detection-method-label"> role="radiogroup"
aria-labelledby="detection-method-label">
<p id="detection-method-label">Choose your preferred bookmarklet:</p> <p id="detection-method-label">Choose your preferred bookmarklet:</p>
<label for="detection-method-server" class="form-radio"> <label for="detection-method-server" class="form-radio">
<input id="detection-method-server" type="radio" name="bookmarklet-type" value="server" checked> <input id="detection-method-server"
type="radio"
name="bookmarklet-type"
value="server"
checked>
<i class="form-icon"></i> <i class="form-icon"></i>
Detect title and description on the server Detect title and description on the server
</label> </label>
<label for="detection-method-client" class="form-radio"> <label for="detection-method-client" class="form-radio">
<input id="detection-method-client" type="radio" name="bookmarklet-type" value="client"> <input id="detection-method-client"
type="radio"
name="bookmarklet-type"
value="client">
<i class="form-icon"></i> <i class="form-icon"></i>
Detect title and description in the browser Detect title and description in the browser
</label> </label>
</div> </div>
<div class="bookmarklet-container"> <div class="bookmarklet-container">
<a id="bookmarklet-server" href="javascript: {% include 'bookmarks/bookmarklet.js' %}" data-turbo="false" <a id="bookmarklet-server"
href="javascript: {% include 'settings/bookmarklet.js' %}"
data-turbo="false"
class="btn btn-primary">📎 Add bookmark</a> class="btn btn-primary">📎 Add bookmark</a>
<a id="bookmarklet-client" href="javascript: {% include 'bookmarks/bookmarklet_clientside.js' %}" <a id="bookmarklet-client"
data-turbo="false" class="btn btn-primary" style="display: none;">📎 Add bookmark</a> href="javascript: {% include 'settings/bookmarklet_clientside.js' %}"
data-turbo="false"
class="btn btn-primary"
style="display: none">📎 Add bookmark</a>
</div> </div>
<script> <script>
(function init() { (function init() {
// Bookmarklet type toggle // Bookmarklet type toggle
@@ -75,40 +94,36 @@
} }
toggleBookmarklet(); toggleBookmarklet();
radioButtons.forEach(function (radio) { radioButtons.forEach(function(radio) {
radio.addEventListener('change', toggleBookmarklet); radio.addEventListener('change', toggleBookmarklet);
}); });
})(); })();
</script> </script>
</section> </section>
<turbo-frame id="api-section"> <turbo-frame id="api-section">
<section aria-labelledby="rest-api-heading"> <section aria-labelledby="rest-api-heading">
<h2 id="rest-api-heading">REST API</h2> <h2 id="rest-api-heading">REST API</h2>
{% if api_success_message %}<div class="toast toast-success mb-2">{{ api_success_message }}</div>{% endif %}
{% if api_success_message %}
<div class="toast toast-success mb-2">
{{ api_success_message }}
</div>
{% endif %}
{% if api_token_name and api_token_key %} {% if api_token_name and api_token_key %}
<div class="mt-4 mb-6"> <div class="mt-4 mb-6">
<p class="mb-2"><strong>Copy this token now, it will only be shown once:</strong></p> <p class="mb-2">
<strong>Copy this token now, it will only be shown once:</strong>
</p>
<label class="text-assistive" for="new-token-key">New token key</label> <label class="text-assistive" for="new-token-key">New token key</label>
<div class="input-group"> <div class="input-group">
<input class="form-input" value="{{ api_token_key }}" readonly id="new-token-key"> <input class="form-input"
value="{{ api_token_key }}"
readonly
id="new-token-key">
<button id="copy-new-token-key" class="btn input-group-btn" type="button">Copy</button> <button id="copy-new-token-key" class="btn input-group-btn" type="button">Copy</button>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<p> <p>
API tokens can be used to authenticate 3rd-party applications against the REST API. <strong>Please treat API tokens can be used to authenticate 3rd-party applications against the REST API. <strong>Please treat
tokens as you would any other credential.</strong> Any party with access to a token can access and manage all tokens as you would any other credential.</strong> Any party with access to a token can access and manage all
your bookmarks. your bookmarks.
</p> </p>
{% if api_tokens %} {% if api_tokens %}
<form method="post" <form method="post"
action="{% url 'linkding:settings.integrations.delete_api_token' %}" action="{% url 'linkding:settings.integrations.delete_api_token' %}"
@@ -130,9 +145,11 @@
<td>{{ token.created|date:"M d, Y H:i" }}</td> <td>{{ token.created|date:"M d, Y H:i" }}</td>
<td class="actions"> <td class="actions">
{% csrf_token %} {% csrf_token %}
<button data-confirm name="token_id" value="{{ token.id }}" type="submit" <button data-confirm
class="btn btn-link">Delete name="token_id"
</button> value="{{ token.id }}"
type="submit"
class="btn btn-link">Delete</button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -140,13 +157,11 @@
</table> </table>
</form> </form>
{% endif %} {% endif %}
<a class="btn"
<a class="btn" href="{% url 'linkding:settings.integrations.create_api_token' %}" href="{% url 'linkding:settings.integrations.create_api_token' %}"
data-turbo-frame="api-modal">Create API token</a> data-turbo-frame="api-modal">Create API token</a>
</section> </section>
<turbo-frame id="api-modal"></turbo-frame> <turbo-frame id="api-modal"></turbo-frame>
<script> <script>
(function init() { (function init() {
// Copy new token key to clipboard // Copy new token key to clipboard
@@ -168,32 +183,41 @@
})(); })();
</script> </script>
</turbo-frame> </turbo-frame>
<section aria-labelledby="rss-feeds-heading"> <section aria-labelledby="rss-feeds-heading">
<h2 id="rss-feeds-heading">RSS Feeds</h2> <h2 id="rss-feeds-heading">RSS Feeds</h2>
<p>The following URLs provide RSS feeds for your bookmarks:</p> <p>The following URLs provide RSS feeds for your bookmarks:</p>
<ul style="list-style-position: outside;"> <ul style="list-style-position: outside;">
<li><a target="_blank" href="{{ all_feed_url }}">All bookmarks</a></li> <li>
<li><a target="_blank" href="{{ unread_feed_url }}">Unread bookmarks</a></li> <a target="_blank" href="{{ all_feed_url }}">All bookmarks</a>
<li><a target="_blank" href="{{ shared_feed_url }}">Shared bookmarks</a></li> </li>
<li><a target="_blank" href="{{ public_shared_feed_url }}">Public shared bookmarks</a><br><span <li>
class="text-small text-secondary">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span> <a target="_blank" href="{{ unread_feed_url }}">Unread bookmarks</a>
</li>
<li>
<a target="_blank" href="{{ shared_feed_url }}">Shared bookmarks</a>
</li>
<li>
<a target="_blank" href="{{ public_shared_feed_url }}">Public shared bookmarks</a>
<br>
<span class="text-small text-secondary">The public shared feed does not contain an authentication token and can be shared with other people. Only shows shared bookmarks from users who have explicitly enabled public sharing.</span>
</li> </li>
</ul> </ul>
<p> <p>All URLs support the following URL parameters:</p>
All URLs support the following URL parameters:
</p>
<ul style="list-style-position: outside;"> <ul style="list-style-position: outside;">
<li>A <code>limit</code> parameter for specifying the maximum number of bookmarks to include in the feed. By <li>
A <code>limit</code> parameter for specifying the maximum number of bookmarks to include in the feed. By
default, only the latest 100 matching bookmarks are included. default, only the latest 100 matching bookmarks are included.
</li> </li>
<li>A <code>q</code> URL parameter for specifying a search query. You can get an example by doing a search in <li>
A <code>q</code> URL parameter for specifying a search query. You can get an example by doing a search in
the bookmarks view and then copying the parameter from the URL. the bookmarks view and then copying the parameter from the URL.
</li> </li>
<li>An <code>unread</code> parameter for filtering for unread or read bookmarks. Use <code>yes</code> for unread <li>
An <code>unread</code> parameter for filtering for unread or read bookmarks. Use <code>yes</code> for unread
bookmarks and <code>no</code> for read bookmarks. bookmarks and <code>no</code> for read bookmarks.
</li> </li>
<li>A <code>shared</code> parameter for filtering for shared or unshared bookmarks. Use <code>yes</code> for <li>
A <code>shared</code> parameter for filtering for shared or unshared bookmarks. Use <code>yes</code> for
shared bookmarks and <code>no</code> for unshared bookmarks. shared bookmarks and <code>no</code> for unshared bookmarks.
</li> </li>
</ul> </ul>
@@ -201,8 +225,8 @@
<strong>Please note that these URLs include an authentication token that should be treated like any other <strong>Please note that these URLs include an authentication token that should be treated like any other
credential.</strong> credential.</strong>
Any party with access to these URLs can read all your bookmarks. Any party with access to these URLs can read all your bookmarks.
If you think that a URL was compromised you can delete the feed token for your user in the <a If you think that a URL was compromised you can delete the feed token for your user in the <a target="_blank"
target="_blank" href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel</a>. href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel</a>.
After deleting the feed token, new URLs will be generated when you reload this settings page. After deleting the feed token, new URLs will be generated when you reload this settings page.
</p> </p>
</section> </section>

View File

@@ -1,6 +1,10 @@
{% load i18n %} {% load i18n %}
{# Force rendering validation errors in English language to align with the rest of the app #} {# Force rendering validation errors in English language to align with the rest of the app #}
{% language 'en-us' %} {% language 'en-us' %}
{% if errors %}<ul class="{{ error_class }}"{% if errors.field_id %} id="{{ errors.field_id }}_error"{% endif %}>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul>{% endif %} {% if errors %}
<ul class="{{ error_class }}"
{% if errors.field_id %}id="{{ errors.field_id }}_error"{% endif %}>
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
</ul>
{% endif %}
{% endlanguage %} {% endlanguage %}

View File

@@ -0,0 +1,64 @@
{% load static %}
<head>
<meta charset="UTF-8">
<link rel="icon" href="{% static 'favicon.ico' %}" sizes="48x48">
<link rel="icon"
href="{% static 'favicon.svg' %}"
sizes="any"
type="image/svg+xml">
<link rel="apple-touch-icon"
sizes="180x180"
href="{% static 'apple-touch-icon.png' %}">
<link rel="mask-icon"
href="{% static 'safari-pinned-tab.svg' %}"
color="#5856e0">
<link rel="manifest" href="{% url 'linkding:manifest' %}">
<link rel="search"
type="application/opensearchdescription+xml"
title="Linkding"
href="{% url 'linkding:opensearch' %}" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="description" content="Self-hosted bookmark service">
<meta name="robots" content="index,follow">
<meta name="author" content="Sascha Ißbrücker">
<title>{{ page_title|default:'Linkding' }}</title>
{# Include specific theme variant based on user profile setting #}
{% if request.user_profile.theme == 'light' %}
<link href="{% static 'theme-light.css' %}?v={{ app_version }}"
rel="stylesheet"
type="text/css" />
<meta name="theme-color" content="#5856e0">
{% elif request.user_profile.theme == 'dark' %}
<link href="{% static 'theme-dark.css' %}?v={{ app_version }}"
rel="stylesheet"
type="text/css" />
<meta name="theme-color" content="#161822">
{% else %}
{# Use auto theme as fallback #}
<link href="{% static 'theme-dark.css' %}?v={{ app_version }}"
rel="stylesheet"
type="text/css"
media="(prefers-color-scheme: dark)" />
<link href="{% static 'theme-light.css' %}?v={{ app_version }}"
rel="stylesheet"
type="text/css"
media="(prefers-color-scheme: light)" />
<meta name="theme-color"
media="(prefers-color-scheme: dark)"
content="#161822">
<meta name="theme-color"
media="(prefers-color-scheme: light)"
content="#5856e0">
{% endif %}
{% if request.user_profile.custom_css %}
<link href="{% url 'linkding:custom_css' %}?hash={{ request.user_profile.custom_css_hash }}"
rel="stylesheet"
type="text/css" />
{% endif %}
<meta name="turbo-cache-control" content="no-preview">
{% if not request.global_settings.enable_link_prefetch %}<meta name="turbo-prefetch" content="false">{% endif %}
{% if rss_feed_url %}<link rel="alternate" type="application/rss+xml" href="{{ rss_feed_url }}" />{% endif %}
<script src="{% static "bundle.js" %}?v={{ app_version }}"></script>
</head>

View File

@@ -0,0 +1,81 @@
{% load static %}
<!DOCTYPE html>
{# Use data attributes as storage for access in static scripts #}
<html lang="en" data-api-base-url="{% url 'linkding:api-root' %}">
{% block head %}
{% include 'shared/head.html' %}
{% endblock %}
<body>
<div class="d-none">
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="ld-icon-unread" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0"></path>
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0"></path>
<path d="M3 6l0 13"></path>
<path d="M12 6l0 13"></path>
<path d="M21 6l0 13"></path>
</symbol>
</svg>
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="ld-icon-share" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M18 6m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M18 18m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M8.7 10.7l6.6 -3.4"></path>
<path d="M8.7 13.3l6.6 3.4"></path>
</symbol>
</svg>
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="ld-icon-note" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path>
<path d="M9 7l6 0"></path>
<path d="M9 11l6 0"></path>
<path d="M9 15l4 0"></path>
</symbol>
</svg>
</div>
<header class="container">
{% if has_toasts %}
<div class="message-list">
<form action="{% url 'linkding:toasts.acknowledge' %}?return_url={{ request.path | urlencode }}"
method="post">
{% csrf_token %}
{% for toast in toast_messages %}
<div class="toast d-flex">
{{ toast.message }}
<button type="submit"
name="toast"
value="{{ toast.id }}"
class="btn btn-clear"></button>
</div>
{% endfor %}
</form>
</div>
{% endif %}
<div class="d-flex justify-between">
<a href="{% url 'linkding:root' %}" class="app-link d-flex align-center">
<img class="app-logo" src="{% static 'logo.png' %}" alt="Application logo">
<span class="app-name">LINKDING</span>
</a>
<nav>
{% if request.user.is_authenticated %}
{# Only show nav items menu when logged in #}
{% include 'shared/nav_menu.html' %}
{% else %}
{# Otherwise show login link #}
<a href="{% url 'login' %}" class="btn btn-link">Login</a>
{% endif %}
</nav>
</div>
</header>
<div class="content container">
{% block content %}{% endblock %}
</div>
<div class="modals">
{% block overlays %}{% endblock %}
</div>
</body>
</html>

View File

@@ -1,9 +1,5 @@
{% if messages %} {% if messages %}
<div class="message-list"> <div class="message-list">
{% for message in messages %} {% for message in messages %}<div class="toast toast-{{ message.tags }}" role="alert">{{ message }}</div>{% endfor %}
<div class="toast toast-{{ message.tags }}" role="alert">
{{ message }}
</div>
{% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -1,8 +1,18 @@
<div class="modal-header"> <div class="modal-header">
<h2 class="title">{{ title }}</h2> <h2 class="title">{{ title }}</h2>
<button type="button" class="btn btn-noborder close" aria-label="Close dialog" data-close-modal> <button type="button"
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" class="btn btn-noborder close"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> aria-label="Close dialog"
data-close-modal>
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 6l-12 12"></path> <path d="M18 6l-12 12"></path>
<path d="M6 6l12 12"></path> <path d="M6 6l12 12"></path>

View File

@@ -0,0 +1,128 @@
{% load shared %}
{% htmlmin %}
{# Basic menu list #}
<div class="hide-md">
<a href="{% url 'linkding:bookmarks.new' %}"
class="btn btn-primary mr-2">Add bookmark</a>
<ld-dropdown class="dropdown">
<button class="btn btn-link dropdown-toggle" tabindex="0">Bookmarks</button>
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}" class="menu-link">Active</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.archived' %}" class="menu-link">Archived</a>
</li>
{% if request.user_profile.enable_sharing %}
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.shared' %}" class="menu-link">Shared</a>
</li>
{% endif %}
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}?unread=yes"
class="menu-link">Unread</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}?q=!untagged"
class="menu-link">Untagged</a>
</li>
</ul>
</ld-dropdown>
<ld-dropdown class="dropdown">
<button class="btn btn-link dropdown-toggle" tabindex="0">Settings</button>
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'linkding:settings.general' %}" class="menu-link">General</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:settings.integrations' %}" class="menu-link">Integrations</a>
</li>
{% if request.user.is_superuser %}
<li class="menu-item">
<a href="{% url 'admin:index' %}" class="menu-link" data-turbo="false">Admin</a>
</li>
{% endif %}
</ul>
</ld-dropdown>
<form class="d-inline"
action="{% url 'logout' %}"
method="post"
data-turbo="false">
{% csrf_token %}
<button type="submit" class="btn btn-link">Logout</button>
</form>
</div>
{# Menu drop-down for smaller devices #}
<div class="show-md">
<a href="{% url 'linkding:bookmarks.new' %}"
aria-label="Add bookmark"
class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style="width: 24px;
height: 24px">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
</a>
<ld-dropdown class="dropdown dropdown-right">
<button class="btn btn-link dropdown-toggle"
aria-label="Navigation menu"
tabindex="0">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
style="width: 24px;
height: 24px">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<!-- menu component -->
<ul class="menu" role="list" tabindex="-1">
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}" class="menu-link">Bookmarks</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.archived' %}" class="menu-link">Archived bookmarks</a>
</li>
{% if request.user_profile.enable_sharing %}
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.shared' %}" class="menu-link">Shared bookmarks</a>
</li>
{% endif %}
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}?unread=yes"
class="menu-link">Unread</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:bookmarks.index' %}?q=!untagged"
class="menu-link">Untagged</a>
</li>
<div class="divider"></div>
<li class="menu-item">
<a href="{% url 'linkding:settings.general' %}" class="menu-link">Settings</a>
</li>
<li class="menu-item">
<a href="{% url 'linkding:settings.integrations' %}" class="menu-link">Integrations</a>
</li>
{% if request.user.is_superuser %}
<li class="menu-item">
<a href="{% url 'admin:index' %}" class="menu-link" data-turbo="false">Admin</a>
</li>
{% endif %}
<div class="divider"></div>
<li class="menu-item">
<form class="d-inline"
action="{% url 'logout' %}"
method="post"
data-turbo="false">
{% csrf_token %}
<button type="submit" class="btn btn-link menu-link">Logout</button>
</form>
</li>
</ul>
</ld-dropdown>
</div>
{% endhtmlmin %}

View File

@@ -1,5 +1,4 @@
{% load shared %} {% load shared %}
<ul class="pagination"> <ul class="pagination">
{% if prev_link %} {% if prev_link %}
<li class="page-item"> <li class="page-item">
@@ -10,7 +9,6 @@
<a href="#" tabindex="-1">Previous</a> <a href="#" tabindex="-1">Previous</a>
</li> </li>
{% endif %} {% endif %}
{% for page_link in page_links %} {% for page_link in page_links %}
{% if page_link %} {% if page_link %}
<li class="page-item {% if page_link.active %}active{% endif %}"> <li class="page-item {% if page_link.active %}active{% endif %}">
@@ -22,7 +20,6 @@
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if next_link %} {% if next_link %}
<li class="page-item"> <li class="page-item">
<a href="{{ next_link }}" tabindex="-1">Next</a> <a href="{{ next_link }}" tabindex="-1">Next</a>

View File

@@ -0,0 +1,6 @@
<html lang="en">
{% include 'shared/head.html' %}
<body>
<!--content-->
</body>
</html>

View File

@@ -1,19 +1,21 @@
<turbo-frame id="tag-modal"> <turbo-frame id="tag-modal">
<form method="post" action="{% url 'linkding:tags.edit' tag.id %}" data-turbo-frame="_top" novalidate> <form method="post"
action="{% url 'linkding:tags.edit' tag.id %}"
data-turbo-frame="_top"
novalidate>
{% csrf_token %} {% csrf_token %}
<ld-modal class="modal tag-edit-modal active" data-close-url="{% url 'linkding:tags.index' %}" <ld-modal class="modal tag-edit-modal active"
data-close-url="{% url 'linkding:tags.index' %}"
data-turbo-frame="tag-modal"> data-turbo-frame="tag-modal">
<div class="modal-overlay" data-close-modal></div> <div class="modal-overlay" data-close-modal></div>
<div class="modal-container" role="dialog" aria-modal="true"> <div class="modal-container" role="dialog" aria-modal="true">
{% include 'shared/modal_header.html' with title="Edit Tag" %} {% include 'shared/modal_header.html' with title="Edit Tag" %}
<div class="modal-body"> <div class="modal-body">{% include 'tags/form.html' %}</div>
{% include 'tags/form.html' %}
</div>
<div class="modal-footer d-flex justify-between"> <div class="modal-footer d-flex justify-between">
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button> <button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
<button type="submit" class="btn btn-primary btn-wide">Save</button> <button type="submit" class="btn btn-primary btn-wide">Save</button>
</div> </div>
</div> </div>
</ld-modal> </ld-modal>
</form> </form>
</turbo-frame> </turbo-frame>

View File

@@ -1,14 +1,9 @@
{% load widget_tweaks %} {% load widget_tweaks %}
<div class="form-group {% if form.name.errors %}has-error{% endif %}"> <div class="form-group {% if form.name.errors %}has-error{% endif %}">
<label class="form-label" for="{{ form.name.id_for_label }}">Name</label> <label class="form-label" for="{{ form.name.id_for_label }}">Name</label>
{{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: "|attr:"autofocus" }} {{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: "|attr:"autofocus" }}
<div class="form-input-hint">Tag names are case-insensitive and cannot contain spaces (spaces will be replaced with
hyphens).
</div>
{% if form.name.errors %}
<div class="form-input-hint"> <div class="form-input-hint">
{{ form.name.errors }} Tag names are case-insensitive and cannot contain spaces (spaces will be replaced with hyphens).
</div> </div>
{% endif %} {% if form.name.errors %}<div class="form-input-hint">{{ form.name.errors }}</div>{% endif %}
</div> </div>

View File

@@ -1,26 +1,24 @@
{% extends "bookmarks/layout.html" %} {% extends "shared/layout.html" %}
{% load shared %} {% load shared %}
{% load pagination %} {% load pagination %}
{% block head %} {% block head %}
{% with page_title="Tags - Linkding" %} {% with page_title="Tags - Linkding" %}{{ block.super }}{% endwith %}
{{ block.super }}
{% endwith %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="tags-page crud-page"> <div class="tags-page crud-page">
<main aria-labelledby="main-heading"> <main aria-labelledby="main-heading">
<div class="crud-header"> <div class="crud-header">
<h1 id="main-heading">Tags</h1> <h1 id="main-heading">Tags</h1>
<div class="d-flex gap-2 ml-auto"> <div class="d-flex gap-2 ml-auto">
<a href="{% url 'linkding:tags.new' %}" data-turbo-frame="tag-modal" class="btn">Create Tag</a> <a href="{% url 'linkding:tags.new' %}"
<a href="{% url 'linkding:tags.merge' %}" data-turbo-frame="tag-modal" class="btn">Merge Tags</a> data-turbo-frame="tag-modal"
class="btn">Create Tag</a>
<a href="{% url 'linkding:tags.merge' %}"
data-turbo-frame="tag-modal"
class="btn">Merge Tags</a>
</div> </div>
</div> </div>
{% include 'shared/messages.html' %} {% include 'shared/messages.html' %}
{# Filters #} {# Filters #}
<div class="crud-filters"> <div class="crud-filters">
<ld-form data-form-reset> <ld-form data-form-reset>
@@ -28,7 +26,11 @@
<div class="form-group"> <div class="form-group">
<label class="form-label text-assistive" for="search">Search tags</label> <label class="form-label text-assistive" for="search">Search tags</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="search" name="search" value="{{ search }}" placeholder="Search tags..." <input type="text"
id="search"
name="search"
value="{{ search }}"
placeholder="Search tags..."
class="form-input"> class="form-input">
<button type="submit" class="btn input-group-btn">Search</button> <button type="submit" class="btn input-group-btn">Search</button>
</div> </div>
@@ -37,10 +39,19 @@
<label class="form-label text-assistive" for="sort">Sort by</label> <label class="form-label text-assistive" for="sort">Sort by</label>
<div class="input-group"> <div class="input-group">
<span class="input-group-addon text-secondary"> <span class="input-group-addon text-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path width="20"
stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 9l4 -4l4 4m-4 -4v14"/><path height="20"
d="M21 15l-4 4l-4 -4m4 4v-14"/></svg> viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 9l4 -4l4 4m-4 -4v14" />
<path d="M21 15l-4 4l-4 -4m4 4v-14" />
</svg>
</span> </span>
<select id="sort" name="sort" class="form-select" data-submit-on-change> <select id="sort" name="sort" class="form-select" data-submit-on-change>
<option value="name-asc" {% if sort == "name-asc" %}selected{% endif %}>Name A-Z</option> <option value="name-asc" {% if sort == "name-asc" %}selected{% endif %}>Name A-Z</option>
@@ -52,7 +63,10 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-checkbox"> <label class="form-checkbox">
<input type="checkbox" name="unused" value="true" {% if unused_only %}checked{% endif %} <input type="checkbox"
name="unused"
value="true"
{% if unused_only %}checked{% endif %}
data-submit-on-change> data-submit-on-change>
<i class="form-icon"></i> Show only unused tags <i class="form-icon"></i> Show only unused tags
</label> </label>
@@ -68,12 +82,10 @@
{% endif %} {% endif %}
</p> </p>
</div> </div>
{# Tags List #} {# Tags List #}
{% if page.object_list %} {% if page.object_list %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<table class="table crud-table"> <table class="table crud-table">
<thead> <thead>
<tr> <tr>
@@ -87,31 +99,29 @@
<tbody> <tbody>
{% for tag in page.object_list %} {% for tag in page.object_list %}
<tr> <tr>
<td> <td>{{ tag.name }}</td>
{{ tag.name }}
</td>
<td style="width: 25%"> <td style="width: 25%">
<a class="btn btn-link" href="{% url 'linkding:bookmarks.index' %}?q=%23{{ tag.name|urlencode }}"> <a class="btn btn-link"
href="{% url 'linkding:bookmarks.index' %}?q=%23{{ tag.name|urlencode }}">
{{ tag.bookmark_count }} {{ tag.bookmark_count }}
</a> </a>
</td> </td>
<td class="actions"> <td class="actions">
<a class="btn btn-link" href="{% url 'linkding:tags.edit' tag.id %}" <a class="btn btn-link"
href="{% url 'linkding:tags.edit' tag.id %}"
data-turbo-frame="tag-modal">Edit</a> data-turbo-frame="tag-modal">Edit</a>
<button type="submit" name="delete_tag" value="{{ tag.id }}" class="btn btn-link text-error" <button type="submit"
data-confirm> name="delete_tag"
Remove value="{{ tag.id }}"
</button> class="btn btn-link text-error"
data-confirm>Remove</button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</form> </form>
{% pagination page %} {% pagination page %}
{% else %} {% else %}
<div class="empty"> <div class="empty">
{% if search or unused_only %} {% if search or unused_only %}

View File

@@ -1,9 +1,13 @@
{% load widget_tweaks %} {% load widget_tweaks %}
<turbo-frame id="tag-modal"> <turbo-frame id="tag-modal">
<form method="post" action="{% url 'linkding:tags.merge' %}" data-turbo-frame="_top" novalidate> <form method="post"
action="{% url 'linkding:tags.merge' %}"
data-turbo-frame="_top"
novalidate>
{% csrf_token %} {% csrf_token %}
<ld-modal class="modal active" data-close-url="{% url 'linkding:tags.index' %}" data-turbo-frame="tag-modal"> <ld-modal class="modal active"
data-close-url="{% url 'linkding:tags.index' %}"
data-turbo-frame="tag-modal">
<div class="modal-overlay" data-close-modal></div> <div class="modal-overlay" data-close-modal></div>
<div class="modal-container" role="dialog" aria-modal="true"> <div class="modal-container" role="dialog" aria-modal="true">
{% include 'shared/modal_header.html' with title="Merge Tags" %} {% include 'shared/modal_header.html' with title="Merge Tags" %}
@@ -19,35 +23,28 @@
<li>The merged tags are deleted</li> <li>The merged tags are deleted</li>
</ol> </ol>
</details> </details>
<div class="form-group {% if form.target_tag.errors %}has-error{% endif %}"> <div class="form-group {% if form.target_tag.errors %}has-error{% endif %}">
<label for="{{ form.target_tag.id_for_label }}" class="form-label">Target tag</label> <label for="{{ form.target_tag.id_for_label }}" class="form-label">Target tag</label>
<ld-tag-autocomplete input-id="{{ form.target_tag.auto_id }}" input-name="{{ form.target_tag.html_name }}" <ld-tag-autocomplete input-id="{{ form.target_tag.auto_id }}"
input-name="{{ form.target_tag.html_name }}"
input-value="{{ form.target_tag.value|default_if_none:'' }}"> input-value="{{ form.target_tag.value|default_if_none:'' }}">
</ld-tag-autocomplete> </ld-tag-autocomplete>
<div class="form-input-hint"> <div class="form-input-hint">
Enter the name of the tag you want to keep. The tags entered below will be merged into this one. Enter the name of the tag you want to keep. The tags entered below will be merged into this one.
</div> </div>
{% if form.target_tag.errors %} {% if form.target_tag.errors %}<div class="form-input-hint">{{ form.target_tag.errors }}</div>{% endif %}
<div class="form-input-hint">
{{ form.target_tag.errors }}
</div> </div>
{% endif %}
</div>
<div class="form-group {% if form.merge_tags.errors %}has-error{% endif %}"> <div class="form-group {% if form.merge_tags.errors %}has-error{% endif %}">
<label for="{{ form.merge_tags.id_for_label }}" class="form-label">Tags to merge</label> <label for="{{ form.merge_tags.id_for_label }}" class="form-label">Tags to merge</label>
<ld-tag-autocomplete input-id="{{ form.merge_tags.auto_id }}" input-name="{{ form.merge_tags.html_name }}" <ld-tag-autocomplete input-id="{{ form.merge_tags.auto_id }}"
input-name="{{ form.merge_tags.html_name }}"
input-value="{{ form.merge_tags.value|default_if_none:'' }}"> input-value="{{ form.merge_tags.value|default_if_none:'' }}">
</ld-tag-autocomplete> </ld-tag-autocomplete>
<div class="form-input-hint">Enter the names of tags to merge into the target tag, separated by spaces. <div class="form-input-hint">
Enter the names of tags to merge into the target tag, separated by spaces.
These tags will be deleted after merging. These tags will be deleted after merging.
</div> </div>
{% if form.merge_tags.errors %} {% if form.merge_tags.errors %}<div class="form-input-hint">{{ form.merge_tags.errors }}</div>{% endif %}
<div class="form-input-hint">
{{ form.merge_tags.errors }}
</div>
{% endif %}
</div> </div>
</div> </div>
<div class="modal-footer d-flex justify-between"> <div class="modal-footer d-flex justify-between">
@@ -56,5 +53,5 @@
</div> </div>
</div> </div>
</ld-modal> </ld-modal>
</form> </form>
</turbo-frame> </turbo-frame>

View File

@@ -1,19 +1,21 @@
<turbo-frame id="tag-modal"> <turbo-frame id="tag-modal">
<form method="post" action="{% url 'linkding:tags.new' %}" data-turbo-frame="_top" novalidate> <form method="post"
action="{% url 'linkding:tags.new' %}"
data-turbo-frame="_top"
novalidate>
{% csrf_token %} {% csrf_token %}
<ld-modal class="modal tag-edit-modal active" data-close-url="{% url 'linkding:tags.index' %}" <ld-modal class="modal tag-edit-modal active"
data-close-url="{% url 'linkding:tags.index' %}"
data-turbo-frame="tag-modal"> data-turbo-frame="tag-modal">
<div class="modal-overlay" data-close-modal></div> <div class="modal-overlay" data-close-modal></div>
<div class="modal-container" role="dialog" aria-modal="true"> <div class="modal-container" role="dialog" aria-modal="true">
{% include 'shared/modal_header.html' with title="Create Tag" %} {% include 'shared/modal_header.html' with title="Create Tag" %}
<div class="modal-body"> <div class="modal-body">{% include 'tags/form.html' %}</div>
{% include 'tags/form.html' %}
</div>
<div class="modal-footer d-flex justify-between"> <div class="modal-footer d-flex justify-between">
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button> <button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
<button type="submit" class="btn btn-primary btn-wide">Save</button> <button type="submit" class="btn btn-primary btn-wide">Save</button>
</div> </div>
</div> </div>
</ld-modal> </ld-modal>
</form> </form>
</turbo-frame> </turbo-frame>

View File

@@ -9,9 +9,7 @@ NUM_ADJACENT_PAGES = 2
register = template.Library() register = template.Library()
@register.inclusion_tag( @register.inclusion_tag("shared/pagination.html", name="pagination", takes_context=True)
"bookmarks/pagination.html", name="pagination", takes_context=True
)
def pagination(context, page: Page): def pagination(context, page: Page):
request = context["request"] request = context["request"]
base_url = request.path base_url = request.path

View File

@@ -448,7 +448,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
soup = self.make_soup(html) soup = self.make_soup(html)
bookmark_list = soup.select_one("ul.bookmark-list") bookmark_list = soup.select_one("ul.bookmark-list")
style = bookmark_list["style"] style = bookmark_list["style"]
self.assertIn("--ld-bookmark-description-max-lines:1;", style) self.assertIn("--ld-bookmark-description-max-lines:1", style)
profile = self.get_or_create_test_user().profile profile = self.get_or_create_test_user().profile
profile.bookmark_description_max_lines = 3 profile.bookmark_description_max_lines = 3
@@ -458,7 +458,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
soup = self.make_soup(html) soup = self.make_soup(html)
bookmark_list = soup.select_one("ul.bookmark-list") bookmark_list = soup.select_one("ul.bookmark-list")
style = bookmark_list["style"] style = bookmark_list["style"]
self.assertIn("--ld-bookmark-description-max-lines:3;", style) self.assertIn("--ld-bookmark-description-max-lines:3", style)
def test_bookmark_tag_ordering(self): def test_bookmark_tag_ordering(self):
bookmark = self.setup_bookmark() bookmark = self.setup_bookmark()

View File

@@ -48,7 +48,7 @@ class TagsEditViewTestCase(TestCase, BookmarkFactoryMixin):
reverse("linkding:tags.edit", args=[tag.id]), {"name": ""} reverse("linkding:tags.edit", args=[tag.id]), {"name": ""}
) )
self.assertContains(response, "This field is required", status_code=422) self.assertContains(response, "This field is required")
tag.refresh_from_db() tag.refresh_from_db()
self.assertEqual(tag.name, "tag1") self.assertEqual(tag.name, "tag1")
@@ -60,9 +60,7 @@ class TagsEditViewTestCase(TestCase, BookmarkFactoryMixin):
reverse("linkding:tags.edit", args=[tag1.id]), {"name": "tag2"} reverse("linkding:tags.edit", args=[tag1.id]), {"name": "tag2"}
) )
self.assertContains( self.assertContains(response, "Tag &quot;tag2&quot; already exists")
response, "Tag &quot;tag2&quot; already exists", status_code=422
)
tag1.refresh_from_db() tag1.refresh_from_db()
self.assertEqual(tag1.name, "tag1") self.assertEqual(tag1.name, "tag1")
@@ -74,9 +72,7 @@ class TagsEditViewTestCase(TestCase, BookmarkFactoryMixin):
reverse("linkding:tags.edit", args=[tag1.id]), {"name": "TAG2"} reverse("linkding:tags.edit", args=[tag1.id]), {"name": "TAG2"}
) )
self.assertContains( self.assertContains(response, "Tag &quot;TAG2&quot; already exists")
response, "Tag &quot;TAG2&quot; already exists", status_code=422
)
tag1.refresh_from_db() tag1.refresh_from_db()
self.assertEqual(tag1.name, "tag1") self.assertEqual(tag1.name, "tag1")

View File

@@ -113,7 +113,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
reverse("linkding:tags.merge"), reverse("linkding:tags.merge"),
{"target_tag": "target_tag", "merge_tags": "merge_tag"}, {"target_tag": "target_tag", "merge_tags": "merge_tag"},
) )
self.assertEqual(response.status_code, 422)
target_tag_group = self.get_form_group(response, "target_tag") target_tag_group = self.get_form_group(response, "target_tag")
self.assertIn( self.assertIn(
@@ -131,8 +130,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
{"target_tag": "", "merge_tags": "merge_tag"}, {"target_tag": "", "merge_tags": "merge_tag"},
) )
self.assertEqual(response.status_code, 422)
target_tag_group = self.get_form_group(response, "target_tag") target_tag_group = self.get_form_group(response, "target_tag")
self.assertIn("This field is required", self.get_text(target_tag_group)) self.assertIn("This field is required", self.get_text(target_tag_group))
self.assertTrue(Tag.objects.filter(id=merge_tag.id).exists()) self.assertTrue(Tag.objects.filter(id=merge_tag.id).exists())
@@ -145,7 +142,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
{"target_tag": "target_tag", "merge_tags": ""}, {"target_tag": "target_tag", "merge_tags": ""},
) )
self.assertEqual(response.status_code, 422)
merge_tags_group = self.get_form_group(response, "merge_tags") merge_tags_group = self.get_form_group(response, "merge_tags")
self.assertIn("This field is required", self.get_text(merge_tags_group)) self.assertIn("This field is required", self.get_text(merge_tags_group))
@@ -157,8 +153,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
{"target_tag": "nonexistent_tag", "merge_tags": "merge_tag"}, {"target_tag": "nonexistent_tag", "merge_tags": "merge_tag"},
) )
self.assertEqual(response.status_code, 422)
target_tag_group = self.get_form_group(response, "target_tag") target_tag_group = self.get_form_group(response, "target_tag")
self.assertIn( self.assertIn(
'Tag "nonexistent_tag" does not exist', self.get_text(target_tag_group) 'Tag "nonexistent_tag" does not exist', self.get_text(target_tag_group)
@@ -173,7 +167,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
{"target_tag": "target_tag", "merge_tags": "merge_tag1 nonexistent_tag"}, {"target_tag": "target_tag", "merge_tags": "merge_tag1 nonexistent_tag"},
) )
self.assertEqual(response.status_code, 422)
merge_tags_group = self.get_form_group(response, "merge_tags") merge_tags_group = self.get_form_group(response, "merge_tags")
self.assertIn( self.assertIn(
'Tag "nonexistent_tag" does not exist', self.get_text(merge_tags_group) 'Tag "nonexistent_tag" does not exist', self.get_text(merge_tags_group)
@@ -188,8 +181,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
{"target_tag": "target_tag1 target_tag2", "merge_tags": "some_tag"}, {"target_tag": "target_tag1 target_tag2", "merge_tags": "some_tag"},
) )
self.assertEqual(response.status_code, 422)
target_tag_group = self.get_form_group(response, "target_tag") target_tag_group = self.get_form_group(response, "target_tag")
self.assertIn( self.assertIn(
"Please enter only one tag name for the target tag", "Please enter only one tag name for the target tag",
@@ -205,8 +196,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
{"target_tag": "target_tag", "merge_tags": "target_tag merge_tag"}, {"target_tag": "target_tag", "merge_tags": "target_tag merge_tag"},
) )
self.assertEqual(response.status_code, 422)
merge_tags_group = self.get_form_group(response, "merge_tags") merge_tags_group = self.get_form_group(response, "merge_tags")
self.assertIn( self.assertIn(
"The target tag cannot be selected for merging", "The target tag cannot be selected for merging",

View File

@@ -20,7 +20,7 @@ class TagsNewViewTestCase(TestCase, BookmarkFactoryMixin):
def test_show_error_for_empty_name(self): def test_show_error_for_empty_name(self):
response = self.client.post(reverse("linkding:tags.new"), {"name": ""}) response = self.client.post(reverse("linkding:tags.new"), {"name": ""})
self.assertContains(response, "This field is required", status_code=422) self.assertContains(response, "This field is required")
self.assertEqual(Tag.objects.count(), 0) self.assertEqual(Tag.objects.count(), 0)
def test_show_error_for_duplicate_name(self): def test_show_error_for_duplicate_name(self):
@@ -30,9 +30,7 @@ class TagsNewViewTestCase(TestCase, BookmarkFactoryMixin):
reverse("linkding:tags.new"), {"name": "existing_tag"} reverse("linkding:tags.new"), {"name": "existing_tag"}
) )
self.assertContains( self.assertContains(response, "Tag &quot;existing_tag&quot; already exists")
response, "Tag &quot;existing_tag&quot; already exists", status_code=422
)
self.assertEqual(Tag.objects.count(), 1) self.assertEqual(Tag.objects.count(), 1)
def test_show_error_for_duplicate_name_different_casing(self): def test_show_error_for_duplicate_name_different_casing(self):
@@ -42,9 +40,7 @@ class TagsNewViewTestCase(TestCase, BookmarkFactoryMixin):
reverse("linkding:tags.new"), {"name": "existing_TAG"} reverse("linkding:tags.new"), {"name": "existing_TAG"}
) )
self.assertContains( self.assertContains(response, "Tag &quot;existing_TAG&quot; already exists")
response, "Tag &quot;existing_TAG&quot; already exists", status_code=422
)
self.assertEqual(Tag.objects.count(), 1) self.assertEqual(Tag.objects.count(), 1)
def test_no_error_for_duplicate_name_different_user(self): def test_no_error_for_duplicate_name_different_user(self):

View File

@@ -5,12 +5,13 @@ from django.urls import reverse
from bookmarks.models import Toast from bookmarks.models import Toast
from bookmarks.tests.helpers import ( from bookmarks.tests.helpers import (
BookmarkFactoryMixin, BookmarkFactoryMixin,
HtmlTestMixin,
random_sentence, random_sentence,
disable_logging, disable_logging,
) )
class ToastsViewTestCase(TestCase, BookmarkFactoryMixin): class ToastsViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def setUp(self) -> None: def setUp(self) -> None:
user = self.get_or_create_test_user() user = self.get_or_create_test_user()
@@ -72,11 +73,13 @@ class ToastsViewTestCase(TestCase, BookmarkFactoryMixin):
def test_form_tag(self): def test_form_tag(self):
self.create_toast() self.create_toast()
expected_form_tag = f'<form action="{reverse("linkding:toasts.acknowledge")}?return_url={reverse("linkding:bookmarks.index")}" method="post">' expected_action = f'{reverse("linkding:toasts.acknowledge")}?return_url={reverse("linkding:bookmarks.index")}'
response = self.client.get(reverse("linkding:bookmarks.index")) response = self.client.get(reverse("linkding:bookmarks.index"))
soup = self.make_soup(response.content.decode())
form = soup.find("form", attrs={"action": expected_action, "method": "post"})
self.assertContains(response, expected_form_tag) self.assertIsNotNone(form)
def test_toast_content(self): def test_toast_content(self):
toast = self.create_toast() toast = self.create_toast()

View File

@@ -35,7 +35,7 @@ from bookmarks.services.bookmarks import (
) )
from bookmarks.type_defs import HttpRequest from bookmarks.type_defs import HttpRequest
from bookmarks.utils import get_safe_return_url from bookmarks.utils import get_safe_return_url
from bookmarks.views import access, contexts, partials, turbo from bookmarks.views import access, contexts, turbo
@login_required @login_required
@@ -66,6 +66,18 @@ def index(request: HttpRequest):
) )
def index_update(request: HttpRequest):
search = BookmarkSearch.from_request(
request, request.GET, request.user_profile.search_preferences
)
bookmark_list = contexts.ActiveBookmarkListContext(request, search)
tag_cloud = contexts.ActiveTagCloudContext(request, search)
details = contexts.get_details_context(
request, contexts.ActiveBookmarkDetailsContext
)
return render_bookmarks_update(request, bookmark_list, tag_cloud, details)
@login_required @login_required
def archived(request: HttpRequest): def archived(request: HttpRequest):
if request.method == "POST": if request.method == "POST":
@@ -94,6 +106,18 @@ def archived(request: HttpRequest):
) )
def archived_update(request: HttpRequest):
search = BookmarkSearch.from_request(
request, request.GET, request.user_profile.search_preferences
)
bookmark_list = contexts.ArchivedBookmarkListContext(request, search)
tag_cloud = contexts.ArchivedTagCloudContext(request, search)
details = contexts.get_details_context(
request, contexts.ArchivedBookmarkDetailsContext
)
return render_bookmarks_update(request, bookmark_list, tag_cloud, details)
def shared(request: HttpRequest): def shared(request: HttpRequest):
if request.method == "POST": if request.method == "POST":
return search_action(request) return search_action(request)
@@ -124,16 +148,24 @@ def shared(request: HttpRequest):
) )
def shared_update(request: HttpRequest):
search = BookmarkSearch.from_request(
request, request.GET, request.user_profile.search_preferences
)
bookmark_list = contexts.SharedBookmarkListContext(request, search)
tag_cloud = contexts.SharedTagCloudContext(request, search)
details = contexts.get_details_context(
request, contexts.SharedBookmarkDetailsContext
)
return render_bookmarks_update(request, bookmark_list, tag_cloud, details)
def render_bookmarks_view(request: HttpRequest, template_name, context): def render_bookmarks_view(request: HttpRequest, template_name, context):
if context["details"]: if context["details"]:
context["page_title"] = "Bookmark details - Linkding" context["page_title"] = "Bookmark details - Linkding"
if turbo.is_frame(request, "details-modal"): if turbo.is_frame(request, "details-modal"):
return render( return turbo.frame(request, "bookmarks/details/modal.html", context)
request,
"bookmarks/updates/details-modal-frame.html",
context,
)
return render( return render(
request, request,
@@ -142,6 +174,30 @@ def render_bookmarks_view(request: HttpRequest, template_name, context):
) )
def render_bookmarks_update(request, bookmark_list, tag_cloud, details):
return turbo.stream(
turbo.update(
request,
"bookmark-list-container",
"bookmarks/bookmark_list.html",
{"bookmark_list": bookmark_list},
),
turbo.update(
request,
"tag-cloud-container",
"bookmarks/tag_cloud.html",
{"tag_cloud": tag_cloud},
),
turbo.replace(
request,
"details-modal",
"bookmarks/details/modal.html",
{"details": details},
method="morph",
),
)
def search_action(request: HttpRequest): def search_action(request: HttpRequest):
if "save" in request.POST: if "save" in request.POST:
if not request.user.is_authenticated: if not request.user.is_authenticated:
@@ -272,7 +328,7 @@ def index_action(request: HttpRequest):
return response return response
if turbo.accept(request): if turbo.accept(request):
return partials.active_bookmark_update(request) return index_update(request)
return utils.redirect_with_query(request, reverse("linkding:bookmarks.index")) return utils.redirect_with_query(request, reverse("linkding:bookmarks.index"))
@@ -289,7 +345,7 @@ def archived_action(request: HttpRequest):
return response return response
if turbo.accept(request): if turbo.accept(request):
return partials.archived_bookmark_update(request) return archived_update(request)
return utils.redirect_with_query(request, reverse("linkding:bookmarks.archived")) return utils.redirect_with_query(request, reverse("linkding:bookmarks.archived"))
@@ -304,7 +360,7 @@ def shared_action(request: HttpRequest):
return response return response
if turbo.accept(request): if turbo.accept(request):
return partials.shared_bookmark_update(request) return shared_update(request)
return utils.redirect_with_query(request, reverse("linkding:bookmarks.shared")) return utils.redirect_with_query(request, reverse("linkding:bookmarks.shared"))

View File

@@ -1,50 +0,0 @@
from bookmarks.models import BookmarkSearch
from bookmarks.views import contexts, turbo
def render_bookmark_update(request, bookmark_list, tag_cloud, details):
return turbo.stream(
request,
"bookmarks/updates/bookmark_view_stream.html",
{
"bookmark_list": bookmark_list,
"tag_cloud": tag_cloud,
"details": details,
},
)
def active_bookmark_update(request):
search = BookmarkSearch.from_request(
request, request.GET, request.user_profile.search_preferences
)
bookmark_list = contexts.ActiveBookmarkListContext(request, search)
tag_cloud = contexts.ActiveTagCloudContext(request, search)
details = contexts.get_details_context(
request, contexts.ActiveBookmarkDetailsContext
)
return render_bookmark_update(request, bookmark_list, tag_cloud, details)
def archived_bookmark_update(request):
search = BookmarkSearch.from_request(
request, request.GET, request.user_profile.search_preferences
)
bookmark_list = contexts.ArchivedBookmarkListContext(request, search)
tag_cloud = contexts.ArchivedTagCloudContext(request, search)
details = contexts.get_details_context(
request, contexts.ArchivedBookmarkDetailsContext
)
return render_bookmark_update(request, bookmark_list, tag_cloud, details)
def shared_bookmark_update(request):
search = BookmarkSearch.from_request(
request, request.GET, request.user_profile.search_preferences
)
bookmark_list = contexts.SharedBookmarkListContext(request, search)
tag_cloud = contexts.SharedTagCloudContext(request, search)
details = contexts.get_details_context(
request, contexts.SharedBookmarkDetailsContext
)
return render_bookmark_update(request, bookmark_list, tag_cloud, details)

View File

@@ -78,8 +78,14 @@ def tag_new(request: HttpRequest):
messages.success(request, f'Tag "{tag.name}" created successfully.') messages.success(request, f'Tag "{tag.name}" created successfully.')
return HttpResponseRedirect(reverse("linkding:tags.index")) return HttpResponseRedirect(reverse("linkding:tags.index"))
else: else:
return turbo.replace( return turbo.stream(
request, "tag-modal", "tags/new.html", {"form": form}, status=422 turbo.replace(
request,
"tag-modal",
"tags/new.html",
{"form": form},
method="morph",
)
) )
return render(request, "tags/new.html", {"form": form}) return render(request, "tags/new.html", {"form": form})
@@ -97,12 +103,14 @@ def tag_edit(request: HttpRequest, tag_id: int):
messages.success(request, f'Tag "{tag.name}" updated successfully.') messages.success(request, f'Tag "{tag.name}" updated successfully.')
return HttpResponseRedirect(reverse("linkding:tags.index")) return HttpResponseRedirect(reverse("linkding:tags.index"))
else: else:
return turbo.replace( return turbo.stream(
turbo.replace(
request, request,
"tag-modal", "tag-modal",
"tags/edit.html", "tags/edit.html",
{"tag": tag, "form": form}, {"tag": tag, "form": form},
status=422, method="morph",
)
) )
return render(request, "tags/edit.html", {"tag": tag, "form": form}) return render(request, "tags/edit.html", {"tag": tag, "form": form})
@@ -154,12 +162,14 @@ def tag_merge(request: HttpRequest):
return HttpResponseRedirect(reverse("linkding:tags.index")) return HttpResponseRedirect(reverse("linkding:tags.index"))
else: else:
return turbo.replace( return turbo.stream(
turbo.replace(
request, request,
"tag-modal", "tag-modal",
"tags/merge.html", "tags/merge.html",
{"form": form}, {"form": form},
status=422, method="morph",
)
) )
return render(request, "tags/merge.html", {"form": form}) return render(request, "tags/merge.html", {"form": form})

View File

@@ -1,5 +1,4 @@
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import render as django_render
from django.template import loader from django.template import loader
@@ -14,24 +13,49 @@ def is_frame(request: HttpRequest, frame: str) -> bool:
return request.headers.get("Turbo-Frame") == frame return request.headers.get("Turbo-Frame") == frame
def stream(request: HttpRequest, template_name: str, context: dict) -> HttpResponse: def frame(request: HttpRequest, template_name: str, context: dict) -> HttpResponse:
response = django_render(request, template_name, context) """
response["Content-Type"] = "text/vnd.turbo-stream.html" Renders the specified template into an HTML skeleton including <head> with
respective metadata. The template should only contain a frame. Used for
Turbo Frame requests that modify the top frame's URL.
"""
html = loader.render_to_string("shared/top_frame.html", context, request)
content = loader.render_to_string(template_name, context, request)
html = html.replace("<!--content-->", content)
response = HttpResponse(html, status=200)
return response return response
def update(
request: HttpRequest,
target: str,
template_name: str,
context: dict,
method: str | None = "",
) -> str:
"""Render a template wrapped in an update turbo-stream element."""
content = loader.render_to_string(template_name, context, request)
method_attr = f' method="{method}"' if method else ""
return f'<turbo-stream action="update"{method_attr} target="{target}"><template>{content}</template></turbo-stream>'
def replace( def replace(
request: HttpRequest, target_id: str, template_name: str, context: dict, status=None request: HttpRequest,
) -> HttpResponse: target: str,
""" template_name: str,
Returns a Turbo steam for replacing a specific target with the rendered context: dict,
template. Mostly useful for updating forms in place after failed submissions, method: str | None = "",
without having to create a separate template. ) -> str:
""" """Render a template wrapped in a replace turbo-stream element."""
if status is None:
status = 200
content = loader.render_to_string(template_name, context, request) content = loader.render_to_string(template_name, context, request)
stream_content = f'<turbo-stream action="replace" method="morph" target="{target_id}"><template>{content}</template></turbo-stream>' method_attr = f' method="{method}"' if method else ""
response = HttpResponse(stream_content, status=status) return f'<turbo-stream action="replace"{method_attr} target="{target}"><template>{content}</template></turbo-stream>'
response["Content-Type"] = "text/vnd.turbo-stream.html"
return response
def stream(*streams: str) -> HttpResponse:
"""Combine multiple stream elements into a turbo-stream response."""
return HttpResponse(
"\n".join(streams),
status=200,
content_type="text/vnd.turbo-stream.html",
)

View File

@@ -27,6 +27,7 @@ dev = [
"black>=25.1.0", "black>=25.1.0",
"coverage>=7.10.4", "coverage>=7.10.4",
"django-debug-toolbar>=6.0.0", "django-debug-toolbar>=6.0.0",
"djlint>=1.36.4",
"playwright>=1.54.0", "playwright>=1.54.0",
"psycopg[binary]>=3.2.9", "psycopg[binary]>=3.2.9",
"pytest>=8.4.1", "pytest>=8.4.1",
@@ -47,3 +48,12 @@ postgres = [
[tool.uv] [tool.uv]
# Prefer system Python for now, less complications when copying the venv in the Docker build # Prefer system Python for now, less complications when copying the venv in the Docker build
python-preference = "system" python-preference = "system"
[tool.djlint]
custom_html="ld-\\w+,ld-\\w+-\\w+"
indent=2
format_js=true
profile="django"
[tool.djlint.js]
indent_size=2

183
uv.lock generated
View File

@@ -245,6 +245,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" },
] ]
[[package]]
name = "cssbeautifier"
version = "1.15.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "editorconfig" },
{ name = "jsbeautifier" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f7/01/fdf41c1e5f93d359681976ba10410a04b299d248e28ecce1d4e88588dde4/cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5", size = 25376, upload-time = "2025-02-27T17:53:51.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/51/ef6c5628e46092f0a54c7cee69acc827adc6b6aab57b55d344fefbdf28f1/cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98", size = 123667, upload-time = "2025-02-27T17:53:43.594Z" },
]
[[package]] [[package]]
name = "django" name = "django"
version = "5.2.5" version = "5.2.5"
@@ -306,6 +320,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" }, { url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
] ]
[[package]]
name = "djlint"
version = "1.36.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "colorama" },
{ name = "cssbeautifier" },
{ name = "jsbeautifier" },
{ name = "json5" },
{ name = "pathspec" },
{ name = "pyyaml" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/89/ecf5be9f5c59a0c53bcaa29671742c5e269cc7d0e2622e3f65f41df251bf/djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1", size = 47849, upload-time = "2024-12-24T13:06:36.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/83/88b4c885812921739f5529a29085c3762705154d41caf7eb9a8886a3380c/djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2", size = 354384, upload-time = "2024-12-24T13:06:20.809Z" },
{ url = "https://files.pythonhosted.org/packages/32/38/67695f7a150b3d9d62fadb65242213d96024151570c3cf5d966effa68b0e/djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835", size = 322971, upload-time = "2024-12-24T13:06:22.185Z" },
{ url = "https://files.pythonhosted.org/packages/ac/7a/cd851393291b12e7fe17cf5d4d8874b8ea133aebbe9235f5314aabc96a52/djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f", size = 410972, upload-time = "2024-12-24T13:06:24.077Z" },
{ url = "https://files.pythonhosted.org/packages/6c/31/56469120394b970d4f079a552fde21ed27702ca729595ab0ed459eb6d240/djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4", size = 362053, upload-time = "2024-12-24T13:06:25.432Z" },
{ url = "https://files.pythonhosted.org/packages/4b/67/f7aeea9be6fb3bd984487af8d0d80225a0b1e5f6f7126e3332d349fb13fe/djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd", size = 52290, upload-time = "2024-12-24T13:06:33.76Z" },
]
[[package]]
name = "editorconfig"
version = "0.17.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/3a/a61d9a1f319a186b05d14df17daea42fcddea63c213bcd61a929fb3a6796/editorconfig-0.17.1.tar.gz", hash = "sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745", size = 14695, upload-time = "2025-06-09T08:21:37.097Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", size = 16360, upload-time = "2025-06-09T08:21:35.654Z" },
]
[[package]] [[package]]
name = "execnet" name = "execnet"
version = "2.1.1" version = "2.1.1"
@@ -379,6 +426,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/0e/e248f059986e376b87e546cd840d244370b9f7ef2b336456f0c2cfb2fef0/josepy-2.1.0-py3-none-any.whl", hash = "sha256:0eadf09b96821bdae9a8b14145425cb9fe0bbee64c6fdfce3ccd4ceb7d7efbbd", size = 29065, upload-time = "2025-07-08T17:20:53.504Z" }, { url = "https://files.pythonhosted.org/packages/47/0e/e248f059986e376b87e546cd840d244370b9f7ef2b336456f0c2cfb2fef0/josepy-2.1.0-py3-none-any.whl", hash = "sha256:0eadf09b96821bdae9a8b14145425cb9fe0bbee64c6fdfce3ccd4ceb7d7efbbd", size = 29065, upload-time = "2025-07-08T17:20:53.504Z" },
] ]
[[package]]
name = "jsbeautifier"
version = "1.15.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "editorconfig" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/98/d6cadf4d5a1c03b2136837a435682418c29fdeb66be137128544cecc5b7a/jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592", size = 75257, upload-time = "2025-02-27T17:53:53.252Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528", size = 94707, upload-time = "2025-02-27T17:53:46.152Z" },
]
[[package]]
name = "json5"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/12/ae/929aee9619e9eba9015207a9d2c1c54db18311da7eb4dcf6d41ad6f0eb67/json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990", size = 52191, upload-time = "2025-08-12T19:47:42.583Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/e2/05328bd2621be49a6fed9e3030b1e51a2d04537d3f816d211b9cc53c5262/json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5", size = 36119, upload-time = "2025-08-12T19:47:41.131Z" },
]
[[package]] [[package]]
name = "linkding" name = "linkding"
version = "1.44.2" version = "1.44.2"
@@ -406,6 +475,7 @@ dev = [
{ name = "black" }, { name = "black" },
{ name = "coverage" }, { name = "coverage" },
{ name = "django-debug-toolbar" }, { name = "django-debug-toolbar" },
{ name = "djlint" },
{ name = "playwright" }, { name = "playwright" },
{ name = "psycopg", extra = ["binary"] }, { name = "psycopg", extra = ["binary"] },
{ name = "pytest" }, { name = "pytest" },
@@ -440,6 +510,7 @@ dev = [
{ name = "black", specifier = ">=25.1.0" }, { name = "black", specifier = ">=25.1.0" },
{ name = "coverage", specifier = ">=7.10.4" }, { name = "coverage", specifier = ">=7.10.4" },
{ name = "django-debug-toolbar", specifier = ">=6.0.0" }, { name = "django-debug-toolbar", specifier = ">=6.0.0" },
{ name = "djlint", specifier = ">=1.36.4" },
{ name = "playwright", specifier = ">=1.54.0" }, { name = "playwright", specifier = ">=1.54.0" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
{ name = "pytest", specifier = ">=8.4.1" }, { name = "pytest", specifier = ">=8.4.1" },
@@ -663,6 +734,106 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
] ]
[[package]]
name = "pyyaml"
version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "regex"
version = "2025.11.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" },
{ url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" },
{ url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" },
{ url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" },
{ url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" },
{ url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" },
{ url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" },
{ url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" },
{ url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" },
{ url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" },
{ url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" },
{ url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" },
{ url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" },
{ url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" },
{ url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" },
{ url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" },
{ url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" },
{ url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" },
{ url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" },
{ url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" },
{ url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" },
{ url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" },
{ url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" },
{ url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" },
{ url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" },
{ url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" },
{ url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" },
{ url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" },
{ url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" },
{ url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" },
{ url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" },
{ url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" },
{ url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" },
{ url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" },
{ url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" },
{ url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" },
{ url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" },
{ url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" },
{ url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" },
{ url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" },
{ url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" },
{ url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" },
{ url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" },
{ url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" },
{ url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" },
{ url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" },
{ url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" },
{ url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.5" version = "2.32.5"
@@ -714,6 +885,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/65/5e726c372da8a5e35022a94388b12252710aad0c2351699c3d76ae8dba78/supervisor-4.3.0-py2.py3-none-any.whl", hash = "sha256:0bcb763fddafba410f35cbde226aa7f8514b9fb82eb05a0c85f6588d1c13f8db", size = 320736, upload-time = "2025-08-23T18:25:00.767Z" }, { url = "https://files.pythonhosted.org/packages/0e/65/5e726c372da8a5e35022a94388b12252710aad0c2351699c3d76ae8dba78/supervisor-4.3.0-py2.py3-none-any.whl", hash = "sha256:0bcb763fddafba410f35cbde226aa7f8514b9fb82eb05a0c85f6588d1c13f8db", size = 320736, upload-time = "2025-08-23T18:25:00.767Z" },
] ]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.14.1" version = "4.14.1"