mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-10 06:04:15 +00:00
Implement dark theme (#49)
This commit is contained in:
parent
3e5e825032
commit
119d8f7efb
28 changed files with 314 additions and 83 deletions
|
@ -16,6 +16,7 @@ The name comes from:
|
||||||
- Bookmark archive
|
- Bookmark archive
|
||||||
- Extensions for [Firefox](https://addons.mozilla.org/de/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe)
|
- Extensions for [Firefox](https://addons.mozilla.org/de/firefox/addon/linkding-extension/) and [Chrome](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe)
|
||||||
- Bookmarklet that should work in most browsers
|
- Bookmarklet that should work in most browsers
|
||||||
|
- Dark mode
|
||||||
- Easy to set up using Docker
|
- Easy to set up using Docker
|
||||||
- Uses SQLite as database
|
- Uses SQLite as database
|
||||||
- Works without Javascript
|
- Works without Javascript
|
||||||
|
@ -161,4 +162,4 @@ The frontend is now available under http://localhost:8000
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
- [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-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)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.translation import ngettext, gettext
|
||||||
from rest_framework.authtoken.admin import TokenAdmin
|
from rest_framework.authtoken.admin import TokenAdmin
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from bookmarks.models import Bookmark, Tag
|
from bookmarks.models import Bookmark, Tag, UserProfile
|
||||||
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
|
from bookmarks.services.bookmarks import archive_bookmark, unarchive_bookmark
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,8 +77,24 @@ class AdminTag(admin.ModelAdmin):
|
||||||
), messages.SUCCESS)
|
), messages.SUCCESS)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUserProfileInline(admin.StackedInline):
|
||||||
|
model = UserProfile
|
||||||
|
can_delete = False
|
||||||
|
verbose_name_plural = 'Profile'
|
||||||
|
fk_name = 'user'
|
||||||
|
|
||||||
|
|
||||||
|
class AdminCustomUser(UserAdmin):
|
||||||
|
inlines = (AdminUserProfileInline,)
|
||||||
|
|
||||||
|
def get_inline_instances(self, request, obj=None):
|
||||||
|
if not obj:
|
||||||
|
return list()
|
||||||
|
return super(AdminCustomUser, self).get_inline_instances(request, obj)
|
||||||
|
|
||||||
|
|
||||||
linkding_admin_site = LinkdingAdminSite()
|
linkding_admin_site = LinkdingAdminSite()
|
||||||
linkding_admin_site.register(Bookmark, AdminBookmark)
|
linkding_admin_site.register(Bookmark, AdminBookmark)
|
||||||
linkding_admin_site.register(Tag, AdminTag)
|
linkding_admin_site.register(Tag, AdminTag)
|
||||||
linkding_admin_site.register(User, UserAdmin)
|
linkding_admin_site.register(User, AdminCustomUser)
|
||||||
linkding_admin_site.register(Token, TokenAdmin)
|
linkding_admin_site.register(Token, TokenAdmin)
|
||||||
|
|
|
@ -264,17 +264,4 @@
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Should be read from theme */
|
|
||||||
.menu-item.selected > a {
|
|
||||||
background: #f1f1fc;
|
|
||||||
color: #5755d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-item, .group-item:hover {
|
|
||||||
color: #999999;
|
|
||||||
text-transform: uppercase;
|
|
||||||
background: none;
|
|
||||||
font-size: 0.6rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -124,10 +124,4 @@
|
||||||
.menu.open {
|
.menu.open {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
/* TODO: Should be read from theme */
|
|
||||||
.menu-item.selected > a {
|
|
||||||
background: #f1f1fc;
|
|
||||||
color: #5755d9;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
43
bookmarks/migrations/0007_userprofile.py
Normal file
43
bookmarks/migrations/0007_userprofile.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# Generated by Django 2.2.18 on 2021-03-26 22:39
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def forwards(apps, schema_editor):
|
||||||
|
User = apps.get_model('auth', 'User')
|
||||||
|
UserProfile = apps.get_model('bookmarks', 'UserProfile')
|
||||||
|
for user in User.objects.all():
|
||||||
|
try:
|
||||||
|
if user.profile:
|
||||||
|
continue
|
||||||
|
except UserProfile.DoesNotExist:
|
||||||
|
profile = UserProfile(user=user)
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('bookmarks', '0006_bookmark_is_archived'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('theme',
|
||||||
|
models.CharField(choices=[('auto', 'Auto'), ('light', 'Light'), ('dark', 'Dark')], default='auto',
|
||||||
|
max_length=10)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile',
|
||||||
|
to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RunPython(forwards, reverse),
|
||||||
|
]
|
|
@ -2,7 +2,10 @@ from typing import List
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from bookmarks.utils import unique
|
from bookmarks.utils import unique
|
||||||
from bookmarks.validators import BookmarkURLValidator
|
from bookmarks.validators import BookmarkURLValidator
|
||||||
|
@ -93,3 +96,33 @@ class BookmarkForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bookmark
|
model = Bookmark
|
||||||
fields = ['url', 'tag_string', 'title', 'description', 'auto_close', 'return_url']
|
fields = ['url', 'tag_string', 'title', 'description', 'auto_close', 'return_url']
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(models.Model):
|
||||||
|
THEME_AUTO = 'auto'
|
||||||
|
THEME_LIGHT = 'light'
|
||||||
|
THEME_DARK = 'dark'
|
||||||
|
THEME_CHOICES = [
|
||||||
|
(THEME_AUTO, 'Auto'),
|
||||||
|
(THEME_LIGHT, 'Light'),
|
||||||
|
(THEME_DARK, 'Dark'),
|
||||||
|
]
|
||||||
|
user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE)
|
||||||
|
theme = models.CharField(max_length=10, choices=THEME_CHOICES, blank=False, default=THEME_AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = UserProfile
|
||||||
|
fields = ['theme']
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=get_user_model())
|
||||||
|
def create_user_profile(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
UserProfile.objects.create(user=instance)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=get_user_model())
|
||||||
|
def save_user_profile(sender, instance, **kwargs):
|
||||||
|
instance.profile.save()
|
||||||
|
|
BIN
bookmarks/static/logo.png
Normal file
BIN
bookmarks/static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
|
@ -10,20 +10,22 @@ header {
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
background-color: $primary-color;
|
width: 28px;
|
||||||
color: $light-color;
|
height: 28px;
|
||||||
padding: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
margin: 0 0 0 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-toggle {
|
.dropdown-toggle {
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,9 +41,18 @@ h2 {
|
||||||
color: $gray-color-dark;
|
color: $gray-color-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button color should not change for anchor elements
|
// Fix up visited styles
|
||||||
.btn:visited:not(.btn-primary) {
|
a:visited {
|
||||||
color: $primary-color;
|
color: $link-color;
|
||||||
|
}
|
||||||
|
a:visited:hover {
|
||||||
|
color: $link-color-dark;
|
||||||
|
}
|
||||||
|
.btn-link:visited:not(.btn-primary) {
|
||||||
|
color: $link-color;
|
||||||
|
}
|
||||||
|
.btn-link:visited:not(.btn-primary):hover {
|
||||||
|
color: $link-color-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increase spacing between columns
|
// Increase spacing between columns
|
||||||
|
@ -57,4 +68,20 @@ h2 {
|
||||||
// Override border color for tab block
|
// Override border color for tab block
|
||||||
.tab-block {
|
.tab-block {
|
||||||
border-bottom: solid 1px $border-color;
|
border-bottom: solid 1px $border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Form auto-complete menu
|
||||||
|
.form-autocomplete .menu {
|
||||||
|
.menu-item.selected > a, .menu-item > a:hover {
|
||||||
|
background: $secondary-color;
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-item, .group-item:hover {
|
||||||
|
color: $gray-color;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: none;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ ul.bookmark-list {
|
||||||
.description {
|
.description {
|
||||||
color: $gray-color-dark;
|
color: $gray-color-dark;
|
||||||
|
|
||||||
a {
|
a, a:visited:hover {
|
||||||
color: $alternative-color;
|
color: $alternative-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ ul.bookmark-list {
|
||||||
|
|
||||||
.tag-cloud {
|
.tag-cloud {
|
||||||
|
|
||||||
a {
|
a, a:visited:hover {
|
||||||
color: $alternative-color;
|
color: $alternative-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
27
bookmarks/styles/dark.scss
Normal file
27
bookmarks/styles/dark.scss
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/* Dark theme overrides */
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn.btn-primary {
|
||||||
|
background: $dt-primary-button-color;
|
||||||
|
border-color: darken($dt-primary-button-color, 5%);
|
||||||
|
|
||||||
|
&:hover, &:active, &:focus {
|
||||||
|
background: darken($dt-primary-button-color, 5%);
|
||||||
|
border-color: darken($dt-primary-button-color, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus ring*/
|
||||||
|
a:focus, .btn:focus {
|
||||||
|
box-shadow: 0 0 0 .1rem rgba($primary-color, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.has-error .form-input, .form-input.is-error, .has-error .form-select, .form-select.is-error {
|
||||||
|
background: darken($error-color, 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination .page-item.active a {
|
||||||
|
background: $dt-primary-button-color;
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
// Font sizes
|
|
||||||
$html-font-size: 18px !default;
|
|
||||||
|
|
||||||
//$alternative-color: #c84e00;
|
|
||||||
//$alternative-color: #FF84E8;
|
|
||||||
//$alternative-color: #98C1D9;
|
|
||||||
//$alternative-color: #7B287D;
|
|
||||||
$alternative-color: #05a6a3;
|
|
||||||
$alternative-color-dark: darken($alternative-color, 5%);
|
|
||||||
|
|
||||||
// Import Spectre CSS lib
|
|
||||||
@import "../../node_modules/spectre.css/src/spectre";
|
|
||||||
@import "../../node_modules/spectre.css/src/autocomplete";
|
|
||||||
// Import Spectre icons
|
|
||||||
@import "../../node_modules/spectre.css/src/icons/icons-core";
|
|
||||||
@import "../../node_modules/spectre.css/src/icons/icons-navigation";
|
|
||||||
@import "../../node_modules/spectre.css/src/icons/icons-action";
|
|
||||||
@import "../../node_modules/spectre.css/src/icons/icons-object";
|
|
||||||
|
|
||||||
|
|
||||||
// Import style modules
|
|
||||||
@import "base";
|
|
||||||
@import "util";
|
|
||||||
@import "shared";
|
|
||||||
@import "bookmarks";
|
|
||||||
@import "settings";
|
|
||||||
@import "auth";
|
|
17
bookmarks/styles/theme-dark.scss
Normal file
17
bookmarks/styles/theme-dark.scss
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Import custom variables
|
||||||
|
@import "variables-dark";
|
||||||
|
|
||||||
|
// Import Spectre CSS lib
|
||||||
|
@import "../../node_modules/spectre.css/src/spectre";
|
||||||
|
@import "../../node_modules/spectre.css/src/autocomplete";
|
||||||
|
|
||||||
|
// Import style modules
|
||||||
|
@import "base";
|
||||||
|
@import "util";
|
||||||
|
@import "shared";
|
||||||
|
@import "bookmarks";
|
||||||
|
@import "settings";
|
||||||
|
@import "auth";
|
||||||
|
|
||||||
|
// Dark theme overrides
|
||||||
|
@import "dark";
|
14
bookmarks/styles/theme-light.scss
Normal file
14
bookmarks/styles/theme-light.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// Import custom variables
|
||||||
|
@import "variables-light";
|
||||||
|
|
||||||
|
// Import Spectre CSS lib
|
||||||
|
@import "../../node_modules/spectre.css/src/spectre";
|
||||||
|
@import "../../node_modules/spectre.css/src/autocomplete";
|
||||||
|
|
||||||
|
// Import style modules
|
||||||
|
@import "base";
|
||||||
|
@import "util";
|
||||||
|
@import "shared";
|
||||||
|
@import "bookmarks";
|
||||||
|
@import "settings";
|
||||||
|
@import "auth";
|
28
bookmarks/styles/variables-dark.scss
Normal file
28
bookmarks/styles/variables-dark.scss
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
$html-font-size: 18px !default;
|
||||||
|
|
||||||
|
$body-bg: #161822 !default;
|
||||||
|
$bg-color: lighten($body-bg, 5%) !default;
|
||||||
|
$bg-color-light: lighten($body-bg, 5%) !default;
|
||||||
|
|
||||||
|
$border-color: #4C4E53 !default;
|
||||||
|
$border-color-dark: $border-color !default;
|
||||||
|
|
||||||
|
$body-font-color: #b5bec8 !default;
|
||||||
|
$light-color: #fafafa !default;
|
||||||
|
|
||||||
|
$gray-color: #7f879b !default;
|
||||||
|
$gray-color-dark: lighten($gray-color, 20%) !default;
|
||||||
|
|
||||||
|
$primary-color: #a8b1ff !default;
|
||||||
|
$primary-color-dark: saturate($primary-color, 5%) !default;
|
||||||
|
$secondary-color: lighten($body-bg, 10%) !default;
|
||||||
|
|
||||||
|
$link-color: $primary-color !default;
|
||||||
|
$link-color-dark: darken($link-color, 5%) !default;
|
||||||
|
$link-color-light: $link-color !default;
|
||||||
|
|
||||||
|
$alternative-color: #59bdb9;
|
||||||
|
$alternative-color-dark: #73f1eb;
|
||||||
|
|
||||||
|
/* Dark theme specific */
|
||||||
|
$dt-primary-button-color: #5761cb !default;
|
4
bookmarks/styles/variables-light.scss
Normal file
4
bookmarks/styles/variables-light.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
$html-font-size: 18px !default;
|
||||||
|
|
||||||
|
$alternative-color: #05a6a3;
|
||||||
|
$alternative-color-dark: darken($alternative-color, 5%);
|
|
@ -2,7 +2,7 @@
|
||||||
<p class="empty-title h5">You have no bookmarks yet</p>
|
<p class="empty-title h5">You have no bookmarks yet</p>
|
||||||
<p class="empty-subtitle">
|
<p class="empty-subtitle">
|
||||||
You can get started by <a href="{% url 'bookmarks:new' %}">adding</a> bookmarks,
|
You can get started by <a href="{% url 'bookmarks:new' %}">adding</a> bookmarks,
|
||||||
<a href="{% url 'bookmarks:settings.data' %}">importing</a> your existing bookmarks or configuring the
|
<a href="{% url 'bookmarks:settings.general' %}">importing</a> your existing bookmarks or configuring the
|
||||||
<a href="{% url 'bookmarks:settings.integrations' %}">browser extension</a> or the <a href="{% url 'bookmarks:settings.integrations' %}">bookmarklet</a>.
|
<a href="{% url 'bookmarks:settings.integrations' %}">browser extension</a> or the <a href="{% url 'bookmarks:settings.integrations' %}">bookmarklet</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
{{ form.return_url|attr:"type:hidden" }}
|
{{ form.return_url|attr:"type:hidden" }}
|
||||||
<div class="form-group {% if form.url.errors %}has-error{% endif %}">
|
<div class="form-group {% if form.url.errors %}has-error{% endif %}">
|
||||||
<label for="{{ form.url.id_for_label }}" class="form-label">URL</label>
|
<label for="{{ form.url.id_for_label }}" class="form-label">URL</label>
|
||||||
{{ form.url|add_class:"form-input"|attr:"autofocus" }}
|
{{ form.url|add_class:"form-input"|attr:"autofocus"|attr:"placeholder: " }}
|
||||||
{% if form.url.errors %}
|
{% if form.url.errors %}
|
||||||
<div class="form-input-hint">
|
<div class="form-input-hint">
|
||||||
{{ form.url.errors }}
|
{{ form.url.errors }}
|
||||||
|
|
|
@ -12,14 +12,24 @@
|
||||||
<meta name="author" content="Sascha Ißbrücker">
|
<meta name="author" content="Sascha Ißbrücker">
|
||||||
<title>linkding</title>
|
<title>linkding</title>
|
||||||
{# Include SASS styles, files are resolved from bookmarks/styles #}
|
{# Include SASS styles, files are resolved from bookmarks/styles #}
|
||||||
<link href="{% sass_src 'index.scss' %}" rel="stylesheet" type="text/css"/>
|
{# Include specific theme variant based on user profile setting #}
|
||||||
|
{% if request.user.profile.theme == 'light' %}
|
||||||
|
<link href="{% sass_src 'theme-light.scss' %}" rel="stylesheet" type="text/css"/>
|
||||||
|
{% elif request.user.profile.theme == 'dark' %}
|
||||||
|
<link href="{% sass_src 'theme-dark.scss' %}" rel="stylesheet" type="text/css"/>
|
||||||
|
{% else %}
|
||||||
|
{# Use auto theme as fallback #}
|
||||||
|
<link href="{% sass_src 'theme-dark.scss' %}" rel="stylesheet" type="text/css"
|
||||||
|
media="(prefers-color-scheme: dark)"/>
|
||||||
|
<link href="{% sass_src 'theme-light.scss' %}" rel="stylesheet" type="text/css"
|
||||||
|
media="(prefers-color-scheme: light)"/>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="navbar container grid-lg">
|
<header class="navbar container grid-lg">
|
||||||
<section class="navbar-section">
|
<section class="navbar-section">
|
||||||
<a href="/" class="navbar-brand text-bold">
|
<a href="/" class="navbar-brand text-bold">
|
||||||
<i class="logo icon icon-link s-circle"></i>
|
<img class="logo" src="{% static 'logo.png' %}" alt="Application logo">
|
||||||
|
|
||||||
<h1>linkding</h1>
|
<h1>linkding</h1>
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -7,12 +7,16 @@
|
||||||
</div>
|
</div>
|
||||||
{# Menu drop-down for smaller devices #}
|
{# Menu drop-down for smaller devices #}
|
||||||
<div class="show-md">
|
<div class="show-md">
|
||||||
<a href="{% url 'bookmarks:new' %}" class="btn btn-primary mr-2">
|
<a href="{% url 'bookmarks:new' %}" class="btn btn-primary">
|
||||||
<i class="icon icon-plus"></i>
|
<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" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown dropdown-right">
|
<div class="dropdown dropdown-right">
|
||||||
<a href="#" id="mobile-nav-menu-trigger" class="btn btn-link dropdown-toggle" tabindex="0">
|
<a href="#" id="mobile-nav-menu-trigger" class="btn btn-link dropdown-toggle" tabindex="0">
|
||||||
<i class="icon icon-menu icon-2x"></i>
|
<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" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<!-- menu component -->
|
<!-- menu component -->
|
||||||
<ul class="menu">
|
<ul class="menu">
|
||||||
|
@ -46,4 +50,4 @@
|
||||||
mobileNavMenuTrigger.addEventListener('blur', function () {
|
mobileNavMenuTrigger.addEventListener('blur', function () {
|
||||||
document.removeEventListener('click', mobileNavMenuOutsideClickHandler);
|
document.removeEventListener('click', mobileNavMenuOutsideClickHandler);
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -20,11 +20,11 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="{{ form.username.id_for_label }}">Username</label>
|
<label class="form-label" for="{{ form.username.id_for_label }}">Username</label>
|
||||||
{{ form.username|add_class:'form-input' }}
|
{{ form.username|add_class:'form-input'|attr:"placeholder: " }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="{{ form.password.id_for_label }}">Password</label>
|
<label class="form-label" for="{{ form.password.id_for_label }}">Password</label>
|
||||||
{{ form.password|add_class:'form-input' }}
|
{{ form.password|add_class:'form-input'|attr:"placeholder: " }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
@ -1,10 +1,26 @@
|
||||||
{% extends "bookmarks/layout.html" %}
|
{% extends "bookmarks/layout.html" %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="settings-page">
|
<div class="settings-page">
|
||||||
|
|
||||||
{% include 'settings/nav.html' %}
|
{% include 'settings/nav.html' %}
|
||||||
|
|
||||||
|
{# Profile section #}
|
||||||
|
<section class="content-area">
|
||||||
|
<h2>Profile</h2>
|
||||||
|
<form action="{% url 'bookmarks:settings.general' %}" method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.theme.id_for_label }}" class="form-label">Theme</label>
|
||||||
|
{{ form.theme|add_class:"form-select col-2 col-sm-12" }}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Save" class="btn btn-primary mt-2">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
{# Import section #}
|
{# Import section #}
|
||||||
<section class="content-area">
|
<section class="content-area">
|
||||||
<h2>Import</h2>
|
<h2>Import</h2>
|
|
@ -1,11 +1,11 @@
|
||||||
{% url 'bookmarks:settings.index' as index_url %}
|
{% url 'bookmarks:settings.index' as index_url %}
|
||||||
{% url 'bookmarks:settings.data' as data_url %}
|
{% url 'bookmarks:settings.general' as general_url %}
|
||||||
{% url 'bookmarks:settings.integrations' as integrations_url %}
|
{% url 'bookmarks:settings.integrations' as integrations_url %}
|
||||||
{% url 'bookmarks:settings.api' as api_url %}
|
{% url 'bookmarks:settings.api' as api_url %}
|
||||||
|
|
||||||
<ul class="tab tab-block">
|
<ul class="tab tab-block">
|
||||||
<li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == data_url%}active{% endif %}">
|
<li class="tab-item {% if request.get_full_path == index_url or request.get_full_path == general_url%}active{% endif %}">
|
||||||
<a href="{% url 'bookmarks:settings.data' %}">Data</a>
|
<a href="{% url 'bookmarks:settings.general' %}">General</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
|
<li class="tab-item {% if request.get_full_path == integrations_url %}active{% endif %}">
|
||||||
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
|
<a href="{% url 'bookmarks:settings.integrations' %}">Integrations</a>
|
||||||
|
@ -16,8 +16,11 @@
|
||||||
<li class="tab-item tooltip tooltip-bottom" data-tooltip="The admin panel provides additional features 
 such as user management and bulk operations.">
|
<li class="tab-item tooltip tooltip-bottom" data-tooltip="The admin panel provides additional features 
 such as user management and bulk operations.">
|
||||||
<a href="{% url 'admin:index' %}" target="_blank">
|
<a href="{% url 'admin:index' %}" target="_blank">
|
||||||
<span>Admin</span>
|
<span>Admin</span>
|
||||||
<i class="icon icon-share ml-1" style="font-size: 12px"></i>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="ml-1" style="width: 1.2em; height: 1.2em; vertical-align: -0.2em;">
|
||||||
|
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
|
||||||
|
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br>
|
<br>
|
||||||
|
|
12
bookmarks/tests/test_user_profile_model.py
Normal file
12
bookmarks/tests/test_user_profile_model.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from bookmarks.models import UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_create_user_should_init_profile(self):
|
||||||
|
user = User.objects.create_user('testuser', 'test@example.com', 'password123')
|
||||||
|
profile = UserProfile.objects.all().filter(user_id=user.id).first()
|
||||||
|
self.assertIsNotNone(profile)
|
|
@ -19,8 +19,8 @@ urlpatterns = [
|
||||||
path('bookmarks/<int:bookmark_id>/archive', views.bookmarks.archive, name='archive'),
|
path('bookmarks/<int:bookmark_id>/archive', views.bookmarks.archive, name='archive'),
|
||||||
path('bookmarks/<int:bookmark_id>/unarchive', views.bookmarks.unarchive, name='unarchive'),
|
path('bookmarks/<int:bookmark_id>/unarchive', views.bookmarks.unarchive, name='unarchive'),
|
||||||
# Settings
|
# Settings
|
||||||
path('settings', views.settings.data, name='settings.index'),
|
path('settings', views.settings.general, name='settings.index'),
|
||||||
path('settings/data', views.settings.data, name='settings.data'),
|
path('settings/general', views.settings.general, name='settings.general'),
|
||||||
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
|
path('settings/integrations', views.settings.integrations, name='settings.integrations'),
|
||||||
path('settings/api', views.settings.api, name='settings.api'),
|
path('settings/api', views.settings.api, name='settings.api'),
|
||||||
path('settings/import', views.settings.bookmark_import, name='settings.import'),
|
path('settings/import', views.settings.bookmark_import, name='settings.import'),
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
|
from bookmarks.models import UserProfileForm
|
||||||
from bookmarks.queries import query_bookmarks
|
from bookmarks.queries import query_bookmarks
|
||||||
from bookmarks.services.exporter import export_netscape_html
|
from bookmarks.services.exporter import export_netscape_html
|
||||||
from bookmarks.services.importer import import_netscape_html
|
from bookmarks.services.importer import import_netscape_html
|
||||||
|
@ -15,10 +16,18 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def data(request):
|
def general(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = UserProfileForm(request.POST, instance=request.user.profile)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
else:
|
||||||
|
form = UserProfileForm(instance=request.user.profile)
|
||||||
|
|
||||||
import_success_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_success')
|
import_success_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_success')
|
||||||
import_errors_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_errors')
|
import_errors_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_errors')
|
||||||
return render(request, 'settings/data.html', {
|
return render(request, 'settings/general.html', {
|
||||||
|
'form': form,
|
||||||
'import_success_message': import_success_message,
|
'import_success_message': import_success_message,
|
||||||
'import_errors_message': import_errors_message,
|
'import_errors_message': import_errors_message,
|
||||||
})
|
})
|
||||||
|
@ -61,7 +70,7 @@ def bookmark_import(request):
|
||||||
messages.error(request, 'An error occurred during bookmark import.', 'bookmark_import_errors')
|
messages.error(request, 'An error occurred during bookmark import.', 'bookmark_import_errors')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('bookmarks:settings.data'))
|
return HttpResponseRedirect(reverse('bookmarks:settings.general'))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -77,7 +86,7 @@ def bookmark_export(request):
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except:
|
except:
|
||||||
return render(request, 'settings/data.html', {
|
return render(request, 'settings/general.html', {
|
||||||
'export_error': 'An error occurred during bookmark export.'
|
'export_error': 'An error occurred during bookmark export.'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ confusable-homoglyphs==3.2.0
|
||||||
Django==2.2.18
|
Django==2.2.18
|
||||||
django-appconf==1.0.3
|
django-appconf==1.0.3
|
||||||
django-compressor==2.3
|
django-compressor==2.3
|
||||||
|
django-debug-toolbar==3.2
|
||||||
django-generate-secret-key==1.0.2
|
django-generate-secret-key==1.0.2
|
||||||
django-picklefield==2.0
|
django-picklefield==2.0
|
||||||
django-registration==3.0.1
|
django-registration==3.0.1
|
||||||
|
|
|
@ -11,6 +11,14 @@ DEBUG = True
|
||||||
# Turn on SASS compilation
|
# Turn on SASS compilation
|
||||||
SASS_PROCESSOR_ENABLED = True
|
SASS_PROCESSOR_ENABLED = True
|
||||||
|
|
||||||
|
# Enable debug toolbar
|
||||||
|
INSTALLED_APPS.append('debug_toolbar')
|
||||||
|
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
||||||
|
|
||||||
|
INTERNAL_IPS = [
|
||||||
|
'127.0.0.1',
|
||||||
|
]
|
||||||
|
|
||||||
# Enable debug logging
|
# Enable debug logging
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
|
|
|
@ -17,7 +17,7 @@ from django.contrib.auth import views as auth_views
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
from bookmarks.admin import linkding_admin_site
|
from bookmarks.admin import linkding_admin_site
|
||||||
from .settings import ALLOW_REGISTRATION
|
from .settings import ALLOW_REGISTRATION, DEBUG
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', linkding_admin_site.urls),
|
path('admin/', linkding_admin_site.urls),
|
||||||
|
@ -28,5 +28,9 @@ urlpatterns = [
|
||||||
path('', include('bookmarks.urls')),
|
path('', include('bookmarks.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
import debug_toolbar
|
||||||
|
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
|
||||||
|
|
||||||
if ALLOW_REGISTRATION:
|
if ALLOW_REGISTRATION:
|
||||||
urlpatterns.append(path('', include('django_registration.backends.one_step.urls')))
|
urlpatterns.append(path('', include('django_registration.backends.one_step.urls')))
|
||||||
|
|
Loading…
Reference in a new issue