From 119d8f7efbd97d19aca91aac468d27763fa76acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Sun, 28 Mar 2021 12:11:56 +0200 Subject: [PATCH] Implement dark theme (#49) --- README.md | 3 +- bookmarks/admin.py | 20 +++++++- .../components/SearchAutoComplete.svelte | 13 ------ bookmarks/components/TagAutocomplete.svelte | 8 +--- bookmarks/migrations/0007_userprofile.py | 43 ++++++++++++++++++ bookmarks/models.py | 33 ++++++++++++++ bookmarks/static/logo.png | Bin 0 -> 2937 bytes bookmarks/styles/base.scss | 43 ++++++++++++++---- bookmarks/styles/bookmarks.scss | 4 +- bookmarks/styles/dark.scss | 27 +++++++++++ bookmarks/styles/index.scss | 27 ----------- bookmarks/styles/theme-dark.scss | 17 +++++++ bookmarks/styles/theme-light.scss | 14 ++++++ bookmarks/styles/variables-dark.scss | 28 ++++++++++++ bookmarks/styles/variables-light.scss | 4 ++ .../templates/bookmarks/empty_bookmarks.html | 2 +- bookmarks/templates/bookmarks/form.html | 2 +- bookmarks/templates/bookmarks/layout.html | 16 +++++-- bookmarks/templates/bookmarks/nav_menu.html | 12 +++-- bookmarks/templates/registration/login.html | 4 +- .../settings/{data.html => general.html} | 16 +++++++ bookmarks/templates/settings/nav.html | 13 ++++-- bookmarks/tests/test_user_profile_model.py | 12 +++++ bookmarks/urls.py | 4 +- bookmarks/views/settings.py | 17 +++++-- requirements.txt | 1 + siteroot/settings/dev.py | 8 ++++ siteroot/urls.py | 6 ++- 28 files changed, 314 insertions(+), 83 deletions(-) create mode 100644 bookmarks/migrations/0007_userprofile.py create mode 100644 bookmarks/static/logo.png create mode 100644 bookmarks/styles/dark.scss delete mode 100644 bookmarks/styles/index.scss create mode 100644 bookmarks/styles/theme-dark.scss create mode 100644 bookmarks/styles/theme-light.scss create mode 100644 bookmarks/styles/variables-dark.scss create mode 100644 bookmarks/styles/variables-light.scss rename bookmarks/templates/settings/{data.html => general.html} (75%) create mode 100644 bookmarks/tests/test_user_profile_model.py diff --git a/README.md b/README.md index 555fc0d..798c356 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The name comes from: - 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) - Bookmarklet that should work in most browsers +- Dark mode - Easy to set up using Docker - Uses SQLite as database - Works without Javascript @@ -161,4 +162,4 @@ The frontend is now available under http://localhost:8000 ## 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) \ No newline at end of file +- [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) diff --git a/bookmarks/admin.py b/bookmarks/admin.py index c351998..834723d 100644 --- a/bookmarks/admin.py +++ b/bookmarks/admin.py @@ -7,7 +7,7 @@ from django.utils.translation import ngettext, gettext from rest_framework.authtoken.admin import TokenAdmin 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 @@ -77,8 +77,24 @@ class AdminTag(admin.ModelAdmin): ), 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.register(Bookmark, AdminBookmark) linkding_admin_site.register(Tag, AdminTag) -linkding_admin_site.register(User, UserAdmin) +linkding_admin_site.register(User, AdminCustomUser) linkding_admin_site.register(Token, TokenAdmin) diff --git a/bookmarks/components/SearchAutoComplete.svelte b/bookmarks/components/SearchAutoComplete.svelte index 5d1b5e3..6b1e9e9 100644 --- a/bookmarks/components/SearchAutoComplete.svelte +++ b/bookmarks/components/SearchAutoComplete.svelte @@ -264,17 +264,4 @@ 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; - } \ No newline at end of file diff --git a/bookmarks/components/TagAutocomplete.svelte b/bookmarks/components/TagAutocomplete.svelte index efbdd18..1ef81aa 100644 --- a/bookmarks/components/TagAutocomplete.svelte +++ b/bookmarks/components/TagAutocomplete.svelte @@ -124,10 +124,4 @@ .menu.open { display: block; } - - /* TODO: Should be read from theme */ - .menu-item.selected > a { - background: #f1f1fc; - color: #5755d9; - } - \ No newline at end of file + diff --git a/bookmarks/migrations/0007_userprofile.py b/bookmarks/migrations/0007_userprofile.py new file mode 100644 index 0000000..20850b5 --- /dev/null +++ b/bookmarks/migrations/0007_userprofile.py @@ -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), + ] diff --git a/bookmarks/models.py b/bookmarks/models.py index fd9905b..7bbb535 100644 --- a/bookmarks/models.py +++ b/bookmarks/models.py @@ -2,7 +2,10 @@ from typing import List from django import forms from django.contrib.auth import get_user_model +from django.contrib.auth.models import User 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.validators import BookmarkURLValidator @@ -93,3 +96,33 @@ class BookmarkForm(forms.ModelForm): class Meta: model = Bookmark 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() diff --git a/bookmarks/static/logo.png b/bookmarks/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..893029e5b79c473ac11769bc135ce29c8c4a685d GIT binary patch literal 2937 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 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; + } +} diff --git a/bookmarks/styles/bookmarks.scss b/bookmarks/styles/bookmarks.scss index 3fe88e9..c1d1a90 100644 --- a/bookmarks/styles/bookmarks.scss +++ b/bookmarks/styles/bookmarks.scss @@ -39,7 +39,7 @@ ul.bookmark-list { .description { color: $gray-color-dark; - a { + a, a:visited:hover { color: $alternative-color; } } @@ -71,7 +71,7 @@ ul.bookmark-list { .tag-cloud { - a { + a, a:visited:hover { color: $alternative-color; } diff --git a/bookmarks/styles/dark.scss b/bookmarks/styles/dark.scss new file mode 100644 index 0000000..7cc4cb4 --- /dev/null +++ b/bookmarks/styles/dark.scss @@ -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; +} diff --git a/bookmarks/styles/index.scss b/bookmarks/styles/index.scss deleted file mode 100644 index 556c57c..0000000 --- a/bookmarks/styles/index.scss +++ /dev/null @@ -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"; diff --git a/bookmarks/styles/theme-dark.scss b/bookmarks/styles/theme-dark.scss new file mode 100644 index 0000000..77ce779 --- /dev/null +++ b/bookmarks/styles/theme-dark.scss @@ -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"; diff --git a/bookmarks/styles/theme-light.scss b/bookmarks/styles/theme-light.scss new file mode 100644 index 0000000..7ba2a2b --- /dev/null +++ b/bookmarks/styles/theme-light.scss @@ -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"; diff --git a/bookmarks/styles/variables-dark.scss b/bookmarks/styles/variables-dark.scss new file mode 100644 index 0000000..384e567 --- /dev/null +++ b/bookmarks/styles/variables-dark.scss @@ -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; diff --git a/bookmarks/styles/variables-light.scss b/bookmarks/styles/variables-light.scss new file mode 100644 index 0000000..f24591d --- /dev/null +++ b/bookmarks/styles/variables-light.scss @@ -0,0 +1,4 @@ +$html-font-size: 18px !default; + +$alternative-color: #05a6a3; +$alternative-color-dark: darken($alternative-color, 5%); diff --git a/bookmarks/templates/bookmarks/empty_bookmarks.html b/bookmarks/templates/bookmarks/empty_bookmarks.html index 7434c09..0018889 100644 --- a/bookmarks/templates/bookmarks/empty_bookmarks.html +++ b/bookmarks/templates/bookmarks/empty_bookmarks.html @@ -2,7 +2,7 @@

You have no bookmarks yet

You can get started by adding bookmarks, - importing your existing bookmarks or configuring the + importing your existing bookmarks or configuring the browser extension or the bookmarklet.

diff --git a/bookmarks/templates/bookmarks/form.html b/bookmarks/templates/bookmarks/form.html index b9496b9..9a1dead 100644 --- a/bookmarks/templates/bookmarks/form.html +++ b/bookmarks/templates/bookmarks/form.html @@ -7,7 +7,7 @@ {{ form.return_url|attr:"type:hidden" }}
- {{ form.url|add_class:"form-input"|attr:"autofocus" }} + {{ form.url|add_class:"form-input"|attr:"autofocus"|attr:"placeholder: " }} {% if form.url.errors %}
{{ form.url.errors }} diff --git a/bookmarks/templates/bookmarks/layout.html b/bookmarks/templates/bookmarks/layout.html index 14bfe60..abc439b 100644 --- a/bookmarks/templates/bookmarks/layout.html +++ b/bookmarks/templates/bookmarks/layout.html @@ -12,14 +12,24 @@ linkding {# Include SASS styles, files are resolved from bookmarks/styles #} - + {# Include specific theme variant based on user profile setting #} + {% if request.user.profile.theme == 'light' %} + + {% elif request.user.profile.theme == 'dark' %} + + {% else %} + {# Use auto theme as fallback #} + + + {% endif %}
{# Menu drop-down for smaller devices #}
- - + + + +