mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-24 20:33:04 +00:00
Add reader mode (#703)
* Add reader mode view * Show link for latest snapshot instead
This commit is contained in:
parent
0586983602
commit
0cbaf927e4
11 changed files with 2551 additions and 16 deletions
2314
bookmarks/static/vendor/Readability.js
vendored
Normal file
2314
bookmarks/static/vendor/Readability.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
27
bookmarks/styles/reader-mode.scss
Normal file
27
bookmarks/styles/reader-mode.scss
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
html.reader-mode {
|
||||||
|
--font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 3rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.byline {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reading-time {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
@import "bookmark-form";
|
@import "bookmark-form";
|
||||||
@import "settings";
|
@import "settings";
|
||||||
@import "markdown";
|
@import "markdown";
|
||||||
|
@import "reader-mode";
|
||||||
|
|
||||||
/* Dark theme overrides */
|
/* Dark theme overrides */
|
||||||
|
|
||||||
|
|
|
@ -12,3 +12,4 @@
|
||||||
@import "bookmark-form";
|
@import "bookmark-form";
|
||||||
@import "settings";
|
@import "settings";
|
||||||
@import "markdown";
|
@import "markdown";
|
||||||
|
@import "reader-mode";
|
||||||
|
|
|
@ -12,6 +12,17 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span>{{ details.bookmark.url }}</span>
|
<span>{{ details.bookmark.url }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
{% if details.latest_snapshot %}
|
||||||
|
<a class="weblink" href="{% url 'bookmarks: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">
|
||||||
|
<use xlink:href="#ld-icon-unread"></use>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
<span>Reader mode</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if details.bookmark.web_archive_snapshot_url %}
|
{% if details.bookmark.web_archive_snapshot_url %}
|
||||||
<a class="weblink" href="{{ details.bookmark.web_archive_snapshot_url }}"
|
<a class="weblink" href="{{ details.bookmark.web_archive_snapshot_url }}"
|
||||||
target="{{ details.profile.bookmark_link_target }}">
|
target="{{ details.profile.bookmark_link_target }}">
|
||||||
|
@ -22,7 +33,7 @@
|
||||||
fill="currentColor" fill-rule="evenodd"/>
|
fill="currentColor" fill-rule="evenodd"/>
|
||||||
</svg>
|
</svg>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span>View on Internet Archive</span>
|
<span>Internet Archive</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
83
bookmarks/templates/bookmarks/read.html
Normal file
83
bookmarks/templates/bookmarks/read.html
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{% load sass_tags %}
|
||||||
|
{% 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">
|
||||||
|
{% if request.user_profile.theme == 'light' %}
|
||||||
|
<link href="{% sass_src 'theme-light.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
|
||||||
|
{% elif request.user_profile.theme == 'dark' %}
|
||||||
|
<link href="{% sass_src 'theme-dark.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css"/>
|
||||||
|
{% else %}
|
||||||
|
{# Use auto theme as fallback #}
|
||||||
|
<link href="{% sass_src 'theme-dark.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css"
|
||||||
|
media="(prefers-color-scheme: dark)"/>
|
||||||
|
<link href="{% sass_src 'theme-light.scss' %}?v={{ app_version }}" rel="stylesheet" type="text/css"
|
||||||
|
media="(prefers-color-scheme: light)"/>
|
||||||
|
{% 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 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;
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.classList.add('container');
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 divider = document.createElement('hr');
|
||||||
|
container.append(divider);
|
||||||
|
|
||||||
|
const articleContent = document.createElement('div');
|
||||||
|
articleContent.innerHTML = article.content;
|
||||||
|
postProcess(articleContent);
|
||||||
|
container.append(articleContent);
|
||||||
|
|
||||||
|
content.replaceWith(container);
|
||||||
|
}
|
||||||
|
makeReadable();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -34,12 +34,12 @@ class BookmarkAssetViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
asset = self.setup_asset(bookmark=bookmark, file=filename)
|
asset = self.setup_asset(bookmark=bookmark, file=filename)
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
def test_view_access(self):
|
def view_access_test(self, view_name: str):
|
||||||
# own bookmark
|
# own bookmark
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# other user's bookmark
|
# other user's bookmark
|
||||||
|
@ -47,14 +47,14 @@ class BookmarkAssetViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
bookmark = self.setup_bookmark(user=other_user)
|
bookmark = self.setup_bookmark(user=other_user)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# shared, sharing disabled
|
# shared, sharing disabled
|
||||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# unshared, sharing enabled
|
# unshared, sharing enabled
|
||||||
|
@ -64,31 +64,31 @@ class BookmarkAssetViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
bookmark = self.setup_bookmark(user=other_user, shared=False)
|
bookmark = self.setup_bookmark(user=other_user, shared=False)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# shared, sharing enabled
|
# shared, sharing enabled
|
||||||
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
bookmark = self.setup_bookmark(user=other_user, shared=True)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_view_access_guest_user(self):
|
def view_access_guest_user_test(self, view_name: str):
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
|
|
||||||
# unshared, sharing disabled
|
# unshared, sharing disabled
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# shared, sharing disabled
|
# shared, sharing disabled
|
||||||
bookmark = self.setup_bookmark(shared=True)
|
bookmark = self.setup_bookmark(shared=True)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# unshared, sharing enabled
|
# unshared, sharing enabled
|
||||||
|
@ -98,14 +98,14 @@ class BookmarkAssetViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
bookmark = self.setup_bookmark(shared=False)
|
bookmark = self.setup_bookmark(shared=False)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# shared, sharing enabled
|
# shared, sharing enabled
|
||||||
bookmark = self.setup_bookmark(shared=True)
|
bookmark = self.setup_bookmark(shared=True)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# unshared, public sharing enabled
|
# unshared, public sharing enabled
|
||||||
|
@ -114,12 +114,24 @@ class BookmarkAssetViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||||
bookmark = self.setup_bookmark(shared=False)
|
bookmark = self.setup_bookmark(shared=False)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# shared, public sharing enabled
|
# shared, public sharing enabled
|
||||||
bookmark = self.setup_bookmark(shared=True)
|
bookmark = self.setup_bookmark(shared=True)
|
||||||
asset = self.setup_asset_with_file(bookmark)
|
asset = self.setup_asset_with_file(bookmark)
|
||||||
|
|
||||||
response = self.client.get(reverse("bookmarks:assets.view", args=[asset.id]))
|
response = self.client.get(reverse(view_name, args=[asset.id]))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_view_access(self):
|
||||||
|
self.view_access_test("bookmarks:assets.view")
|
||||||
|
|
||||||
|
def test_view_access_guest_user(self):
|
||||||
|
self.view_access_guest_user_test("bookmarks:assets.view")
|
||||||
|
|
||||||
|
def test_reader_view_access(self):
|
||||||
|
self.view_access_test("bookmarks:assets.read")
|
||||||
|
|
||||||
|
def test_reader_view_access_guest_user(self):
|
||||||
|
self.view_access_guest_user_test("bookmarks:assets.read")
|
||||||
|
|
|
@ -46,6 +46,9 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||||
def find_weblink(self, soup, url):
|
def find_weblink(self, soup, url):
|
||||||
return soup.find("a", {"class": "weblink", "href": url})
|
return soup.find("a", {"class": "weblink", "href": url})
|
||||||
|
|
||||||
|
def count_weblinks(self, soup):
|
||||||
|
return len(soup.find_all("a", {"class": "weblink"}))
|
||||||
|
|
||||||
def find_asset(self, soup, asset):
|
def find_asset(self, soup, asset):
|
||||||
return soup.find("div", {"data-asset-id": asset.id})
|
return soup.find("div", {"data-asset-id": asset.id})
|
||||||
|
|
||||||
|
@ -172,6 +175,48 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||||
self.assertIsNotNone(image)
|
self.assertIsNotNone(image)
|
||||||
self.assertEqual(image["src"], "/static/example.png")
|
self.assertEqual(image["src"], "/static/example.png")
|
||||||
|
|
||||||
|
def test_reader_mode_link(self):
|
||||||
|
# no latest snapshot
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
soup = self.get_details(bookmark)
|
||||||
|
self.assertEqual(self.count_weblinks(soup), 1)
|
||||||
|
|
||||||
|
# snapshot is not complete
|
||||||
|
self.setup_asset(
|
||||||
|
bookmark,
|
||||||
|
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
|
||||||
|
status=BookmarkAsset.STATUS_PENDING,
|
||||||
|
)
|
||||||
|
self.setup_asset(
|
||||||
|
bookmark,
|
||||||
|
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
|
||||||
|
status=BookmarkAsset.STATUS_FAILURE,
|
||||||
|
)
|
||||||
|
soup = self.get_details(bookmark)
|
||||||
|
self.assertEqual(self.count_weblinks(soup), 1)
|
||||||
|
|
||||||
|
# not a snapshot
|
||||||
|
self.setup_asset(
|
||||||
|
bookmark,
|
||||||
|
asset_type="upload",
|
||||||
|
status=BookmarkAsset.STATUS_COMPLETE,
|
||||||
|
)
|
||||||
|
soup = self.get_details(bookmark)
|
||||||
|
self.assertEqual(self.count_weblinks(soup), 1)
|
||||||
|
|
||||||
|
# snapshot is complete
|
||||||
|
asset = self.setup_asset(
|
||||||
|
bookmark,
|
||||||
|
asset_type=BookmarkAsset.TYPE_SNAPSHOT,
|
||||||
|
status=BookmarkAsset.STATUS_COMPLETE,
|
||||||
|
)
|
||||||
|
soup = self.get_details(bookmark)
|
||||||
|
self.assertEqual(self.count_weblinks(soup), 2)
|
||||||
|
|
||||||
|
reader_mode_url = reverse("bookmarks:assets.read", args=[asset.id])
|
||||||
|
link = self.find_weblink(soup, reader_mode_url)
|
||||||
|
self.assertIsNotNone(link)
|
||||||
|
|
||||||
def test_internet_archive_link(self):
|
def test_internet_archive_link(self):
|
||||||
# without snapshot url
|
# without snapshot url
|
||||||
bookmark = self.setup_bookmark()
|
bookmark = self.setup_bookmark()
|
||||||
|
@ -185,7 +230,7 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||||
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
|
||||||
self.assertIsNotNone(link)
|
self.assertIsNotNone(link)
|
||||||
self.assertEqual(link["href"], bookmark.web_archive_snapshot_url)
|
self.assertEqual(link["href"], bookmark.web_archive_snapshot_url)
|
||||||
self.assertEqual(link.text.strip(), "View on Internet Archive")
|
self.assertEqual(link.text.strip(), "Internet Archive")
|
||||||
|
|
||||||
# favicons disabled
|
# favicons disabled
|
||||||
bookmark = self.setup_bookmark(
|
bookmark = self.setup_bookmark(
|
||||||
|
|
|
@ -55,6 +55,11 @@ urlpatterns = [
|
||||||
views.assets.view,
|
views.assets.view,
|
||||||
name="assets.view",
|
name="assets.view",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"assets/<int:asset_id>/read",
|
||||||
|
views.assets.read,
|
||||||
|
name="assets.read",
|
||||||
|
),
|
||||||
# Partials
|
# Partials
|
||||||
path(
|
path(
|
||||||
"bookmarks/partials/bookmark-list/active",
|
"bookmarks/partials/bookmark-list/active",
|
||||||
|
|
|
@ -6,11 +6,12 @@ from django.http import (
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
Http404,
|
Http404,
|
||||||
)
|
)
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
from bookmarks.models import BookmarkAsset
|
from bookmarks.models import BookmarkAsset
|
||||||
|
|
||||||
|
|
||||||
def view(request, asset_id: int):
|
def _access_asset(request, asset_id: int):
|
||||||
try:
|
try:
|
||||||
asset = BookmarkAsset.objects.get(pk=asset_id)
|
asset = BookmarkAsset.objects.get(pk=asset_id)
|
||||||
except BookmarkAsset.DoesNotExist:
|
except BookmarkAsset.DoesNotExist:
|
||||||
|
@ -28,6 +29,10 @@ def view(request, asset_id: int):
|
||||||
if not is_owner and not is_shared and not is_public_shared:
|
if not is_owner and not is_shared and not is_public_shared:
|
||||||
raise Http404("Bookmark does not exist")
|
raise Http404("Bookmark does not exist")
|
||||||
|
|
||||||
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
def _get_asset_content(asset):
|
||||||
filepath = os.path.join(settings.LD_ASSET_FOLDER, asset.file)
|
filepath = os.path.join(settings.LD_ASSET_FOLDER, asset.file)
|
||||||
|
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
|
@ -40,4 +45,25 @@ def view(request, asset_id: int):
|
||||||
with open(filepath, "rb") as f:
|
with open(filepath, "rb") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def view(request, asset_id: int):
|
||||||
|
asset = _access_asset(request, asset_id)
|
||||||
|
content = _get_asset_content(asset)
|
||||||
|
|
||||||
return HttpResponse(content, content_type=asset.content_type)
|
return HttpResponse(content, content_type=asset.content_type)
|
||||||
|
|
||||||
|
|
||||||
|
def read(request, asset_id: int):
|
||||||
|
asset = _access_asset(request, asset_id)
|
||||||
|
content = _get_asset_content(asset)
|
||||||
|
content = content.decode("utf-8")
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bookmarks/read.html",
|
||||||
|
{
|
||||||
|
"content": content,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
@ -346,6 +346,7 @@ class BookmarkAssetItem:
|
||||||
|
|
||||||
self.id = asset.id
|
self.id = asset.id
|
||||||
self.display_name = asset.display_name
|
self.display_name = asset.display_name
|
||||||
|
self.asset_type = asset.asset_type
|
||||||
self.content_type = asset.content_type
|
self.content_type = asset.content_type
|
||||||
self.file = asset.file
|
self.file = asset.file
|
||||||
self.file_size = asset.file_size
|
self.file_size = asset.file_size
|
||||||
|
@ -393,3 +394,12 @@ class BookmarkDetailsContext:
|
||||||
self.has_pending_assets = any(
|
self.has_pending_assets = any(
|
||||||
asset.status == BookmarkAsset.STATUS_PENDING for asset in self.assets
|
asset.status == BookmarkAsset.STATUS_PENDING for asset in self.assets
|
||||||
)
|
)
|
||||||
|
self.latest_snapshot = next(
|
||||||
|
(
|
||||||
|
asset
|
||||||
|
for asset in self.assets
|
||||||
|
if asset.asset.asset_type == BookmarkAsset.TYPE_SNAPSHOT
|
||||||
|
and asset.status == BookmarkAsset.STATUS_COMPLETE
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue