Repository: sissbruecker/linkding

Issue:
Feature request: custom css
I found the bookmark list very confusing because the individual bookmarks are not very good visually separated. I changed this directly in the CSS so that a horizontal line appears under each bookmark, but that's not the point of editing in the code.
It would be really nice to have either an input field for a custom css classes, or the possibility of a path to a custom css file on the server. The effort for implementing the second solution suggestion should be more or less simple. The custom css should not replace the default css but override settings of the original. 


Discussion:
The request for custom CSS often means that the design is lacking in some way. I'd be interested to know what your custom CSS looks like, so I understand better what problem, and you are solving it :). 
I would not want to claim that things lacks. I love this app and I'm very happy that I could switch with this app from raindrop.io to this selfhosted app. I would like to emphasize that my impression is very, very subjective and not a critism of the design of the app. For me the design was to poppy and very confusing because the bright colors set the wrong accents. This made it difficult for me to grasp things quickly. I started with a divider line between each item, then I remember what I prefer in terms of eye guided design and raise some things. As result I changed the colors for better separate the important from the less important things, made the title bold and made also
 some indentions for items 

Overall this fits much better to my idea of good readable. But as written before - this is subjective. I guess there are a lot of users who are very satisfied with the default design. For my opinion individual design claims should solved by the user, not by the developer. Easiest way could be to add as last ccs link on the page which reffers to a custom css. 

![Bildschirmfoto 2024-02-05 um 13 13 57](https://github.com/sissbruecker/linkding/assets/19834496/ab30b12a-9650-46ce-a3a8-789952d83a10)


No plans to change the default design, but being able to add custom CSS would be neat. Could just be a plain text area in the settings where you can dump your custom CSS that would then get rendered into the HTML document. Maybe allow mounting a custom CSS file into the container, though that seems less helpful / convenient than the other option.



Patch:
diff --git a/bookmarks/migrations/0026_userprofile_custom_css.py b/bookmarks/migrations/0026_userprofile_custom_css.py
new file mode 100644
index 00000000..f1232460
--- /dev/null
+++ b/bookmarks/migrations/0026_userprofile_custom_css.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-03-16 23:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("bookmarks", "0025_userprofile_search_preferences"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="userprofile",
+            name="custom_css",
+            field=models.TextField(blank=True),
+        ),
+    ]
diff --git a/bookmarks/models.py b/bookmarks/models.py
index 87711ceb..87142830 100644
--- a/bookmarks/models.py
+++ b/bookmarks/models.py
@@ -331,6 +331,7 @@ class UserProfile(models.Model):
     enable_favicons = models.BooleanField(default=False, null=False)
     display_url = models.BooleanField(default=False, null=False)
     permanent_notes = models.BooleanField(default=False, null=False)
+    custom_css = models.TextField(blank=True, null=False)
     search_preferences = models.JSONField(default=dict, null=False)
 
 
@@ -348,6 +349,7 @@ class Meta:
             "enable_favicons",
             "display_url",
             "permanent_notes",
+            "custom_css",
         ]
 
 
diff --git a/bookmarks/styles/settings.scss b/bookmarks/styles/settings.scss
index b533abbe..b62e76f5 100644
--- a/bookmarks/styles/settings.scss
+++ b/bookmarks/styles/settings.scss
@@ -7,6 +7,10 @@
     }
   }
 
+  textarea.custom-css {
+    font-family: monospace;
+  }
+
   .input-group > input[type=submit] {
     height: auto;
   }
diff --git a/bookmarks/templates/bookmarks/layout.html b/bookmarks/templates/bookmarks/layout.html
index ff4f336b..fca6a9a2 100644
--- a/bookmarks/templates/bookmarks/layout.html
+++ b/bookmarks/templates/bookmarks/layout.html
@@ -34,6 +34,9 @@
     <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#161822">
     <meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0">
   {% endif %}
+  {% if request.user_profile.custom_css %}
+    <style>{{ request.user_profile.custom_css }}</style>
+  {% endif %}
 </head>
 <body ld-global-shortcuts>
 
diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html
index fa3f1b96..823f0b1c 100644
--- a/bookmarks/templates/settings/general.html
+++ b/bookmarks/templates/settings/general.html
@@ -124,6 +124,18 @@ <h2>Profile</h2>
               href="{% url 'bookmarks:shared' %}">shared bookmarks page</a>.
           </div>
         </div>
+      <div class="form-group">
+        <details {% if form.custom_css.value %}open{% endif %}>
+          <summary>Custom CSS</summary>
+          <label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label>
+          <div class="mt-2">
+            {{ form.custom_css|add_class:"form-input custom-css"|attr:"rows:6" }}
+          </div>
+        </details>
+        <div class="form-input-hint">
+          Allows to add custom CSS to the page.
+        </div>
+      </div>
         <div class="form-group">
           <input type="submit" name="update_profile" value="Save" class="btn btn-primary mt-2">
           {% if update_profile_success_message %}
diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py
index 373cfa0a..c0723920 100644
--- a/siteroot/settings/base.py
+++ b/siteroot/settings/base.py
@@ -131,8 +131,6 @@
 
 # Turn off SASS compilation by default
 SASS_PROCESSOR_ENABLED = False
-# Location where generated CSS files are saved
-SASS_PROCESSOR_ROOT = os.path.join(BASE_DIR, "bookmarks", "static")
 
 # Add SASS preprocessor finder to resolve generated CSS
 STATICFILES_FINDERS = [


Test patch:
diff --git a/bookmarks/tests/test_custom_css.py b/bookmarks/tests/test_custom_css.py
new file mode 100644
index 00000000..ce0e91ea
--- /dev/null
+++ b/bookmarks/tests/test_custom_css.py
@@ -0,0 +1,21 @@
+from django.test import TestCase
+from django.urls import reverse
+
+from bookmarks.tests.helpers import BookmarkFactoryMixin
+
+
+class CustomCssTestCase(TestCase, BookmarkFactoryMixin):
+    def setUp(self):
+        self.client.force_login(self.get_or_create_test_user())
+
+    def test_does_not_render_custom_style_tag_by_default(self):
+        response = self.client.get(reverse("bookmarks:index"))
+        self.assertNotContains(response, "<style>")
+
+    def test_renders_custom_style_tag_if_user_has_custom_css(self):
+        profile = self.get_or_create_test_user().profile
+        profile.custom_css = "body { background-color: red; }"
+        profile.save()
+
+        response = self.client.get(reverse("bookmarks:index"))
+        self.assertContains(response, "<style>body { background-color: red; }</style>")
diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py
index 61868302..f9ed787b 100644
--- a/bookmarks/tests/test_settings_general_view.py
+++ b/bookmarks/tests/test_settings_general_view.py
@@ -32,6 +32,7 @@ def create_profile_form_data(self, overrides=None):
             "tag_search": UserProfile.TAG_SEARCH_STRICT,
             "display_url": False,
             "permanent_notes": False,
+            "custom_css": "",
         }
 
         return {**form_data, **overrides}
@@ -63,6 +64,7 @@ def test_update_profile(self):
             "tag_search": UserProfile.TAG_SEARCH_LAX,
             "display_url": True,
             "permanent_notes": True,
+            "custom_css": "body { background-color: #000; }",
         }
         response = self.client.post(reverse("bookmarks:settings.general"), form_data)
         html = response.content.decode()
@@ -93,6 +95,7 @@ def test_update_profile(self):
         self.assertEqual(
             self.user.profile.permanent_notes, form_data["permanent_notes"]
         )
+        self.assertEqual(self.user.profile.custom_css, form_data["custom_css"])
         self.assertInHTML(
             """
                 <p class="form-input-hint">Profile updated</p>