Compare commits

...

16 Commits

Author SHA1 Message Date
Sascha Ißbrücker
22e8750c24 Bump version 2023-07-29 11:22:34 +02:00
dependabot[bot]
ac75fd2ebd Bump django from 4.1.9 to 4.1.10 (#494)
Bumps [django](https://github.com/django/django) from 4.1.9 to 4.1.10.
- [Commits](https://github.com/django/django/compare/4.1.9...4.1.10)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-29 10:37:10 +02:00
dependabot[bot]
b05bf2534c Bump certifi from 2022.12.7 to 2023.7.22 (#497)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-29 10:32:51 +02:00
Sascha Ißbrücker
86a39e0433 Remove padding from multiline code blocks 2023-05-31 17:53:04 +02:00
Sascha Ißbrücker
4220ea0b4c Fix website loader content encoding detection (#482) 2023-05-30 22:04:54 +02:00
Sascha Ißbrücker
5d48c64b2b Enable WAL to avoid locked databases (#480) 2023-05-30 09:41:53 +02:00
acbgbca
424df155d8 Allow passing title and description to new bookmark form (#479)
* Added ability to set title and description #118

* Updated bookmarklet to pass site title #118

* Revert "Updated bookmarklet to pass site title #118"

This reverts commit 873d90130b.
2023-05-30 09:19:17 +02:00
dependabot[bot]
d87611dbcb Bump requests from 2.28.1 to 2.31.0 (#478)
Bumps [requests](https://github.com/psf/requests) from 2.28.1 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.28.1...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 21:21:47 +02:00
acbgbca
cd66dcee7b Added Apple web-app meta tag #358 (#359)
* Added Apple web-app meta tag #358

* Added manifest file for web app

* Changed manifest to use template #358

* Small tweaks, add tests

---------

Co-authored-by: Sascha Ißbrücker <sascha.issbruecker@gmail.com>
2023-05-23 21:20:58 +02:00
Sascha Ißbrücker
84f13dd792 Reorganize scripts and E2E tests 2023-05-21 14:32:24 +02:00
acbgbca
417dce785a Added Dev Container support (#474)
* Added dev container configuration

* Fixed container creation

* Added DevContainer detail to readme

* Ignoring dev container files

* Added playwright deps for tests

* Removed playwright installation #473
2023-05-21 13:35:00 +02:00
Sascha Ißbrücker
b28fc05d06 Update asset 2023-05-21 10:30:20 +02:00
Sascha Ißbrücker
17ab203f4f Document keyboard shortcuts 2023-05-20 21:08:04 +02:00
Sascha Ißbrücker
a06f9035cf Update README 2023-05-20 20:01:58 +02:00
Matt Sephton
5f28e87877 Update README.md to add Postman collection (#476)
I notice that for some time the list has not had items added in alphabetical order. I have not reordered any items.
2023-05-20 17:44:19 +02:00
Sascha Ißbrücker
f2ad826b11 Update CHANGELOG.md 2023-05-20 13:17:58 +02:00
42 changed files with 329 additions and 65 deletions

View File

@@ -0,0 +1,29 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "Python 3",
"image": "mcr.microsoft.com/devcontainers/python:0-3.10",
"features": {
"ghcr.io/devcontainers/features/node:1": {}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [8000],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pip3 install --user -r requirements.txt && npm install && mkdir -p data && python3 manage.py migrate",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
"ms-python.python"
]
}
},
"remoteUser": "vscode"
}

View File

@@ -6,12 +6,15 @@
/tmp
/docs
/static
/scripts
/build
/out
/.git
/.devcontainer
/.dockerignore
/.gitignore
/.gitattributes
/Dockerfile
/docker-compose.yml
/*.sh

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
* text=auto
*.sh text eol=lf

View File

@@ -44,4 +44,4 @@ jobs:
python manage.py compilescss
python manage.py collectstatic --ignore=*.scss
- name: Run tests
run: python manage.py test bookmarks.e2e
run: python manage.py test bookmarks.e2e --pattern="e2e_test_*.py"

View File

@@ -1,6 +0,0 @@
module.exports = {
ignoreIssuesWith: [
"wontfix",
"duplicate"
]
}

View File

@@ -1,5 +1,15 @@
# Changelog
## v1.19.0 (20/05/2023)
### What's Changed
* Add notes to bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/472
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.18.0...v1.19.0
---
## v1.18.0 (18/05/2023)
### What's Changed

View File

@@ -17,11 +17,12 @@
- [Documentation](#documentation)
- [Browser Extension](#browser-extension)
- [Community](#community)
- [Acknowledgements](#acknowledgements)
- [Development](#development)
## Introduction
linkding is a simple bookmark service that you can host yourself.
linkding is a bookmark manager that you can host yourself.
It's designed be to be minimal, fast, and easy to set up using Docker.
The name comes from:
@@ -30,22 +31,23 @@ The name comes from:
- ...so basically something for managing your links
**Feature Overview:**
- Clean UI optimized for readability
- Organize bookmarks with tags
- Add notes using Markdown
- Read it later functionality
- Share bookmarks with other users
- Bulk editing
- Bookmark archive
- Automatically provides titles and descriptions of bookmarked websites
- Automatically provides titles, descriptions and icons of bookmarked websites
- Automatically creates snapshots of bookmarked websites on [the Internet Archive Wayback Machine](https://archive.org/web/)
- Import and export bookmarks in Netscape HTML format
- Extensions for [Firefox](https://addons.mozilla.org/de/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe), as well as a bookmarklet
- Light and dark themes
- REST API for developing 3rd party apps
- Admin panel for user self-service and raw data access
- Easy setup using Docker, uses SQLite as database
- Easy setup using Docker and a SQLite database, with PostgreSQL as an option
**Demo:** https://demo.linkding.link/ (configured with open registration)
**Demo:** https://demo.linkding.link/ ([see here](https://github.com/sissbruecker/linkding/issues/408) if you have trouble accessing it)
**Screenshot:**
@@ -63,15 +65,13 @@ Alternatively linkding supports PostgreSQL, see the [database options](docs/Opti
To install linkding using Docker you can just run the [latest image](https://hub.docker.com/repository/docker/sissbruecker/linkding) from Docker Hub:
```shell
docker run --name linkding -p 9090:9090 -d sissbruecker/linkding:latest
```
By default, the application runs on port `9090`, you can map it to a different host port by modifying the port mapping in the command above. If everything completed successfully, the application should now be running and can be accessed at http://localhost:9090, provided you did not change the port mapping.
Note that the command above will store the linkding SQLite database in the container, which means that deleting the container, for example when upgrading the installation, will also remove the database. For hosting an actual installation you usually want to store the database on the host system, rather than in the container. To do so, run the following command, and replace the `{host-data-folder}` placeholder with an absolute path to a folder on your host system where you want to store the linkding database:
```shell
docker run --name linkding -p 9090:9090 -v {host-data-folder}:/etc/linkding/data -d sissbruecker/linkding:latest
```
In the command above, replace the `{host-data-folder}` placeholder with an absolute path to a folder on your host system where you want to store the linkding database.
If everything completed successfully, the application should now be running and can be accessed at http://localhost:9090.
To upgrade the installation to a new version, remove the existing container, pull the latest version of the linkding Docker image, and then start a new container using the same command that you used above. There is a [shell script](https://github.com/sissbruecker/linkding/blob/master/install-linkding.sh) available to automate these steps. The script can be configured using environment variables, or you can just modify it.
To complete the setup, you still have to [create an initial user](#user-setup), so that you can access your installation.
@@ -101,6 +101,8 @@ docker-compose exec linkding python manage.py createsuperuser --username=joe --e
The command will prompt you for a secure password. After the command has completed you can start using the application by logging into the UI with your credentials.
Alternatively you can automatically create an initial superuser on startup using the [`LD_SUPERUSER_NAME` option](docs/Options.md#LD_SUPERUSER_NAME).
### Reverse Proxy Setup
When using a reverse proxy, such as Nginx or Apache, you may need to configure your proxy to correctly forward the `Host` header to linkding, otherwise certain requests, such as login, might fail.
@@ -158,7 +160,7 @@ Instead of configuring header forwarding in your proxy, you can also configure t
### Managed Hosting Options
Self-hosting web applications on your own hardware (unfortunately) still requires a lot of technical know-how, and commitment to maintenance, with regard to keeping everything up-to-date and secure. This can be a huge entry barrier for people who are interested in self-hosting linkding, but lack the technical knowledge to do so. This section is intended to provide alternatives in form of managed hosting solutions. Note that these options are usually commercial offerings, that require paying a (usually monthly) fee for the convenience of being managed by another party. The technical knowledge required to make use of individual options is going to vary, and no guarantees can be made that every option is accessible for everyone. That being said, I hope this section helps in making the application accessible to a wider audience.
Self-hosting web applications still requires a lot of technical know-how, and commitment to maintenance, with regard to keeping everything up-to-date and secure. This can be a huge entry barrier for people who are interested in self-hosting, but lack the technical knowledge to do so. This section is intended to provide alternatives in form of managed hosting solutions. Note that these options are usually commercial offerings, that require paying a fee for the convenience of being managed by another party. The technical knowledge required to make use of individual options is going to vary, and no guarantees can be made that every option is accessible for everyone. That being said, I hope this section helps in making the application accessible to a wider audience.
- [linkding on fly.io](https://github.com/fspoettel/linkding-on-fly) - Guide for hosting a linkding installation on [fly.io](https://fly.io). By [fspoettel](https://github.com/fspoettel)
- [PikaPods.com](https://www.pikapods.com/) - Managed hosting for linkding, EU and US regions available. [1-click setup link](https://www.pikapods.com/pods?run=linkding)
@@ -171,6 +173,7 @@ Self-hosting web applications on your own hardware (unfortunately) still require
| [Backups](https://github.com/sissbruecker/linkding/blob/master/docs/backup.md) | How to backup the linkding database |
| [Troubleshooting](https://github.com/sissbruecker/linkding/blob/master/docs/troubleshooting.md) | Advice for troubleshooting common problems |
| [How To](https://github.com/sissbruecker/linkding/blob/master/docs/how-to.md) | Tips and tricks around using linking |
| [Keyboard shortcuts](https://github.com/sissbruecker/linkding/blob/master/docs/shortcuts.md) | List of available keyboard shortcuts |
| [Admin documentation](https://github.com/sissbruecker/linkding/blob/master/docs/Admin.md) | User documentation for the Admin UI |
| [API documentation](https://github.com/sissbruecker/linkding/blob/master/docs/API.md) | Documentation for the REST API |
@@ -186,14 +189,15 @@ The extension is open-source as well, and can be found [here](https://github.com
This section lists community projects around using linkding, in alphabetical order. If you have a project that you want to share with the linkding community, feel free to submit a PR to add your project to this section.
- [aiolinkding](https://github.com/bachya/aiolinkding) A Python3, async library to interact with the linkding REST API. By [bachya](https://github.com/bachya)
- [Helm Chart](https://charts.pascaliske.dev/charts/linkding/) Helm Chart for deploying linkding inside a Kubernetes cluster. By [pascaliske](https://github.com/pascaliske)
- [Linka!](https://github.com/cmsax/linka) Web app (also a PWA) for quickly searching & opening bookmarks in linkding, support multi keywords, exclude mode and other advance options. By [cmsax](https://github.com/cmsax)
- [linkding-cli](https://github.com/bachya/linkding-cli) A command-line interface (CLI) to interact with the linkding REST API. Powered by [aiolinkding](https://github.com/bachya/aiolinkding). By [bachya](https://github.com/bachya)
- [linkding-extension](https://github.com/jeroenpardon/linkding-extension) Chromium compatible extension that wraps the linkding bookmarklet. Tested with Chrome, Edge, Brave. By [jeroenpardon](https://github.com/jeroenpardon)
- [linkding-injector](https://github.com/Fivefold/linkding-injector) Injects search results from linkding into the sidebar of search pages like google and duckduckgo. Tested with Firefox and Chrome. By [Fivefold](https://github.com/Fivefold)
- [aiolinkding](https://github.com/bachya/aiolinkding) A Python3, async library to interact with the linkding REST API. By [bachya](https://github.com/bachya)
- [linkding-cli](https://github.com/bachya/linkding-cli) A command-line interface (CLI) to interact with the linkding REST API. Powered by [aiolinkding](https://github.com/bachya/aiolinkding). By [bachya](https://github.com/bachya)
- [Open all links bookmarklet](https://gist.github.com/ukcuddlyguy/336dd7339e6d35fc64a75ccfc9323c66) A browser bookmarklet to open all links on the current Linkding page in new tabs. By [ukcuddlyguy](https://github.com/ukcuddlyguy)
- [LinkThing](https://apps.apple.com/us/app/linkthing/id1666031776) An iOS client for linkding. By [amoscardino](https://github.com/amoscardino)
- [Open all links bookmarklet](https://gist.github.com/ukcuddlyguy/336dd7339e6d35fc64a75ccfc9323c66) A browser bookmarklet to open all links on the current Linkding page in new tabs. By [ukcuddlyguy](https://github.com/ukcuddlyguy)
- [Postman collection](https://gist.github.com/gingerbeardman/f0b42502f3bc9344e92ce63afd4360d3) a group of saved request templates for API testing. By [gingerbeardman](https://github.com/gingerbeardman)
## Acknowledgements
@@ -243,3 +247,23 @@ Start the Django development server with:
python3 manage.py runserver
```
The frontend is now available under http://localhost:8000
### DevContainers
This repository also supports DevContainers: [![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=git@github.com:sissbruecker/linkding.git)
Once checked out, only the following commands are required to get started:
Create a user for the frontend:
```
python3 manage.py createsuperuser --username=joe --email=joe@example.com
```
Start the Node.js development server (used for compiling JavaScript components like tag auto-completion) with:
```
npm run dev
```
Start the Django development server with:
```
python3 manage.py runserver
```
The frontend is now available under http://localhost:8000

View File

@@ -0,0 +1,24 @@
import logging
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import connections
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Enable WAL journal mode when using an SQLite database"
def handle(self, *args, **options):
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3':
return
connection = connections['default']
with connection.cursor() as cursor:
cursor.execute("PRAGMA journal_mode")
current_mode = cursor.fetchone()[0]
logger.info(f'Current journal mode: {current_mode}')
if current_mode != 'wal':
cursor.execute("PRAGMA journal_mode=wal;")
logger.info('Switched to WAL journal mode')

View File

@@ -71,8 +71,10 @@ def load_page(url: str):
logger.debug(f'Loaded chunk (iteration={iteration}, total={size / 1024})')
# Stop reading if we have parsed end of head tag
if '</head>'.encode('utf-8') in content:
end_of_head = '</head>'.encode('utf-8')
if end_of_head in content:
logger.debug(f'Found closing head tag after {size} bytes')
content = content.split(end_of_head)[0] + end_of_head
break
# Stop reading if we exceed limit
if size > MAX_CONTENT_LIMIT:

View File

@@ -256,6 +256,7 @@ ul.bookmark-list .notes-content {
pre code {
background: none;
box-shadow: none;
padding: 0;
}
> pre:first-child:last-child {

View File

@@ -8,6 +8,8 @@
<meta charset="UTF-8">
<link rel="icon" href="{% static 'favicon.png' %}"/>
<link rel="apple-touch-icon" href="{% static 'apple-touch-icon.png' %}">
<link rel="manifest" href="{% url 'bookmarks:manifest' %}">
<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">

View File

@@ -75,6 +75,25 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
'placeholder=" " autofocus class="form-input" required '
'id="id_url">',
html)
def test_should_prefill_title_from_url_parameter(self):
response = self.client.get(reverse('bookmarks:new') + '?title=Example%20Title')
html = response.content.decode()
self.assertInHTML(
'<input type="text" name="title" value="Example Title" '
'class="form-input" maxlength="512" autocomplete="off" '
'id="id_title">',
html)
def test_should_prefill_description_from_url_parameter(self):
response = self.client.get(reverse('bookmarks:new') + '?description=Example%20Site%20Description')
html = response.content.decode()
self.assertInHTML(
'<textarea name="description" class="form-input" cols="40" '
'rows="2" id="id_description">Example Site Description</textarea>',
html)
def test_should_enable_auto_close_when_specified_in_url_parameter(self):
response = self.client.get(

View File

@@ -0,0 +1,33 @@
from django.test import TestCase, override_settings
class MetadataViewTestCase(TestCase):
def test_default_manifest(self):
response = self.client.get("/manifest.json")
self.assertEqual(response.status_code, 200)
response_body = response.json()
expected_body = {
"short_name": "linkding",
"start_url": "bookmarks",
"display": "standalone",
"scope": "/"
}
self.assertDictEqual(response_body, expected_body)
@override_settings(LD_CONTEXT_PATH="linkding/")
def test_manifest_respects_context_path(self):
response = self.client.get("/manifest.json")
self.assertEqual(response.status_code, 200)
response_body = response.json()
expected_body = {
"short_name": "linkding",
"start_url": "bookmarks",
"display": "standalone",
"scope": "/linkding/"
}
self.assertDictEqual(response_body, expected_body)

View File

@@ -59,7 +59,7 @@ class WebsiteLoaderTestCase(TestCase):
expected_content_size = 6 * 1024 * 1000
self.assertEqual(expected_content_size, len(content))
def test_load_page_stops_reading_at_closing_head_tag(self):
def test_load_page_stops_reading_at_end_of_head(self):
with mock.patch('requests.get') as mock_get:
mock_get.return_value = MockStreamingResponse(num_chunks=10, chunk_size=1024 * 1000,
insert_head_after_chunk=0)
@@ -69,6 +69,18 @@ class WebsiteLoaderTestCase(TestCase):
expected_content_size = 1 * 1024 * 1000 + len('</head>')
self.assertEqual(expected_content_size, len(content))
def test_load_page_removes_bytes_after_end_of_head(self):
with mock.patch('requests.get') as mock_get:
mock_response = MockStreamingResponse(num_chunks=1, chunk_size=0)
mock_response.chunks[0] = '<head>人</head>'.encode('utf-8')
# add a single byte that can't be decoded to utf-8
mock_response.chunks[0] += 0xff.to_bytes(1, 'big')
mock_get.return_value = mock_response
content = website_loader.load_page('https://example.com')
# verify that byte after head was removed, content parsed as utf-8
self.assertEqual(content, '<head>人</head>')
def test_load_website_metadata(self):
with mock.patch('bookmarks.services.website_loader.load_page') as mock_load_page:
mock_load_page.return_value = self.render_html_document('test title', 'test description')

View File

@@ -32,5 +32,7 @@ urlpatterns = [
path('feeds/<str:feed_key>/all', AllBookmarksFeed(), name='feeds.all'),
path('feeds/<str:feed_key>/unread', UnreadBookmarksFeed(), name='feeds.unread'),
# Health check
path('health', views.health, name='health')
path('health', views.health, name='health'),
# Manifest
path("manifest.json", views.manifest, name='manifest')
]

View File

@@ -2,3 +2,4 @@ from .bookmarks import *
from .settings import *
from .toasts import *
from .health import health
from .manifest import manifest

View File

@@ -114,6 +114,8 @@ def convert_tag_string(tag_string: str):
@login_required
def new(request):
initial_url = request.GET.get('url')
initial_title = request.GET.get('title')
initial_description = request.GET.get('description')
initial_auto_close = 'auto_close' in request.GET
if request.method == 'POST':
@@ -131,6 +133,10 @@ def new(request):
form = BookmarkForm()
if initial_url:
form.initial['url'] = initial_url
if initial_title:
form.initial['title'] = initial_title
if initial_description:
form.initial['description'] = initial_description
if initial_auto_close:
form.initial['auto_close'] = 'true'

View File

@@ -0,0 +1,13 @@
from django.http import JsonResponse
from django.conf import settings
def manifest(request):
response = {
"short_name": "linkding",
"start_url": "bookmarks",
"display": "standalone",
"scope": "/" + settings.LD_CONTEXT_PATH
}
return JsonResponse(response, status=200)

View File

@@ -10,6 +10,8 @@ mkdir -p data/favicons
# Run database migration
python manage.py migrate
# Enable WAL journal mode for SQLite databases
python manage.py enable_wal
# Generate secret key file if it does not exist
python manage.py generate_secret_key
# Create initial superuser if defined in options / environment variables

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
rm -rf static
npm run build
python manage.py compilescss
python manage.py collectstatic --ignore=*.scss
python manage.py compilescss --delete-files

Binary file not shown.

BIN
docs/header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 2599 591" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.5;">
<svg width="100%" height="100%" viewBox="0 0 2126 591" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g transform="matrix(1.18075,0,0,1.18075,-1265.31,-1395.82)">
<circle cx="1314.98" cy="1424.52" r="190.496" style="fill:rgb(88,86,224);"/>
</g>
@@ -13,29 +13,6 @@
</g>
</g>
<g transform="matrix(8.26174,0,0,8.26174,-5762.21,-2037.46)">
<g transform="matrix(50,0,0,50,770.835,299.13)">
<path d="M0.078,-0.714L0.078,-0L0.551,-0L0.551,-0.08L0.173,-0.08L0.173,-0.714L0.078,-0.714Z" style="fill:rgb(132,132,228);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,798.635,299.13)">
<rect x="0.082" y="-0.714" width="0.095" height="0.714" style="fill:rgb(132,132,228);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,811.585,299.13)">
<path d="M0.077,-0.714L0.077,-0L0.167,-0L0.167,-0.573L0.169,-0.573L0.542,-0L0.646,-0L0.646,-0.714L0.556,-0.714L0.556,-0.135L0.554,-0.135L0.178,-0.714L0.077,-0.714Z" style="fill:rgb(132,132,228);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,847.685,299.13)">
<path d="M0.078,-0.714L0.078,-0L0.173,-0L0.173,-0.25L0.292,-0.361L0.55,-0L0.67,-0L0.357,-0.426L0.658,-0.714L0.535,-0.714L0.173,-0.358L0.173,-0.714L0.078,-0.714Z" style="fill:rgb(132,132,228);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,881.035,299.13)">
<path d="M0.173,-0.08L0.173,-0.634L0.333,-0.634C0.377,-0.634 0.414,-0.628 0.444,-0.616C0.474,-0.603 0.498,-0.585 0.518,-0.562C0.537,-0.538 0.55,-0.509 0.559,-0.476C0.567,-0.442 0.571,-0.404 0.571,-0.361C0.571,-0.317 0.567,-0.28 0.558,-0.249C0.549,-0.218 0.537,-0.192 0.523,-0.171C0.509,-0.15 0.493,-0.134 0.476,-0.122C0.458,-0.11 0.44,-0.101 0.422,-0.095C0.404,-0.088 0.387,-0.084 0.371,-0.083C0.355,-0.081 0.342,-0.08 0.331,-0.08L0.173,-0.08ZM0.078,-0.714L0.078,-0L0.323,-0C0.382,-0 0.434,-0.008 0.477,-0.025C0.52,-0.042 0.556,-0.066 0.584,-0.098C0.612,-0.129 0.633,-0.168 0.646,-0.215C0.659,-0.261 0.666,-0.314 0.666,-0.374C0.666,-0.489 0.636,-0.574 0.577,-0.63C0.518,-0.686 0.433,-0.714 0.323,-0.714L0.078,-0.714Z" style="fill:rgb(132,132,228);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,916.235,299.13)">
<rect x="0.082" y="-0.714" width="0.095" height="0.714" style="fill:rgb(132,132,228);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,929.185,299.13)">
<path d="M0.077,-0.714L0.077,-0L0.167,-0L0.167,-0.573L0.169,-0.573L0.542,-0L0.646,-0L0.646,-0.714L0.556,-0.714L0.556,-0.135L0.554,-0.135L0.178,-0.714L0.077,-0.714Z" style="fill:rgb(132,132,228);fill-rule:nonzero;"/>
</g>
<g transform="matrix(50,0,0,50,965.285,299.13)">
<path d="M0.612,-0.089L0.637,-0L0.697,-0L0.697,-0.376L0.384,-0.376L0.384,-0.296L0.612,-0.296C0.613,-0.263 0.609,-0.233 0.599,-0.205C0.589,-0.176 0.574,-0.152 0.555,-0.131C0.535,-0.11 0.511,-0.093 0.482,-0.081C0.453,-0.069 0.42,-0.063 0.383,-0.063C0.343,-0.063 0.308,-0.071 0.278,-0.087C0.247,-0.102 0.222,-0.123 0.201,-0.15C0.18,-0.176 0.165,-0.206 0.154,-0.241C0.143,-0.275 0.138,-0.311 0.138,-0.348C0.138,-0.386 0.143,-0.423 0.152,-0.46C0.161,-0.496 0.176,-0.528 0.196,-0.557C0.215,-0.585 0.241,-0.608 0.272,-0.625C0.303,-0.642 0.34,-0.651 0.383,-0.651C0.41,-0.651 0.435,-0.648 0.459,-0.642C0.482,-0.635 0.503,-0.626 0.522,-0.613C0.541,-0.6 0.556,-0.584 0.569,-0.565C0.582,-0.545 0.59,-0.521 0.595,-0.494L0.69,-0.494C0.683,-0.536 0.671,-0.572 0.653,-0.602C0.634,-0.631 0.612,-0.656 0.585,-0.675C0.558,-0.694 0.527,-0.708 0.493,-0.718C0.458,-0.727 0.422,-0.731 0.383,-0.731C0.326,-0.731 0.277,-0.721 0.235,-0.7C0.192,-0.679 0.157,-0.65 0.129,-0.615C0.1,-0.58 0.079,-0.539 0.065,-0.492C0.05,-0.444 0.043,-0.395 0.043,-0.343C0.043,-0.296 0.051,-0.251 0.066,-0.208C0.081,-0.165 0.104,-0.126 0.133,-0.093C0.162,-0.06 0.198,-0.033 0.24,-0.014C0.282,0.006 0.33,0.016 0.383,0.016C0.425,0.016 0.467,0.008 0.508,-0.009C0.549,-0.025 0.584,-0.052 0.612,-0.089Z" style="fill:rgb(132,132,228);fill-rule:nonzero;"/>
</g>
<text x="770.835px" y="299.13px" style="font-family:'HelveticaNeue', 'Helvetica Neue';font-size:50px;fill:rgb(94,94,219);">l<tspan x="782.685px 794.535px 823.085px 849.785px 880.185px 892.035px 920.585px " y="299.13px 299.13px 299.13px 299.13px 299.13px 299.13px 299.13px ">inkding</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

After

Width:  |  Height:  |  Size: 374 KiB

10
docs/shortcuts.md Normal file
View File

@@ -0,0 +1,10 @@
# Keyboard Shortcuts
The following keyboard shortcuts are currently available:
| Action | Shortcut |
|-------------------------------------------------------------------------------------------|-------------------------------------|
| Add new bookmark | <kbd>n</kbd> |
| Focus search input | <kbd>s</kbd> |
| Navigate bookmarks | <kbd></kbd>, <kbd></kbd> |
| Toggle bookmark notes | <kbd>e</kbd> |

Binary file not shown.

BIN
docs/social-preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "linkding",
"version": "1.19.0",
"version": "1.19.1",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -2,11 +2,11 @@ asgiref==3.5.2
beautifulsoup4==4.11.1
bleach==6.0.0
bleach-allowlist==1.0.3
certifi==2022.12.7
certifi==2023.7.22
charset-normalizer==2.1.1
click==8.1.3
confusable-homoglyphs==3.2.0
Django==4.1.9
Django==4.1.10
django-generate-secret-key==1.0.2
django-registration==3.3
django-sass-processor==1.2.1
@@ -18,7 +18,7 @@ Markdown==3.4.3
psycopg2==2.9.5
python-dateutil==2.8.2
pytz==2022.2.1
requests==2.28.1
requests==2.31.0
soupsieve==2.3.2.post1
sqlparse==0.4.4
supervisor==4.2.4

View File

@@ -2,12 +2,12 @@ asgiref==3.5.2
beautifulsoup4==4.11.1
bleach==6.0.0
bleach-allowlist==1.0.3
certifi==2022.12.7
certifi==2023.7.22
charset-normalizer==2.1.1
click==8.1.3
confusable-homoglyphs==3.2.0
coverage==5.5
Django==4.1.9
Django==4.1.10
django-appconf==1.0.5
django-compressor==4.1
django-debug-toolbar==3.6.0
@@ -27,7 +27,7 @@ pyee==9.0.4
python-dateutil==2.8.2
pytz==2022.2.1
rcssmin==1.1.0
requests==2.28.1
requests==2.31.0
rjsmin==1.2.0
six==1.16.0
soupsieve==2.3.2.post1

49
scripts/generate-changelog.py Executable file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
import requests
from datetime import datetime
def load_releases_page(page):
url = f'https://api.github.com/repos/sissbruecker/linkding/releases?page={page}'
return requests.get(url).json()
def load_all_releases():
load_next_page = True
page = 1
releases = []
while load_next_page:
page_result = load_releases_page(page)
releases = releases + page_result
load_next_page = len(page_result) > 0
page = page + 1
return releases
def render_release_section(release):
date = datetime.fromisoformat(release['published_at'].replace("Z", "+00:00"))
formatted_date = date.strftime('%d/%m/%Y')
section = f'## {release["name"]} ({formatted_date})\n\n'
body = release['body']
# increase heading for body content
body = body.replace("## What's Changed", "### What's Changed")
body = body.replace("## New Contributors", "### New Contributors")
section += body.strip()
return section
def generate_change_log():
releases = load_all_releases()
change_log = '# Changelog\n\n'
sections = [render_release_section(release) for release in releases]
body = '\n\n---\n\n'.join(sections)
change_log = change_log + body
with open("CHANGELOG.md", "w") as file:
file.write(change_log)
generate_change_log()

10
scripts/run-docker.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
docker build -t sissbruecker/linkding:local .
docker rm -f linkding-local || true
docker run --name linkding-local --rm -p 9090:9090 \
-e LD_SUPERUSER_NAME=admin \
-e LD_SUPERUSER_PASSWORD=admin \
sissbruecker/linkding:local

14
scripts/test-e2e.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Make sure Chromium is installed
playwright install chromium
# Test server loads assets from static folder, so make sure files there are up-to-date
rm -rf static
npm run build
python manage.py compilescss
python manage.py collectstatic --ignore=*.scss
python manage.py compilescss --delete-files
# Run E2E tests
python manage.py test bookmarks.e2e --pattern="e2e_test_*.py"

29
scripts/test-postgres.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -euo pipefail
# Remove previous container if exists
docker rm -f linkding-postgres-test || true
# Run postgres container
docker run -d \
-e POSTGRES_DB=linkding \
-e POSTGRES_USER=linkding \
-e POSTGRES_PASSWORD=linkding \
-p 5432:5432 \
--name linkding-postgres-test \
postgres
# Wait until postgres has started
echo >&2 "$(date +%Y%m%dt%H%M%S) Waiting for postgres container"
sleep 15
# Run tests using postgres
export LD_DB_ENGINE=postgres
export LD_DB_USER=linkding
export LD_DB_PASSWORD=linkding
./scripts/test.sh
# Remove postgres container
docker rm -f linkding-postgres-test || true

1
scripts/test-unit.sh Executable file
View File

@@ -0,0 +1 @@
python manage.py test bookmarks.tests

2
scripts/test.sh Executable file
View File

@@ -0,0 +1,2 @@
./scripts/test-unit.sh
./scripts/test-e2e.sh

View File

@@ -1 +1 @@
1.19.0
1.19.1