mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-27 22:43:15 +08:00
Template improvements
This commit is contained in:
1
Makefile
1
Makefile
@@ -16,6 +16,7 @@ test:
|
||||
|
||||
format:
|
||||
uv run black bookmarks
|
||||
uv run djlint bookmarks/templates --reformat --quiet
|
||||
npx prettier bookmarks/frontend --write
|
||||
npx prettier bookmarks/styles --write
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ make test
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
|
||||
{% block content %}
|
||||
<table style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Args</th>
|
||||
<th>Retries</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Args</th>
|
||||
<th>Retries</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for task in tasks %}
|
||||
<tr>
|
||||
<td>{{ task.id }}</td>
|
||||
<td>{{ task.name }}</td>
|
||||
<td>{{ task.args }}</td>
|
||||
<td>{{ task.retries }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for task in tasks %}
|
||||
<tr>
|
||||
<td>{{ task.id }}</td>
|
||||
<td>{{ task.name }}</td>
|
||||
<td>{{ task.args }}</td>
|
||||
<td>{{ task.retries }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="paginator">
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
{% extends "shared/layout.html" %}
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
{% load bookmarks %}
|
||||
|
||||
{% block content %}
|
||||
<ld-bookmark-page
|
||||
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
|
||||
|
||||
<ld-bookmark-page class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
|
||||
{# Bookmark list #}
|
||||
<main class="main col-2" aria-labelledby="main-heading">
|
||||
<div class="section-header mb-0">
|
||||
@@ -19,19 +16,15 @@
|
||||
</ld-filter-drawer-trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions"
|
||||
action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post" autocomplete="off">
|
||||
method="post"
|
||||
autocomplete="off">
|
||||
{% csrf_token %}
|
||||
{% 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>
|
||||
</main>
|
||||
|
||||
{# Filters #}
|
||||
<div class="side-panel col-1 hide-md">
|
||||
{% include 'bookmarks/bundle_section.html' %}
|
||||
@@ -39,12 +32,6 @@
|
||||
</div>
|
||||
</ld-bookmark-page>
|
||||
{% endblock %}
|
||||
|
||||
{% block overlays %}
|
||||
{# Bookmark details #}
|
||||
<turbo-frame id="details-modal" target="_top">
|
||||
{% if details %}
|
||||
{% include 'bookmarks/details/modal.html' %}
|
||||
{% endif %}
|
||||
</turbo-frame>
|
||||
{% include 'bookmarks/details/modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
{% load pagination %}
|
||||
|
||||
{% if bookmark_list.is_empty %}
|
||||
{% include 'bookmarks/empty_bookmarks.html' %}
|
||||
{% else %}
|
||||
<section aria-label="Bookmark list">
|
||||
<ul class="bookmark-list{% if bookmark_list.show_notes %} show-notes{% endif %}"
|
||||
role="list" tabindex="-1"
|
||||
style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }};"
|
||||
role="list"
|
||||
tabindex="-1"
|
||||
style="--ld-bookmark-description-max-lines:{{ bookmark_list.description_max_lines }}"
|
||||
data-bookmarks-total="{{ bookmark_list.bookmarks_total }}">
|
||||
{% 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 %}>
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
@@ -24,41 +25,35 @@
|
||||
{% if bookmark_item.favicon_file and bookmark_list.show_favicons %}
|
||||
<img class="favicon" src="{% static bookmark_item.favicon_file %}" alt="">
|
||||
{% 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>
|
||||
</a>
|
||||
</div>
|
||||
{% if bookmark_list.show_url %}
|
||||
<div class="url-path truncate">
|
||||
<a href="{{ bookmark_item.url }}" target="{{ bookmark_list.link_target }}" rel="noopener"
|
||||
class="url-display">
|
||||
{{ bookmark_item.url }}
|
||||
</a>
|
||||
<a href="{{ bookmark_item.url }}"
|
||||
target="{{ bookmark_list.link_target }}"
|
||||
rel="noopener"
|
||||
class="url-display">{{ bookmark_item.url }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if bookmark_list.description_display == 'inline' %}
|
||||
<div class="description inline truncate">
|
||||
{% if bookmark_item.tags %}
|
||||
<span class="tags">
|
||||
{% for tag in bookmark_item.tags %}
|
||||
<a href="?{{ tag.query_string }}">#{{ tag.name }}</a>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if bookmark_item.tags and bookmark_item.description %} | {% endif %}
|
||||
{% if bookmark_item.description %}
|
||||
<span>{{ bookmark_item.description }}</span>
|
||||
{% for tag in bookmark_item.tags %}<a href="?{{ tag.query_string }}">#{{ tag.name }}</a>{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if bookmark_item.tags and bookmark_item.description %}|{% endif %}
|
||||
{% if bookmark_item.description %}<span>{{ bookmark_item.description }}</span>{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if bookmark_item.description %}
|
||||
<div class="description separate">{{ bookmark_item.description }}</div>
|
||||
{% endif %}
|
||||
{% if bookmark_item.description %}<div class="description separate">{{ bookmark_item.description }}</div>{% endif %}
|
||||
{% if bookmark_item.tags %}
|
||||
<div class="tags">
|
||||
{% for tag in bookmark_item.tags %}
|
||||
<a href="?{{ tag.query_string }}">#{{ tag.name }}</a>
|
||||
{% endfor %}
|
||||
{% for tag in bookmark_item.tags %}<a href="?{{ tag.query_string }}">#{{ tag.name }}</a>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -73,21 +68,19 @@
|
||||
<a href="{{ bookmark_item.snapshot_url }}"
|
||||
title="{{ bookmark_item.snapshot_title }}"
|
||||
target="{{ bookmark_list.link_target }}"
|
||||
rel="noopener">
|
||||
{{ bookmark_item.display_date }}
|
||||
</a>
|
||||
rel="noopener">{{ bookmark_item.display_date }}</a>
|
||||
{% else %}
|
||||
<span>{{ bookmark_item.display_date }}</span>
|
||||
{% endif %}
|
||||
{% if not bookmark_list.is_preview %}
|
||||
<span>|</span>
|
||||
{% endif %}
|
||||
{% if not bookmark_list.is_preview %}<span>|</span>{% endif %}
|
||||
{% endif %}
|
||||
{% if not bookmark_list.is_preview %}
|
||||
{# View link is visible for both owned and shared bookmarks #}
|
||||
{% if bookmark_list.show_view_action %}
|
||||
<a href="{{ bookmark_item.details_url }}" class="view-action"
|
||||
data-turbo-action="replace" data-turbo-frame="details-modal">View</a>
|
||||
<a href="{{ bookmark_item.details_url }}"
|
||||
class="view-action"
|
||||
data-turbo-action="replace"
|
||||
data-turbo-frame="details-modal">View</a>
|
||||
{% endif %}
|
||||
{% if bookmark_item.is_editable %}
|
||||
{# Bookmark owner actions #}
|
||||
@@ -96,33 +89,40 @@
|
||||
{% endif %}
|
||||
{% if bookmark_list.show_archive_action %}
|
||||
{% if bookmark_item.is_archived %}
|
||||
<button type="submit" name="unarchive" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Unarchive
|
||||
</button>
|
||||
<button type="submit"
|
||||
name="unarchive"
|
||||
value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Unarchive</button>
|
||||
{% else %}
|
||||
<button type="submit" name="archive" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Archive
|
||||
</button>
|
||||
<button type="submit"
|
||||
name="archive"
|
||||
value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Archive</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if bookmark_list.show_remove_action %}
|
||||
<button data-confirm type="submit" name="remove" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Remove
|
||||
</button>
|
||||
<button data-confirm
|
||||
type="submit"
|
||||
name="remove"
|
||||
value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm">Remove</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# Shared bookmark actions #}
|
||||
<span>Shared by
|
||||
<a href="?{% replace_query_param user=bookmark_item.owner.username %}">{{ bookmark_item.owner.username }}</a>
|
||||
</span>
|
||||
<a href="?{% replace_query_param user=bookmark_item.owner.username %}">{{ bookmark_item.owner.username }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if bookmark_item.has_extra_actions %}
|
||||
<div class="extra-actions">
|
||||
<span class="hide-sm">|</span>
|
||||
{% 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"
|
||||
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">
|
||||
<use xlink:href="#ld-icon-unread"></use>
|
||||
</svg>
|
||||
@@ -130,9 +130,12 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
{% 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"
|
||||
data-confirm data-confirm-question="Unshare?">
|
||||
data-confirm
|
||||
data-confirm-question="Unshare?">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<use xlink:href="#ld-icon-share"></use>
|
||||
</svg>
|
||||
@@ -154,19 +157,22 @@
|
||||
</div>
|
||||
{% if bookmark_list.show_preview_images %}
|
||||
{% 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 %}
|
||||
<div class="preview-image placeholder">
|
||||
<div class="img"/>
|
||||
</div>
|
||||
<div class="img" /></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="bookmark-pagination{% if request.user_profile.sticky_pagination %} sticky{% endif %}">
|
||||
{% pagination bookmark_list.bookmarks_page %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
<script>
|
||||
document.dispatchEvent(new CustomEvent('bookmark-list-updated'));
|
||||
</script>
|
||||
|
||||
@@ -1,42 +1,41 @@
|
||||
{% load shared %}
|
||||
{% htmlmin %}
|
||||
<div class="bulk-edit-bar">
|
||||
<div class="bulk-edit-actions">
|
||||
<label class="form-checkbox bulk-edit-checkbox all">
|
||||
<input type="checkbox">
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
<select name="bulk_action" class="form-select select-sm">
|
||||
{% if not 'bulk_archive' in disable_actions %}
|
||||
<option value="bulk_archive">Archive</option>
|
||||
{% endif %}
|
||||
{% if not 'bulk_unarchive' in disable_actions %}
|
||||
<option value="bulk_unarchive">Unarchive</option>
|
||||
{% endif %}
|
||||
<option value="bulk_delete">Delete</option>
|
||||
<option value="bulk_tag">Add tags</option>
|
||||
<option value="bulk_untag">Remove tags</option>
|
||||
<option value="bulk_read">Mark as read</option>
|
||||
<option value="bulk_unread">Mark as unread</option>
|
||||
{% if request.user_profile.enable_sharing %}
|
||||
<option value="bulk_share">Share</option>
|
||||
<option value="bulk_unshare">Unshare</option>
|
||||
{% endif %}
|
||||
<option value="bulk_refresh">Refresh from website</option>
|
||||
{% if bookmark_list.snapshot_feature_enabled %}
|
||||
<option value="bulk_snapshot">Create HTML snapshot</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<ld-tag-autocomplete input-name="bulk_tag_string" 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>
|
||||
</button>
|
||||
|
||||
<label class="form-checkbox select-across d-none">
|
||||
<input type="checkbox" name="bulk_select_across">
|
||||
<i class="form-icon"></i>
|
||||
All pages (<span class="total">{{ bookmark_list.bookmarks_total }}</span> bookmarks)
|
||||
</label>
|
||||
</div>
|
||||
<div class="bulk-edit-bar">
|
||||
<div class="bulk-edit-actions">
|
||||
<label class="form-checkbox bulk-edit-checkbox all">
|
||||
<input type="checkbox">
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
<select name="bulk_action" class="form-select select-sm">
|
||||
{% if not 'bulk_archive' in disable_actions %}<option value="bulk_archive">Archive</option>{% endif %}
|
||||
{% if not 'bulk_unarchive' in disable_actions %}<option value="bulk_unarchive">Unarchive</option>{% endif %}
|
||||
<option value="bulk_delete">Delete</option>
|
||||
<option value="bulk_tag">Add tags</option>
|
||||
<option value="bulk_untag">Remove tags</option>
|
||||
<option value="bulk_read">Mark as read</option>
|
||||
<option value="bulk_unread">Mark as unread</option>
|
||||
{% if request.user_profile.enable_sharing %}
|
||||
<option value="bulk_share">Share</option>
|
||||
<option value="bulk_unshare">Unshare</option>
|
||||
{% endif %}
|
||||
<option value="bulk_refresh">Refresh from website</option>
|
||||
{% if bookmark_list.snapshot_feature_enabled %}<option value="bulk_snapshot">Create HTML snapshot</option>{% endif %}
|
||||
</select>
|
||||
<ld-tag-autocomplete input-name="bulk_tag_string"
|
||||
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>
|
||||
</button>
|
||||
<label class="form-checkbox select-across d-none">
|
||||
<input type="checkbox" name="bulk_select_across">
|
||||
<i class="form-icon"></i>
|
||||
All pages (<span class="total">{{ bookmark_list.bookmarks_total }}</span> bookmarks)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endhtmlmin %}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<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"
|
||||
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 xmlns="http://www.w3.org/2000/svg"
|
||||
width="20px"
|
||||
height="20px"
|
||||
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="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>
|
||||
</button>
|
||||
|
||||
@@ -4,12 +4,19 @@
|
||||
<h2 id="bundles-heading">Bundles</h2>
|
||||
<ld-dropdown class="dropdown dropdown-right ml-auto">
|
||||
<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"
|
||||
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 xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
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="M4 6l16 0" />
|
||||
<path d="M4 12l16 0" />
|
||||
<path d="M4 18l16 0" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="menu" role="list" tabindex="-1">
|
||||
@@ -18,8 +25,9 @@
|
||||
</li>
|
||||
{% if bookmark_list.search.q %}
|
||||
<li class="menu-item">
|
||||
<a href="{% url 'linkding:bundles.new' %}?q={{ bookmark_list.search.q|urlencode }}" class="menu-link">Create
|
||||
bundle from search</a>
|
||||
<a href="{% url 'linkding:bundles.new' %}?q={{ bookmark_list.search.q|urlencode }}"
|
||||
class="menu-link">Create
|
||||
bundle from search</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
|
||||
{% extends "shared/layout.html" %}
|
||||
{% block content %}
|
||||
<script type="application/javascript">
|
||||
window.close()
|
||||
</script>
|
||||
<p>You can now close this window.</p>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,42 +1,70 @@
|
||||
{% 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"
|
||||
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="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 xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
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="M14 3v4a1 1 0 0 0 1 1h4" />
|
||||
<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>
|
||||
{% 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"
|
||||
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="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 xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
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="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>
|
||||
{% 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"
|
||||
stroke="currentColor" 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 xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
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="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>
|
||||
{% else %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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="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 xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
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="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>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -3,49 +3,55 @@
|
||||
<div class="item-list assets">
|
||||
{% for asset in details.assets %}
|
||||
<div class="list-item" data-asset-id="{{ asset.id }}">
|
||||
<div class="list-item-icon {{ asset.icon_classes }}">
|
||||
{% include 'bookmarks/details/asset_icon.html' %}
|
||||
</div>
|
||||
<div class="list-item-icon {{ asset.icon_classes }}">{% include 'bookmarks/details/asset_icon.html' %}</div>
|
||||
<div class="list-item-text {{ asset.text_classes }}">
|
||||
<span class="truncate">
|
||||
{{ asset.display_name }}
|
||||
{% if asset.status == 'pending' %}(queued){% endif %}
|
||||
{% if asset.status == 'failure' %}(failed){% endif %}
|
||||
</span>
|
||||
{% if asset.file_size %}
|
||||
<span class="filesize">{{ asset.file_size|filesizeformat }}</span>
|
||||
{% endif %}
|
||||
<span class="truncate">
|
||||
{{ asset.display_name }}
|
||||
{% if asset.status == 'pending' %}(queued){% endif %}
|
||||
{% if asset.status == 'failure' %}(failed){% endif %}
|
||||
</span>
|
||||
{% if asset.file_size %}<span class="filesize">{{ asset.file_size|filesizeformat }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="list-item-actions">
|
||||
{% 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 %}
|
||||
{% if details.is_editable %}
|
||||
<button data-confirm type="submit" name="remove_asset" value="{{ asset.id }}" class="btn btn-link">
|
||||
Remove
|
||||
</button>
|
||||
<button data-confirm
|
||||
type="submit"
|
||||
name="remove_asset"
|
||||
value="{{ asset.id }}"
|
||||
class="btn btn-link">Remove</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if details.is_editable %}
|
||||
<div class="assets-actions">
|
||||
{% if details.snapshots_enabled %}
|
||||
<button type="submit" name="create_html_snapshot" value="{{ details.bookmark.id }}" class="btn btn-sm"
|
||||
{% if details.has_pending_assets %}disabled{% endif %}>Create HTML snapshot
|
||||
</button>
|
||||
<button type="submit"
|
||||
name="create_html_snapshot"
|
||||
value="{{ details.bookmark.id }}"
|
||||
class="btn btn-sm"
|
||||
{% if details.has_pending_assets %}disabled{% endif %}>Create HTML snapshot</button>
|
||||
{% endif %}
|
||||
{% if details.uploads_enabled %}
|
||||
<ld-upload-button>
|
||||
<button id="upload-asset" name="upload_asset" value="{{ details.bookmark.id }}" type="submit"
|
||||
class="btn btn-sm">Upload file
|
||||
</button>
|
||||
<input id="upload-asset-file" name="upload_asset_file" type="file" class="d-hide">
|
||||
<button id="upload-asset"
|
||||
name="upload_asset"
|
||||
value="{{ details.bookmark.id }}"
|
||||
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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
|
||||
<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 %}
|
||||
<input type="hidden" name="update_state" value="{{ details.bookmark.id }}">
|
||||
|
||||
<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 }}">
|
||||
{% 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 %}
|
||||
<span>{{ details.bookmark.url }}</span>
|
||||
</a>
|
||||
{% 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 }}">
|
||||
{% if details.show_link_icons %}
|
||||
<svg class="favicon" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -26,13 +31,14 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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 }}">
|
||||
{% if details.show_link_icons %}
|
||||
<svg class="favicon" viewBox="0 0 76 86" xmlns="http://www.w3.org/2000/svg">
|
||||
<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 class="favicon"
|
||||
viewBox="0 0 76 86"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<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>
|
||||
{% endif %}
|
||||
<span>Internet Archive</span>
|
||||
@@ -41,7 +47,7 @@
|
||||
</div>
|
||||
{% if details.preview_image_enabled and details.bookmark.preview_image_file %}
|
||||
<div class="preview-image">
|
||||
<img src="{% static details.bookmark.preview_image_file %}" alt=""/>
|
||||
<img src="{% static details.bookmark.preview_image_file %}" alt="" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="sections grid columns-2 columns-sm-1 gap-0">
|
||||
@@ -51,14 +57,18 @@
|
||||
<div class="d-flex" style="gap: .8rem">
|
||||
<div class="form-group">
|
||||
<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 %}>
|
||||
<i class="form-icon"></i> Archived
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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 %}>
|
||||
<i class="form-icon"></i> Unread
|
||||
</label>
|
||||
@@ -66,7 +76,9 @@
|
||||
{% if details.profile.enable_sharing %}
|
||||
<div class="form-group">
|
||||
<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 %}>
|
||||
<i class="form-icon"></i> Shared
|
||||
</label>
|
||||
@@ -77,9 +89,7 @@
|
||||
{% endif %}
|
||||
<section class="files col-2">
|
||||
<h3>Files</h3>
|
||||
<div>
|
||||
{% include 'bookmarks/details/assets.html' %}
|
||||
</div>
|
||||
<div>{% include 'bookmarks/details/assets.html' %}</div>
|
||||
</section>
|
||||
{% if details.bookmark.tag_names %}
|
||||
<section class="tags col-1">
|
||||
@@ -111,4 +121,4 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</ld-form>
|
||||
</ld-form>
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
<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">
|
||||
<div class="modal-overlay" data-close-modal></div>
|
||||
<div class="modal-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title=details.bookmark.resolved_title %}
|
||||
<div class="modal-body">
|
||||
{% include 'bookmarks/details/form.html' %}
|
||||
</div>
|
||||
{% if details.is_editable %}
|
||||
<div class="modal-footer">
|
||||
<div class="actions">
|
||||
<div class="left-actions">
|
||||
<a class="btn btn-wide"
|
||||
href="{% url 'linkding:bookmarks.edit' details.bookmark.id %}?return_url={{ details.edit_return_url|urlencode }}">Edit</a>
|
||||
</div>
|
||||
<div class="right-actions">
|
||||
<form action="{{ details.delete_url }}" method="post" data-turbo-action="replace">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="disable_turbo" value="true">
|
||||
<button data-confirm class="btn btn-error btn-wide"
|
||||
type="submit" name="remove" value="{{ details.bookmark.id }}">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
<turbo-frame id="details-modal" target="_top">
|
||||
{% 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">
|
||||
<div class="modal-overlay" data-close-modal></div>
|
||||
<div class="modal-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title=details.bookmark.resolved_title %}
|
||||
<div class="modal-body">{% include 'bookmarks/details/form.html' %}</div>
|
||||
{% if details.is_editable %}
|
||||
<div class="modal-footer">
|
||||
<div class="actions">
|
||||
<div class="left-actions">
|
||||
<a class="btn btn-wide"
|
||||
href="{% url 'linkding:bookmarks.edit' details.bookmark.id %}?return_url={{ details.edit_return_url|urlencode }}">Edit</a>
|
||||
</div>
|
||||
<div class="right-actions">
|
||||
<form action="{{ details.delete_url }}"
|
||||
method="post"
|
||||
data-turbo-action="replace">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="disable_turbo" value="true">
|
||||
<button data-confirm
|
||||
class="btn btn-error btn-wide"
|
||||
type="submit"
|
||||
name="remove"
|
||||
value="{{ details.bookmark.id }}">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</ld-details-modal>
|
||||
{% endif %}
|
||||
</div>
|
||||
</ld-details-modal>
|
||||
{% endif %}
|
||||
</turbo-frame>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% block head %}
|
||||
{% with page_title="Edit bookmark - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Edit bookmark - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bookmarks-form-page">
|
||||
<main aria-labelledby="main-heading">
|
||||
@@ -13,7 +9,8 @@
|
||||
<h1 id="main-heading">Edit bookmark</h1>
|
||||
</div>
|
||||
<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>
|
||||
{% include 'bookmarks/form.html' %}
|
||||
</form>
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
<p class="empty-subtitle">
|
||||
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.integrations' %}">browser extension</a> or the <a
|
||||
href="{% url 'linkding:settings.integrations' %}">bookmarklet</a>.
|
||||
<a href="{% url 'linkding:settings.integrations' %}">browser extension</a> or the <a href="{% url 'linkding:settings.integrations' %}">bookmarklet</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
|
||||
<div class="bookmarks-form">
|
||||
{% csrf_token %}
|
||||
{{ form.auto_close|attr:"type:hidden" }}
|
||||
@@ -11,11 +10,7 @@
|
||||
{{ form.url|form_field:"validation"|add_class:"form-input"|attr:"autofocus" }}
|
||||
<i class="form-icon loading"></i>
|
||||
</div>
|
||||
{% if form.url.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.url.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.url.errors %}<div class="form-input-hint">{{ form.url.errors }}</div>{% endif %}
|
||||
<div class="form-input-hint bookmark-exists">
|
||||
This URL is already bookmarked.
|
||||
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 class="form-group">
|
||||
<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">
|
||||
</ld-tag-autocomplete>
|
||||
<div id="{{ form.tag_string.auto_id }}_help" class="form-input-hint">
|
||||
@@ -39,9 +36,7 @@
|
||||
<div class="flex">
|
||||
<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 }}">
|
||||
<button class="ml-2 btn btn-link suffix-button" type="button">
|
||||
Clear
|
||||
</button>
|
||||
<button class="ml-2 btn btn-link suffix-button" type="button">Clear</button>
|
||||
</ld-clear-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,9 +47,7 @@
|
||||
<div class="d-flex justify-between align-baseline">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
|
||||
<ld-clear-button data-for="{{ form.description.id_for_label }}">
|
||||
<button class="btn btn-link suffix-button" type="button">
|
||||
Clear
|
||||
</button>
|
||||
<button class="btn btn-link suffix-button" type="button">Clear</button>
|
||||
</ld-clear-button>
|
||||
</div>
|
||||
{{ form.description|add_class:"form-input"|attr:"rows:3" }}
|
||||
@@ -67,9 +60,7 @@
|
||||
</summary>
|
||||
<label for="{{ form.notes.id_for_label }}" class="text-assistive">Notes</label>
|
||||
{{ form.notes|form_field:"help"|add_class:"form-input"|attr:"rows:8" }}
|
||||
<div id="{{ form.notes.auto_id }}_help" class="form-input-hint">
|
||||
Additional notes, supports Markdown.
|
||||
</div>
|
||||
<div id="{{ form.notes.auto_id }}_help" class="form-input-hint">Additional notes, supports Markdown.</div>
|
||||
</details>
|
||||
{{ form.notes.errors }}
|
||||
</div>
|
||||
@@ -104,151 +95,153 @@
|
||||
{% if form.is_auto_close %}
|
||||
<input type="submit" value="Save and close" class="btn btn-primary btn-wide">
|
||||
{% 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 %}
|
||||
<a href="{{ return_url }}" class="btn">Cancel</a>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
/**
|
||||
* - Pre-fill title and description with metadata from website as soon as URL changes
|
||||
* - Show hint if URL is already bookmarked
|
||||
*/
|
||||
(function init() {
|
||||
const urlInput = document.getElementById('{{ form.url.id_for_label }}');
|
||||
const titleInput = document.getElementById('{{ form.title.id_for_label }}');
|
||||
const descriptionInput = document.getElementById('{{ form.description.id_for_label }}');
|
||||
const notesDetails = document.querySelector('form details.notes');
|
||||
const notesInput = document.getElementById('{{ form.notes.id_for_label }}');
|
||||
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
|
||||
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
|
||||
const editedBookmarkId = {{ form.instance.id|default:0 }};
|
||||
let isTitleModified = !!titleInput.value;
|
||||
let isDescriptionModified = !!descriptionInput.value;
|
||||
/**
|
||||
* - Pre-fill title and description with metadata from website as soon as URL changes
|
||||
* - Show hint if URL is already bookmarked
|
||||
*/
|
||||
(function init() {
|
||||
const urlInput = document.getElementById('{{ form.url.id_for_label }}');
|
||||
const titleInput = document.getElementById('{{ form.title.id_for_label }}');
|
||||
const descriptionInput = document.getElementById('{{ form.description.id_for_label }}');
|
||||
const notesDetails = document.querySelector('form details.notes');
|
||||
const notesInput = document.getElementById('{{ form.notes.id_for_label }}');
|
||||
const unreadCheckbox = document.getElementById('{{ form.unread.id_for_label }}');
|
||||
const sharedCheckbox = document.getElementById('{{ form.shared.id_for_label }}');
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
const bookmarkExistsHint = document.querySelector('.form-input-hint.bookmark-exists');
|
||||
const editedBookmarkId = parseInt('{{ form.instance.id|default:0 }}');
|
||||
let isTitleModified = !!titleInput.value;
|
||||
let isDescriptionModified = !!descriptionInput.value;
|
||||
|
||||
function toggleLoadingIcon(input, show) {
|
||||
const icon = input.parentNode.querySelector('i.form-icon');
|
||||
icon.style['visibility'] = show ? 'visible' : 'hidden';
|
||||
}
|
||||
function toggleLoadingIcon(input, show) {
|
||||
const icon = input.parentNode.querySelector('i.form-icon');
|
||||
icon.style['visibility'] = show ? 'visible' : 'hidden';
|
||||
}
|
||||
|
||||
function updateInput(input, value) {
|
||||
if (!input) {
|
||||
return;
|
||||
function updateInput(input, value) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event('value-changed'));
|
||||
}
|
||||
|
||||
function updateCheckbox(input, value) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
input.checked = value;
|
||||
}
|
||||
|
||||
function checkUrl() {
|
||||
if (!urlInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleLoadingIcon(urlInput, true);
|
||||
|
||||
const websiteUrl = encodeURIComponent(urlInput.value);
|
||||
const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}`;
|
||||
fetch(requestUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const metadata = data.metadata;
|
||||
toggleLoadingIcon(urlInput, false);
|
||||
|
||||
// Display hint if URL is already bookmarked
|
||||
const existingBookmark = data.bookmark;
|
||||
bookmarkExistsHint.style['display'] = existingBookmark ? 'block' : 'none';
|
||||
refreshButton.style['display'] = existingBookmark ? 'inline-block' : 'none';
|
||||
|
||||
// Prefill form with existing bookmark data
|
||||
if (existingBookmark) {
|
||||
// Workaround: tag input will be replaced by tag autocomplete, so
|
||||
// defer getting the input until we need it
|
||||
const tagsInput = document.getElementById('{{ form.tag_string.id_for_label }}');
|
||||
|
||||
bookmarkExistsHint.style['display'] = 'block';
|
||||
notesDetails.open = !!existingBookmark.notes;
|
||||
updateInput(titleInput, existingBookmark.title);
|
||||
updateInput(descriptionInput, existingBookmark.description);
|
||||
updateInput(notesInput, existingBookmark.notes);
|
||||
updateInput(tagsInput, existingBookmark.tag_names.join(" "));
|
||||
updateCheckbox(unreadCheckbox, existingBookmark.unread);
|
||||
updateCheckbox(sharedCheckbox, existingBookmark.shared);
|
||||
} else {
|
||||
// Update title and description with website metadata, unless they have been modified
|
||||
if (!isTitleModified) {
|
||||
updateInput(titleInput, metadata.title);
|
||||
}
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event('value-changed'));
|
||||
}
|
||||
|
||||
function updateCheckbox(input, value) {
|
||||
if (!input) {
|
||||
return;
|
||||
if (!isDescriptionModified) {
|
||||
updateInput(descriptionInput, metadata.description);
|
||||
}
|
||||
input.checked = value;
|
||||
}
|
||||
}
|
||||
|
||||
function checkUrl() {
|
||||
if (!urlInput.value) {
|
||||
return;
|
||||
}
|
||||
// Preview auto tags
|
||||
const autoTags = data.auto_tags;
|
||||
const autoTagsHint = document.querySelector('.form-input-hint.auto-tags');
|
||||
|
||||
toggleLoadingIcon(urlInput, true);
|
||||
if (autoTags.length > 0) {
|
||||
autoTags.sort();
|
||||
autoTagsHint.style['display'] = 'block';
|
||||
autoTagsHint.innerHTML = `Auto tags: ${autoTags.join(" ")}`;
|
||||
} else {
|
||||
autoTagsHint.style['display'] = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const websiteUrl = encodeURIComponent(urlInput.value);
|
||||
const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}`;
|
||||
fetch(requestUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const metadata = data.metadata;
|
||||
toggleLoadingIcon(urlInput, false);
|
||||
function refreshMetadata() {
|
||||
if (!urlInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display hint if URL is already bookmarked
|
||||
const existingBookmark = data.bookmark;
|
||||
bookmarkExistsHint.style['display'] = existingBookmark ? 'block' : 'none';
|
||||
refreshButton.style['display'] = existingBookmark ? 'inline-block' : 'none';
|
||||
toggleLoadingIcon(urlInput, true);
|
||||
|
||||
// Prefill form with existing bookmark data
|
||||
if (existingBookmark) {
|
||||
// Workaround: tag input will be replaced by tag autocomplete, so
|
||||
// defer getting the input until we need it
|
||||
const tagsInput = document.getElementById('{{ form.tag_string.id_for_label }}');
|
||||
const websiteUrl = encodeURIComponent(urlInput.value);
|
||||
const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}&ignore_cache=true`;
|
||||
|
||||
bookmarkExistsHint.style['display'] = 'block';
|
||||
notesDetails.open = !!existingBookmark.notes;
|
||||
updateInput(titleInput, existingBookmark.title);
|
||||
updateInput(descriptionInput, existingBookmark.description);
|
||||
updateInput(notesInput, existingBookmark.notes);
|
||||
updateInput(tagsInput, existingBookmark.tag_names.join(" "));
|
||||
updateCheckbox(unreadCheckbox, existingBookmark.unread);
|
||||
updateCheckbox(sharedCheckbox, existingBookmark.shared);
|
||||
} else {
|
||||
// Update title and description with website metadata, unless they have been modified
|
||||
if (!isTitleModified) {
|
||||
updateInput(titleInput, metadata.title);
|
||||
}
|
||||
if (!isDescriptionModified) {
|
||||
updateInput(descriptionInput, metadata.description);
|
||||
}
|
||||
}
|
||||
fetch(requestUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const metadata = data.metadata;
|
||||
const existingBookmark = data.bookmark;
|
||||
toggleLoadingIcon(urlInput, false);
|
||||
|
||||
// Preview auto tags
|
||||
const autoTags = data.auto_tags;
|
||||
const autoTagsHint = document.querySelector('.form-input-hint.auto-tags');
|
||||
if (metadata.title && metadata.title !== existingBookmark?.title) {
|
||||
titleInput.value = metadata.title;
|
||||
titleInput.classList.add("modified");
|
||||
}
|
||||
|
||||
if (autoTags.length > 0) {
|
||||
autoTags.sort();
|
||||
autoTagsHint.style['display'] = 'block';
|
||||
autoTagsHint.innerHTML = `Auto tags: ${autoTags.join(" ")}`;
|
||||
} else {
|
||||
autoTagsHint.style['display'] = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
if (metadata.description && metadata.description !== existingBookmark?.description) {
|
||||
descriptionInput.value = metadata.description;
|
||||
descriptionInput.classList.add("modified");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshMetadata() {
|
||||
if (!urlInput.value) {
|
||||
return;
|
||||
}
|
||||
refreshButton.addEventListener('click', refreshMetadata);
|
||||
|
||||
toggleLoadingIcon(urlInput, true);
|
||||
|
||||
const websiteUrl = encodeURIComponent(urlInput.value);
|
||||
const requestUrl = `{% url 'linkding:api-root' %}bookmarks/check?url=${websiteUrl}&ignore_cache=true`;
|
||||
|
||||
fetch(requestUrl)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const metadata = data.metadata;
|
||||
const existingBookmark = data.bookmark;
|
||||
toggleLoadingIcon(urlInput, false);
|
||||
|
||||
if (metadata.title && metadata.title !== existingBookmark?.title) {
|
||||
titleInput.value = metadata.title;
|
||||
titleInput.classList.add("modified");
|
||||
}
|
||||
|
||||
if (metadata.description && metadata.description !== existingBookmark?.description) {
|
||||
descriptionInput.value = metadata.description;
|
||||
descriptionInput.classList.add("modified");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshButton.addEventListener('click', refreshMetadata);
|
||||
|
||||
// Fetch website metadata when page loads and when URL changes, unless we are editing an existing bookmark
|
||||
if (!editedBookmarkId) {
|
||||
checkUrl();
|
||||
urlInput.addEventListener('input', checkUrl);
|
||||
titleInput.addEventListener('input', () => {
|
||||
isTitleModified = true;
|
||||
});
|
||||
descriptionInput.addEventListener('input', () => {
|
||||
isDescriptionModified = true;
|
||||
});
|
||||
} else {
|
||||
refreshButton.style['display'] = 'inline-block';
|
||||
}
|
||||
})();
|
||||
// Fetch website metadata when page loads and when URL changes, unless we are editing an existing bookmark
|
||||
if (!editedBookmarkId) {
|
||||
checkUrl();
|
||||
urlInput.addEventListener('input', checkUrl);
|
||||
titleInput.addEventListener('input', () => {
|
||||
isTitleModified = true;
|
||||
});
|
||||
descriptionInput.addEventListener('input', () => {
|
||||
isDescriptionModified = true;
|
||||
});
|
||||
} else {
|
||||
refreshButton.style['display'] = 'inline-block';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,14 +1,10 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
{% extends "shared/layout.html" %}
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
{% load bookmarks %}
|
||||
|
||||
{% block title %}Bookmarks - Linkding{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<ld-bookmark-page
|
||||
class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
|
||||
|
||||
<ld-bookmark-page class="bookmarks-page grid columns-md-1 {% if bookmark_list.collapse_side_panel %}collapse-side-panel{% endif %}">
|
||||
{# Bookmark list #}
|
||||
<main class="main col-2" aria-labelledby="main-heading">
|
||||
<div class="section-header mb-0">
|
||||
@@ -21,19 +17,15 @@
|
||||
</ld-filter-drawer-trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions"
|
||||
action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post" autocomplete="off">
|
||||
method="post"
|
||||
autocomplete="off">
|
||||
{% csrf_token %}
|
||||
{% 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>
|
||||
</main>
|
||||
|
||||
{# Filters #}
|
||||
<div class="side-panel col-1 hide-md">
|
||||
{% include 'bookmarks/bundle_section.html' %}
|
||||
@@ -41,12 +33,6 @@
|
||||
</div>
|
||||
</ld-bookmark-page>
|
||||
{% endblock %}
|
||||
|
||||
{% block overlays %}
|
||||
{# Bookmark details #}
|
||||
<turbo-frame id="details-modal" target="_top">
|
||||
{% if details %}
|
||||
{% include 'bookmarks/details/modal.html' %}
|
||||
{% endif %}
|
||||
</turbo-frame>
|
||||
{% endblock %}
|
||||
{% include 'bookmarks/details/modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -1,11 +1,7 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% block head %}
|
||||
{% with page_title="New bookmark - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="New bookmark - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bookmarks-form-page">
|
||||
<main aria-labelledby="main-heading">
|
||||
|
||||
@@ -1,90 +1,106 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="reader-mode">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Reader view</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
|
||||
{# 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 %}
|
||||
</head>
|
||||
<body>
|
||||
<template id="content">{{ content|safe }}</template>
|
||||
<script src="{% static 'vendor/Readability.js' %}" type="application/javascript"></script>
|
||||
<script type="application/javascript">
|
||||
function estimateReadingTime(charCount, wordsPerMinute) {
|
||||
const avgWordLength = 5;
|
||||
const totalWords = charCount / avgWordLength;
|
||||
return Math.ceil(totalWords / wordsPerMinute);
|
||||
}
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Reader view</title>
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimal-ui">
|
||||
{# 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 %}
|
||||
</head>
|
||||
<body>
|
||||
<template id="content">{{ content|safe }}</template>
|
||||
<script src="{% static 'vendor/Readability.js' %}"
|
||||
type="application/javascript"></script>
|
||||
<script type="application/javascript">
|
||||
function estimateReadingTime(charCount, wordsPerMinute) {
|
||||
const avgWordLength = 5;
|
||||
const totalWords = charCount / avgWordLength;
|
||||
return Math.ceil(totalWords / wordsPerMinute);
|
||||
}
|
||||
|
||||
function postProcess(articleContent) {
|
||||
articleContent.querySelectorAll('table').forEach(table => {
|
||||
table.classList.add('table');
|
||||
});
|
||||
}
|
||||
function postProcess(articleContent) {
|
||||
articleContent.querySelectorAll('table').forEach(table => {
|
||||
table.classList.add('table');
|
||||
});
|
||||
}
|
||||
|
||||
function makeReadable() {
|
||||
const content = document.getElementById('content');
|
||||
const contentHtml = content.innerHTML;
|
||||
const dom = new DOMParser().parseFromString(contentHtml, 'text/html');
|
||||
const article = new Readability(dom).parse();
|
||||
function makeReadable() {
|
||||
const content = document.getElementById('content');
|
||||
const contentHtml = content.innerHTML;
|
||||
const dom = new DOMParser().parseFromString(contentHtml, 'text/html');
|
||||
const article = new Readability(dom).parse();
|
||||
|
||||
document.title = article.title;
|
||||
document.title = article.title;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('container');
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('container');
|
||||
|
||||
const articleTitle = document.createElement('h1');
|
||||
articleTitle.textContent = article.title;
|
||||
container.append(articleTitle);
|
||||
const articleTitle = document.createElement('h1');
|
||||
articleTitle.textContent = article.title;
|
||||
container.append(articleTitle);
|
||||
|
||||
const byline = [article.byline, article.siteName].filter(Boolean);
|
||||
if (byline.length > 0) {
|
||||
const articleByline = document.createElement('p');
|
||||
articleByline.textContent = byline.join(' | ');
|
||||
articleByline.classList.add('byline');
|
||||
container.append(articleByline);
|
||||
}
|
||||
const byline = [article.byline, article.siteName].filter(Boolean);
|
||||
if (byline.length > 0) {
|
||||
const articleByline = document.createElement('p');
|
||||
articleByline.textContent = byline.join(' | ');
|
||||
articleByline.classList.add('byline');
|
||||
container.append(articleByline);
|
||||
}
|
||||
|
||||
if(article.length) {
|
||||
const minTime = estimateReadingTime(article.length, 225);
|
||||
const maxTime = estimateReadingTime(article.length, 175);
|
||||
if (article.length) {
|
||||
const minTime = estimateReadingTime(article.length, 225);
|
||||
const maxTime = estimateReadingTime(article.length, 175);
|
||||
|
||||
const articleReadingTime = document.createElement('p');
|
||||
articleReadingTime.textContent = `${minTime}-${maxTime} minutes`;
|
||||
articleReadingTime.classList.add('reading-time');
|
||||
container.append(articleReadingTime);
|
||||
}
|
||||
const articleReadingTime = document.createElement('p');
|
||||
articleReadingTime.textContent = `${minTime}-${maxTime} minutes`;
|
||||
articleReadingTime.classList.add('reading-time');
|
||||
container.append(articleReadingTime);
|
||||
}
|
||||
|
||||
const divider = document.createElement('hr');
|
||||
container.append(divider);
|
||||
const divider = document.createElement('hr');
|
||||
container.append(divider);
|
||||
|
||||
const articleContent = document.createElement('div');
|
||||
articleContent.innerHTML = article.content;
|
||||
postProcess(articleContent);
|
||||
container.append(articleContent);
|
||||
const articleContent = document.createElement('div');
|
||||
articleContent.innerHTML = article.content;
|
||||
postProcess(articleContent);
|
||||
container.append(articleContent);
|
||||
|
||||
content.replaceWith(container);
|
||||
}
|
||||
makeReadable();
|
||||
</script>
|
||||
</body>
|
||||
content.replaceWith(container);
|
||||
}
|
||||
makeReadable();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<div class="search-container">
|
||||
<form id="search" action="" method="get" role="search">
|
||||
<ld-search-autocomplete
|
||||
input-name="q"
|
||||
input-placeholder="Search for words or #tags"
|
||||
input-value="{{ search.q|default_if_none:'' }}"
|
||||
target="{{ request.user_profile.bookmark_link_target }}"
|
||||
mode="{{ mode }}"
|
||||
user="{{ search.user }}"
|
||||
shared="{{ search.shared }}"
|
||||
unread="{{ search.unread }}">
|
||||
<ld-search-autocomplete input-name="q"
|
||||
input-placeholder="Search for words or #tags"
|
||||
input-value="{{ search.q|default_if_none:'' }}"
|
||||
target="{{ request.user_profile.bookmark_link_target }}"
|
||||
mode="{{ mode }}"
|
||||
user="{{ search.user }}"
|
||||
shared="{{ search.shared }}"
|
||||
unread="{{ search.unread }}">
|
||||
</ld-search-autocomplete>
|
||||
<input type="submit" value="Search" class="d-none">
|
||||
{% for hidden_field in search_form.hidden_fields %}
|
||||
{{ hidden_field }}
|
||||
{% endfor %}
|
||||
{% for hidden_field in search_form.hidden_fields %}{{ hidden_field }}{% endfor %}
|
||||
</form>
|
||||
<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 %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
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 d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
||||
<path d="M6 4v4"></path>
|
||||
@@ -45,7 +49,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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"
|
||||
class="form-label{% if 'shared' in search.modified_params %} text-bold{% endif %}">
|
||||
Shared filter
|
||||
@@ -60,7 +66,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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"
|
||||
class="form-label{% if 'unread' in search.modified_params %} text-bold{% endif %}">
|
||||
Unread filter
|
||||
@@ -80,10 +88,7 @@
|
||||
<button type="submit" class="btn btn-sm" name="save">Save as default</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% for hidden_field in preferences_form.hidden_fields %}
|
||||
{{ hidden_field }}
|
||||
{% endfor %}
|
||||
{% for hidden_field in preferences_form.hidden_fields %}{{ hidden_field }}{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
</ld-dropdown>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
{% extends "shared/layout.html" %}
|
||||
{% load static %}
|
||||
{% load shared %}
|
||||
{% load bookmarks %}
|
||||
|
||||
{% block content %}
|
||||
<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 #}
|
||||
<main class="main col-2" aria-labelledby="main-heading">
|
||||
<div class="section-header">
|
||||
@@ -18,17 +16,14 @@
|
||||
</ld-filter-drawer-trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions"
|
||||
action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post" autocomplete="off">
|
||||
method="post"
|
||||
autocomplete="off">
|
||||
{% csrf_token %}
|
||||
<div id="bookmark-list-container">
|
||||
{% include 'bookmarks/bookmark_list.html' %}
|
||||
</div>
|
||||
<div id="bookmark-list-container">{% include 'bookmarks/bookmark_list.html' %}</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
{# Filters #}
|
||||
<div class="side-panel col-1 hide-md">
|
||||
<section aria-labelledby="user-heading">
|
||||
@@ -44,12 +39,6 @@
|
||||
</div>
|
||||
</ld-bookmark-page>
|
||||
{% endblock %}
|
||||
|
||||
{% block overlays %}
|
||||
{# Bookmark details #}
|
||||
<turbo-frame id="details-modal" target="_top">
|
||||
{% if details %}
|
||||
{% include 'bookmarks/details/modal.html' %}
|
||||
{% endif %}
|
||||
</turbo-frame>
|
||||
{% include 'bookmarks/details/modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,37 +1,33 @@
|
||||
{% load shared %}
|
||||
{% htmlmin %}
|
||||
<div class="tag-cloud">
|
||||
{% if tag_cloud.has_selected_tags %}
|
||||
<p class="selected-tags">
|
||||
{% for tag in tag_cloud.selected_tags %}
|
||||
<a href="?{{ tag.query_string }}"
|
||||
class="text-bold mr-2">
|
||||
<span>-{{ tag.name }}</span>
|
||||
</a>
|
||||
<div class="tag-cloud">
|
||||
{% if tag_cloud.has_selected_tags %}
|
||||
<p class="selected-tags">
|
||||
{% for tag in tag_cloud.selected_tags %}
|
||||
<a href="?{{ tag.query_string }}" class="text-bold mr-2">
|
||||
<span>-{{ tag.name }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="unselected-tags">
|
||||
{% for group in tag_cloud.groups %}
|
||||
<p class="group">
|
||||
{% for tag in group.tags %}
|
||||
{# Highlight first char of first tag in group if grouping is enabled #}
|
||||
{% if group.highlight_first_char and forloop.counter == 1 %}
|
||||
<a href="?{{ tag.query_string }}" class="mr-2" data-is-tag-item>
|
||||
<span class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{# Render tags normally #}
|
||||
<a href="?{{ tag.query_string }}" class="mr-2" data-is-tag-item>
|
||||
<span>{{ tag.name }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="unselected-tags">
|
||||
{% for group in tag_cloud.groups %}
|
||||
<p class="group">
|
||||
{% for tag in group.tags %}
|
||||
{# Highlight first char of first tag in group if grouping is enabled #}
|
||||
{% if group.highlight_first_char and forloop.counter == 1 %}
|
||||
<a href="?{{ tag.query_string }}"
|
||||
class="mr-2" data-is-tag-item>
|
||||
<span
|
||||
class="highlight-char">{{ tag.name|first_char }}</span><span>{{ tag.name|remaining_chars:1 }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{# Render tags normally #}
|
||||
<a href="?{{ tag.query_string }}"
|
||||
class="mr-2" data-is-tag-item>
|
||||
<span>{{ tag.name }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endhtmlmin %}
|
||||
|
||||
@@ -4,12 +4,19 @@
|
||||
{% if user.is_authenticated %}
|
||||
<ld-dropdown class="dropdown dropdown-right ml-auto">
|
||||
<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"
|
||||
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 xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
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="M4 6l16 0" />
|
||||
<path d="M4 12l16 0" />
|
||||
<path d="M4 18l16 0" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="menu" role="list" tabindex="-1">
|
||||
@@ -20,7 +27,5 @@
|
||||
</ld-dropdown>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="tag-cloud-container">
|
||||
{% include 'bookmarks/tag_cloud.html' %}
|
||||
</div>
|
||||
<div id="tag-cloud-container">{% include 'bookmarks/tag_cloud.html' %}</div>
|
||||
</section>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,10 +1,7 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<ld-form data-form-reset>
|
||||
<form id="user-select" action="" method="get">
|
||||
{% for hidden_field in form.hidden_fields %}
|
||||
{{ hidden_field }}
|
||||
{% endfor %}
|
||||
{% for hidden_field in form.hidden_fields %}{{ hidden_field }}{% endfor %}
|
||||
<div class="form-group">
|
||||
<div class="d-flex">
|
||||
{% render_field form.user class+="form-select" data-submit-on-change="" %}
|
||||
@@ -14,4 +11,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ld-form>
|
||||
</ld-form>
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="Edit bundle - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Edit bundle - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bundles-editor-page grid columns-md-1">
|
||||
<main aria-labelledby="main-heading">
|
||||
<div class="section-header">
|
||||
<h1 id="main-heading">Edit bundle</h1>
|
||||
</div>
|
||||
|
||||
{% include 'shared/messages.html' %}
|
||||
|
||||
<form id="bundle-form" action="{% url 'linkding:bundles.edit' bundle.id %}" method="post" novalidate>
|
||||
<form id="bundle-form"
|
||||
action="{% url 'linkding:bundles.edit' bundle.id %}"
|
||||
method="post"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
{% include 'bundles/form.html' %}
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<aside class="col-2" aria-labelledby="preview-heading">
|
||||
<div class="section-header">
|
||||
<h2 id="preview-heading">Preview</h2>
|
||||
</div>
|
||||
|
||||
{% include 'bundles/preview.html' %}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -1,96 +1,83 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<div class="form-group {% if form.name.errors %}has-error{% endif %}">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">Name</label>
|
||||
{{ form.name|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }}
|
||||
{% if form.name.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.name.errors %}<div class="form-input-hint">{{ form.name.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group {% if form.search.errors %}has-error{% endif %}">
|
||||
<label for="{{ form.search.id_for_label }}" class="form-label">Search terms</label>
|
||||
{{ form.search|add_class:"form-input"|attr:"autocomplete:off"|attr:"placeholder: " }}
|
||||
{% if form.search.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.search.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-input-hint">
|
||||
All of these search terms must be present in a bookmark to match.
|
||||
</div>
|
||||
{% if form.search.errors %}<div class="form-input-hint">{{ 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 class="form-group">
|
||||
<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:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<div class="form-input-hint">
|
||||
At least one of these tags must be present in a bookmark to match.
|
||||
</div>
|
||||
<div class="form-input-hint">At least one of these tags must be present in a bookmark to match.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<div class="form-input-hint">
|
||||
All of these tags must be present in a bookmark to match.
|
||||
</div>
|
||||
<div class="form-input-hint">All of these tags must be present in a bookmark to match.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<div class="form-input-hint">
|
||||
None of these tags must be present in a bookmark to match.
|
||||
</div>
|
||||
<div class="form-input-hint">None of these tags must be present in a bookmark to match.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-footer d-flex mt-4">
|
||||
<input type="submit" name="save" 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>
|
||||
<input type="submit"
|
||||
name="save"
|
||||
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>
|
||||
|
||||
<script>
|
||||
(function init() {
|
||||
const bundleForm = document.getElementById('bundle-form');
|
||||
const previewLink = document.getElementById('preview-link');
|
||||
(function init() {
|
||||
const bundleForm = document.getElementById('bundle-form');
|
||||
const previewLink = document.getElementById('preview-link');
|
||||
|
||||
let pendingUpdate;
|
||||
let pendingUpdate;
|
||||
|
||||
function scheduleUpdate() {
|
||||
if (pendingUpdate) {
|
||||
clearTimeout(pendingUpdate);
|
||||
}
|
||||
pendingUpdate = setTimeout(() => {
|
||||
// Ignore if link has been removed (e.g. form submit or navigation)
|
||||
if (!previewLink.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = previewLink.href.split('?')[0];
|
||||
const params = new URLSearchParams();
|
||||
const inputs = bundleForm.querySelectorAll('input[type="text"]:not([name="csrfmiddlewaretoken"]), textarea, select');
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (input.name && input.value.trim()) {
|
||||
params.set(input.name, input.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
previewLink.href = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
||||
previewLink.click();
|
||||
}, 500)
|
||||
function scheduleUpdate() {
|
||||
if (pendingUpdate) {
|
||||
clearTimeout(pendingUpdate);
|
||||
}
|
||||
pendingUpdate = setTimeout(() => {
|
||||
// Ignore if link has been removed (e.g. form submit or navigation)
|
||||
if (!previewLink.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
bundleForm.addEventListener('input', scheduleUpdate);
|
||||
})();
|
||||
const baseUrl = previewLink.href.split('?')[0];
|
||||
const params = new URLSearchParams();
|
||||
const inputs = bundleForm.querySelectorAll('input[type="text"]:not([name="csrfmiddlewaretoken"]), textarea, select');
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (input.name && input.value.trim()) {
|
||||
params.set(input.name, input.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
previewLink.href = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
||||
previewLink.click();
|
||||
}, 500)
|
||||
}
|
||||
|
||||
bundleForm.addEventListener('input', scheduleUpdate);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -1,61 +1,65 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
|
||||
{% extends "shared/layout.html" %}
|
||||
{% block head %}
|
||||
{% with page_title="Bundles - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Bundles - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="bundles-page crud-page" aria-labelledby="main-heading">
|
||||
<div class="crud-header">
|
||||
<h1 id="main-heading">Bundles</h1>
|
||||
<a href="{% url 'linkding:bundles.new' %}" class="btn">Add bundle</a>
|
||||
</div>
|
||||
|
||||
{% include 'shared/messages.html' %}
|
||||
|
||||
{% if bundles %}
|
||||
<form action="{% url 'linkding:bundles.action' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<table class="table crud-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="actions">
|
||||
<span class="text-assistive">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="actions">
|
||||
<span class="text-assistive">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bundle in bundles %}
|
||||
<tr data-bundle-id="{{ bundle.id }}" draggable="true">
|
||||
<td>
|
||||
<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"
|
||||
stroke="currentColor" stroke-width="2" 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>
|
||||
<span>{{ bundle.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a class="btn btn-link" href="{% url 'linkding:bundles.edit' bundle.id %}">Edit</a>
|
||||
<button data-confirm type="submit" name="remove_bundle" value="{{ bundle.id }}"
|
||||
class="btn btn-link">Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for bundle in bundles %}
|
||||
<tr data-bundle-id="{{ bundle.id }}" draggable="true">
|
||||
<td>
|
||||
<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"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
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>
|
||||
<span>{{ bundle.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a class="btn btn-link"
|
||||
href="{% url 'linkding:bundles.edit' bundle.id %}">Edit</a>
|
||||
<button data-confirm
|
||||
type="submit"
|
||||
name="remove_bundle"
|
||||
value="{{ bundle.id }}"
|
||||
class="btn btn-link">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<input type="submit" name="move_bundle" value="" class="d-none">
|
||||
<input type="hidden" name="move_position" value="">
|
||||
</form>
|
||||
@@ -66,7 +70,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function init() {
|
||||
const tableBody = document.querySelector(".crud-table tbody");
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="New bundle - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="New bundle - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bundles-editor-page grid columns-md-1">
|
||||
<main aria-labelledby="main-heading">
|
||||
<div class="section-header">
|
||||
<h1 id="main-heading">New bundle</h1>
|
||||
</div>
|
||||
|
||||
{% include 'shared/messages.html' %}
|
||||
|
||||
<form id="bundle-form" action="{% url 'linkding:bundles.new' %}" method="post" novalidate>
|
||||
<form id="bundle-form"
|
||||
action="{% url 'linkding:bundles.new' %}"
|
||||
method="post"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
{% include 'bundles/form.html' %}
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<aside class="col-2" aria-labelledby="preview-heading">
|
||||
<div class="section-header">
|
||||
<h2 id="preview-heading">Preview</h2>
|
||||
</div>
|
||||
|
||||
{% include 'bundles/preview.html' %}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<turbo-frame id="preview">
|
||||
{% if bookmark_list.is_empty %}
|
||||
<div>
|
||||
No bookmarks match the current bundle.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-4">
|
||||
Found {{ bookmark_list.bookmarks_total }} bookmarks matching this bundle.
|
||||
</div>
|
||||
{% include 'bookmarks/bookmark_list.html' %}
|
||||
{% endif %}
|
||||
{% if bookmark_list.is_empty %}
|
||||
<div>No bookmarks match the current bundle.</div>
|
||||
{% else %}
|
||||
<div class="mb-4">Found {{ bookmark_list.bookmarks_total }} bookmarks matching this bundle.</div>
|
||||
{% include 'bookmarks/bookmark_list.html' %}
|
||||
{% endif %}
|
||||
</turbo-frame>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="Registration complete - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Registration complete - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
|
||||
<div class="section-header">
|
||||
<h1 id="main-heading">Registration complete</h1>
|
||||
</div>
|
||||
<p class="text-success">
|
||||
You can now use the application.
|
||||
</p>
|
||||
<p class="text-success">You can now use the application.</p>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="Registration - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Registration - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
|
||||
<div class="section-header">
|
||||
<h1 id="main-heading">Register</h1>
|
||||
</div>
|
||||
<form method="post" action="{% url 'django_registration_register' %}" novalidate>
|
||||
<form method="post"
|
||||
action="{% url 'django_registration_register' %}"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="form-group {% if form.errors.username %}has-error{% endif %}">
|
||||
<label class="form-label" for="{{ form.username.id_for_label }}">Username</label>
|
||||
@@ -34,7 +32,7 @@
|
||||
{{ form.password2|add_class:'form-input'|attr:"placeholder: " }}
|
||||
<div class="form-input-hint">{{ form.errors.password2 }}</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br />
|
||||
<input type="submit" value="Register" class="btn btn-primary btn-wide">
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
</form>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="Login - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Login - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
|
||||
<div class="section-header">
|
||||
@@ -27,13 +23,14 @@
|
||||
<label class="form-label" for="{{ form.password.id_for_label }}">Password</label>
|
||||
{{ form.password|add_class:'form-input'|attr:'placeholder: ' }}
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br />
|
||||
<div class="d-flex justify-between">
|
||||
<input type="submit" value="Login" class="btn btn-primary btn-wide"/>
|
||||
<input type="hidden" name="next" value="{{ next }}"/>
|
||||
<input type="submit" value="Login" class="btn btn-primary btn-wide" />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
{% 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 %}
|
||||
{% if allow_registration %}
|
||||
<a href="{% url 'django_registration_register' %}" class="btn btn-link">Register</a>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="Password changed - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Password changed - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
|
||||
<div class="section-header">
|
||||
<h1 id="main-heading">Password Changed</h1>
|
||||
</div>
|
||||
<p class="text-success">
|
||||
Your password was changed successfully.
|
||||
</p>
|
||||
<p class="text-success">Your password was changed successfully.</p>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
{% extends 'bookmarks/layout.html' %}
|
||||
{% extends 'shared/layout.html' %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="Change password - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Change password - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="mx-auto width-50 width-md-100" aria-labelledby="main-heading">
|
||||
<div class="section-header">
|
||||
@@ -17,33 +13,22 @@
|
||||
<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>
|
||||
{{ form.old_password|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.old_password.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.old_password.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.old_password.errors %}<div class="form-input-hint">{{ form.old_password.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<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>
|
||||
{{ form.new_password1|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.new_password1.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.new_password1.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.new_password1.errors %}<div class="form-input-hint">{{ form.new_password1.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<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>
|
||||
{{ form.new_password2|add_class:'form-input'|attr:"placeholder: " }}
|
||||
{% if form.new_password2.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.new_password2.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.new_password2.errors %}<div class="form-input-hint">{{ form.new_password2.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<input type="submit" value="Change Password" class="btn btn-primary btn-wide">
|
||||
<br />
|
||||
<input type="submit"
|
||||
value="Change Password"
|
||||
class="btn btn-primary btn-wide">
|
||||
</form>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
<turbo-frame id="api-modal">
|
||||
<form method="post" action="{% url 'linkding:settings.integrations.create_api_token' %}"
|
||||
data-turbo-frame="api-section">
|
||||
{% csrf_token %}
|
||||
<ld-modal class="modal active" data-close-url="{% url 'linkding:settings.integrations' %}"
|
||||
data-turbo-frame="api-modal">
|
||||
<div class="modal-overlay" data-close-modal></div>
|
||||
<div class="modal-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title="Create API Token" %}
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="token-name">Token name</label>
|
||||
<input type="text"
|
||||
class="form-input"
|
||||
id="token-name"
|
||||
name="name"
|
||||
placeholder="e.g., Browser Extension, Mobile App"
|
||||
value="API Token"
|
||||
maxlength="128">
|
||||
<p class="form-input-hint">A descriptive name to identify the purpose of the token</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Token</button>
|
||||
<form method="post"
|
||||
action="{% url 'linkding:settings.integrations.create_api_token' %}"
|
||||
data-turbo-frame="api-section">
|
||||
{% csrf_token %}
|
||||
<ld-modal class="modal active"
|
||||
data-close-url="{% url 'linkding:settings.integrations' %}"
|
||||
data-turbo-frame="api-modal">
|
||||
<div class="modal-overlay" data-close-modal></div>
|
||||
<div class="modal-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title="Create API Token" %}
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="token-name">Token name</label>
|
||||
<input type="text"
|
||||
class="form-input"
|
||||
id="token-name"
|
||||
name="name"
|
||||
placeholder="e.g., Browser Extension, Mobile App"
|
||||
value="API Token"
|
||||
maxlength="128">
|
||||
<p class="form-input-hint">A descriptive name to identify the purpose of the token</p>
|
||||
</div>
|
||||
</div>
|
||||
</ld-modal>
|
||||
</form>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Token</button>
|
||||
</div>
|
||||
</div>
|
||||
</ld-modal>
|
||||
</form>
|
||||
</turbo-frame>
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
{% extends "shared/layout.html" %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="Settings - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Settings - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="settings-page" aria-labelledby="main-heading">
|
||||
<h1 id="main-heading">Settings</h1>
|
||||
|
||||
{# Profile section #}
|
||||
{% if success_message %}
|
||||
<div class="toast toast-success mb-4">{{ success_message }}</div>
|
||||
{% endif %}
|
||||
{% if error_message %}
|
||||
<div class="toast toast-error mb-4">{{ error_message }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success_message %}<div class="toast toast-success mb-4">{{ success_message }}</div>{% endif %}
|
||||
{% if error_message %}<div class="toast toast-error mb-4">{{ error_message }}</div>{% endif %}
|
||||
<section aria-labelledby="profile-heading">
|
||||
<h2 id="profile-heading">Profile</h2>
|
||||
<p>
|
||||
<a href="{% url 'change_password' %}">Change password</a>
|
||||
</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 %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
|
||||
@@ -34,7 +27,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<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" }}
|
||||
<div class="form-input-hint">
|
||||
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 class="form-group">
|
||||
<label for="{{ form.bookmark_description_display.id_for_label }}" class="form-label">Bookmark
|
||||
description</label>
|
||||
<label for="{{ form.bookmark_description_display.id_for_label }}"
|
||||
class="form-label">
|
||||
Bookmark
|
||||
description
|
||||
</label>
|
||||
{{ form.bookmark_description_display|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
Whether to show bookmark descriptions and tags in the same line, or as separate blocks.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group {% if request.user_profile.bookmark_description_display == 'inline' %}d-hide{% endif %}">
|
||||
<label for="{{ form.bookmark_description_max_lines.id_for_label }}" class="form-label">Bookmark description
|
||||
max lines</label>
|
||||
<div class="form-group {% if request.user_profile.bookmark_description_display == 'inline' %}d-hide{% endif %}">
|
||||
<label for="{{ form.bookmark_description_max_lines.id_for_label }}"
|
||||
class="form-label">
|
||||
Bookmark description
|
||||
max lines
|
||||
</label>
|
||||
{{ form.bookmark_description_max_lines|add_class:"form-input width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
Limits the number of lines that are displayed for the bookmark description.
|
||||
</div>
|
||||
<div class="form-input-hint">Limits the number of lines that are displayed for the bookmark description.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.display_url.id_for_label }}" class="form-checkbox">
|
||||
{{ form.display_url }}
|
||||
<i class="form-icon"></i> Show bookmark URL
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
When enabled, this setting displays the bookmark URL below the title.
|
||||
</div>
|
||||
<div class="form-input-hint">When enabled, this setting displays the bookmark URL below the title.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.permanent_notes.id_for_label }}" class="form-checkbox">
|
||||
@@ -79,45 +74,41 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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 }}
|
||||
<i class="form-icon"></i> View
|
||||
</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 }}
|
||||
<i class="form-icon"></i> Edit
|
||||
</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 }}
|
||||
<i class="form-icon"></i> Archive
|
||||
</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 }}
|
||||
<i class="form-icon"></i> Remove
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
Which actions to display for each bookmark.
|
||||
</div>
|
||||
<div class="form-input-hint">Which actions to display for each bookmark.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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" }}
|
||||
<div class="form-input-hint">
|
||||
Whether to open bookmarks a new page or in the same page.
|
||||
</div>
|
||||
<div class="form-input-hint">Whether to open bookmarks a new page or in the same page.</div>
|
||||
</div>
|
||||
<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>
|
||||
{{ form.items_per_page|add_class:"form-input width-25 width-sm-100"|attr:"min:10" }}
|
||||
{% if form.items_per_page.errors %}
|
||||
<div class="form-input-hint is-error">
|
||||
{{ form.items_per_page.errors }}
|
||||
</div>
|
||||
<div class="form-input-hint is-error">{{ form.items_per_page.errors }}</div>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
<div class="form-input-hint">
|
||||
The number of bookmarks to display per page.
|
||||
</div>
|
||||
<div class="form-input-hint">The number of bookmarks to display per page.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.sticky_pagination.id_for_label }}" class="form-checkbox">
|
||||
@@ -130,7 +121,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 }}
|
||||
<i class="form-icon"></i> Collapse side panel
|
||||
</label>
|
||||
@@ -144,9 +136,7 @@
|
||||
{{ form.hide_bundles }}
|
||||
<i class="form-icon"></i> Hide bundles
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
Allows to hide the bundles in the side panel if you don't intend to use them.
|
||||
</div>
|
||||
<div class="form-input-hint">Allows to hide the bundles in the side panel if you don't intend to use them.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.tag_search.id_for_label }}" class="form-label">Tag search</label>
|
||||
@@ -166,7 +156,8 @@
|
||||
<div class="form-input-hint">
|
||||
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.
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,10 +174,9 @@
|
||||
<summary>
|
||||
<span class="form-label d-inline-block">Auto Tagging</span>
|
||||
</summary>
|
||||
<label for="{{ form.auto_tagging_rules.id_for_label }}" class="text-assistive">Auto Tagging</label>
|
||||
<div>
|
||||
{{ form.auto_tagging_rules|add_class:"form-input monospace"|attr:"rows:6" }}
|
||||
</div>
|
||||
<label for="{{ form.auto_tagging_rules.id_for_label }}"
|
||||
class="text-assistive">Auto Tagging</label>
|
||||
<div>{{ form.auto_tagging_rules|add_class:"form-input monospace"|attr:"rows:6" }}</div>
|
||||
</details>
|
||||
<div class="form-input-hint">
|
||||
Automatically adds tags to bookmarks based on predefined rules.
|
||||
@@ -204,9 +194,9 @@ reddit.com/r/Music music reddit</pre>
|
||||
Automatically loads favicons for bookmarked websites and displays them next to each bookmark.
|
||||
Enabling this feature automatically downloads all missing 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
|
||||
href="https://linkding.link/options/#ld_favicon_provider"
|
||||
target="_blank">options documentation</a> on how to configure a custom favicon provider.
|
||||
If you don't want to use this service, check the
|
||||
<a href="https://linkding.link/options/#ld_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.
|
||||
</div>
|
||||
{% if request.user_profile.enable_favicons and enable_refresh_favicons %}
|
||||
@@ -214,7 +204,8 @@ reddit.com/r/Music music reddit</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
<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 }}
|
||||
<i class="form-icon"></i> Enable Preview Images
|
||||
</label>
|
||||
@@ -224,17 +215,18 @@ reddit.com/r/Music music reddit</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.web_archive_integration.id_for_label }}" class="form-label">Internet Archive
|
||||
integration</label>
|
||||
<label for="{{ form.web_archive_integration.id_for_label }}"
|
||||
class="form-label">
|
||||
Internet Archive
|
||||
integration
|
||||
</label>
|
||||
{{ form.web_archive_integration|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
Enabling this feature will automatically create snapshots of bookmarked websites on the <a
|
||||
href="https://web.archive.org/" target="_blank" rel="noopener">Internet Archive Wayback
|
||||
Machine</a>.
|
||||
Enabling this feature will automatically create snapshots of bookmarked websites on the
|
||||
<a href="https://web.archive.org/" target="_blank" rel="noopener">Internet Archive Wayback Machine</a>.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -248,19 +240,20 @@ reddit.com/r/Music music reddit</pre>
|
||||
</div>
|
||||
</div>
|
||||
<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 }}
|
||||
<i class="form-icon"></i> Enable public bookmark sharing
|
||||
</label>
|
||||
<div class="form-input-hint">
|
||||
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
|
||||
href="{% url 'linkding:bookmarks.shared' %}">shared bookmarks page</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>.
|
||||
</div>
|
||||
</div>
|
||||
{% if has_snapshot_support %}
|
||||
<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 }}
|
||||
<i class="form-icon"></i> Automatically create HTML snapshots
|
||||
</label>
|
||||
@@ -272,7 +265,8 @@ reddit.com/r/Music music reddit</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
<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 }}
|
||||
<i class="form-icon"></i> Create bookmarks as unread by default
|
||||
</label>
|
||||
@@ -283,7 +277,8 @@ reddit.com/r/Music music reddit</pre>
|
||||
</div>
|
||||
</div>
|
||||
<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 }}
|
||||
<i class="form-icon"></i> Create bookmarks as shared by default
|
||||
</label>
|
||||
@@ -299,36 +294,39 @@ reddit.com/r/Music music reddit</pre>
|
||||
<span class="form-label d-inline-block">Custom CSS</span>
|
||||
</summary>
|
||||
<label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label>
|
||||
<div>
|
||||
{{ form.custom_css|add_class:"form-input monospace"|attr:"rows:6" }}
|
||||
</div>
|
||||
<div>{{ form.custom_css|add_class:"form-input monospace"|attr:"rows:6" }}</div>
|
||||
</details>
|
||||
<div class="form-input-hint">
|
||||
Allows to add custom CSS to the page.
|
||||
</div>
|
||||
<div class="form-input-hint">Allows to add custom CSS to the page.</div>
|
||||
</div>
|
||||
<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>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{# Global settings section #}
|
||||
{% if global_settings_form %}
|
||||
<section aria-labelledby="global-settings-heading">
|
||||
<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 %}
|
||||
<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" }}
|
||||
<div class="form-input-hint">
|
||||
The page that unauthenticated users are redirected to when accessing the root URL.
|
||||
</div>
|
||||
<div class="form-input-hint">The page that unauthenticated users are redirected to when accessing the root URL.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ global_settings_form.guest_profile_user.id_for_label }}" class="form-label">Guest user
|
||||
profile</label>
|
||||
<label for="{{ global_settings_form.guest_profile_user.id_for_label }}"
|
||||
class="form-label">
|
||||
Guest user
|
||||
profile
|
||||
</label>
|
||||
{{ global_settings_form.guest_profile_user|add_class:"form-select width-25 width-sm-100" }}
|
||||
<div class="form-input-hint">
|
||||
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 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 }}
|
||||
<i class="form-icon"></i> Enable prefetching links on hover
|
||||
</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.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# Import section #}
|
||||
<section aria-labelledby="import-heading">
|
||||
<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
|
||||
added and existing ones are updated.</p>
|
||||
<form method="post" enctype="multipart/form-data" action="{% url 'linkding:settings.import' %}">
|
||||
<p>
|
||||
Import bookmarks and tags in the Netscape HTML format. This will execute a sync where new bookmarks are
|
||||
added and existing ones are updated.
|
||||
</p>
|
||||
<form method="post"
|
||||
enctype="multipart/form-data"
|
||||
action="{% url 'linkding:settings.import' %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="import_map_private_flag" class="form-checkbox">
|
||||
@@ -381,48 +385,43 @@ reddit.com/r/Music music reddit</pre>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{# Export section #}
|
||||
<section aria-labelledby="export-heading">
|
||||
<h2 id="export-heading">Export</h2>
|
||||
<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 %}
|
||||
<div class="has-error">
|
||||
<p class="form-input-hint">
|
||||
{{ export_error }}
|
||||
</p>
|
||||
<p class="form-input-hint">{{ export_error }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{# About section #}
|
||||
<section class="about" aria-labelledby="about-heading">
|
||||
<h2 id="about-heading">About</h2>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{{ version_info }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top">Links</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<a href="https://github.com/sissbruecker/linkding/"
|
||||
target="_blank">GitHub</a>
|
||||
<a href="https://linkding.link/"
|
||||
target="_blank">Documentation</a>
|
||||
<a href="https://github.com/sissbruecker/linkding/blob/master/CHANGELOG.md"
|
||||
target="_blank">Changelog</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{{ version_info }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top">Links</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<a href="https://github.com/sissbruecker/linkding/" target="_blank">GitHub</a>
|
||||
<a href="https://linkding.link/" target="_blank">Documentation</a>
|
||||
<a href="https://github.com/sissbruecker/linkding/blob/master/CHANGELOG.md"
|
||||
target="_blank">Changelog</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function init() {
|
||||
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);
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
|
||||
{% extends "shared/layout.html" %}
|
||||
{% block head %}
|
||||
{% with page_title="Integrations - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Integrations - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="settings-page" aria-labelledby="main-heading">
|
||||
<h1 id="main-heading">Integrations</h1>
|
||||
|
||||
<section aria-labelledby="browser-extension-heading">
|
||||
<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
|
||||
extension is available in the official extension stores for:</p>
|
||||
<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>
|
||||
<li><a href="https://addons.mozilla.org/firefox/addon/linkding-extension/" target="_blank">Firefox</a></li>
|
||||
<li><a href="https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe"
|
||||
target="_blank">Chrome</a></li>
|
||||
<li>
|
||||
<a href="https://addons.mozilla.org/firefox/addon/linkding-extension/"
|
||||
target="_blank">Firefox</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe"
|
||||
target="_blank">Chrome</a>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<li>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>)
|
||||
<li>
|
||||
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>)
|
||||
</li>
|
||||
<li>Drag the bookmarklet below into your browser's bookmark bar / toolbar</li>
|
||||
<li>Open the website that you want to bookmark</li>
|
||||
@@ -35,86 +42,94 @@
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<div class="form-group radio-group" role="radiogroup" aria-labelledby="detection-method-label">
|
||||
<div class="form-group radio-group"
|
||||
role="radiogroup"
|
||||
aria-labelledby="detection-method-label">
|
||||
<p id="detection-method-label">Choose your preferred bookmarklet:</p>
|
||||
<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>
|
||||
Detect title and description on the server
|
||||
</label>
|
||||
<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>
|
||||
Detect title and description in the browser
|
||||
</label>
|
||||
</div>
|
||||
<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>
|
||||
<a id="bookmarklet-client" href="javascript: {% include 'bookmarks/bookmarklet_clientside.js' %}"
|
||||
data-turbo="false" class="btn btn-primary" style="display: none;">📎 Add bookmark</a>
|
||||
<a id="bookmarklet-client"
|
||||
href="javascript: {% include 'settings/bookmarklet_clientside.js' %}"
|
||||
data-turbo="false"
|
||||
class="btn btn-primary"
|
||||
style="display: none">📎 Add bookmark</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function init() {
|
||||
// Bookmarklet type toggle
|
||||
const radioButtons = document.querySelectorAll('input[name="bookmarklet-type"]');
|
||||
const serverBookmarklet = document.getElementById('bookmarklet-server');
|
||||
const clientBookmarklet = document.getElementById('bookmarklet-client');
|
||||
(function init() {
|
||||
// Bookmarklet type toggle
|
||||
const radioButtons = document.querySelectorAll('input[name="bookmarklet-type"]');
|
||||
const serverBookmarklet = document.getElementById('bookmarklet-server');
|
||||
const clientBookmarklet = document.getElementById('bookmarklet-client');
|
||||
|
||||
function toggleBookmarklet() {
|
||||
const selectedValue = document.querySelector('input[name="bookmarklet-type"]:checked').value;
|
||||
if (selectedValue === 'server') {
|
||||
serverBookmarklet.style.display = 'inline-block';
|
||||
clientBookmarklet.style.display = 'none';
|
||||
} else {
|
||||
serverBookmarklet.style.display = 'none';
|
||||
clientBookmarklet.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
function toggleBookmarklet() {
|
||||
const selectedValue = document.querySelector('input[name="bookmarklet-type"]:checked').value;
|
||||
if (selectedValue === 'server') {
|
||||
serverBookmarklet.style.display = 'inline-block';
|
||||
clientBookmarklet.style.display = 'none';
|
||||
} else {
|
||||
serverBookmarklet.style.display = 'none';
|
||||
clientBookmarklet.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
|
||||
toggleBookmarklet();
|
||||
radioButtons.forEach(function (radio) {
|
||||
radio.addEventListener('change', toggleBookmarklet);
|
||||
});
|
||||
})();
|
||||
toggleBookmarklet();
|
||||
radioButtons.forEach(function(radio) {
|
||||
radio.addEventListener('change', toggleBookmarklet);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</section>
|
||||
|
||||
<turbo-frame id="api-section">
|
||||
<section aria-labelledby="rest-api-heading">
|
||||
<h2 id="rest-api-heading">REST API</h2>
|
||||
|
||||
{% if api_success_message %}
|
||||
<div class="toast toast-success mb-2">
|
||||
{{ api_success_message }}
|
||||
<section aria-labelledby="rest-api-heading">
|
||||
<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_token_name and api_token_key %}
|
||||
<div class="mt-4 mb-6">
|
||||
<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>
|
||||
<div class="input-group">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if api_token_name and api_token_key %}
|
||||
<div class="mt-4 mb-6">
|
||||
<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>
|
||||
<div class="input-group">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
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
|
||||
your bookmarks.
|
||||
</p>
|
||||
|
||||
{% if api_tokens %}
|
||||
<form method="post"
|
||||
action="{% url 'linkding:settings.integrations.delete_api_token' %}"
|
||||
data-turbo-frame="api-section">
|
||||
<table class="table crud-table mb-6">
|
||||
<thead>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>
|
||||
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
|
||||
your bookmarks.
|
||||
</p>
|
||||
{% if api_tokens %}
|
||||
<form method="post"
|
||||
action="{% url 'linkding:settings.integrations.delete_api_token' %}"
|
||||
data-turbo-frame="api-section">
|
||||
<table class="table crud-table mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created</th>
|
||||
@@ -122,87 +137,96 @@
|
||||
<span class="text-assistive">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for token in api_tokens %}
|
||||
<tr>
|
||||
<td>{{ token.name }}</td>
|
||||
<td>{{ token.created|date:"M d, Y H:i" }}</td>
|
||||
<td class="actions">
|
||||
{% csrf_token %}
|
||||
<button data-confirm name="token_id" value="{{ token.id }}" type="submit"
|
||||
class="btn btn-link">Delete
|
||||
</button>
|
||||
<button data-confirm
|
||||
name="token_id"
|
||||
value="{{ token.id }}"
|
||||
type="submit"
|
||||
class="btn btn-link">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn" href="{% url 'linkding:settings.integrations.create_api_token' %}"
|
||||
data-turbo-frame="api-modal">Create API token</a>
|
||||
</section>
|
||||
|
||||
<turbo-frame id="api-modal"></turbo-frame>
|
||||
|
||||
<script>
|
||||
(function init() {
|
||||
// Copy new token key to clipboard
|
||||
const copyButton = document.getElementById('copy-new-token-key');
|
||||
if (copyButton) {
|
||||
copyButton.addEventListener('click', () => {
|
||||
const tokenInput = document.getElementById('new-token-key');
|
||||
const tokenValue = tokenInput.value;
|
||||
navigator.clipboard.writeText(tokenValue).then(() => {
|
||||
copyButton.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
copyButton.textContent = 'Copy';
|
||||
}, 2000);
|
||||
}, (err) => {
|
||||
console.error('Could not copy text: ', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{% endif %}
|
||||
<a class="btn"
|
||||
href="{% url 'linkding:settings.integrations.create_api_token' %}"
|
||||
data-turbo-frame="api-modal">Create API token</a>
|
||||
</section>
|
||||
<turbo-frame id="api-modal"></turbo-frame>
|
||||
<script>
|
||||
(function init() {
|
||||
// Copy new token key to clipboard
|
||||
const copyButton = document.getElementById('copy-new-token-key');
|
||||
if (copyButton) {
|
||||
copyButton.addEventListener('click', () => {
|
||||
const tokenInput = document.getElementById('new-token-key');
|
||||
const tokenValue = tokenInput.value;
|
||||
navigator.clipboard.writeText(tokenValue).then(() => {
|
||||
copyButton.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
copyButton.textContent = 'Copy';
|
||||
}, 2000);
|
||||
}, (err) => {
|
||||
console.error('Could not copy text: ', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</turbo-frame>
|
||||
|
||||
<section aria-labelledby="rss-feeds-heading">
|
||||
<h2 id="rss-feeds-heading">RSS Feeds</h2>
|
||||
<p>The following URLs provide RSS feeds for your bookmarks:</p>
|
||||
<ul style="list-style-position: outside;">
|
||||
<li><a target="_blank" href="{{ all_feed_url }}">All bookmarks</a></li>
|
||||
<li><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>
|
||||
<a target="_blank" href="{{ all_feed_url }}">All bookmarks</a>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</ul>
|
||||
<p>
|
||||
All URLs support the following URL parameters:
|
||||
</p>
|
||||
<p>All URLs support the following URL parameters:</p>
|
||||
<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.
|
||||
</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.
|
||||
</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.
|
||||
</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.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<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.
|
||||
If you think that a URL was compromised you can delete the feed token for your user in the <a
|
||||
target="_blank" href="{% url 'admin:bookmarks_feedtoken_changelist' %}">admin panel</a>.
|
||||
If you think that a URL was compromised you can delete the feed token for your user in the <a target="_blank"
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{% load i18n %}
|
||||
|
||||
{# Force rendering validation errors in English language to align with the rest of the app #}
|
||||
{% 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 %}
|
||||
{% endlanguage %}
|
||||
{% 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 %}
|
||||
|
||||
64
bookmarks/templates/shared/head.html
Normal file
64
bookmarks/templates/shared/head.html
Normal 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>
|
||||
81
bookmarks/templates/shared/layout.html
Normal file
81
bookmarks/templates/shared/layout.html
Normal 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>
|
||||
@@ -1,9 +1,5 @@
|
||||
{% if messages %}
|
||||
<div class="message-list">
|
||||
{% for message in messages %}
|
||||
<div class="toast toast-{{ message.tags }}" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for message in messages %}<div class="toast toast-{{ message.tags }}" role="alert">{{ message }}</div>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
<div class="modal-header">
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<button type="button" class="btn btn-noborder close" 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">
|
||||
<button type="button"
|
||||
class="btn btn-noborder close"
|
||||
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 d="M18 6l-12 12"></path>
|
||||
<path d="M6 6l12 12"></path>
|
||||
|
||||
128
bookmarks/templates/shared/nav_menu.html
Normal file
128
bookmarks/templates/shared/nav_menu.html
Normal 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 %}
|
||||
@@ -1,5 +1,4 @@
|
||||
{% load shared %}
|
||||
|
||||
<ul class="pagination">
|
||||
{% if prev_link %}
|
||||
<li class="page-item">
|
||||
@@ -10,7 +9,6 @@
|
||||
<a href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for page_link in page_links %}
|
||||
{% if page_link %}
|
||||
<li class="page-item {% if page_link.active %}active{% endif %}">
|
||||
@@ -22,7 +20,6 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if next_link %}
|
||||
<li class="page-item">
|
||||
<a href="{{ next_link }}" tabindex="-1">Next</a>
|
||||
@@ -32,4 +29,4 @@
|
||||
<a href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</ul>
|
||||
6
bookmarks/templates/shared/top_frame.html
Normal file
6
bookmarks/templates/shared/top_frame.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<html lang="en">
|
||||
{% include 'shared/head.html' %}
|
||||
<body>
|
||||
<!--content-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +1,21 @@
|
||||
<turbo-frame id="tag-modal">
|
||||
<form method="post" action="{% url 'linkding:tags.edit' tag.id %}" data-turbo-frame="_top" novalidate>
|
||||
{% csrf_token %}
|
||||
<ld-modal class="modal tag-edit-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-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title="Edit Tag" %}
|
||||
<div class="modal-body">
|
||||
{% include 'tags/form.html' %}
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary btn-wide">Save</button>
|
||||
</div>
|
||||
<form method="post"
|
||||
action="{% url 'linkding:tags.edit' tag.id %}"
|
||||
data-turbo-frame="_top"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
<ld-modal class="modal tag-edit-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-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title="Edit Tag" %}
|
||||
<div class="modal-body">{% include 'tags/form.html' %}</div>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary btn-wide">Save</button>
|
||||
</div>
|
||||
</ld-modal>
|
||||
</form>
|
||||
</div>
|
||||
</ld-modal>
|
||||
</form>
|
||||
</turbo-frame>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<div class="form-group {% if form.name.errors %}has-error{% endif %}">
|
||||
<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" }}
|
||||
<div class="form-input-hint">Tag names are case-insensitive and cannot contain spaces (spaces will be replaced with
|
||||
hyphens).
|
||||
<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">
|
||||
{{ form.name.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form.name.errors %}<div class="form-input-hint">{{ form.name.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
{% extends "bookmarks/layout.html" %}
|
||||
{% extends "shared/layout.html" %}
|
||||
{% load shared %}
|
||||
{% load pagination %}
|
||||
|
||||
{% block head %}
|
||||
{% with page_title="Tags - Linkding" %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% with page_title="Tags - Linkding" %}{{ block.super }}{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="tags-page crud-page">
|
||||
<main aria-labelledby="main-heading">
|
||||
<div class="crud-header">
|
||||
<h1 id="main-heading">Tags</h1>
|
||||
<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.merge' %}" data-turbo-frame="tag-modal" class="btn">Merge Tags</a>
|
||||
<a href="{% url 'linkding:tags.new' %}"
|
||||
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>
|
||||
|
||||
{% include 'shared/messages.html' %}
|
||||
|
||||
{# Filters #}
|
||||
<div class="crud-filters">
|
||||
<ld-form data-form-reset>
|
||||
@@ -28,7 +26,11 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label text-assistive" for="search">Search tags</label>
|
||||
<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">
|
||||
<button type="submit" class="btn input-group-btn">Search</button>
|
||||
</div>
|
||||
@@ -36,12 +38,21 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label text-assistive" for="sort">Sort by</label>
|
||||
<div class="input-group">
|
||||
<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"
|
||||
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 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"
|
||||
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>
|
||||
<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-desc" {% if sort == "name-desc" %}selected{% endif %}>Name Z-A</option>
|
||||
@@ -52,7 +63,10 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<i class="form-icon"></i> Show only unused tags
|
||||
</label>
|
||||
@@ -68,50 +82,46 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Tags List #}
|
||||
{% if page.object_list %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<table class="table crud-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="width: 25%">Bookmarks</th>
|
||||
<th class="actions">
|
||||
<span class="text-assistive">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th style="width: 25%">Bookmarks</th>
|
||||
<th class="actions">
|
||||
<span class="text-assistive">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tag in page.object_list %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ tag.name }}
|
||||
</td>
|
||||
<td style="width: 25%">
|
||||
<a class="btn btn-link" href="{% url 'linkding:bookmarks.index' %}?q=%23{{ tag.name|urlencode }}">
|
||||
{{ tag.bookmark_count }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a class="btn btn-link" href="{% url 'linkding:tags.edit' tag.id %}"
|
||||
data-turbo-frame="tag-modal">Edit</a>
|
||||
<button type="submit" name="delete_tag" value="{{ tag.id }}" class="btn btn-link text-error"
|
||||
data-confirm>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for tag in page.object_list %}
|
||||
<tr>
|
||||
<td>{{ tag.name }}</td>
|
||||
<td style="width: 25%">
|
||||
<a class="btn btn-link"
|
||||
href="{% url 'linkding:bookmarks.index' %}?q=%23{{ tag.name|urlencode }}">
|
||||
{{ tag.bookmark_count }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<a class="btn btn-link"
|
||||
href="{% url 'linkding:tags.edit' tag.id %}"
|
||||
data-turbo-frame="tag-modal">Edit</a>
|
||||
<button type="submit"
|
||||
name="delete_tag"
|
||||
value="{{ tag.id }}"
|
||||
class="btn btn-link text-error"
|
||||
data-confirm>Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
|
||||
{% pagination page %}
|
||||
|
||||
{% else %}
|
||||
<div class="empty">
|
||||
{% if search or unused_only %}
|
||||
|
||||
@@ -1,60 +1,57 @@
|
||||
{% load widget_tweaks %}
|
||||
|
||||
<turbo-frame id="tag-modal">
|
||||
<form method="post" action="{% url 'linkding:tags.merge' %}" data-turbo-frame="_top" novalidate>
|
||||
{% csrf_token %}
|
||||
<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-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title="Merge Tags" %}
|
||||
<div class="modal-body">
|
||||
<details class="mb-4">
|
||||
<summary>
|
||||
<span class="text-bold mb-1">How to merge tags</span>
|
||||
</summary>
|
||||
<ol>
|
||||
<li>Enter the name of the tag you want to keep</li>
|
||||
<li>Enter the names of tags to merge into the target tag</li>
|
||||
<li>The target tag is added to all bookmarks that have any of the merge tags</li>
|
||||
<li>The merged tags are deleted</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<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>
|
||||
<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:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<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.
|
||||
</div>
|
||||
{% if form.target_tag.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.target_tag.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<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.
|
||||
</div>
|
||||
{% if form.merge_tags.errors %}
|
||||
<div class="form-input-hint">
|
||||
{{ form.merge_tags.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post"
|
||||
action="{% url 'linkding:tags.merge' %}"
|
||||
data-turbo-frame="_top"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
<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-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title="Merge Tags" %}
|
||||
<div class="modal-body">
|
||||
<details class="mb-4">
|
||||
<summary>
|
||||
<span class="text-bold mb-1">How to merge tags</span>
|
||||
</summary>
|
||||
<ol>
|
||||
<li>Enter the name of the tag you want to keep</li>
|
||||
<li>Enter the names of tags to merge into the target tag</li>
|
||||
<li>The target tag is added to all bookmarks that have any of the merge tags</li>
|
||||
<li>The merged tags are deleted</li>
|
||||
</ol>
|
||||
</details>
|
||||
<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>
|
||||
<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:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<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.
|
||||
</div>
|
||||
{% if form.target_tag.errors %}<div class="form-input-hint">{{ form.target_tag.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary btn-wide">Merge Tags</button>
|
||||
<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>
|
||||
<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:'' }}">
|
||||
</ld-tag-autocomplete>
|
||||
<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.
|
||||
</div>
|
||||
{% if form.merge_tags.errors %}<div class="form-input-hint">{{ form.merge_tags.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</ld-modal>
|
||||
</form>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary btn-wide">Merge Tags</button>
|
||||
</div>
|
||||
</div>
|
||||
</ld-modal>
|
||||
</form>
|
||||
</turbo-frame>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<turbo-frame id="tag-modal">
|
||||
<form method="post" action="{% url 'linkding:tags.new' %}" data-turbo-frame="_top" novalidate>
|
||||
{% csrf_token %}
|
||||
<ld-modal class="modal tag-edit-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-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title="Create Tag" %}
|
||||
<div class="modal-body">
|
||||
{% include 'tags/form.html' %}
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary btn-wide">Save</button>
|
||||
</div>
|
||||
<form method="post"
|
||||
action="{% url 'linkding:tags.new' %}"
|
||||
data-turbo-frame="_top"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
<ld-modal class="modal tag-edit-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-container" role="dialog" aria-modal="true">
|
||||
{% include 'shared/modal_header.html' with title="Create Tag" %}
|
||||
<div class="modal-body">{% include 'tags/form.html' %}</div>
|
||||
<div class="modal-footer d-flex justify-between">
|
||||
<button type="button" class="btn btn-wide" data-close-modal>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary btn-wide">Save</button>
|
||||
</div>
|
||||
</ld-modal>
|
||||
</form>
|
||||
</div>
|
||||
</ld-modal>
|
||||
</form>
|
||||
</turbo-frame>
|
||||
|
||||
@@ -9,9 +9,7 @@ NUM_ADJACENT_PAGES = 2
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag(
|
||||
"bookmarks/pagination.html", name="pagination", takes_context=True
|
||||
)
|
||||
@register.inclusion_tag("shared/pagination.html", name="pagination", takes_context=True)
|
||||
def pagination(context, page: Page):
|
||||
request = context["request"]
|
||||
base_url = request.path
|
||||
|
||||
@@ -448,7 +448,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
soup = self.make_soup(html)
|
||||
bookmark_list = soup.select_one("ul.bookmark-list")
|
||||
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.bookmark_description_max_lines = 3
|
||||
@@ -458,7 +458,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
soup = self.make_soup(html)
|
||||
bookmark_list = soup.select_one("ul.bookmark-list")
|
||||
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):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
@@ -48,7 +48,7 @@ class TagsEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
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()
|
||||
self.assertEqual(tag.name, "tag1")
|
||||
|
||||
@@ -60,9 +60,7 @@ class TagsEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
reverse("linkding:tags.edit", args=[tag1.id]), {"name": "tag2"}
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response, "Tag "tag2" already exists", status_code=422
|
||||
)
|
||||
self.assertContains(response, "Tag "tag2" already exists")
|
||||
tag1.refresh_from_db()
|
||||
self.assertEqual(tag1.name, "tag1")
|
||||
|
||||
@@ -74,9 +72,7 @@ class TagsEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
reverse("linkding:tags.edit", args=[tag1.id]), {"name": "TAG2"}
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response, "Tag "TAG2" already exists", status_code=422
|
||||
)
|
||||
self.assertContains(response, "Tag "TAG2" already exists")
|
||||
tag1.refresh_from_db()
|
||||
self.assertEqual(tag1.name, "tag1")
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
reverse("linkding:tags.merge"),
|
||||
{"target_tag": "target_tag", "merge_tags": "merge_tag"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
target_tag_group = self.get_form_group(response, "target_tag")
|
||||
self.assertIn(
|
||||
@@ -131,8 +130,6 @@ class TagsMergeViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
{"target_tag": "", "merge_tags": "merge_tag"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
target_tag_group = self.get_form_group(response, "target_tag")
|
||||
self.assertIn("This field is required", self.get_text(target_tag_group))
|
||||
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": ""},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
merge_tags_group = self.get_form_group(response, "merge_tags")
|
||||
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"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
target_tag_group = self.get_form_group(response, "target_tag")
|
||||
self.assertIn(
|
||||
'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"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
merge_tags_group = self.get_form_group(response, "merge_tags")
|
||||
self.assertIn(
|
||||
'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"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
target_tag_group = self.get_form_group(response, "target_tag")
|
||||
self.assertIn(
|
||||
"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"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
merge_tags_group = self.get_form_group(response, "merge_tags")
|
||||
self.assertIn(
|
||||
"The target tag cannot be selected for merging",
|
||||
|
||||
@@ -20,7 +20,7 @@ class TagsNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
def test_show_error_for_empty_name(self):
|
||||
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)
|
||||
|
||||
def test_show_error_for_duplicate_name(self):
|
||||
@@ -30,9 +30,7 @@ class TagsNewViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
reverse("linkding:tags.new"), {"name": "existing_tag"}
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response, "Tag "existing_tag" already exists", status_code=422
|
||||
)
|
||||
self.assertContains(response, "Tag "existing_tag" already exists")
|
||||
self.assertEqual(Tag.objects.count(), 1)
|
||||
|
||||
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"}
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response, "Tag "existing_TAG" already exists", status_code=422
|
||||
)
|
||||
self.assertContains(response, "Tag "existing_TAG" already exists")
|
||||
self.assertEqual(Tag.objects.count(), 1)
|
||||
|
||||
def test_no_error_for_duplicate_name_different_user(self):
|
||||
|
||||
@@ -5,12 +5,13 @@ from django.urls import reverse
|
||||
from bookmarks.models import Toast
|
||||
from bookmarks.tests.helpers import (
|
||||
BookmarkFactoryMixin,
|
||||
HtmlTestMixin,
|
||||
random_sentence,
|
||||
disable_logging,
|
||||
)
|
||||
|
||||
|
||||
class ToastsViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
class ToastsViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = self.get_or_create_test_user()
|
||||
@@ -72,11 +73,13 @@ class ToastsViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
|
||||
def test_form_tag(self):
|
||||
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"))
|
||||
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):
|
||||
toast = self.create_toast()
|
||||
|
||||
@@ -35,7 +35,7 @@ from bookmarks.services.bookmarks import (
|
||||
)
|
||||
from bookmarks.type_defs import HttpRequest
|
||||
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
|
||||
@@ -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
|
||||
def archived(request: HttpRequest):
|
||||
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):
|
||||
if request.method == "POST":
|
||||
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):
|
||||
if context["details"]:
|
||||
context["page_title"] = "Bookmark details - Linkding"
|
||||
|
||||
if turbo.is_frame(request, "details-modal"):
|
||||
return render(
|
||||
request,
|
||||
"bookmarks/updates/details-modal-frame.html",
|
||||
context,
|
||||
)
|
||||
return turbo.frame(request, "bookmarks/details/modal.html", context)
|
||||
|
||||
return render(
|
||||
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):
|
||||
if "save" in request.POST:
|
||||
if not request.user.is_authenticated:
|
||||
@@ -272,7 +328,7 @@ def index_action(request: HttpRequest):
|
||||
return response
|
||||
|
||||
if turbo.accept(request):
|
||||
return partials.active_bookmark_update(request)
|
||||
return index_update(request)
|
||||
|
||||
return utils.redirect_with_query(request, reverse("linkding:bookmarks.index"))
|
||||
|
||||
@@ -289,7 +345,7 @@ def archived_action(request: HttpRequest):
|
||||
return response
|
||||
|
||||
if turbo.accept(request):
|
||||
return partials.archived_bookmark_update(request)
|
||||
return archived_update(request)
|
||||
|
||||
return utils.redirect_with_query(request, reverse("linkding:bookmarks.archived"))
|
||||
|
||||
@@ -304,7 +360,7 @@ def shared_action(request: HttpRequest):
|
||||
return response
|
||||
|
||||
if turbo.accept(request):
|
||||
return partials.shared_bookmark_update(request)
|
||||
return shared_update(request)
|
||||
|
||||
return utils.redirect_with_query(request, reverse("linkding:bookmarks.shared"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -78,8 +78,14 @@ def tag_new(request: HttpRequest):
|
||||
messages.success(request, f'Tag "{tag.name}" created successfully.')
|
||||
return HttpResponseRedirect(reverse("linkding:tags.index"))
|
||||
else:
|
||||
return turbo.replace(
|
||||
request, "tag-modal", "tags/new.html", {"form": form}, status=422
|
||||
return turbo.stream(
|
||||
turbo.replace(
|
||||
request,
|
||||
"tag-modal",
|
||||
"tags/new.html",
|
||||
{"form": form},
|
||||
method="morph",
|
||||
)
|
||||
)
|
||||
|
||||
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.')
|
||||
return HttpResponseRedirect(reverse("linkding:tags.index"))
|
||||
else:
|
||||
return turbo.replace(
|
||||
request,
|
||||
"tag-modal",
|
||||
"tags/edit.html",
|
||||
{"tag": tag, "form": form},
|
||||
status=422,
|
||||
return turbo.stream(
|
||||
turbo.replace(
|
||||
request,
|
||||
"tag-modal",
|
||||
"tags/edit.html",
|
||||
{"tag": tag, "form": form},
|
||||
method="morph",
|
||||
)
|
||||
)
|
||||
|
||||
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"))
|
||||
else:
|
||||
return turbo.replace(
|
||||
request,
|
||||
"tag-modal",
|
||||
"tags/merge.html",
|
||||
{"form": form},
|
||||
status=422,
|
||||
return turbo.stream(
|
||||
turbo.replace(
|
||||
request,
|
||||
"tag-modal",
|
||||
"tags/merge.html",
|
||||
{"form": form},
|
||||
method="morph",
|
||||
)
|
||||
)
|
||||
|
||||
return render(request, "tags/merge.html", {"form": form})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render as django_render
|
||||
from django.template import loader
|
||||
|
||||
|
||||
@@ -14,24 +13,49 @@ def is_frame(request: HttpRequest, frame: str) -> bool:
|
||||
return request.headers.get("Turbo-Frame") == frame
|
||||
|
||||
|
||||
def stream(request: HttpRequest, template_name: str, context: dict) -> HttpResponse:
|
||||
response = django_render(request, template_name, context)
|
||||
response["Content-Type"] = "text/vnd.turbo-stream.html"
|
||||
def frame(request: HttpRequest, template_name: str, context: dict) -> HttpResponse:
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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(
|
||||
request: HttpRequest, target_id: str, template_name: str, context: dict, status=None
|
||||
) -> HttpResponse:
|
||||
"""
|
||||
Returns a Turbo steam for replacing a specific target with the rendered
|
||||
template. Mostly useful for updating forms in place after failed submissions,
|
||||
without having to create a separate template.
|
||||
"""
|
||||
if status is None:
|
||||
status = 200
|
||||
request: HttpRequest,
|
||||
target: str,
|
||||
template_name: str,
|
||||
context: dict,
|
||||
method: str | None = "",
|
||||
) -> str:
|
||||
"""Render a template wrapped in a replace turbo-stream element."""
|
||||
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>'
|
||||
response = HttpResponse(stream_content, status=status)
|
||||
response["Content-Type"] = "text/vnd.turbo-stream.html"
|
||||
return response
|
||||
method_attr = f' method="{method}"' if method else ""
|
||||
return f'<turbo-stream action="replace"{method_attr} target="{target}"><template>{content}</template></turbo-stream>'
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ dev = [
|
||||
"black>=25.1.0",
|
||||
"coverage>=7.10.4",
|
||||
"django-debug-toolbar>=6.0.0",
|
||||
"djlint>=1.36.4",
|
||||
"playwright>=1.54.0",
|
||||
"psycopg[binary]>=3.2.9",
|
||||
"pytest>=8.4.1",
|
||||
@@ -47,3 +48,12 @@ postgres = [
|
||||
[tool.uv]
|
||||
# Prefer system Python for now, less complications when copying the venv in the Docker build
|
||||
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
183
uv.lock
generated
@@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "django"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "execnet"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "linkding"
|
||||
version = "1.44.2"
|
||||
@@ -406,6 +475,7 @@ dev = [
|
||||
{ name = "black" },
|
||||
{ name = "coverage" },
|
||||
{ name = "django-debug-toolbar" },
|
||||
{ name = "djlint" },
|
||||
{ name = "playwright" },
|
||||
{ name = "psycopg", extra = ["binary"] },
|
||||
{ name = "pytest" },
|
||||
@@ -440,6 +510,7 @@ dev = [
|
||||
{ name = "black", specifier = ">=25.1.0" },
|
||||
{ name = "coverage", specifier = ">=7.10.4" },
|
||||
{ name = "django-debug-toolbar", specifier = ">=6.0.0" },
|
||||
{ name = "djlint", specifier = ">=1.36.4" },
|
||||
{ name = "playwright", specifier = ">=1.54.0" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "requests"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
|
||||
Reference in New Issue
Block a user