mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-28 06:53:12 +08:00
Template improvements
This commit is contained in:
@@ -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",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user