Merge branch 'main' into add-edition

This commit is contained in:
Mouse Reeve 2022-03-16 16:16:55 -07:00
commit 68dc5962ee
153 changed files with 10991 additions and 6067 deletions

View file

@ -1,3 +0,0 @@
@charset "utf-8";
// Copy this file to bookwyrm/static/css/ and set your instance custom styles.

3
.gitignore vendored
View file

@ -17,7 +17,8 @@
.env .env
/images/ /images/
bookwyrm/static/css/bookwyrm.css bookwyrm/static/css/bookwyrm.css
bookwyrm/static/css/_instance-settings.scss bookwyrm/static/css/themes/
!bookwyrm/static/css/themes/bookwyrm-*.scss
# Testing # Testing
.coverage .coverage

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
**/vendor/*

View file

@ -13,7 +13,7 @@ Social reading and reviewing, decentralized with ActivityPub
- [Set up Bookwyrm](#set-up-bookwyrm) - [Set up Bookwyrm](#set-up-bookwyrm)
## Joining BookWyrm ## Joining BookWyrm
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://docs.joinbookwyrm.com/instances.html) list. BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
You can request an invite by entering your email address at https://bookwyrm.social. You can request an invite by entering your email address at https://bookwyrm.social.

View file

@ -39,4 +39,5 @@ class Person(ActivityObject):
bookwyrmUser: bool = False bookwyrmUser: bool = False
manuallyApprovesFollowers: str = False manuallyApprovesFollowers: str = False
discoverable: str = False discoverable: str = False
hideFollows: str = False
type: str = "Person" type: str = "Person"

View file

@ -19,11 +19,11 @@ def download_file(url, destination):
with open(destination, "b+w") as outfile: with open(destination, "b+w") as outfile:
outfile.write(stream.read()) outfile.write(stream.read())
except (urllib.error.HTTPError, urllib.error.URLError): except (urllib.error.HTTPError, urllib.error.URLError):
logger.error("Failed to download file %s", url) logger.info("Failed to download file %s", url)
except OSError: except OSError:
logger.error("Couldn't open font file %s for writing", destination) logger.info("Couldn't open font file %s for writing", destination)
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
logger.exception("Unknown error in file download") logger.info("Unknown error in file download")
class BookwyrmConfig(AppConfig): class BookwyrmConfig(AppConfig):

View file

@ -131,7 +131,7 @@ class AbstractConnector(AbstractMinimalConnector):
try: try:
work_data = self.get_work_from_edition_data(data) work_data = self.get_work_from_edition_data(data)
except (KeyError, ConnectorException) as err: except (KeyError, ConnectorException) as err:
logger.exception(err) logger.info(err)
work_data = data work_data = data
if not work_data or not edition_data: if not work_data or not edition_data:
@ -270,7 +270,7 @@ def get_data(url, params=None, timeout=10):
timeout=timeout, timeout=timeout,
) )
except RequestException as err: except RequestException as err:
logger.exception(err) logger.info(err)
raise ConnectorException(err) raise ConnectorException(err)
if not resp.ok: if not resp.ok:
@ -278,7 +278,7 @@ def get_data(url, params=None, timeout=10):
try: try:
data = resp.json() data = resp.json()
except ValueError as err: except ValueError as err:
logger.exception(err) logger.info(err)
raise ConnectorException(err) raise ConnectorException(err)
return data return data
@ -296,7 +296,7 @@ def get_image(url, timeout=10):
timeout=timeout, timeout=timeout,
) )
except RequestException as err: except RequestException as err:
logger.exception(err) logger.info(err)
return None, None return None, None
if not resp.ok: if not resp.ok:
@ -305,7 +305,7 @@ def get_image(url, timeout=10):
image_content = ContentFile(resp.content) image_content = ContentFile(resp.content)
extension = imghdr.what(None, image_content.read()) extension = imghdr.what(None, image_content.read())
if not extension: if not extension:
logger.exception("File requested was not an image: %s", url) logger.info("File requested was not an image: %s", url)
return None, None return None, None
return image_content, extension return image_content, extension

View file

@ -39,7 +39,7 @@ def search(query, min_confidence=0.1, return_first=False):
try: try:
result_set = connector.isbn_search(isbn) result_set = connector.isbn_search(isbn)
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
logger.exception(err) logger.info(err)
# if this fails, we can still try regular search # if this fails, we can still try regular search
# if no isbn search results, we fallback to generic search # if no isbn search results, we fallback to generic search
@ -48,7 +48,7 @@ def search(query, min_confidence=0.1, return_first=False):
result_set = connector.search(query, min_confidence=min_confidence) result_set = connector.search(query, min_confidence=min_confidence)
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
# we don't want *any* error to crash the whole search page # we don't want *any* error to crash the whole search page
logger.exception(err) logger.info(err)
continue continue
if return_first and result_set: if return_first and result_set:

View file

@ -8,8 +8,20 @@ def site_settings(request): # pylint: disable=unused-argument
if not request.is_secure(): if not request.is_secure():
request_protocol = "http://" request_protocol = "http://"
site = models.SiteSettings.objects.get()
theme = "css/themes/bookwyrm-light.scss"
if (
hasattr(request, "user")
and request.user.is_authenticated
and request.user.theme
):
theme = request.user.theme.path
elif site.default_theme:
theme = site.default_theme.path
return { return {
"site": models.SiteSettings.objects.get(), "site": site,
"site_theme": theme,
"active_announcements": models.Announcement.active_announcements(), "active_announcements": models.Announcement.active_announcements(),
"thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION, "thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION,
"media_full_url": settings.MEDIA_FULL_URL, "media_full_url": settings.MEDIA_FULL_URL,

View file

@ -1,582 +0,0 @@
""" using django model forms """
import datetime
from collections import defaultdict
from urllib.parse import urlparse
from django import forms
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
from django.forms.widgets import Textarea
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from bookwyrm.models.user import FeedFilterChoices
class CustomForm(ModelForm):
"""add css classes to the forms"""
def __init__(self, *args, **kwargs):
css_classes = defaultdict(lambda: "")
css_classes["text"] = "input"
css_classes["password"] = "input"
css_classes["email"] = "input"
css_classes["number"] = "input"
css_classes["checkbox"] = "checkbox"
css_classes["textarea"] = "textarea"
# pylint: disable=super-with-arguments
super(CustomForm, self).__init__(*args, **kwargs)
for visible in self.visible_fields():
if hasattr(visible.field.widget, "input_type"):
input_type = visible.field.widget.input_type
if isinstance(visible.field.widget, Textarea):
input_type = "textarea"
visible.field.widget.attrs["rows"] = 5
visible.field.widget.attrs["class"] = css_classes[input_type]
# pylint: disable=missing-class-docstring
class LoginForm(CustomForm):
class Meta:
model = models.User
fields = ["localname", "password"]
help_texts = {f: None for f in fields}
widgets = {
"password": PasswordInput(),
}
class RegisterForm(CustomForm):
class Meta:
model = models.User
fields = ["localname", "email", "password"]
help_texts = {f: None for f in fields}
widgets = {"password": PasswordInput()}
def clean(self):
"""Check if the username is taken"""
cleaned_data = super().clean()
localname = cleaned_data.get("localname").strip()
if models.User.objects.filter(localname=localname).first():
self.add_error("localname", _("User with this username already exists"))
class RatingForm(CustomForm):
class Meta:
model = models.ReviewRating
fields = ["user", "book", "rating", "privacy"]
class ReviewForm(CustomForm):
class Meta:
model = models.Review
fields = [
"user",
"book",
"name",
"content",
"rating",
"content_warning",
"sensitive",
"privacy",
]
class CommentForm(CustomForm):
class Meta:
model = models.Comment
fields = [
"user",
"book",
"content",
"content_warning",
"sensitive",
"privacy",
"progress",
"progress_mode",
"reading_status",
]
class QuotationForm(CustomForm):
class Meta:
model = models.Quotation
fields = [
"user",
"book",
"quote",
"content",
"content_warning",
"sensitive",
"privacy",
"position",
"position_mode",
]
class ReplyForm(CustomForm):
class Meta:
model = models.Status
fields = [
"user",
"content",
"content_warning",
"sensitive",
"reply_parent",
"privacy",
]
class StatusForm(CustomForm):
class Meta:
model = models.Status
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
class DirectForm(CustomForm):
class Meta:
model = models.Status
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
class EditUserForm(CustomForm):
class Meta:
model = models.User
fields = [
"avatar",
"name",
"email",
"summary",
"show_goal",
"show_suggested_users",
"manually_approves_followers",
"default_post_privacy",
"discoverable",
"preferred_timezone",
"preferred_language",
]
help_texts = {f: None for f in fields}
widgets = {
"avatar": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_avatar"}
),
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
"email": forms.EmailInput(attrs={"aria-describedby": "desc_email"}),
"discoverable": forms.CheckboxInput(
attrs={"aria-describedby": "desc_discoverable"}
),
}
class LimitedEditUserForm(CustomForm):
class Meta:
model = models.User
fields = [
"avatar",
"name",
"summary",
"manually_approves_followers",
"discoverable",
]
help_texts = {f: None for f in fields}
widgets = {
"avatar": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_avatar"}
),
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
"discoverable": forms.CheckboxInput(
attrs={"aria-describedby": "desc_discoverable"}
),
}
class DeleteUserForm(CustomForm):
class Meta:
model = models.User
fields = ["password"]
class UserGroupForm(CustomForm):
class Meta:
model = models.User
fields = ["groups"]
class FeedStatusTypesForm(CustomForm):
class Meta:
model = models.User
fields = ["feed_status_types"]
help_texts = {f: None for f in fields}
widgets = {
"feed_status_types": widgets.CheckboxSelectMultiple(
choices=FeedFilterChoices,
),
}
class CoverForm(CustomForm):
class Meta:
model = models.Book
fields = ["cover"]
help_texts = {f: None for f in fields}
class LinkDomainForm(CustomForm):
class Meta:
model = models.LinkDomain
fields = ["name"]
class FileLinkForm(CustomForm):
class Meta:
model = models.FileLink
fields = ["url", "filetype", "availability", "book", "added_by"]
def clean(self):
"""make sure the domain isn't blocked or pending"""
cleaned_data = super().clean()
url = cleaned_data.get("url")
filetype = cleaned_data.get("filetype")
book = cleaned_data.get("book")
domain = urlparse(url).netloc
if models.LinkDomain.objects.filter(domain=domain).exists():
status = models.LinkDomain.objects.get(domain=domain).status
if status == "blocked":
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This domain is blocked. Please contact your administrator if you think this is an error."
),
)
elif models.FileLink.objects.filter(
url=url, book=book, filetype=filetype
).exists():
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
),
)
class EditionFromWorkForm(CustomForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# make all fields hidden
for visible in self.visible_fields():
visible.field.widget = forms.HiddenInput()
class Meta:
model = models.Work
fields = [
"title",
"subtitle",
"authors",
"description",
"languages",
"series",
"series_number",
"subjects",
"subject_places",
"cover",
"first_published_date",
]
class EditionForm(CustomForm):
class Meta:
model = models.Edition
exclude = [
"authors",
"parent_work",
"remote_id",
"origin_id",
"created_date",
"updated_date",
"edition_rank",
"shelves",
"connector",
"search_vector",
"links",
"file_links",
]
widgets = {
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
"subtitle": forms.TextInput(attrs={"aria-describedby": "desc_subtitle"}),
"description": forms.Textarea(
attrs={"aria-describedby": "desc_description"}
),
"series": forms.TextInput(attrs={"aria-describedby": "desc_series"}),
"series_number": forms.TextInput(
attrs={"aria-describedby": "desc_series_number"}
),
"languages": forms.TextInput(
attrs={"aria-describedby": "desc_languages_help desc_languages"}
),
"publishers": forms.TextInput(
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
),
"first_published_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_first_published_date"}
),
"published_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_published_date"}
),
"cover": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_cover"}
),
"physical_format": forms.Select(
attrs={"aria-describedby": "desc_physical_format"}
),
"physical_format_detail": forms.TextInput(
attrs={"aria-describedby": "desc_physical_format_detail"}
),
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
"openlibrary_key": forms.TextInput(
attrs={"aria-describedby": "desc_openlibrary_key"}
),
"inventaire_id": forms.TextInput(
attrs={"aria-describedby": "desc_inventaire_id"}
),
"oclc_number": forms.TextInput(
attrs={"aria-describedby": "desc_oclc_number"}
),
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
}
class AuthorForm(CustomForm):
class Meta:
model = models.Author
fields = [
"last_edited_by",
"name",
"aliases",
"bio",
"wikipedia_link",
"born",
"died",
"openlibrary_key",
"inventaire_id",
"librarything_key",
"goodreads_key",
"isni",
]
widgets = {
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"aliases": forms.TextInput(attrs={"aria-describedby": "desc_aliases"}),
"bio": forms.Textarea(attrs={"aria-describedby": "desc_bio"}),
"wikipedia_link": forms.TextInput(
attrs={"aria-describedby": "desc_wikipedia_link"}
),
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
"oepnlibrary_key": forms.TextInput(
attrs={"aria-describedby": "desc_oepnlibrary_key"}
),
"inventaire_id": forms.TextInput(
attrs={"aria-describedby": "desc_inventaire_id"}
),
"librarything_key": forms.TextInput(
attrs={"aria-describedby": "desc_librarything_key"}
),
"goodreads_key": forms.TextInput(
attrs={"aria-describedby": "desc_goodreads_key"}
),
}
class ImportForm(forms.Form):
csv_file = forms.FileField()
class ExpiryWidget(widgets.Select):
def value_from_datadict(self, data, files, name):
"""human-readable exiration time buckets"""
selected_string = super().value_from_datadict(data, files, name)
if selected_string == "day":
interval = datetime.timedelta(days=1)
elif selected_string == "week":
interval = datetime.timedelta(days=7)
elif selected_string == "month":
interval = datetime.timedelta(days=31) # Close enough?
elif selected_string == "forever":
return None
else:
return selected_string # This will raise
return timezone.now() + interval
class InviteRequestForm(CustomForm):
def clean(self):
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
email = cleaned_data.get("email")
if email and models.User.objects.filter(email=email).exists():
self.add_error("email", _("A user with this email already exists."))
class Meta:
model = models.InviteRequest
fields = ["email"]
class CreateInviteForm(CustomForm):
class Meta:
model = models.SiteInvite
exclude = ["code", "user", "times_used", "invitees"]
widgets = {
"expiry": ExpiryWidget(
choices=[
("day", _("One Day")),
("week", _("One Week")),
("month", _("One Month")),
("forever", _("Does Not Expire")),
]
),
"use_limit": widgets.Select(
choices=[(i, _(f"{i} uses")) for i in [1, 5, 10, 25, 50, 100]]
+ [(None, _("Unlimited"))]
),
}
class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
fields = ["user", "name", "privacy", "description"]
class GoalForm(CustomForm):
class Meta:
model = models.AnnualGoal
fields = ["user", "year", "goal", "privacy"]
class SiteForm(CustomForm):
class Meta:
model = models.SiteSettings
exclude = ["admin_code", "install_mode"]
widgets = {
"instance_short_description": forms.TextInput(
attrs={"aria-describedby": "desc_instance_short_description"}
),
"require_confirm_email": forms.CheckboxInput(
attrs={"aria-describedby": "desc_require_confirm_email"}
),
"invite_request_text": forms.Textarea(
attrs={"aria-describedby": "desc_invite_request_text"}
),
}
class AnnouncementForm(CustomForm):
class Meta:
model = models.Announcement
exclude = ["remote_id"]
widgets = {
"preview": forms.TextInput(attrs={"aria-describedby": "desc_preview"}),
"content": forms.Textarea(attrs={"aria-describedby": "desc_content"}),
"event_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_event_date"}
),
"start_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_start_date"}
),
"end_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_end_date"}
),
"active": forms.CheckboxInput(attrs={"aria-describedby": "desc_active"}),
}
class ListForm(CustomForm):
class Meta:
model = models.List
fields = ["user", "name", "description", "curation", "privacy", "group"]
class ListItemForm(CustomForm):
class Meta:
model = models.ListItem
fields = ["user", "book", "book_list", "notes"]
class GroupForm(CustomForm):
class Meta:
model = models.Group
fields = ["user", "privacy", "name", "description"]
class ReportForm(CustomForm):
class Meta:
model = models.Report
fields = ["user", "reporter", "status", "links", "note"]
class EmailBlocklistForm(CustomForm):
class Meta:
model = models.EmailBlocklist
fields = ["domain"]
widgets = {
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
}
class IPBlocklistForm(CustomForm):
class Meta:
model = models.IPBlocklist
fields = ["address"]
class ServerForm(CustomForm):
class Meta:
model = models.FederatedServer
exclude = ["remote_id"]
class SortListForm(forms.Form):
sort_by = ChoiceField(
choices=(
("order", _("List Order")),
("title", _("Book Title")),
("rating", _("Rating")),
),
label=_("Sort By"),
)
direction = ChoiceField(
choices=(
("ascending", _("Ascending")),
("descending", _("Descending")),
),
)
class ReadThroughForm(CustomForm):
def clean(self):
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
finish_date = cleaned_data.get("finish_date")
if start_date and finish_date and start_date > finish_date:
self.add_error(
"finish_date", _("Reading finish date cannot be before start date.")
)
class Meta:
model = models.ReadThrough
fields = ["user", "book", "start_date", "finish_date"]
class AutoModRuleForm(CustomForm):
class Meta:
model = models.AutoMod
fields = ["string_match", "flag_users", "flag_statuses", "created_by"]

View file

@ -0,0 +1,12 @@
""" make forms available to the app """
# site admin
from .admin import *
from .author import *
from .books import *
from .edit_user import *
from .forms import *
from .groups import *
from .landing import *
from .links import *
from .lists import *
from .status import *

141
bookwyrm/forms/admin.py Normal file
View file

@ -0,0 +1,141 @@
""" using django model forms """
import datetime
from django import forms
from django.forms import widgets
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import IntervalSchedule
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class ExpiryWidget(widgets.Select):
def value_from_datadict(self, data, files, name):
"""human-readable exiration time buckets"""
selected_string = super().value_from_datadict(data, files, name)
if selected_string == "day":
interval = datetime.timedelta(days=1)
elif selected_string == "week":
interval = datetime.timedelta(days=7)
elif selected_string == "month":
interval = datetime.timedelta(days=31) # Close enough?
elif selected_string == "forever":
return None
else:
return selected_string # This will raise
return timezone.now() + interval
class CreateInviteForm(CustomForm):
class Meta:
model = models.SiteInvite
exclude = ["code", "user", "times_used", "invitees"]
widgets = {
"expiry": ExpiryWidget(
choices=[
("day", _("One Day")),
("week", _("One Week")),
("month", _("One Month")),
("forever", _("Does Not Expire")),
]
),
"use_limit": widgets.Select(
choices=[(i, _(f"{i} uses")) for i in [1, 5, 10, 25, 50, 100]]
+ [(None, _("Unlimited"))]
),
}
class SiteForm(CustomForm):
class Meta:
model = models.SiteSettings
exclude = ["admin_code", "install_mode"]
widgets = {
"instance_short_description": forms.TextInput(
attrs={"aria-describedby": "desc_instance_short_description"}
),
"require_confirm_email": forms.CheckboxInput(
attrs={"aria-describedby": "desc_require_confirm_email"}
),
"invite_request_text": forms.Textarea(
attrs={"aria-describedby": "desc_invite_request_text"}
),
}
class ThemeForm(CustomForm):
class Meta:
model = models.Theme
fields = ["name", "path"]
widgets = {
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"path": forms.TextInput(
attrs={
"aria-describedby": "desc_path",
"placeholder": "css/themes/theme-name.scss",
}
),
}
class AnnouncementForm(CustomForm):
class Meta:
model = models.Announcement
exclude = ["remote_id"]
widgets = {
"preview": forms.TextInput(attrs={"aria-describedby": "desc_preview"}),
"content": forms.Textarea(attrs={"aria-describedby": "desc_content"}),
"event_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_event_date"}
),
"start_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_start_date"}
),
"end_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_end_date"}
),
"active": forms.CheckboxInput(attrs={"aria-describedby": "desc_active"}),
}
class EmailBlocklistForm(CustomForm):
class Meta:
model = models.EmailBlocklist
fields = ["domain"]
widgets = {
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
}
class IPBlocklistForm(CustomForm):
class Meta:
model = models.IPBlocklist
fields = ["address"]
class ServerForm(CustomForm):
class Meta:
model = models.FederatedServer
exclude = ["remote_id"]
class AutoModRuleForm(CustomForm):
class Meta:
model = models.AutoMod
fields = ["string_match", "flag_users", "flag_statuses", "created_by"]
class IntervalScheduleForm(CustomForm):
class Meta:
model = IntervalSchedule
fields = ["every", "period"]
widgets = {
"every": forms.NumberInput(attrs={"aria-describedby": "desc_every"}),
"period": forms.Select(attrs={"aria-describedby": "desc_period"}),
}

47
bookwyrm/forms/author.py Normal file
View file

@ -0,0 +1,47 @@
""" using django model forms """
from django import forms
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class AuthorForm(CustomForm):
class Meta:
model = models.Author
fields = [
"last_edited_by",
"name",
"aliases",
"bio",
"wikipedia_link",
"born",
"died",
"openlibrary_key",
"inventaire_id",
"librarything_key",
"goodreads_key",
"isni",
]
widgets = {
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"aliases": forms.TextInput(attrs={"aria-describedby": "desc_aliases"}),
"bio": forms.Textarea(attrs={"aria-describedby": "desc_bio"}),
"wikipedia_link": forms.TextInput(
attrs={"aria-describedby": "desc_wikipedia_link"}
),
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
"oepnlibrary_key": forms.TextInput(
attrs={"aria-describedby": "desc_oepnlibrary_key"}
),
"inventaire_id": forms.TextInput(
attrs={"aria-describedby": "desc_inventaire_id"}
),
"librarything_key": forms.TextInput(
attrs={"aria-describedby": "desc_librarything_key"}
),
"goodreads_key": forms.TextInput(
attrs={"aria-describedby": "desc_goodreads_key"}
),
}

111
bookwyrm/forms/books.py Normal file
View file

@ -0,0 +1,111 @@
""" using django model forms """
from django import forms
from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class CoverForm(CustomForm):
class Meta:
model = models.Book
fields = ["cover"]
help_texts = {f: None for f in fields}
class ArrayWidget(forms.widgets.TextInput):
# pylint: disable=unused-argument
# pylint: disable=no-self-use
def value_from_datadict(self, data, files, name):
"""get all values for this name"""
return [i for i in data.getlist(name) if i]
class EditionForm(CustomForm):
class Meta:
model = models.Edition
exclude = [
"remote_id",
"origin_id",
"created_date",
"updated_date",
"edition_rank",
"authors",
"parent_work",
"shelves",
"connector",
"search_vector",
"links",
"file_links",
]
widgets = {
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
"subtitle": forms.TextInput(attrs={"aria-describedby": "desc_subtitle"}),
"description": forms.Textarea(
attrs={"aria-describedby": "desc_description"}
),
"series": forms.TextInput(attrs={"aria-describedby": "desc_series"}),
"series_number": forms.TextInput(
attrs={"aria-describedby": "desc_series_number"}
),
"subjects": ArrayWidget(),
"languages": forms.TextInput(
attrs={"aria-describedby": "desc_languages_help desc_languages"}
),
"publishers": forms.TextInput(
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
),
"first_published_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_first_published_date"}
),
"published_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_published_date"}
),
"cover": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_cover"}
),
"physical_format": forms.Select(
attrs={"aria-describedby": "desc_physical_format"}
),
"physical_format_detail": forms.TextInput(
attrs={"aria-describedby": "desc_physical_format_detail"}
),
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
"openlibrary_key": forms.TextInput(
attrs={"aria-describedby": "desc_openlibrary_key"}
),
"inventaire_id": forms.TextInput(
attrs={"aria-describedby": "desc_inventaire_id"}
),
"oclc_number": forms.TextInput(
attrs={"aria-describedby": "desc_oclc_number"}
),
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
}
class EditionFromWorkForm(CustomForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# make all fields hidden
for visible in self.visible_fields():
visible.field.widget = forms.HiddenInput()
class Meta:
model = models.Work
fields = [
"title",
"subtitle",
"authors",
"description",
"languages",
"series",
"series_number",
"subjects",
"subject_places",
"cover",
"first_published_date",
]

View file

@ -0,0 +1,26 @@
""" Overrides django's default form class """
from collections import defaultdict
from django.forms import ModelForm
from django.forms.widgets import Textarea
class CustomForm(ModelForm):
"""add css classes to the forms"""
def __init__(self, *args, **kwargs):
css_classes = defaultdict(lambda: "")
css_classes["text"] = "input"
css_classes["password"] = "input"
css_classes["email"] = "input"
css_classes["number"] = "input"
css_classes["checkbox"] = "checkbox"
css_classes["textarea"] = "textarea"
# pylint: disable=super-with-arguments
super(CustomForm, self).__init__(*args, **kwargs)
for visible in self.visible_fields():
if hasattr(visible.field.widget, "input_type"):
input_type = visible.field.widget.input_type
if isinstance(visible.field.widget, Textarea):
input_type = "textarea"
visible.field.widget.attrs["rows"] = 5
visible.field.widget.attrs["class"] = css_classes[input_type]

View file

@ -0,0 +1,68 @@
""" using django model forms """
from django import forms
from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class EditUserForm(CustomForm):
class Meta:
model = models.User
fields = [
"avatar",
"name",
"email",
"summary",
"show_goal",
"show_suggested_users",
"manually_approves_followers",
"default_post_privacy",
"discoverable",
"hide_follows",
"preferred_timezone",
"preferred_language",
"theme",
]
help_texts = {f: None for f in fields}
widgets = {
"avatar": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_avatar"}
),
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
"email": forms.EmailInput(attrs={"aria-describedby": "desc_email"}),
"discoverable": forms.CheckboxInput(
attrs={"aria-describedby": "desc_discoverable"}
),
}
class LimitedEditUserForm(CustomForm):
class Meta:
model = models.User
fields = [
"avatar",
"name",
"summary",
"manually_approves_followers",
"discoverable",
]
help_texts = {f: None for f in fields}
widgets = {
"avatar": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_avatar"}
),
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
"discoverable": forms.CheckboxInput(
attrs={"aria-describedby": "desc_discoverable"}
),
}
class DeleteUserForm(CustomForm):
class Meta:
model = models.User
fields = ["password"]

59
bookwyrm/forms/forms.py Normal file
View file

@ -0,0 +1,59 @@
""" using django model forms """
from django import forms
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from bookwyrm.models.user import FeedFilterChoices
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class FeedStatusTypesForm(CustomForm):
class Meta:
model = models.User
fields = ["feed_status_types"]
help_texts = {f: None for f in fields}
widgets = {
"feed_status_types": widgets.CheckboxSelectMultiple(
choices=FeedFilterChoices,
),
}
class ImportForm(forms.Form):
csv_file = forms.FileField()
class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
fields = ["user", "name", "privacy", "description"]
class GoalForm(CustomForm):
class Meta:
model = models.AnnualGoal
fields = ["user", "year", "goal", "privacy"]
class ReportForm(CustomForm):
class Meta:
model = models.Report
fields = ["user", "reporter", "status", "links", "note"]
class ReadThroughForm(CustomForm):
def clean(self):
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
finish_date = cleaned_data.get("finish_date")
if start_date and finish_date and start_date > finish_date:
self.add_error(
"finish_date", _("Reading finish date cannot be before start date.")
)
class Meta:
model = models.ReadThrough
fields = ["user", "book", "start_date", "finish_date"]

16
bookwyrm/forms/groups.py Normal file
View file

@ -0,0 +1,16 @@
""" using django model forms """
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class UserGroupForm(CustomForm):
class Meta:
model = models.User
fields = ["groups"]
class GroupForm(CustomForm):
class Meta:
model = models.Group
fields = ["user", "privacy", "name", "description"]

45
bookwyrm/forms/landing.py Normal file
View file

@ -0,0 +1,45 @@
""" Forms for the landing pages """
from django.forms import PasswordInput
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class LoginForm(CustomForm):
class Meta:
model = models.User
fields = ["localname", "password"]
help_texts = {f: None for f in fields}
widgets = {
"password": PasswordInput(),
}
class RegisterForm(CustomForm):
class Meta:
model = models.User
fields = ["localname", "email", "password"]
help_texts = {f: None for f in fields}
widgets = {"password": PasswordInput()}
def clean(self):
"""Check if the username is taken"""
cleaned_data = super().clean()
localname = cleaned_data.get("localname").strip()
if models.User.objects.filter(localname=localname).first():
self.add_error("localname", _("User with this username already exists"))
class InviteRequestForm(CustomForm):
def clean(self):
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
email = cleaned_data.get("email")
if email and models.User.objects.filter(email=email).exists():
self.add_error("email", _("A user with this email already exists."))
class Meta:
model = models.InviteRequest
fields = ["email"]

48
bookwyrm/forms/links.py Normal file
View file

@ -0,0 +1,48 @@
""" using django model forms """
from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class LinkDomainForm(CustomForm):
class Meta:
model = models.LinkDomain
fields = ["name"]
class FileLinkForm(CustomForm):
class Meta:
model = models.FileLink
fields = ["url", "filetype", "availability", "book", "added_by"]
def clean(self):
"""make sure the domain isn't blocked or pending"""
cleaned_data = super().clean()
url = cleaned_data.get("url")
filetype = cleaned_data.get("filetype")
book = cleaned_data.get("book")
domain = urlparse(url).netloc
if models.LinkDomain.objects.filter(domain=domain).exists():
status = models.LinkDomain.objects.get(domain=domain).status
if status == "blocked":
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This domain is blocked. Please contact your administrator if you think this is an error."
),
)
elif models.FileLink.objects.filter(
url=url, book=book, filetype=filetype
).exists():
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
),
)

37
bookwyrm/forms/lists.py Normal file
View file

@ -0,0 +1,37 @@
""" using django model forms """
from django import forms
from django.forms import ChoiceField
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class ListForm(CustomForm):
class Meta:
model = models.List
fields = ["user", "name", "description", "curation", "privacy", "group"]
class ListItemForm(CustomForm):
class Meta:
model = models.ListItem
fields = ["user", "book", "book_list", "notes"]
class SortListForm(forms.Form):
sort_by = ChoiceField(
choices=(
("order", _("List Order")),
("title", _("Book Title")),
("rating", _("Rating")),
),
label=_("Sort By"),
)
direction = ChoiceField(
choices=(
("ascending", _("Ascending")),
("descending", _("Descending")),
),
)

82
bookwyrm/forms/status.py Normal file
View file

@ -0,0 +1,82 @@
""" using django model forms """
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class RatingForm(CustomForm):
class Meta:
model = models.ReviewRating
fields = ["user", "book", "rating", "privacy"]
class ReviewForm(CustomForm):
class Meta:
model = models.Review
fields = [
"user",
"book",
"name",
"content",
"rating",
"content_warning",
"sensitive",
"privacy",
]
class CommentForm(CustomForm):
class Meta:
model = models.Comment
fields = [
"user",
"book",
"content",
"content_warning",
"sensitive",
"privacy",
"progress",
"progress_mode",
"reading_status",
]
class QuotationForm(CustomForm):
class Meta:
model = models.Quotation
fields = [
"user",
"book",
"quote",
"content",
"content_warning",
"sensitive",
"privacy",
"position",
"position_mode",
]
class ReplyForm(CustomForm):
class Meta:
model = models.Status
fields = [
"user",
"content",
"content_warning",
"sensitive",
"reply_parent",
"privacy",
]
class StatusForm(CustomForm):
class Meta:
model = models.Status
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
class DirectForm(CustomForm):
class Meta:
model = models.Status
fields = ["user", "content", "content_warning", "sensitive", "privacy"]

View file

@ -0,0 +1,54 @@
""" Get your admin code to allow install """
from django.core.management.base import BaseCommand
from bookwyrm import models
from bookwyrm.settings import VERSION
# pylint: disable=no-self-use
class Command(BaseCommand):
"""command-line options"""
help = "What version is this?"
def add_arguments(self, parser):
"""specify which function to run"""
parser.add_argument(
"--current",
action="store_true",
help="Version stored in database",
)
parser.add_argument(
"--target",
action="store_true",
help="Version stored in settings",
)
parser.add_argument(
"--update",
action="store_true",
help="Update database version",
)
# pylint: disable=unused-argument
def handle(self, *args, **options):
"""execute init"""
site = models.SiteSettings.objects.get()
current = site.version or "0.0.1"
target = VERSION
if options.get("current"):
print(current)
return
if options.get("target"):
print(target)
return
if options.get("update"):
site.version = target
site.save()
return
if current != target:
print(f"{current}/{target}")
else:
print(current)

View file

@ -0,0 +1,68 @@
# Generated by Django 3.2.12 on 2022-02-27 17:52
from django.db import migrations, models
import django.db.models.deletion
def add_default_themes(apps, schema_editor):
"""add light and dark themes"""
db_alias = schema_editor.connection.alias
theme_model = apps.get_model("bookwyrm", "Theme")
theme_model.objects.using(db_alias).create(
name="BookWyrm Light",
path="css/themes/bookwyrm-light.scss",
)
theme_model.objects.using(db_alias).create(
name="BookWyrm Dark",
path="css/themes/bookwyrm-dark.scss",
)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0141_alter_report_status"),
]
operations = [
migrations.CreateModel(
name="Theme",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("name", models.CharField(max_length=50, unique=True)),
("path", models.CharField(max_length=50, unique=True)),
],
),
migrations.AddField(
model_name="sitesettings",
name="default_theme",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="bookwyrm.theme",
),
),
migrations.AddField(
model_name="user",
name="theme",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="bookwyrm.theme",
),
),
migrations.RunPython(
add_default_themes, reverse_code=migrations.RunPython.noop
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2022-02-28 19:44
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0141_alter_report_status"),
]
operations = [
migrations.AddField(
model_name="user",
name="hide_follows",
field=bookwyrm.models.fields.BooleanField(default=False),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.12 on 2022-02-28 21:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0142_auto_20220227_1752"),
("bookwyrm", "0142_user_hide_follows"),
]
operations = []

View file

@ -0,0 +1,39 @@
# Generated by Django 3.2.12 on 2022-03-01 18:46
from django.db import migrations, models
def remove_white(apps, schema_editor):
"""don't hardcode white announcements"""
db_alias = schema_editor.connection.alias
announcement_model = apps.get_model("bookwyrm", "Announcement")
announcement_model.objects.using(db_alias).filter(display_type="white-ter").update(
display_type=None
)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0143_merge_0142_auto_20220227_1752_0142_user_hide_follows"),
]
operations = [
migrations.AlterField(
model_name="announcement",
name="display_type",
field=models.CharField(
blank=True,
choices=[
("primary-light", "Primary"),
("success-light", "Success"),
("link-light", "Link"),
("warning-light", "Warning"),
("danger-light", "Danger"),
],
max_length=20,
null=True,
),
),
migrations.RunPython(remove_white, reverse_code=migrations.RunPython.noop),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-03-16 18:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0144_alter_announcement_display_type"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="version",
field=models.CharField(blank=True, max_length=10, null=True),
),
]

View file

@ -26,7 +26,7 @@ from .group import Group, GroupMember, GroupMemberInvitation
from .import_job import ImportJob, ImportItem from .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite from .site import SiteSettings, Theme, SiteInvite
from .site import PasswordReset, InviteRequest from .site import PasswordReset, InviteRequest
from .announcement import Announcement from .announcement import Announcement
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task

View file

@ -8,7 +8,6 @@ from .base_model import BookWyrmModel
DisplayTypes = [ DisplayTypes = [
("white-ter", _("None")),
("primary-light", _("Primary")), ("primary-light", _("Primary")),
("success-light", _("Success")), ("success-light", _("Success")),
("link-light", _("Link")), ("link-light", _("Link")),
@ -28,11 +27,7 @@ class Announcement(BookWyrmModel):
end_date = models.DateTimeField(blank=True, null=True) end_date = models.DateTimeField(blank=True, null=True)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
display_type = models.CharField( display_type = models.CharField(
max_length=20, max_length=20, choices=DisplayTypes, null=True, blank=True
blank=False,
null=False,
choices=DisplayTypes,
default="white-ter",
) )
@classmethod @classmethod

View file

@ -24,6 +24,10 @@ class SiteSettings(models.Model):
) )
instance_description = models.TextField(default="This instance has no description.") instance_description = models.TextField(default="This instance has no description.")
instance_short_description = models.CharField(max_length=255, blank=True, null=True) instance_short_description = models.CharField(max_length=255, blank=True, null=True)
default_theme = models.ForeignKey(
"Theme", null=True, blank=True, on_delete=models.SET_NULL
)
version = models.CharField(null=True, blank=True, max_length=10)
# admin setup options # admin setup options
install_mode = models.BooleanField(default=False) install_mode = models.BooleanField(default=False)
@ -104,6 +108,18 @@ class SiteSettings(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Theme(models.Model):
"""Theme files"""
created_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=50, unique=True)
path = models.CharField(max_length=50, unique=True)
def __str__(self):
# pylint: disable=invalid-str-returned
return self.name
class SiteInvite(models.Model): class SiteInvite(models.Model):
"""gives someone access to create an account on the instance""" """gives someone access to create an account on the instance"""

View file

@ -227,7 +227,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@classmethod @classmethod
def privacy_filter(cls, viewer, privacy_levels=None): def privacy_filter(cls, viewer, privacy_levels=None):
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels) queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
return queryset.filter(deleted=False) return queryset.filter(deleted=False, user__is_active=True)
@classmethod @classmethod
def direct_filter(cls, queryset, viewer): def direct_filter(cls, queryset, viewer):

View file

@ -136,6 +136,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
last_active_date = models.DateTimeField(default=timezone.now) last_active_date = models.DateTimeField(default=timezone.now)
manually_approves_followers = fields.BooleanField(default=False) manually_approves_followers = fields.BooleanField(default=False)
theme = models.ForeignKey("Theme", null=True, blank=True, on_delete=models.SET_NULL)
hide_follows = fields.BooleanField(default=False)
# options to turn features on and off # options to turn features on and off
show_goal = models.BooleanField(default=True) show_goal = models.BooleanField(default=True)
@ -478,10 +480,13 @@ def set_remote_server(user_id):
get_remote_reviews.delay(user.outbox) get_remote_reviews.delay(user.outbox)
def get_or_create_remote_server(domain): def get_or_create_remote_server(domain, refresh=False):
"""get info on a remote server""" """get info on a remote server"""
server = FederatedServer()
try: try:
return FederatedServer.objects.get(server_name=domain) server = FederatedServer.objects.get(server_name=domain)
if not refresh:
return server
except FederatedServer.DoesNotExist: except FederatedServer.DoesNotExist:
pass pass
@ -496,13 +501,15 @@ def get_or_create_remote_server(domain):
application_type = data.get("software", {}).get("name") application_type = data.get("software", {}).get("name")
application_version = data.get("software", {}).get("version") application_version = data.get("software", {}).get("version")
except ConnectorException: except ConnectorException:
if server.id:
return server
application_type = application_version = None application_type = application_version = None
server = FederatedServer.objects.create( server.server_name = domain
server_name=domain, server.application_type = application_type
application_type=application_type, server.application_version = application_version
application_version=application_version,
) server.save()
return server return server

View file

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
env = Env() env = Env()
env.read_env() env.read_env()
DOMAIN = env("DOMAIN") DOMAIN = env("DOMAIN")
VERSION = "0.3.1" VERSION = "0.3.4"
RELEASE_API = env( RELEASE_API = env(
"RELEASE_API", "RELEASE_API",
@ -21,7 +21,7 @@ RELEASE_API = env(
PAGE_LENGTH = env("PAGE_LENGTH", 15) PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "a60e5a55" JS_CACHE = "bc93172a"
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
@ -90,6 +90,7 @@ INSTALLED_APPS = [
"sass_processor", "sass_processor",
"bookwyrm", "bookwyrm",
"celery", "celery",
"django_celery_beat",
"imagekit", "imagekit",
"storages", "storages",
] ]
@ -188,10 +189,7 @@ STATICFILES_FINDERS = [
] ]
SASS_PROCESSOR_INCLUDE_FILE_PATTERN = r"^.+\.[s]{0,1}(?:a|c)ss$" SASS_PROCESSOR_INCLUDE_FILE_PATTERN = r"^.+\.[s]{0,1}(?:a|c)ss$"
SASS_PROCESSOR_ENABLED = True
SASS_PROCESSOR_INCLUDE_DIRS = [
os.path.join(BASE_DIR, ".css-config-sample"),
]
# minify css is production but not dev # minify css is production but not dev
if not DEBUG: if not DEBUG:

View file

@ -1,7 +1,4 @@
@charset "utf-8"; @charset "utf-8";
@import "instance-settings";
@import "themes/light.scss";
@import "vendor/bulma/bulma.sass"; @import "vendor/bulma/bulma.sass";
@import "vendor/icons.css";
@import "bookwyrm/all.scss"; @import "bookwyrm/all.scss";

View file

@ -1,6 +1,7 @@
/** Imports /** Imports
******************************************************************************/ ******************************************************************************/
@import "components/avatar"; @import "components/avatar";
@import "components/barcode";
@import "components/book_cover"; @import "components/book_cover";
@import "components/book_grid"; @import "components/book_grid";
@import "components/book_list"; @import "components/book_list";
@ -115,7 +116,7 @@ button .button-invisible-overlay {
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
background: rgba($scheme-invert, 0.66); background: $invisible-overlay-background-color;
color: white; color: white;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;

View file

@ -0,0 +1,26 @@
/* Barcode scanner CSS */
#barcode-scanner {
position: relative;
max-width: 100%;
text-align: center;
height: calc(70vh - 200px);
video {
height: calc(70vh - 200px);
max-width: 100%;
}
canvas {
position: absolute;
top: 0;
left: 0;
right: 0;
margin: auto;
height: calc(70vh - 200px);
max-width: 100%;
}
}
#barcode-camera-list {
float: right;
}

View file

@ -53,7 +53,7 @@ details.dropdown .dropdown-menu a:focus-visible {
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
details.dropdown[open] summary.dropdown-trigger::before { details.dropdown[open] summary.dropdown-trigger::before {
background-color: rgba($scheme-invert, 0.5); background-color: $modal-background-background-color;
z-index: 30; z-index: 30;
} }

View file

@ -3,7 +3,7 @@
.toggle-button[aria-pressed="true"], .toggle-button[aria-pressed="true"],
.toggle-button[aria-pressed="true"]:hover { .toggle-button[aria-pressed="true"]:hover {
background-color: hsl(171deg, 100%, 41%); background-color: $primary;
color: white; color: white;
} }

View file

@ -39,6 +39,7 @@
<glyph unicode="&#xe91e;" glyph-name="graphic-heart" d="M934.176 791.52c-116.128 115.072-301.824 117.472-422.112 9.216-120.32 108.256-305.952 105.856-422.144-9.216-119.712-118.528-119.712-310.688 0-429.28 34.208-33.888 353.696-350.112 353.696-350.112 37.856-37.504 99.072-37.504 136.896 0 0 0 349.824 346.304 353.696 350.112 119.744 118.592 119.744 310.752-0.032 429.28zM888.576 407.424l-353.696-350.112c-12.576-12.512-33.088-12.512-45.6 0l-353.696 350.112c-94.4 93.44-94.4 245.472 0 338.912 91.008 90.080 237.312 93.248 333.088 7.104l43.392-39.040 43.36 39.040c95.808 86.144 242.112 83.008 333.12-7.104 94.4-93.408 94.4-245.44 0.032-338.912zM296.096 719.968c8.864 0 16-7.168 16-16s-7.168-16-16-16h-0.032c-57.408 0-103.968-46.56-103.968-103.968v-0.032c0-8.832-7.168-16-16-16s-16 7.168-16 16v0c0 75.072 60.832 135.904 135.872 135.968 0.064 0 0.064 0.032 0.128 0.032z" /> <glyph unicode="&#xe91e;" glyph-name="graphic-heart" d="M934.176 791.52c-116.128 115.072-301.824 117.472-422.112 9.216-120.32 108.256-305.952 105.856-422.144-9.216-119.712-118.528-119.712-310.688 0-429.28 34.208-33.888 353.696-350.112 353.696-350.112 37.856-37.504 99.072-37.504 136.896 0 0 0 349.824 346.304 353.696 350.112 119.744 118.592 119.744 310.752-0.032 429.28zM888.576 407.424l-353.696-350.112c-12.576-12.512-33.088-12.512-45.6 0l-353.696 350.112c-94.4 93.44-94.4 245.472 0 338.912 91.008 90.080 237.312 93.248 333.088 7.104l43.392-39.040 43.36 39.040c95.808 86.144 242.112 83.008 333.12-7.104 94.4-93.408 94.4-245.44 0.032-338.912zM296.096 719.968c8.864 0 16-7.168 16-16s-7.168-16-16-16h-0.032c-57.408 0-103.968-46.56-103.968-103.968v-0.032c0-8.832-7.168-16-16-16s-16 7.168-16 16v0c0 75.072 60.832 135.904 135.872 135.968 0.064 0 0.064 0.032 0.128 0.032z" />
<glyph unicode="&#xe91f;" glyph-name="graphic-paperplane" d="M1009.376 954.88c-5.312 3.424-11.36 5.12-17.376 5.12-6.176 0-12.384-1.76-17.76-5.376l-960-640c-9.888-6.56-15.328-18.112-14.048-29.952 1.216-11.808 8.896-22.016 19.936-26.368l250.368-100.192 117.728-206.016c5.632-9.888 16.096-16 27.424-16.128 0.128 0 0.224 0 0.352 0 11.232 0 21.664 5.952 27.424 15.552l66.464 110.816 310.24-124.064c3.808-1.536 7.808-2.272 11.872-2.272 5.44 0 10.816 1.376 15.68 4.128 8.448 4.736 14.24 13.056 15.872 22.624l160 960c2.080 12.576-3.488 25.184-14.176 32.128zM100.352 295.136l741.6 494.432-539.2-577.184c-2.848 1.696-5.376 3.936-8.512 5.184l-193.888 77.568zM326.048 189.888c-0.064 0.128-0.16 0.192-0.224 0.32l606.176 648.8-516.768-805.184-89.184 156.064zM806.944 12.512l-273.312 109.312c-6.496 2.56-13.248 3.424-19.936 3.808l420.864 652.416-127.616-765.536z" /> <glyph unicode="&#xe91f;" glyph-name="graphic-paperplane" d="M1009.376 954.88c-5.312 3.424-11.36 5.12-17.376 5.12-6.176 0-12.384-1.76-17.76-5.376l-960-640c-9.888-6.56-15.328-18.112-14.048-29.952 1.216-11.808 8.896-22.016 19.936-26.368l250.368-100.192 117.728-206.016c5.632-9.888 16.096-16 27.424-16.128 0.128 0 0.224 0 0.352 0 11.232 0 21.664 5.952 27.424 15.552l66.464 110.816 310.24-124.064c3.808-1.536 7.808-2.272 11.872-2.272 5.44 0 10.816 1.376 15.68 4.128 8.448 4.736 14.24 13.056 15.872 22.624l160 960c2.080 12.576-3.488 25.184-14.176 32.128zM100.352 295.136l741.6 494.432-539.2-577.184c-2.848 1.696-5.376 3.936-8.512 5.184l-193.888 77.568zM326.048 189.888c-0.064 0.128-0.16 0.192-0.224 0.32l606.176 648.8-516.768-805.184-89.184 156.064zM806.944 12.512l-273.312 109.312c-6.496 2.56-13.248 3.424-19.936 3.808l420.864 652.416-127.616-765.536z" />
<glyph unicode="&#xe920;" glyph-name="graphic-banknote" d="M1005.28 621.248l-320 320c-15.872 15.872-38.88 22.24-60.672 16.864-11.488-2.816-21.76-8.736-29.888-16.864-7.264-7.264-12.736-16.256-15.872-26.304-14.496-47.008-39.552-87.872-76.64-124.928-49.536-49.504-114.048-87.008-182.304-126.656-72.448-41.984-147.296-85.504-208.64-146.816-52.128-52.192-87.616-110.24-108.416-177.632-7.008-22.752-0.896-47.36 15.872-64.192l320-320c15.872-15.872 38.88-22.24 60.672-16.864 11.488 2.88 21.76 8.736 29.888 16.864 7.264 7.264 12.736 16.256 15.872 26.368 14.528 47.008 39.584 87.872 76.704 124.928 49.504 49.504 113.984 86.944 182.304 126.56 72.384 42.048 147.264 85.568 208.576 146.88 52.128 52.128 87.616 110.24 108.448 177.632 6.976 22.72 0.832 47.424-15.904 64.16zM384 0c-105.984 105.984-214.016 214.048-320 320 90.944 294.432 485.12 281.568 576 576 105.984-105.952 214.048-214.016 320.064-320-90.976-294.368-485.152-281.568-576.064-576zM625.984 483.2c-10.432 8.736-20.928 14.688-31.488 17.632-10.496 2.944-20.992 4.128-31.616 3.36-10.496-0.8-21.248-3.2-32-7.328-10.752-4.192-21.568-8.736-32.448-14.016-17.184 19.744-34.368 39.264-51.552 57.376 7.744 7.008 15.264 10.56 22.496 10.816 7.264 0.32 14.24-0.448 20.864-2.112 6.752-1.696 12.928-3.136 18.624-4.256 5.76-1.12 10.752 0.128 15.136 3.808 4.64 4 7.2 9.184 7.552 15.424 0.32 6.304-2.048 12.448-7.328 18.432-6.752 7.744-14.88 12.448-24.64 14.176-9.632 1.696-19.488 1.568-29.76-0.672-10.112-2.304-19.744-6.112-28.864-11.488s-16.448-10.88-21.888-16.256c-2.080 1.984-4.16 3.936-6.24 5.888-2.304 2.112-5.184 3.264-8.64 3.2-3.488 0-6.368-1.504-8.736-4.256-2.304-2.688-3.36-5.824-2.944-9.12 0.32-3.424 1.696-6.048 4.064-8.064 2.080-1.76 4.16-3.488 6.24-5.312-8.192-9.888-14.944-20.8-20.256-32.32-5.376-11.552-8.576-23.008-9.76-34.112-1.248-11.2-0.064-21.44 3.36-30.944 3.424-9.568 9.76-17.696 19.008-25.376 15.072-12.512 32.8-17.824 53.376-16.64 20.512 1.248 42.624 7.36 66.4 20.128 18.88-21.824 37.824-43.488 56.736-63.616-8-6.752-15.008-10.624-21.184-11.872-6.176-1.312-11.68-1.184-16.672 0.32-4.992 1.568-9.632 3.808-13.888 6.688-4.256 2.944-8.448 5.44-12.64 7.488-4.128 2.048-8.384 3.2-12.736 3.264s-8.992-2.048-14.112-6.432c-5.248-4.576-7.872-9.888-7.872-15.872 0-5.952 2.752-12 8.128-18.112 5.44-6.112 12.512-11.264 21.056-15.328s18.208-6.624 28.832-7.328c10.624-0.736 21.824 0.864 33.632 5.248 11.872 4.32 23.616 12.128 35.2 23.744 5.568-5.44 11.2-10.624 16.8-15.616 2.368-2.048 5.248-3.072 8.736-2.816 3.36 0.128 6.304 1.696 8.64 4.512 2.368 2.88 3.36 6.048 3.008 9.376-0.32 3.36-1.696 5.952-4 7.808-5.632 4.512-11.264 9.248-16.864 14.24 9.568 11.744 17.248 24.128 22.944 36.384 5.696 12.32 9.056 24.192 10.176 35.2 1.12 11.072-0.192 21.056-3.808 30.112-3.584 9.184-9.952 17.056-19.072 24.64zM447.072 461.504c-9.056-0.384-16.96 2.624-23.872 9.312-2.944 2.816-4.992 6.24-6.24 10.304-1.312 4.064-1.76 8.512-1.248 13.376 0.448 4.8 1.888 9.824 4.384 14.88 2.368 5.056 5.888 10.112 10.368 15.008 16.224-16.128 32.416-33.824 48.64-52.128-12.288-6.752-22.976-10.368-32.032-10.752zM598.016 397.44c-2.88-5.312-6.176-10.048-10.048-14.176-17.952 18.112-35.872 38.016-53.76 58.432 4.576 2.048 9.376 4.192 14.56 6.368s10.368 3.616 15.552 4.512c5.312 0.8 10.56 0.576 15.808-0.672 5.184-1.312 10.112-4.128 14.688-8.576 4.512-4.512 7.36-9.184 8.512-14.24 1.248-5.12 1.312-10.304 0.448-15.616-0.928-5.344-2.816-10.656-5.76-16.032zM470.944 250.24c6.304 5.088 15.584 4.832 21.376-1.056 6.272-6.24 6.272-16.448 0-22.688-0.512-0.512-1.056-0.864-1.632-1.312l0.064-0.064c-20.256-15.392-36.896-29.248-54.848-47.2-16.224-16.192-30.88-33.248-43.552-50.56l-20.448-28c-0.64-1.152-1.408-2.208-2.368-3.2-6.272-6.24-16.48-6.24-22.72 0-5.44 5.44-6.112 13.824-2.112 20.064l-0.064 0.064 21.888 29.888c13.664 18.688 29.376 36.992 46.752 54.368 18.080 18.144 37.6 34.336 57.6 49.696h0.064zM588.096 713.12c16.192 16.192 30.816 33.184 43.52 50.592l21.248 29.12c0.768 1.376 1.632 2.752 2.816 3.936 6.304 6.304 16.512 6.304 22.816 0 5.984-6.016 6.24-15.52 0.8-21.888l0.064-0.064-21.888-30.016c-13.696-18.688-29.376-36.928-46.752-54.304-18.080-18.080-37.568-34.336-57.568-49.696l-0.128 0.064c-6.368-5.856-16.256-5.728-22.368 0.448-6.304 6.304-6.304 16.576 0 22.88 1.12 1.184 2.432 2.016 3.744 2.752 18.816 14.368 36.96 29.44 53.696 46.176z" /> <glyph unicode="&#xe920;" glyph-name="graphic-banknote" d="M1005.28 621.248l-320 320c-15.872 15.872-38.88 22.24-60.672 16.864-11.488-2.816-21.76-8.736-29.888-16.864-7.264-7.264-12.736-16.256-15.872-26.304-14.496-47.008-39.552-87.872-76.64-124.928-49.536-49.504-114.048-87.008-182.304-126.656-72.448-41.984-147.296-85.504-208.64-146.816-52.128-52.192-87.616-110.24-108.416-177.632-7.008-22.752-0.896-47.36 15.872-64.192l320-320c15.872-15.872 38.88-22.24 60.672-16.864 11.488 2.88 21.76 8.736 29.888 16.864 7.264 7.264 12.736 16.256 15.872 26.368 14.528 47.008 39.584 87.872 76.704 124.928 49.504 49.504 113.984 86.944 182.304 126.56 72.384 42.048 147.264 85.568 208.576 146.88 52.128 52.128 87.616 110.24 108.448 177.632 6.976 22.72 0.832 47.424-15.904 64.16zM384 0c-105.984 105.984-214.016 214.048-320 320 90.944 294.432 485.12 281.568 576 576 105.984-105.952 214.048-214.016 320.064-320-90.976-294.368-485.152-281.568-576.064-576zM625.984 483.2c-10.432 8.736-20.928 14.688-31.488 17.632-10.496 2.944-20.992 4.128-31.616 3.36-10.496-0.8-21.248-3.2-32-7.328-10.752-4.192-21.568-8.736-32.448-14.016-17.184 19.744-34.368 39.264-51.552 57.376 7.744 7.008 15.264 10.56 22.496 10.816 7.264 0.32 14.24-0.448 20.864-2.112 6.752-1.696 12.928-3.136 18.624-4.256 5.76-1.12 10.752 0.128 15.136 3.808 4.64 4 7.2 9.184 7.552 15.424 0.32 6.304-2.048 12.448-7.328 18.432-6.752 7.744-14.88 12.448-24.64 14.176-9.632 1.696-19.488 1.568-29.76-0.672-10.112-2.304-19.744-6.112-28.864-11.488s-16.448-10.88-21.888-16.256c-2.080 1.984-4.16 3.936-6.24 5.888-2.304 2.112-5.184 3.264-8.64 3.2-3.488 0-6.368-1.504-8.736-4.256-2.304-2.688-3.36-5.824-2.944-9.12 0.32-3.424 1.696-6.048 4.064-8.064 2.080-1.76 4.16-3.488 6.24-5.312-8.192-9.888-14.944-20.8-20.256-32.32-5.376-11.552-8.576-23.008-9.76-34.112-1.248-11.2-0.064-21.44 3.36-30.944 3.424-9.568 9.76-17.696 19.008-25.376 15.072-12.512 32.8-17.824 53.376-16.64 20.512 1.248 42.624 7.36 66.4 20.128 18.88-21.824 37.824-43.488 56.736-63.616-8-6.752-15.008-10.624-21.184-11.872-6.176-1.312-11.68-1.184-16.672 0.32-4.992 1.568-9.632 3.808-13.888 6.688-4.256 2.944-8.448 5.44-12.64 7.488-4.128 2.048-8.384 3.2-12.736 3.264s-8.992-2.048-14.112-6.432c-5.248-4.576-7.872-9.888-7.872-15.872 0-5.952 2.752-12 8.128-18.112 5.44-6.112 12.512-11.264 21.056-15.328s18.208-6.624 28.832-7.328c10.624-0.736 21.824 0.864 33.632 5.248 11.872 4.32 23.616 12.128 35.2 23.744 5.568-5.44 11.2-10.624 16.8-15.616 2.368-2.048 5.248-3.072 8.736-2.816 3.36 0.128 6.304 1.696 8.64 4.512 2.368 2.88 3.36 6.048 3.008 9.376-0.32 3.36-1.696 5.952-4 7.808-5.632 4.512-11.264 9.248-16.864 14.24 9.568 11.744 17.248 24.128 22.944 36.384 5.696 12.32 9.056 24.192 10.176 35.2 1.12 11.072-0.192 21.056-3.808 30.112-3.584 9.184-9.952 17.056-19.072 24.64zM447.072 461.504c-9.056-0.384-16.96 2.624-23.872 9.312-2.944 2.816-4.992 6.24-6.24 10.304-1.312 4.064-1.76 8.512-1.248 13.376 0.448 4.8 1.888 9.824 4.384 14.88 2.368 5.056 5.888 10.112 10.368 15.008 16.224-16.128 32.416-33.824 48.64-52.128-12.288-6.752-22.976-10.368-32.032-10.752zM598.016 397.44c-2.88-5.312-6.176-10.048-10.048-14.176-17.952 18.112-35.872 38.016-53.76 58.432 4.576 2.048 9.376 4.192 14.56 6.368s10.368 3.616 15.552 4.512c5.312 0.8 10.56 0.576 15.808-0.672 5.184-1.312 10.112-4.128 14.688-8.576 4.512-4.512 7.36-9.184 8.512-14.24 1.248-5.12 1.312-10.304 0.448-15.616-0.928-5.344-2.816-10.656-5.76-16.032zM470.944 250.24c6.304 5.088 15.584 4.832 21.376-1.056 6.272-6.24 6.272-16.448 0-22.688-0.512-0.512-1.056-0.864-1.632-1.312l0.064-0.064c-20.256-15.392-36.896-29.248-54.848-47.2-16.224-16.192-30.88-33.248-43.552-50.56l-20.448-28c-0.64-1.152-1.408-2.208-2.368-3.2-6.272-6.24-16.48-6.24-22.72 0-5.44 5.44-6.112 13.824-2.112 20.064l-0.064 0.064 21.888 29.888c13.664 18.688 29.376 36.992 46.752 54.368 18.080 18.144 37.6 34.336 57.6 49.696h0.064zM588.096 713.12c16.192 16.192 30.816 33.184 43.52 50.592l21.248 29.12c0.768 1.376 1.632 2.752 2.816 3.936 6.304 6.304 16.512 6.304 22.816 0 5.984-6.016 6.24-15.52 0.8-21.888l0.064-0.064-21.888-30.016c-13.696-18.688-29.376-36.928-46.752-54.304-18.080-18.080-37.568-34.336-57.568-49.696l-0.128 0.064c-6.368-5.856-16.256-5.728-22.368 0.448-6.304 6.304-6.304 16.576 0 22.88 1.12 1.184 2.432 2.016 3.744 2.752 18.816 14.368 36.96 29.44 53.696 46.176z" />
<glyph unicode="&#xe937;" glyph-name="barcode" d="M0 832h128v-640h-128zM192 832h64v-640h-64zM320 832h64v-640h-64zM512 832h64v-640h-64zM768 832h64v-640h-64zM960 832h64v-640h-64zM640 832h32v-640h-32zM448 832h32v-640h-32zM864 832h32v-640h-32zM0 128h64v-64h-64zM192 128h64v-64h-64zM320 128h64v-64h-64zM640 128h64v-64h-64zM960 128h64v-64h-64zM768 128h128v-64h-128zM448 128h128v-64h-128z" />
<glyph unicode="&#xe97a;" glyph-name="spinner" d="M384 832c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM655.53 719.53c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM832 448c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM719.53 176.47c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM448.002 64c0 0 0 0 0 0 0 35.346 28.654 64 64 64s64-28.654 64-64c0 0 0 0 0 0 0-35.346-28.654-64-64-64s-64 28.654-64 64zM176.472 176.47c0 0 0 0 0 0 0 35.346 28.654 64 64 64s64-28.654 64-64c0 0 0 0 0 0 0-35.346-28.654-64-64-64s-64 28.654-64 64zM144.472 719.53c0 0 0 0 0 0 0 53.019 42.981 96 96 96s96-42.981 96-96c0 0 0 0 0 0 0-53.019-42.981-96-96-96s-96 42.981-96 96zM56 448c0 39.765 32.235 72 72 72s72-32.235 72-72c0-39.765-32.235-72-72-72s-72 32.235-72 72z" /> <glyph unicode="&#xe97a;" glyph-name="spinner" d="M384 832c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM655.53 719.53c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM832 448c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM719.53 176.47c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM448.002 64c0 0 0 0 0 0 0 35.346 28.654 64 64 64s64-28.654 64-64c0 0 0 0 0 0 0-35.346-28.654-64-64-64s-64 28.654-64 64zM176.472 176.47c0 0 0 0 0 0 0 35.346 28.654 64 64 64s64-28.654 64-64c0 0 0 0 0 0 0-35.346-28.654-64-64-64s-64 28.654-64 64zM144.472 719.53c0 0 0 0 0 0 0 53.019 42.981 96 96 96s96-42.981 96-96c0 0 0 0 0 0 0-53.019-42.981-96-96-96s-96 42.981-96 96zM56 448c0 39.765 32.235 72 72 72s72-32.235 72-72c0-39.765-32.235-72-72-72s-72 32.235-72 72z" />
<glyph unicode="&#xe986;" glyph-name="search" d="M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256z" /> <glyph unicode="&#xe986;" glyph-name="search" d="M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256z" />
<glyph unicode="&#xe9d7;" glyph-name="star-empty" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" /> <glyph unicode="&#xe9d7;" glyph-name="star-empty" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,84 @@
@import "../vendor/bulma/sass/utilities/initial-variables.sass";
/* Colors
******************************************************************************/
/* states */
$primary: #005e50;
$primary-light: #1d2b28;
$info: #1f4666;
$success: #246447;
$warning: #8b6c15;
$danger: #872538;
$danger-light: #481922;
$light: #393939;
$red: #ffa1b4;
/* book cover standins */
$no-cover-color: #002549;
/* background colors */
$scheme-main: rgb(24, 27, 28);
$scheme-invert: #fff;
$scheme-main-bis: rgb(28, 30, 32);
$scheme-main-ter: rgb(32, 34, 36);
$background-body: rgb(24, 27, 28);
$background-secondary: rgb(28, 30, 32);
$background-tertiary: rgb(32, 34, 36);
$modal-background-background-color: rgba($black, 0.8);
/* highlight colors */
$primary-highlight: $primary;
$info-highlight: $info;
$success-highlight: $success;
/* borders */
$border: #2b3031;
$border-light: #444;
$border-hover: #51595d;
/* text */
$text: $grey-lightest;
$text-light: $grey-lighter;
$text-strong: $white-ter;
/* links */
$link: #2e7eb9;
$link-background: $background-tertiary;
$link-hover: $white-bis;
$link-hover-border: #51595d;
$link-focus: $white-bis;
$link-active: $white-bis;
/* bulma overrides */
$background: $background-secondary;
$menu-item-active-background-color: $link-background;
$navbar-dropdown-item-hover-color: $white;
/* These element's colors are hardcoded, probably a bug in bulma? */
@media screen and (min-width: 769px) {
.navbar-dropdown {
box-shadow: 0 8px 8px rgba($black, 0.2) !important;
}
}
@media screen and (max-width: 768px) {
.navbar-menu {
box-shadow: 0 8px 8px rgba($black, 0.2) !important;
}
}
/* misc */
$shadow: 0 0.5em 1em -0.125em rgba($black, 0.2), 0 0px 0 1px rgba($black, 0.02);
$card-header-shadow: 0 0.125em 0.25em rgba($black, 0.1);
$invisible-overlay-background-color: rgba($black, 0.66);
$progress-value-background-color: $border-light;
/* Fonts
******************************************************************************/
$family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif;
@import "../bookwyrm.scss";
@import "../vendor/icons.css";

View file

@ -47,7 +47,13 @@ $link-active: $grey-darker;
$background: $background-secondary; $background: $background-secondary;
$menu-item-active-background-color: $link-background; $menu-item-active-background-color: $link-background;
/* misc */
$invisible-overlay-background-color: rgba($scheme-invert, 0.66);
/* Fonts /* Fonts
******************************************************************************/ ******************************************************************************/
$family-primary: $family-sans-serif; $family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif; $family-secondary: $family-sans-serif;
@import "../bookwyrm.scss";
@import "../vendor/icons.css";

View file

@ -1,55 +0,0 @@
@import "../vendor/bulma/sass/utilities/derived-variables.sass";
/* Colors
******************************************************************************/
/* states */
$primary: #016a5b;
$info: #1f4666;
$success: #246447;
$warning: #8b6c15;
$danger: #872538;
/* book cover standins */
$no-cover-color: #002549;
/* background colors */
$scheme-main: $grey-darker;
$scheme-main-bis: $black-ter;
$background-body: $grey-darker;
$background-secondary: $grey-dark;
$background-tertiary: #555;
/* highlight colors */
$primary-highlight: $primary;
$info-highlight: $info;
$success-highlight: $success;
/* borders */
$border: $grey;
$border-hover: $grey-light;
$border-light: $grey;
$border-light-hover: $grey-light;
/* text */
$text: $grey-lightest;
$text-light: $grey-lighter;
$text-strong: $white-ter;
/* links */
$link: $white;
$link-background: $background-tertiary;
$link-hover: $white-bis;
$link-focus: $white-bis;
$link-active: $white-bis;
/* misc */
/* bulma overrides */
$background: $background-secondary;
$menu-item-active-background-color: $link-background;
/* Fonts
******************************************************************************/
$family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif;

View file

@ -149,3 +149,6 @@
.icon-download:before { .icon-download:before {
content: "\ea36"; content: "\ea36";
} }
.icon-barcode:before {
content: "\e937";
}

View file

@ -1,5 +1,5 @@
/* exported BookWyrm */ /* exported BookWyrm */
/* globals TabGroup */ /* globals TabGroup, Quagga */
let BookWyrm = new (class { let BookWyrm = new (class {
constructor() { constructor() {
@ -38,15 +38,15 @@ let BookWyrm = new (class {
.querySelectorAll("[data-modal-open]") .querySelectorAll("[data-modal-open]")
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this))); .forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
document
.querySelectorAll("[data-duplicate]")
.forEach((node) => node.addEventListener("click", this.duplicateInput.bind(this)));
document document
.querySelectorAll("details.dropdown") .querySelectorAll("details.dropdown")
.forEach((node) => .forEach((node) =>
node.addEventListener("toggle", this.handleDetailsDropdown.bind(this)) node.addEventListener("toggle", this.handleDetailsDropdown.bind(this))
); );
document
.querySelector("#barcode-scanner-modal")
.addEventListener("open", this.openBarcodeScanner.bind(this));
} }
/** /**
@ -427,9 +427,11 @@ let BookWyrm = new (class {
}); });
modalElement.addEventListener("keydown", handleFocusTrap); modalElement.addEventListener("keydown", handleFocusTrap);
modalElement.dispatchEvent(new Event("open"));
} }
function handleModalClose(modalElement) { function handleModalClose(modalElement) {
modalElement.dispatchEvent(new Event("close"));
modalElement.removeEventListener("keydown", handleFocusTrap); modalElement.removeEventListener("keydown", handleFocusTrap);
htmlElement.classList.remove("is-clipped"); htmlElement.classList.remove("is-clipped");
modalElement.classList.remove("is-active"); modalElement.classList.remove("is-active");
@ -489,26 +491,6 @@ let BookWyrm = new (class {
window.open(url, windowName, "left=100,top=100,width=430,height=600"); window.open(url, windowName, "left=100,top=100,width=430,height=600");
} }
duplicateInput(event) {
const trigger = event.currentTarget;
const input_id = trigger.dataset.duplicate;
const orig = document.getElementById(input_id);
const parent = orig.parentNode;
const new_count = parent.querySelectorAll("input").length + 1;
let input = orig.cloneNode();
input.id += "-" + new_count;
input.value = "";
let label = parent.querySelector("label").cloneNode();
label.setAttribute("for", input.id);
parent.appendChild(label);
parent.appendChild(input);
}
/** /**
* Set up a "click-to-copy" component from a textarea element * Set up a "click-to-copy" component from a textarea element
* with `data-copytext`, `data-copytext-label`, `data-copytext-success` * with `data-copytext`, `data-copytext-label`, `data-copytext-success`
@ -632,4 +614,174 @@ let BookWyrm = new (class {
} }
} }
} }
openBarcodeScanner(event) {
const scannerNode = document.getElementById("barcode-scanner");
const statusNode = document.getElementById("barcode-status");
const cameraListNode = document.querySelector("#barcode-camera-list > select");
cameraListNode.addEventListener("change", onChangeCamera);
function onChangeCamera(event) {
initBarcodes(event.target.value);
}
function toggleStatus(status) {
for (const child of statusNode.children) {
BookWyrm.toggleContainer(child, !child.classList.contains(status));
}
}
function initBarcodes(cameraId = null) {
toggleStatus("grant-access");
if (!cameraId) {
cameraId = sessionStorage.getItem("preferredCam");
} else {
sessionStorage.setItem("preferredCam", cameraId);
}
scannerNode.replaceChildren();
Quagga.stop();
Quagga.init(
{
inputStream: {
name: "Live",
type: "LiveStream",
target: scannerNode,
constraints: {
facingMode: "environment",
deviceId: cameraId,
},
},
decoder: {
readers: [
"ean_reader",
{
format: "ean_reader",
config: {
supplements: ["ean_2_reader", "ean_5_reader"],
},
},
],
multiple: false,
},
},
(err) => {
if (err) {
scannerNode.replaceChildren();
console.log(err);
toggleStatus("access-denied");
return;
}
let activeId = null;
const track = Quagga.CameraAccess.getActiveTrack();
if (track) {
activeId = track.getSettings().deviceId;
}
Quagga.CameraAccess.enumerateVideoDevices().then((devices) => {
cameraListNode.replaceChildren();
for (const device of devices) {
const child = document.createElement("option");
child.value = device.deviceId;
child.innerText = device.label.slice(0, 30);
if (activeId === child.value) {
child.selected = true;
}
cameraListNode.appendChild(child);
}
});
toggleStatus("scanning");
Quagga.start();
}
);
}
function cleanup(clearDrawing = true) {
Quagga.stop();
cameraListNode.removeEventListener("change", onChangeCamera);
if (clearDrawing) {
scannerNode.replaceChildren();
}
}
Quagga.onProcessed((result) => {
const drawingCtx = Quagga.canvas.ctx.overlay;
const drawingCanvas = Quagga.canvas.dom.overlay;
if (result) {
if (result.boxes) {
drawingCtx.clearRect(
0,
0,
parseInt(drawingCanvas.getAttribute("width")),
parseInt(drawingCanvas.getAttribute("height"))
);
result.boxes
.filter((box) => box !== result.box)
.forEach((box) => {
Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
color: "green",
lineWidth: 2,
});
});
}
if (result.box) {
Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, {
color: "#00F",
lineWidth: 2,
});
}
if (result.codeResult && result.codeResult.code) {
Quagga.ImageDebug.drawPath(result.line, { x: "x", y: "y" }, drawingCtx, {
color: "red",
lineWidth: 3,
});
}
}
});
let lastDetection = null;
let numDetected = 0;
Quagga.onDetected((result) => {
// Detect the same code 3 times as an extra check to avoid bogus scans.
if (lastDetection === null || lastDetection !== result.codeResult.code) {
numDetected = 1;
lastDetection = result.codeResult.code;
return;
} else if (numDetected++ < 3) {
return;
}
const code = result.codeResult.code;
statusNode.querySelector(".isbn").innerText = code;
toggleStatus("found");
const search = new URL("/search", document.location);
search.searchParams.set("q", code);
cleanup(false);
location.assign(search);
});
event.target.addEventListener("close", cleanup, { once: true });
initBarcodes();
}
})(); })();

View file

@ -0,0 +1,49 @@
(function () {
"use strict";
/**
* Remoev input field
*
* @param {event} the button click event
*/
function removeInput(event) {
const trigger = event.currentTarget;
const input_id = trigger.dataset.remove;
const input = document.getElementById(input_id);
input.remove();
}
/**
* Duplicate an input field
*
* @param {event} the click even on the associated button
*/
function duplicateInput(event) {
const trigger = event.currentTarget;
const input_id = trigger.dataset.duplicate;
const orig = document.getElementById(input_id);
const parent = orig.parentNode;
const new_count = parent.querySelectorAll("input").length + 1;
let input = orig.cloneNode();
input.id += "-" + new_count;
input.value = "";
let label = parent.querySelector("label").cloneNode();
label.setAttribute("for", input.id);
parent.appendChild(label);
parent.appendChild(input);
}
document
.querySelectorAll("[data-duplicate]")
.forEach((node) => node.addEventListener("click", duplicateInput));
document
.querySelectorAll("[data-remove]")
.forEach((node) => node.addEventListener("click", removeInput));
})();

View file

@ -1,11 +1,11 @@
/* exported TabGroup */ /* exported TabGroup */
/* /*
* The content below is licensed according to the W3C Software License at * The content below is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
* Heavily modified to web component by Zach Leatherman * Heavily modified to web component by Zach Leatherman
* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman * Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman
*/ */
class TabGroup { class TabGroup {
constructor(container) { constructor(container) {
this.container = container; this.container = container;
@ -15,7 +15,7 @@ class TabGroup {
this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]'); this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]');
this.delay = this.determineDelay(); this.delay = this.determineDelay();
if(!this.tablist || !this.tabs.length || !this.panels.length) { if (!this.tablist || !this.tabs.length || !this.panels.length) {
return; return;
} }
@ -32,7 +32,7 @@ class TabGroup {
left: 37, left: 37,
up: 38, up: 38,
right: 39, right: 39,
down: 40 down: 40,
}; };
} }
@ -42,23 +42,21 @@ class TabGroup {
37: -1, 37: -1,
38: -1, 38: -1,
39: 1, 39: 1,
40: 1 40: 1,
}; };
} }
initTabs() { initTabs() {
let count = 0; let count = 0;
for(let tab of this.tabs) {
for (let tab of this.tabs) {
let isSelected = tab.getAttribute("aria-selected") === "true"; let isSelected = tab.getAttribute("aria-selected") === "true";
tab.setAttribute("tabindex", isSelected ? "0" : "-1"); tab.setAttribute("tabindex", isSelected ? "0" : "-1");
tab.addEventListener('click', this.clickEventListener.bind(this)); tab.addEventListener("click", this.clickEventListener.bind(this));
tab.addEventListener('keydown', this.keydownEventListener.bind(this)); tab.addEventListener("keydown", this.keydownEventListener.bind(this));
tab.addEventListener('keyup', this.keyupEventListener.bind(this)); tab.addEventListener("keyup", this.keyupEventListener.bind(this));
if (isSelected) {
tab.scrollIntoView();
}
tab.index = count++; tab.index = count++;
} }
@ -68,8 +66,9 @@ class TabGroup {
let selectedPanelId = this.tablist let selectedPanelId = this.tablist
.querySelector('[role="tab"][aria-selected="true"]') .querySelector('[role="tab"][aria-selected="true"]')
.getAttribute("aria-controls"); .getAttribute("aria-controls");
for(let panel of this.panels) {
if(panel.getAttribute("id") !== selectedPanelId) { for (let panel of this.panels) {
if (panel.getAttribute("id") !== selectedPanelId) {
panel.setAttribute("hidden", ""); panel.setAttribute("hidden", "");
} }
panel.setAttribute("tabindex", "0"); panel.setAttribute("tabindex", "0");
@ -86,16 +85,18 @@ class TabGroup {
// Handle keydown on tabs // Handle keydown on tabs
keydownEventListener(event) { keydownEventListener(event) {
var key = event.keyCode; const key = event.keyCode;
switch (key) { switch (key) {
case this.keys.end: case this.keys.end:
event.preventDefault(); event.preventDefault();
// Activate last tab // Activate last tab
this.activateTab(this.tabs[this.tabs.length - 1]); this.activateTab(this.tabs[this.tabs.length - 1]);
break; break;
case this.keys.home: case this.keys.home:
event.preventDefault(); event.preventDefault();
// Activate first tab // Activate first tab
this.activateTab(this.tabs[0]); this.activateTab(this.tabs[0]);
break; break;
@ -111,7 +112,7 @@ class TabGroup {
// Handle keyup on tabs // Handle keyup on tabs
keyupEventListener(event) { keyupEventListener(event) {
var key = event.keyCode; const key = event.keyCode;
switch (key) { switch (key) {
case this.keys.left: case this.keys.left:
@ -125,17 +126,16 @@ class TabGroup {
// only up and down arrow should function. // only up and down arrow should function.
// In all other cases only left and right arrow function. // In all other cases only left and right arrow function.
determineOrientation(event) { determineOrientation(event) {
var key = event.keyCode; const key = event.keyCode;
var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical'; const vertical = this.tablist.getAttribute("aria-orientation") == "vertical";
var proceed = false; let proceed = false;
if (vertical) { if (vertical) {
if (key === this.keys.up || key === this.keys.down) { if (key === this.keys.up || key === this.keys.down) {
event.preventDefault(); event.preventDefault();
proceed = true; proceed = true;
} }
} } else {
else {
if (key === this.keys.left || key === this.keys.right) { if (key === this.keys.left || key === this.keys.right) {
proceed = true; proceed = true;
} }
@ -149,22 +149,21 @@ class TabGroup {
// Either focus the next, previous, first, or last tab // Either focus the next, previous, first, or last tab
// depending on key pressed // depending on key pressed
switchTabOnArrowPress(event) { switchTabOnArrowPress(event) {
var pressed = event.keyCode; const pressed = event.keyCode;
for (let tab of this.tabs) { for (let tab of this.tabs) {
tab.addEventListener('focus', this.focusEventHandler.bind(this)); tab.addEventListener("focus", this.focusEventHandler.bind(this));
} }
if (this.direction[pressed]) { if (this.direction[pressed]) {
var target = event.target; const target = event.target;
if (target.index !== undefined) { if (target.index !== undefined) {
if (this.tabs[target.index + this.direction[pressed]]) { if (this.tabs[target.index + this.direction[pressed]]) {
this.tabs[target.index + this.direction[pressed]].focus(); this.tabs[target.index + this.direction[pressed]].focus();
} } else if (pressed === this.keys.left || pressed === this.keys.up) {
else if (pressed === this.keys.left || pressed === this.keys.up) {
this.focusLastTab(); this.focusLastTab();
} } else if (pressed === this.keys.right || pressed == this.keys.down) {
else if (pressed === this.keys.right || pressed == this.keys.down) {
this.focusFirstTab(); this.focusFirstTab();
} }
} }
@ -172,8 +171,8 @@ class TabGroup {
} }
// Activates any given tab panel // Activates any given tab panel
activateTab (tab, setFocus) { activateTab(tab, setFocus) {
if(tab.getAttribute("role") !== "tab") { if (tab.getAttribute("role") !== "tab") {
tab = tab.closest('[role="tab"]'); tab = tab.closest('[role="tab"]');
} }
@ -183,19 +182,19 @@ class TabGroup {
this.deactivateTabs(); this.deactivateTabs();
// Remove tabindex attribute // Remove tabindex attribute
tab.removeAttribute('tabindex'); tab.removeAttribute("tabindex");
// Set the tab as selected // Set the tab as selected
tab.setAttribute('aria-selected', 'true'); tab.setAttribute("aria-selected", "true");
// Give the tab is-active class // Give the tab is-active class
tab.classList.add('is-active'); tab.classList.add("is-active");
// Get the value of aria-controls (which is an ID) // Get the value of aria-controls (which is an ID)
var controls = tab.getAttribute('aria-controls'); const controls = tab.getAttribute("aria-controls");
// Remove hidden attribute from tab panel to make it visible // Remove hidden attribute from tab panel to make it visible
document.getElementById(controls).removeAttribute('hidden'); document.getElementById(controls).removeAttribute("hidden");
// Set focus when required // Set focus when required
if (setFocus) { if (setFocus) {
@ -206,14 +205,14 @@ class TabGroup {
// Deactivate all tabs and tab panels // Deactivate all tabs and tab panels
deactivateTabs() { deactivateTabs() {
for (let tab of this.tabs) { for (let tab of this.tabs) {
tab.classList.remove('is-active'); tab.classList.remove("is-active");
tab.setAttribute('tabindex', '-1'); tab.setAttribute("tabindex", "-1");
tab.setAttribute('aria-selected', 'false'); tab.setAttribute("aria-selected", "false");
tab.removeEventListener('focus', this.focusEventHandler.bind(this)); tab.removeEventListener("focus", this.focusEventHandler.bind(this));
} }
for (let panel of this.panels) { for (let panel of this.panels) {
panel.setAttribute('hidden', 'hidden'); panel.setAttribute("hidden", "hidden");
} }
} }
@ -228,15 +227,15 @@ class TabGroup {
// Determine whether there should be a delay // Determine whether there should be a delay
// when user navigates with the arrow keys // when user navigates with the arrow keys
determineDelay() { determineDelay() {
var hasDelay = this.tablist.hasAttribute('data-delay'); const hasDelay = this.tablist.hasAttribute("data-delay");
var delay = 0; let delay = 0;
if (hasDelay) { if (hasDelay) {
var delayValue = this.tablist.getAttribute('data-delay'); const delayValue = this.tablist.getAttribute("data-delay");
if (delayValue) { if (delayValue) {
delay = delayValue; delay = delayValue;
} } else {
else {
// If no value is specified, default to 300ms // If no value is specified, default to 300ms
delay = 300; delay = 300;
} }
@ -246,7 +245,7 @@ class TabGroup {
} }
focusEventHandler(event) { focusEventHandler(event) {
var target = event.target; const target = event.target;
setTimeout(this.checkTabFocus.bind(this), this.delay, target); setTimeout(this.checkTabFocus.bind(this), this.delay, target);
} }

File diff suppressed because one or more lines are too long

View file

@ -14,23 +14,25 @@
{% cache 604800 about_page %} {% cache 604800 about_page %}
{% get_book_superlatives as superlatives %} {% get_book_superlatives as superlatives %}
<section class="content pb-4"> <section class=" pb-4">
<h2> <div class="content">
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %} <h2>
</h2> {% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
</h2>
<p class="subtitle notification has-background-primary-highlight"> <p class="subtitle notification has-background-primary-highlight">
{% blocktrans trimmed with site_name=site.name %} {% blocktrans trimmed with site_name=site.name %}
{{ site_name }} is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers. {{ site_name }} is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers.
While you can interact seamlessly with users anywhere in the <a href="https://joinbookwyrm.com/instances/" target="_blank">BookWyrm network</a>, this community is unique. While you can interact seamlessly with users anywhere in the <a href="https://joinbookwyrm.com/instances/" target="_blank">BookWyrm network</a>, this community is unique.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
</div>
<div class="columns"> <div class="columns">
{% if superlatives.top_rated %} {% if superlatives.top_rated %}
{% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %} {% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %}
<div class="column is-one-third is-flex"> <div class="column is-one-third is-flex">
<div class="media notification"> <div class="media notification is-clipped">
<div class="media-left"> <div class="media-left">
<a href="{{ book.local_path }}"> <a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %} {% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
@ -49,7 +51,7 @@
{% if superlatives.wanted %} {% if superlatives.wanted %}
{% with book=superlatives.wanted.default_edition %} {% with book=superlatives.wanted.default_edition %}
<div class="column is-one-third is-flex"> <div class="column is-one-third is-flex">
<div class="media notification"> <div class="media notification is-clipped">
<div class="media-left"> <div class="media-left">
<a href="{{ book.local_path }}"> <a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %} {% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
@ -68,7 +70,7 @@
{% if superlatives.controversial %} {% if superlatives.controversial %}
{% with book=superlatives.controversial.default_edition %} {% with book=superlatives.controversial.default_edition %}
<div class="column is-one-third is-flex"> <div class="column is-one-third is-flex">
<div class="media notification"> <div class="media notification is-clipped">
<div class="media-left"> <div class="media-left">
<a href="{{ book.local_path }}"> <a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %} {% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}

View file

@ -19,8 +19,10 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button> <div class="buttons is-right is-flex-grow-1">
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
</div>
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %} {% block modal-form-close %}</form>{% endblock %}

View file

@ -12,6 +12,15 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if update_error %}
<div class="notification is-danger is-light">
<span class="icon icon-x" aria-hidden="true"></span>
<span>
{% trans "Unable to connect to remote source." %}
</span>
</div>
{% endif %}
{% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %} {% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %}
<div class="block" itemscope itemtype="https://schema.org/Book"> <div class="block" itemscope itemtype="https://schema.org/Book">
<div class="columns is-mobile"> <div class="columns is-mobile">
@ -352,7 +361,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% if request.user.list_set.exists %} {% if list_options.exists %}
<form name="list-add" method="post" action="{% url 'list-add-book' %}"> <form name="list-add" method="post" action="{% url 'list-add-book' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
@ -361,7 +370,7 @@
<div class="field has-addons"> <div class="field has-addons">
<div class="select control is-clipped"> <div class="select control is-clipped">
<select name="book_list" id="id_list"> <select name="book_list" id="id_list">
{% for list in user.list_set.all %} {% for list in list_options %}
<option value="{{ list.id }}">{{ list.name }}</option> <option value="{{ list.id }}">{{ list.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
@ -386,6 +395,6 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script> <script src="{% static "js/tabs.js" %}?v={{ js_cache }}"></script>
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script> <script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
{% endblock %} {% endblock %}

View file

@ -28,8 +28,10 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Add" %}</button> <div class="buttons is-right is-flex-grow-1">
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> <button class="button is-primary" type="submit">{% trans "Add" %}</button>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
</div>
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %} {% block modal-form-close %}</form>{% endblock %}

View file

@ -1,4 +1,5 @@
{% load i18n %} {% load i18n %}
{% load static %}
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="block"> <div class="block">
@ -23,7 +24,7 @@
{% trans "Title:" %} {% trans "Title:" %}
</label> </label>
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title" aria-describedby="desc_title"> <input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title" aria-describedby="desc_title">
{% include 'snippets/form_errors.html' with errors_list=form.title.errors id="desc_title" %} {% include 'snippets/form_errors.html' with errors_list=form.title.errors id="desc_title" %}
</div> </div>
@ -32,7 +33,7 @@
{% trans "Subtitle:" %} {% trans "Subtitle:" %}
</label> </label>
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle" aria-describedby="desc_subtitle"> <input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle" aria-describedby="desc_subtitle">
{% include 'snippets/form_errors.html' with errors_list=form.subtitle.errors id="desc_subtitle" %} {% include 'snippets/form_errors.html' with errors_list=form.subtitle.errors id="desc_subtitle" %}
</div> </div>
@ -41,7 +42,7 @@
{% trans "Description:" %} {% trans "Description:" %}
</label> </label>
{{ form.description }} {{ form.description }}
{% include 'snippets/form_errors.html' with errors_list=form.description.errors id="desc_description" %} {% include 'snippets/form_errors.html' with errors_list=form.description.errors id="desc_description" %}
</div> </div>
@ -52,7 +53,7 @@
{% trans "Series:" %} {% trans "Series:" %}
</label> </label>
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}" aria-describedby="desc_series"> <input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}" aria-describedby="desc_series">
{% include 'snippets/form_errors.html' with errors_list=form.series.errors id="desc_series" %} {% include 'snippets/form_errors.html' with errors_list=form.series.errors id="desc_series" %}
</div> </div>
</div> </div>
@ -62,7 +63,7 @@
{% trans "Series number:" %} {% trans "Series number:" %}
</label> </label>
{{ form.series_number }} {{ form.series_number }}
{% include 'snippets/form_errors.html' with errors_list=form.series_number.errors id="desc_series_number" %} {% include 'snippets/form_errors.html' with errors_list=form.series_number.errors id="desc_series_number" %}
</div> </div>
</div> </div>
@ -76,9 +77,60 @@
<span class="help" id="desc_languages_help"> <span class="help" id="desc_languages_help">
{% trans "Separate multiple values with commas." %} {% trans "Separate multiple values with commas." %}
</span> </span>
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %} {% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
</div> </div>
<div>
<label class="label" for="id_add_subjects">
{% trans "Subjects:" %}
</label>
{% for subject in book.subjects %}
<label class="label is-sr-only" for="id_add_subject={% if not forloop.first %}-{{forloop.counter}}{% endif %}">
{% trans "Add subject" %}
</label>
<div class="field has-addons" id="subject_field_wrapper_{{ forloop.counter }}">
<div class="control is-expanded">
<input
id="id_add_subject-{{ forloop.counter }}"
type="text"
name="subjects"
value="{{ subject }}"
class="input"
>
</div>
<div class="control">
<button
class="button is-danger is-light"
type="button"
data-remove="subject_field_wrapper_{{ forloop.counter }}"
>
{% trans "Remove subject" as text %}
<span class="icon icon-x" title="{{ text }}">
<span class="is-sr-only">{{ text }}</span>
</span>
</button>
</div>
</div>
{% endfor %}
<input
class="input"
type="text"
name="subjects"
id="id_add_subject"
value="{{ subject }}"
{% if confirm_mode %}readonly{% endif %}
>
{% include 'snippets/form_errors.html' with errors_list=form.subjects.errors id="desc_subjects" %}
</div>
<span class="help">
<button class="button is-small" type="button" data-duplicate="id_add_subject" id="another_subject_field">
<span class="icon icon-plus" aria-hidden="true"></span>
<span>{% trans "Add Another Subject" %}</span>
</button>
</span>
</div> </div>
</section> </section>
@ -95,7 +147,7 @@
<span class="help" id="desc_publishers_help"> <span class="help" id="desc_publishers_help">
{% trans "Separate multiple values with commas." %} {% trans "Separate multiple values with commas." %}
</span> </span>
{% include 'snippets/form_errors.html' with errors_list=form.publishers.errors id="desc_publishers" %} {% include 'snippets/form_errors.html' with errors_list=form.publishers.errors id="desc_publishers" %}
</div> </div>
@ -104,7 +156,7 @@
{% trans "First published date:" %} {% trans "First published date:" %}
</label> </label>
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %} aria-describedby="desc_first_published_date"> <input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %} aria-describedby="desc_first_published_date">
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %} {% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
</div> </div>
@ -113,7 +165,7 @@
{% trans "Published date:" %} {% trans "Published date:" %}
</label> </label>
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %} aria-describedby="desc_published_date"> <input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %} aria-describedby="desc_published_date">
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %} {% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
</div> </div>
</div> </div>
@ -153,7 +205,12 @@
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}> <input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}>
{% endfor %} {% endfor %}
</div> </div>
<span class="help"><button class="button is-small" type="button" data-duplicate="id_add_author" id="another_author_field">{% trans "Add Another Author" %}</button></span> <span class="help">
<button class="button is-small" type="button" data-duplicate="id_add_author" id="another_author_field">
<span class="icon icon-plus" aria-hidden="true"></span>
<span>{% trans "Add Another Author" %}</span>
</button>
</span>
</div> </div>
</section> </section>
</div> </div>
@ -184,7 +241,7 @@
</label> </label>
<input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}" aria-describedby="desc_cover"> <input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}" aria-describedby="desc_cover">
</div> </div>
{% include 'snippets/form_errors.html' with errors_list=form.cover.errors id="desc_cover" %} {% include 'snippets/form_errors.html' with errors_list=form.cover.errors id="desc_cover" %}
</div> </div>
</div> </div>
@ -205,7 +262,7 @@
<div class="select"> <div class="select">
{{ form.physical_format }} {{ form.physical_format }}
</div> </div>
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %} {% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
</div> </div>
</div> </div>
@ -215,7 +272,7 @@
{% trans "Format details:" %} {% trans "Format details:" %}
</label> </label>
{{ form.physical_format_detail }} {{ form.physical_format_detail }}
{% include 'snippets/form_errors.html' with errors_list=form.physical_format_detail.errors id="desc_physical_format_detail" %} {% include 'snippets/form_errors.html' with errors_list=form.physical_format_detail.errors id="desc_physical_format_detail" %}
</div> </div>
</div> </div>
@ -226,7 +283,7 @@
{% trans "Pages:" %} {% trans "Pages:" %}
</label> </label>
{{ form.pages }} {{ form.pages }}
{% include 'snippets/form_errors.html' with errors_list=form.pages.errors id="desc_pages" %} {% include 'snippets/form_errors.html' with errors_list=form.pages.errors id="desc_pages" %}
</div> </div>
</div> </div>
@ -242,7 +299,7 @@
{% trans "ISBN 13:" %} {% trans "ISBN 13:" %}
</label> </label>
{{ form.isbn_13 }} {{ form.isbn_13 }}
{% include 'snippets/form_errors.html' with errors_list=form.isbn_13.errors id="desc_isbn_13" %} {% include 'snippets/form_errors.html' with errors_list=form.isbn_13.errors id="desc_isbn_13" %}
</div> </div>
@ -251,7 +308,7 @@
{% trans "ISBN 10:" %} {% trans "ISBN 10:" %}
</label> </label>
{{ form.isbn_10 }} {{ form.isbn_10 }}
{% include 'snippets/form_errors.html' with errors_list=form.isbn_10.errors id="desc_isbn_10" %} {% include 'snippets/form_errors.html' with errors_list=form.isbn_10.errors id="desc_isbn_10" %}
</div> </div>
@ -260,7 +317,7 @@
{% trans "Openlibrary ID:" %} {% trans "Openlibrary ID:" %}
</label> </label>
{{ form.openlibrary_key }} {{ form.openlibrary_key }}
{% include 'snippets/form_errors.html' with errors_list=form.openlibrary_key.errors id="desc_openlibrary_key" %} {% include 'snippets/form_errors.html' with errors_list=form.openlibrary_key.errors id="desc_openlibrary_key" %}
</div> </div>
@ -269,7 +326,7 @@
{% trans "Inventaire ID:" %} {% trans "Inventaire ID:" %}
</label> </label>
{{ form.inventaire_id }} {{ form.inventaire_id }}
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %} {% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
</div> </div>
@ -278,7 +335,7 @@
{% trans "OCLC Number:" %} {% trans "OCLC Number:" %}
</label> </label>
{{ form.oclc_number }} {{ form.oclc_number }}
{% include 'snippets/form_errors.html' with errors_list=form.oclc_number.errors id="desc_oclc_number" %} {% include 'snippets/form_errors.html' with errors_list=form.oclc_number.errors id="desc_oclc_number" %}
</div> </div>
@ -287,10 +344,14 @@
{% trans "ASIN:" %} {% trans "ASIN:" %}
</label> </label>
{{ form.asin }} {{ form.asin }}
{% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %} {% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %}
</div> </div>
</div> </div>
</section> </section>
</div> </div>
</div> </div>
{% block scripts %}
<script src="{% static "js/forms.js" %}"></script>
{% endblock %}

View file

@ -55,8 +55,10 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Save" %}</button> <div class="buttons is-right is-flex-grow-1">
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</div>
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %} {% block modal-form-close %}</form>{% endblock %}

View file

@ -17,13 +17,13 @@ Is that where you'd like to go?
{% block modal-footer %} {% block modal-footer %}
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="button is-primary">{% trans "Continue" %}</a>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="has-text-right is-flex-grow-1"> <div class="is-flex-grow-1">
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a> <a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
</div> </div>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="button is-primary">{% trans "Continue" %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -19,8 +19,10 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button> <div class="buttons is-right is-flex-grow-1">
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
</div>
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %} {% block modal-form-close %}</form>{% endblock %}

View file

@ -33,7 +33,7 @@
</div> </div>
<footer class="card-footer"> <footer class="card-footer">
{% if user != request.user %} {% if user != request.user %}
{% if user.mutuals %} {% if user.mutuals and not user.hide_follows %}
<div class="card-footer-item"> <div class="card-footer-item">
<div class="has-text-centered"> <div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.mutuals }}</p> <p class="title is-6 mb-0">{{ user.mutuals }}</p>

View file

@ -31,5 +31,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script> <script src="{% static "js/tabs.js" %}?v={{ js_cache }}"></script>
{% endblock %} {% endblock %}

View file

@ -2,7 +2,7 @@
{% load i18n %} {% load i18n %}
{% block header %} {% block header %}
{% trans "Create Group" %} {% trans "Create group" %}
{% endblock %} {% endblock %}
{% block form %} {% block form %}

View file

@ -8,13 +8,15 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<form name="delete-group-{{ group.id }}" action="{% url 'delete-group' group.id %}" method="POST"> <form name="delete-group-{{ group.id }}" action="{% url 'delete-group' group.id %}" method="POST" class="is-flex-grow-1">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ group.id }}"> <input type="hidden" name="id" value="{{ group.id }}">
<button class="button is-danger" type="submit"> <div class="buttons is-right is-flex-grow-1">
{% trans "Delete" %} <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
</button> <button class="button is-danger" type="submit">
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> {% trans "Delete" %}
</button>
</div>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -16,18 +16,21 @@
</div> </div>
<div class="is-flex"> <div class="is-flex">
{% if group.id %} {% if group.id %}
<div class="is-flex-grow-1"> <div>
<button type="button" data-modal-open="delete_group" class="button is-danger"> <button type="button" data-modal-open="delete_group" class="button is-danger">
{% trans "Delete group" %} {% trans "Delete group" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
<div class="field has-addons">
<div class="control"> <div class="is-flex is-flex-grow-1 is-justify-content-flex-end">
{% include 'snippets/privacy_select_no_followers.html' with current=group.privacy %} <div class="field has-addons">
</div> <div class="control">
<div class="control"> {% include 'snippets/privacy_select_no_followers.html' with current=group.privacy %}
<button type="submit" class="button is-primary">{% trans "Save" %}</button> </div>
<div class="control">
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -15,7 +15,7 @@
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span> <span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
</a> </a>
{% include 'snippets/add_to_group_button.html' with user=user group=group %} {% include 'snippets/add_to_group_button.html' with user=user group=group %}
{% if user.mutuals %} {% if user.mutuals and not user.hide_follows %}
<p class="help"> <p class="help">
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %} {% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
{{ mutuals }} follower you follow {{ mutuals }} follower you follow

View file

@ -1,14 +1,14 @@
{% load layout %} {% load layout %}
{% load sass_tags %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load sass_tags %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{% get_lang %}"> <html lang="{% get_lang %}">
<head> <head>
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title> <title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="{% sass_src 'css/bookwyrm.scss' %}" rel="stylesheet" type="text/css" /> <link href="{% sass_src site_theme %}" rel="stylesheet" type="text/css" />
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" /> <link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" />
@ -56,8 +56,16 @@
</span> </span>
</button> </button>
</div> </div>
<div class="control">
<button class="button" type="button" data-modal-open="barcode-scanner-modal">
<span class="icon icon-barcode" title="{% trans 'Scan Barcode' %}">
<span class="is-sr-only">{% trans "Scan Barcode" %}</span>
</span>
</button>
</div>
</div> </div>
</form> </form>
{% include "search/barcode_modal.html" with id="barcode-scanner-modal" %}
<button type="button" tabindex="0" class="navbar-burger pulldown-menu my-4" data-controls="main_nav" aria-expanded="false"> <button type="button" tabindex="0" class="navbar-burger pulldown-menu my-4" data-controls="main_nav" aria-expanded="false">
<i class="icon icon-dots-three-vertical" aria-hidden="true"></i> <i class="icon icon-dots-three-vertical" aria-hidden="true"></i>
@ -266,6 +274,7 @@
<script src="{% static "js/bookwyrm.js" %}?v={{ js_cache }}"></script> <script src="{% static "js/bookwyrm.js" %}?v={{ js_cache }}"></script>
<script src="{% static "js/localstorage.js" %}?v={{ js_cache }}"></script> <script src="{% static "js/localstorage.js" %}?v={{ js_cache }}"></script>
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script> <script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
<script src="{% static "js/vendor/quagga.min.js" %}?v={{ js_cache }}"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}

View file

@ -32,14 +32,16 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<button type="submit" class="button is-link"> <div class="buttons is-right is-flex-grow-1">
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %} <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% trans "Add" %} <button type="submit" class="button is-link">
{% else %} {% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
{% trans "Suggest" %} {% trans "Add" %}
{% endif %} {% else %}
</button> {% trans "Suggest" %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> {% endif %}
</button>
</div>
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %} {% block modal-form-close %}</form>{% endblock %}

View file

@ -8,15 +8,17 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<form name="delete-list-{{ list.id }}" action="{% url 'delete-list' list.id %}" method="POST"> <form name="delete-list-{{ list.id }}" action="{% url 'delete-list' list.id %}" method="POST" class="is-flex-grow-1">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ list.id }}"> <input type="hidden" name="id" value="{{ list.id }}">
<button class="button is-danger" type="submit"> <div class="buttons is-right is-flex-grow-1">
{% trans "Delete" %} <button type="button" class="button" data-modal-close>
</button> {% trans "Cancel" %}
<button type="button" class="button" data-modal-close> </button>
{% trans "Cancel" %} <button class="button is-danger" type="submit">
</button> {% trans "Delete" %}
</button>
</div>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -114,7 +114,7 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div class="is-flex"> <div class="is-flex is-justify-content-end">
{% if list.id %} {% if list.id %}
<div class="is-flex-grow-1"> <div class="is-flex-grow-1">
<button type="button" data-modal-open="delete_list" class="button is-danger"> <button type="button" data-modal-open="delete_list" class="button is-danger">

View file

@ -30,13 +30,23 @@
<div class="columns mt-3"> <div class="columns mt-3">
<section class="column is-three-quarters"> <section class="column is-three-quarters">
{% if request.GET.updated %} {% if add_failed %}
<div class="notification is-primary"> <div class="notification is-danger is-light">
{% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %} <span class="icon icon-x" aria-hidden="true"></span>
{% trans "You successfully suggested a book for this list!" %} <span>
{% else %} {% trans "That book is already on this list." %}
{% trans "You successfully added a book to this list!" %} </span>
{% endif %} </div>
{% elif add_succeeded %}
<div class="notification is-success is-light">
<span class="icon icon-check" aria-hidden="true"></span>
<span>
{% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %}
{% trans "You successfully suggested a book for this list!" %}
{% else %}
{% trans "You successfully added a book to this list!" %}
{% endif %}
</span>
</div> </div>
{% endif %} {% endif %}
@ -107,7 +117,7 @@
<summary> <summary>
<span role="heading" aria-level="3"> <span role="heading" aria-level="3">
{% trans "Add notes" %} {% trans "Add notes" %}
<span class="details-close icon icon-plus" aria-hidden="true"></span> <span class="details-close icon icon-x" aria-hidden="true"></span>
</span> </span>
</summary> </summary>
{% include "lists/edit_item_form.html" with book=item.book %} {% include "lists/edit_item_form.html" with book=item.book %}

View file

@ -10,7 +10,7 @@
{% block profile-tabs %} {% block profile-tabs %}
<ul class="menu-list"> <ul class="menu-list">
<li><a href="#profile">{% trans "Profile" %}</a></li> <li><a href="#profile">{% trans "Profile" %}</a></li>
<li><a href="#display-preferences">{% trans "Display preferences" %}</a></li> <li><a href="#display-preferences">{% trans "Display" %}</a></li>
<li><a href="#privacy">{% trans "Privacy" %}</a></li> <li><a href="#privacy">{% trans "Privacy" %}</a></li>
</ul> </ul>
{% endblock %} {% endblock %}
@ -61,7 +61,7 @@
<hr aria-hidden="true"> <hr aria-hidden="true">
<section class="block" id="display-preferences"> <section class="block" id="display-preferences">
<h2 class="title is-4">{% trans "Display preferences" %}</h2> <h2 class="title is-4">{% trans "Display" %}</h2>
<div class="box"> <div class="box">
<div class="field"> <div class="field">
<label class="checkbox label" for="id_show_goal"> <label class="checkbox label" for="id_show_goal">
@ -97,6 +97,12 @@
{{ form.preferred_language }} {{ form.preferred_language }}
</div> </div>
</div> </div>
<div class="field">
<label class="label" for="id_them">{% trans "Theme:" %}</label>
<div class="select">
{{ form.theme }}
</div>
</div>
</div> </div>
</section> </section>
@ -111,6 +117,12 @@
{% trans "Manually approve followers" %} {% trans "Manually approve followers" %}
</label> </label>
</div> </div>
<div class="field">
<label class="checkbox label" for="id_hide_follows">
{{ form.hide_follows }}
{% trans "Hide followers and following on profile" %}
</label>
</div>
<div class="field"> <div class="field">
<label class="label" for="id_default_post_privacy"> <label class="label" for="id_default_post_privacy">
{% trans "Default post privacy:" %} {% trans "Default post privacy:" %}

View file

@ -14,12 +14,20 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<form name="delete-readthrough-{{ readthrough.id }}" action="/delete-readthrough" method="POST"> <form
name="delete-readthrough-{{ readthrough.id }}"
action="/delete-readthrough"
method="POST"
class="is-flex-grow-1"
>
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}"> <input type="hidden" name="id" value="{{ readthrough.id }}">
<button class="button is-danger" type="submit">
{% trans "Delete" %} <div class="buttons is-right">
</button> <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> <button class="button is-danger" type="submit">
{% trans "Delete" %}
</button>
</div>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -69,8 +69,10 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Save" %}</button> <div class="buttons is-right is-flex-grow-1">
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</div>
{% endblock %} {% endblock %}
{% block modal-form-close %} {% block modal-form-close %}

View file

@ -0,0 +1,48 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
{% blocktrans %}
Scan Barcode
{% endblocktrans %}
{% endblock %}
{% block modal-body %}
<div class="block">
<div id="barcode-scanner"></div>
</div>
<div id="barcode-camera-list" class="select is-small">
<select>
</select>
</div>
<div id="barcode-status" class="block">
<div class="grant-access is-hidden">
<span class="icon icon-lock"></span>
<span class="is-size-5">{% trans "Requesting camera..." %}</span></br>
<span>{% trans "Grant access to the camera to scan a book's barcode." %}</span>
</div>
<div class="access-denied is-hidden">
<span class="icon icon-warning"></span>
<span class="is-size-5">Access denied</span><br/>
<span>{% trans "Could not access camera" %}</span>
</div>
<div class="scanning is-hidden">
<span class="icon icon-barcode"></span>
<span class="is-size-5">{% trans "Scanning..." context "barcode scanner" %}</span><br/>
<span>{% trans "Align your book's barcode with the camera." %}</span>
</div>
<div class="found is-hidden">
<span class="icon icon-check"></span>
<span class="is-size-5">{% trans "ISBN scanned" context "barcode scanner" %}</span><br/>
{% trans "Searching for book:" context "followed by ISBN" %} <span class="isbn"></span>...
</div>
</div>
{% endblock %}
{% block modal-footer %}
<button class="button" type="button" data-modal-close>{% trans "Cancel" %}</button>
{% endblock %}

View file

@ -1,5 +1,6 @@
{% extends 'settings/layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %}
{% load utilities %} {% load utilities %}
{% block title %} {% block title %}
@ -16,12 +17,81 @@
<p> <p>
{% trans "Auto-moderation rules will create reports for any local user or status with fields matching the provided string." %} {% trans "Auto-moderation rules will create reports for any local user or status with fields matching the provided string." %}
{% trans "Users or statuses that have already been reported (regardless of whether the report was resolved) will not be flagged." %} {% trans "Users or statuses that have already been reported (regardless of whether the report was resolved) will not be flagged." %}
{% trans "At this time, reports are <em>not</em> being generated automatically, and you must manually trigger a scan." %}
</p> </p>
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}"> </div>
<div class="box block">
{% if task %}
<dl class="block">
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Schedule:" %}
</dt>
<dd>
{{ task.schedule }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Last run:" %}
</dt>
<dd>
{{ task.last_run_at|naturaltime }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Total run count:" %}
</dt>
<dd>
{{ task.total_run_count }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Enabled:" %}
</dt>
<dd>
<span class="tag {% if task.enabled %}is-success{% else %}is-danger{% endif %}">
{{ task.enabled|yesno }}
</span>
</dd>
</dl>
<div class="is-flex is-justify-content-space-between block">
<form name="unschedule-scan" method="POST" action="{% url 'settings-automod-unschedule' task.id %}">
{% csrf_token %}
<button class="button is-danger">{% trans "Delete schedule" %}</button>
</form>
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}">
{% csrf_token %}
<button class="button">{% trans "Run now" %}</button>
<p class="help">{% trans "Last run date will not be updated" %}</p>
</form>
</div>
{% else %}
<h2 class="title is-4">{% trans "Schedule scan" %}</h2>
<form name="schedule-scan" method="POST" action="{% url 'settings-automod-schedule' %}">
{% csrf_token %} {% csrf_token %}
<button class="button is-warning">{% trans "Run scan" %}</button> <div class="field">
<label class="label" for="id_every">
{{ task_form.every.label }}
</label>
{{ task_form.every }}
<p class="help" id="desc_every">
{{ task_form.every.help_text }}
</p>
</div>
<div class="field">
<label class="label" for="id_period">
{{ task_form.period.label }}
</label>
<div class="select">
{{ task_form.period }}
</div>
<p class="help" id="desc_period">
{{ task_form.period.help_text }}
</p>
</div>
<button class="button is-warning">{% trans "Schedule scan" %}</button>
</form> </form>
{% endif %}
</div> </div>
{% if success %} {% if success %}

View file

@ -4,7 +4,19 @@
{% block header %} {% block header %}
{% trans "Add instance" %} {% trans "Add instance" %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a> {% endblock %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% trans "Add instance" %}
</a>
</li>
</ul>
</nav>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}
@ -73,9 +85,13 @@
<label class="label" for="id_notes"> <label class="label" for="id_notes">
{% trans "Notes:" %} {% trans "Notes:" %}
</label> </label>
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes"> <textarea
{{ form.notes.value|default:'' }} name="notes"
</textarea> cols="40"
rows="5"
class="textarea"
id="id_notes"
>{{ form.notes.value|default:'' }}</textarea>
</div> </div>
<button type="submit" class="button is-primary"> <button type="submit" class="button is-primary">

View file

@ -9,8 +9,26 @@
{% if server.status == "blocked" %}<span class="icon icon-x has-text-danger is-size-5" title="{% trans 'Blocked' %}"><span class="is-sr-only">{% trans "Blocked" %}</span></span> {% if server.status == "blocked" %}<span class="icon icon-x has-text-danger is-size-5" title="{% trans 'Blocked' %}"><span class="is-sr-only">{% trans "Blocked" %}</span></span>
{% endif %} {% endif %}
{% endblock %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to list" %}</a> {% block edit-button %}
<form name="reload" method="POST" action="{% url 'settings-federated-server-refresh' server.id %}">
{% csrf_token %}
<button class="button" type="submit">{% trans "Refresh data" %}</button>
</form>
{% endblock %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{{ server.server_name }}
</a>
</li>
</ul>
</nav>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}

View file

@ -4,7 +4,19 @@
{% block header %} {% block header %}
{% trans "Import Blocklist" %} {% trans "Import Blocklist" %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a> {% endblock %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% trans "Import Blocklist" %}
</a>
</li>
</ul>
</nav>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}

View file

@ -16,11 +16,11 @@
<ul> <ul>
{% url 'settings-federation' status='federated' as url %} {% url 'settings-federation' status='federated' as url %}
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}> <li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Federated" %}</a> <a href="{{ url }}">{% trans "Federated" %} ({{ federated_count }})</a>
</li> </li>
{% url 'settings-federation' status='blocked' as url %} {% url 'settings-federation' status='blocked' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}> <li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Blocked" %}</a> <a href="{{ url }}">{% trans "Blocked" %} ({{ blocked_count }})</a>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -86,6 +86,10 @@
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
{% block site-subtabs %}{% endblock %} {% block site-subtabs %}{% endblock %}
</li> </li>
<li>
{% url 'settings-themes' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Themes" %}</a>
</li>
</ul> </ul>
{% endif %} {% endif %}
</nav> </nav>

View file

@ -18,8 +18,10 @@
{% endblock %} {% endblock %}
{% block modal-footer %} {% block modal-footer %}
<button type="submit" class="button is-primary">{% trans "Set" %}</button> <div class="buttons is-right is-flex-grow-1">
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
<button type="submit" class="button is-primary">{% trans "Set" %}</button>
</div>
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %} {% block modal-form-close %}</form>{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'settings/users/username_filter.html' %}
{% include 'directory/community_filter.html' %}
{% include 'settings/users/server_filter.html' %}
{% endblock %}

View file

@ -30,7 +30,7 @@
</ul> </ul>
</div> </div>
{% include 'settings/users/user_admin_filters.html' %} {% include 'settings/reports/report_filters.html' %}
<div class="block"> <div class="block">
{% if not reports %} {% if not reports %}

View file

@ -8,7 +8,7 @@
{% block site-subtabs %} {% block site-subtabs %}
<ul class="menu-list"> <ul class="menu-list">
<li><a href="#instance-info">{% trans "Instance Info" %}</a></li> <li><a href="#instance-info">{% trans "Instance Info" %}</a></li>
<li><a href="#images">{% trans "Images" %}</a></li> <li><a href="#display">{% trans "Display" %}</a></li>
<li><a href="#footer">{% trans "Footer Content" %}</a></li> <li><a href="#footer">{% trans "Footer Content" %}</a></li>
<li><a href="#registration">{% trans "Registration" %}</a></li> <li><a href="#registration">{% trans "Registration" %}</a></li>
</ul> </ul>
@ -33,7 +33,12 @@
</div> </div>
{% endif %} {% endif %}
<form action="{% url 'settings-site' %}" method="POST" class="content" enctype="multipart/form-data"> <form
action="{% url 'settings-site' %}"
method="POST"
class="content"
enctype="multipart/form-data"
>
{% csrf_token %} {% csrf_token %}
<section class="block" id="instance_info"> <section class="block" id="instance_info">
<h2 class="title is-4">{% trans "Instance Info" %}</h2> <h2 class="title is-4">{% trans "Instance Info" %}</h2>
@ -68,20 +73,33 @@
<hr aria-hidden="true"> <hr aria-hidden="true">
<section class="block" id="images"> <section class="block" id="display">
<h2 class="title is-4">{% trans "Images" %}</h2> <h2 class="title is-4">{% trans "Display" %}</h2>
<div class="box is-flex"> <div class="box">
<div> <h3 class="title is-5">{% trans "Images" %}</h3>
<label class="label" for="id_logo">{% trans "Logo:" %}</label> <div class="block is-flex">
{{ site_form.logo }} <div>
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }}
</div>
<div>
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
{{ site_form.logo_small }}
</div>
<div>
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
{{ site_form.favicon }}
</div>
</div> </div>
<div>
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label> <h3 class="title is-5">{% trans "Themes" %}</h3>
{{ site_form.logo_small }} <div class="block">
</div> <label class="label" for="id_default_theme">
<div> {% trans "Default theme:" %}
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label> </label>
{{ site_form.favicon }} <div class="select">
{{ site_form.default_theme }}
</div>
</div> </div>
</div> </div>
</section> </section>

View file

@ -0,0 +1,122 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% block title %}{% trans "Themes" %}{% endblock %}
{% block header %}{% trans "Themes" %}{% endblock %}
{% block breadcrumbs %}
<a class="subtitle help is-link" href="{% url 'settings-site' %}/#display">
{% trans "Set instance default theme" %}
</a>
{% endblock %}
{% block panel %}
{% if success %}
<div class="notification is-success is-light">
<span class="icon icon-check" aria-hidden="true"></span>
<span>
{% trans "Successfully added theme" %}
</span>
</div>
{% endif %}
<section class="block">
<div class="notification content">
<h2 class="title is-5">{% trans "How to add a theme" %}</h2>
<ol>
<li>
{% trans "Copy the theme file into the <code>bookwyrm/static/css/themes</code> directory on your server from the command line." %}
</li>
<li>
{% trans "Run <code>./bw-dev collectstatic</code>." %}
</li>
<li>
{% trans "Add the file name using the form below to make it available in the application interface." %}
</li>
</ol>
</div>
</section>
<section class="block content">
<h2 class="title is-4">{% trans "Add theme" %}</h2>
{% if theme_form.errors %}
<div class="notification is-danger is-light">
<span class="icon icon-x" aria-hidden="true"></span>
<span>
{% trans "Unable to save theme" %}
</span>
</div>
{% endif %}
<form
method="POST"
action="{% url 'settings-themes' %}"
class="box"
enctype="multipart/form-data"
>
<fieldset>
{% csrf_token %}
<div class="columns">
<div class="column is-half">
<label class="label" for="id_name">
{% trans "Theme name" %}
</label>
<div class="control">
{{ theme_form.name }}
{% include 'snippets/form_errors.html' with errors_list=theme_form.name.errors id="desc_name" %}
</div>
</div>
<div class="column">
<label class="label" for="id_path">
{% trans "Theme filename" %}
</label>
<div class="control">
{{ theme_form.path }}
{% include 'snippets/form_errors.html' with errors_list=theme_form.path.errors id="desc_path" %}
</div>
</div>
</div>
<button type="submit" class="button">{% trans "Add theme" %}</button>
</fieldset>
</form>
</section>
<section class="block content">
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
<div class="table-container">
<table class="table is-striped">
<tr>
<th>
{% trans "Theme name" %}
</th>
<th>
{% trans "File" %}
</th>
<th>
{% trans "Actions" %}
</th>
</tr>
{% for theme in themes %}
<tr>
<td>{{ theme.name }}</td>
<td><code>{{ theme.path }}</code></td>
<td>
<form method="POST" action="{% url 'settings-themes-delete' theme.id %}">
{% csrf_token %}
<button type="submit" class="button is-danger is-light is-small">
<span class="icon icon-x" aria-hideen="true"></span>
<span>{% trans "Remove theme" %}</span>
</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
</section>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends 'snippets/filters_panel/filter_field.html' %}
{% load i18n %}
{% block filter %}
<label class="label" for="id_email">{% trans "Email" %}</label>
<div class="control">
<input
type="text"
class="input"
name="email"
value="{{ request.GET.email|default:'' }}"
id="id_email" placeholder="user@email.com"
>
</div>
{% endblock %}

View file

@ -1,10 +1,23 @@
{% extends 'settings/layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %}
{% block title %}{{ user.username }}{% endblock %} {% block title %}{{ user.username }}{% endblock %}
{% block header %} {% block header %}
{{ user.username }} {{ user.username }}
<a class="help has-text-weight-normal" href="{% url 'settings-users' %}">{% trans "Back to users" %}</a> {% endblock %}
{% block breadcrumbs %}
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'settings-users' %}">{% trans "Users" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{{ user|username }}
</a>
</li>
</ul>
</nav>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}

View file

@ -1,5 +1,7 @@
{% extends 'settings/layout.html' %} {% extends 'settings/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %}
{% block title %}{% trans "Users" %}{% endblock %} {% block title %}{% trans "Users" %}{% endblock %}
{% block header %} {% block header %}
@ -15,46 +17,67 @@
{% include 'settings/users/user_admin_filters.html' %} {% include 'settings/users/user_admin_filters.html' %}
<table class="table is-striped"> <div class="block">
<tr> <div class="tabs">
{% url 'settings-users' as url %} <ul>
<th> {% url 'settings-users' as url %}
{% trans "Username" as text %} <li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
{% include 'snippets/table-sort-header.html' with field="username" sort=sort text=text %} <a href="{{ url }}">{% trans "Local users" %}</a>
</th> </li>
<th> {% url 'settings-users' status="federated" as url %}
{% trans "Date Added" as text %} <li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %} <a href="{{ url }}">{% trans "Federated community" %}</a>
</th> </li>
<th> </ul>
{% trans "Last Active" as text %} </div>
{% include 'snippets/table-sort-header.html' with field="last_active_date" sort=sort text=text %} </div>
</th>
<th> <div class="table-container block">
{% trans "Status" as text %} <table class="table is-striped">
{% include 'snippets/table-sort-header.html' with field="is_active" sort=sort text=text %} <tr>
</th> {% url 'settings-users' as url %}
<th> <th>
{% trans "Remote instance" as text %} {% trans "Username" as text %}
{% include 'snippets/table-sort-header.html' with field="federated_server__server_name" sort=sort text=text %} {% include 'snippets/table-sort-header.html' with field="username" sort=sort text=text %}
</th> </th>
</tr> <th>
{% for user in users %} {% trans "Date Added" as text %}
<tr> {% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
<td><a href="{% url 'settings-user' user.id %}">{{ user.username }}</a></td> </th>
<td>{{ user.created_date }}</td> <th>
<td>{{ user.last_active_date }}</td> {% trans "Last Active" as text %}
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td> {% include 'snippets/table-sort-header.html' with field="last_active_date" sort=sort text=text %}
<td> </th>
{% if user.federated_server %} <th>
<a href="{% url 'settings-federated-server' user.federated_server.id %}">{{ user.federated_server.server_name }}</a> {% trans "Status" as text %}
{% elif not user.local %} {% include 'snippets/table-sort-header.html' with field="is_active" sort=sort text=text %}
<em>{% trans "Not set" %}</em> </th>
{% if status != "local" %}
<th>
{% trans "Remote instance" as text %}
{% include 'snippets/table-sort-header.html' with field="federated_server__server_name" sort=sort text=text %}
</th>
{% endif %} {% endif %}
</td> </tr>
</tr> {% for user in users %}
{% endfor %} <tr>
</table> <td><a href="{% url 'settings-user' user.id %}">{{ user|username }}</a></td>
<td>{{ user.created_date }}</td>
<td>{{ user.last_active_date }}</td>
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>
{% if status != "local" %}
<td>
{% if user.federated_server %}
<a href="{% url 'settings-federated-server' user.federated_server.id %}">{{ user.federated_server.server_name }}</a>
{% else %}
<em>{% trans "Not set" %}</em>
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% include 'snippets/pagination.html' with page=users path=request.path %} {% include 'snippets/pagination.html' with page=users path=request.path %}
{% endblock %} {% endblock %}

View file

@ -2,6 +2,11 @@
{% block filter_fields %} {% block filter_fields %}
{% include 'settings/users/username_filter.html' %} {% include 'settings/users/username_filter.html' %}
{% include 'directory/community_filter.html' %}
{% if status != "local" %}
{% include 'settings/users/server_filter.html' %} {% include 'settings/users/server_filter.html' %}
{% else %}
{% include 'settings/users/email_filter.html' %}
{% endif %}
{% endblock %} {% endblock %}

View file

@ -71,14 +71,14 @@
<dd>{{ user.last_active_date }}</dd> <dd>{{ user.last_active_date }}</dd>
<dt class="is-pulled-left mr-5">{% trans "Manually approved followers:" %}</dt> <dt class="is-pulled-left mr-5">{% trans "Manually approved followers:" %}</dt>
<dd>{{ user.manually_approves_followers }}</dd> <dd>{{ user.manually_approves_followers|yesno }}</dd>
<dt class="is-pulled-left mr-5">{% trans "Discoverable:" %}</dt> <dt class="is-pulled-left mr-5">{% trans "Discoverable:" %}</dt>
<dd>{{ user.discoverable }}</dd> <dd>{{ user.discoverable|yesno }}</dd>
{% if not user.is_active %} {% if not user.is_active %}
<dt class="is-pulled-left mr-5">{% trans "Deactivation reason:" %}</dt> <dt class="is-pulled-left mr-5">{% trans "Deactivation reason:" %}</dt>
<dd>{{ user.deactivation_reason }}</dd> <dd>{{ user.get_deactivation_reason_display }}</dd>
{% endif %} {% endif %}
{% if not user.is_active and user.deactivation_reason == "pending" %} {% if not user.is_active and user.deactivation_reason == "pending" %}
@ -104,7 +104,7 @@
<dd>{{ server.application_version }}</dd> <dd>{{ server.application_version }}</dd>
<dt class="is-pulled-left mr-5">{% trans "Status:" %}</dt> <dt class="is-pulled-left mr-5">{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd> <dd>{{ server.get_status_display }}</dd>
</dl> </dl>
{% if server.notes %} {% if server.notes %}
<h5>{% trans "Notes" %}</h5> <h5>{% trans "Notes" %}</h5>

View file

@ -2,7 +2,7 @@
{% load i18n %} {% load i18n %}
{% block header %} {% block header %}
{% trans "Create Shelf" %} {% trans "Create shelf" %}
{% endblock %} {% endblock %}
{% block form %} {% block form %}

View file

@ -17,7 +17,7 @@
<label class="label" for="id_description_{{ uuid }}">{% trans "Description:" %}</label> <label class="label" for="id_description_{{ uuid }}">{% trans "Description:" %}</label>
<textarea name="description" cols="40" rows="5" maxlength="500" class="textarea" id="id_description_{{ uuid }}">{{ form.description.value|default:'' }}</textarea> <textarea name="description" cols="40" rows="5" maxlength="500" class="textarea" id="id_description_{{ uuid }}">{{ form.description.value|default:'' }}</textarea>
</div> </div>
<div class="field has-addons"> <div class="field has-addons is-justify-content-end">
<div class="control"> <div class="control">
{% include 'snippets/privacy_select.html' with current=privacy %} {% include 'snippets/privacy_select.html' with current=privacy %}
</div> </div>

View file

@ -1,7 +1,7 @@
{% load humanize %}{% load i18n %}{% load utilities %} {% load humanize %}{% load i18n %}{% load utilities %}
{% with announcement.id|uuid as uuid %} {% with announcement.id|uuid as uuid %}
<aside <aside
class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y has-background-{{ announcement.display_type }}" class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y {% if announcement.display_type %}has-background-{{ announcement.display_type }}{% endif %}"
{% if not admin_mode %}data-hide="hide_announcement_{{ announcement.id }}"{% endif %} {% if not admin_mode %}data-hide="hide_announcement_{{ announcement.id }}"{% endif %}
> >
<details> <details>

View file

@ -1,22 +1,33 @@
{% load i18n %} {% load i18n %}
<div <div class="field is-relative">
class="field{% if not reply_parent.content_warning and not draft.content_warning %} is-hidden{% endif %}" <details
id="spoilers_{{ uuid }}{{ local_uuid }}" {% if reply_parent.content_warning or draft.content_warning %}open{% endif %}
>
<label
class="label"
for="id_content_warning_{{ uuid }}{{ local_uuid }}"
>
{% trans "Content warning:" %}
</label>
<input
type="text"
name="content_warning"
maxlength="255"
class="input"
id="id_content_warning_{{ uuid }}{{ local_uuid }}"
placeholder="{% trans 'Spoilers ahead!' %}"
value="{% firstof draft.content_warning reply_parent.content_warning '' %}"
{% if not draft %}data-cache-draft="id_content_warning_{{ book.id }}_{{ type }}"{% endif %}
> >
<summary class="is-flex">
<span class="icon icon-warning is-size-5 mr-1" aria-hidden="true"></span>
<span>
{% trans "Include spoiler alert" %}
</span>
<span class="details-close icon icon-x" aria-hidden="true"></span>
</summary>
<label
class="label"
for="id_content_warning_{{ uuid }}{{ local_uuid }}"
>
{% trans "Spoilers/content warnings:" %}
</label>
<div class="control">
<input
type="text"
name="content_warning"
maxlength="255"
class="input"
id="id_content_warning_{{ uuid }}{{ local_uuid }}"
placeholder="{% trans 'Spoilers ahead!' %}"
value="{% firstof draft.content_warning reply_parent.content_warning '' %}"
{% if not draft %}data-cache-draft="id_content_warning_{{ book.id }}_{{ type }}"{% endif %}
>
</div>
</details>
</div> </div>

View file

@ -1,17 +0,0 @@
{% load i18n %}
<div class="control">
<input
type="checkbox"
class="is-hidden"
name="sensitive"
id="id_show_spoilers_{{ uuid }}{{ local_uuid }}"
{% if draft.content_warning or status.content_warning %}checked{% endif %}
aria-hidden="true"
{% if not draft %}data-cache-draft="id_sensitive_{{ book.id }}_{{ type }}{{ reply_parent.id }}"{% endif %}
>
{% trans "Include spoiler alert" as button_text %}
{% firstof draft.content_warning status.content_warning as pressed %}
{% firstof local_uuid '' as local_uuid %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid|add:local_uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=pressed %}
</div>

View file

@ -37,8 +37,6 @@ reply_parent: the Status object this post will be in reply to, if applicable
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% include "snippets/create_status/content_warning_field.html" %}
{# fields that go between the content warnings and the content field (ie, quote) #} {# fields that go between the content warnings and the content field (ie, quote) #}
{% block pre_content_additions %}{% endblock %} {% block pre_content_additions %}{% endblock %}
@ -55,6 +53,8 @@ reply_parent: the Status object this post will be in reply to, if applicable
{# additional fields that go after the content block (ie, progress) #} {# additional fields that go after the content block (ie, progress) #}
{% block post_content_additions %}{% endblock %} {% block post_content_additions %}{% endblock %}
{% include "snippets/create_status/content_warning_field.html" %}
{% block options_block %} {% block options_block %}
{# cw, post privacy, and submit button #} {# cw, post privacy, and submit button #}
{% include "snippets/create_status/post_options_block.html" %} {% include "snippets/create_status/post_options_block.html" %}

View file

@ -1,8 +1,6 @@
{% load i18n %} {% load i18n %}
<div class="columns mt-1"> <div class="field has-addons is-justify-content-end">
<div class="field has-addons column"> <div class="control">
{% include "snippets/create_status/content_warning_toggle.html" %}
<div class="control">
{% if type == 'direct' %} {% if type == 'direct' %}
<input type="hidden" name="privacy" value="direct"> <input type="hidden" name="privacy" value="direct">
<button type="button" class="button" aria-label="Privacy" disabled>{% trans "Private" %}</button> <button type="button" class="button" aria-label="Privacy" disabled>{% trans "Private" %}</button>
@ -13,13 +11,11 @@
{% include 'snippets/privacy_select.html' with current=reply_parent.privacy %} {% include 'snippets/privacy_select.html' with current=reply_parent.privacy %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</div>
</div> </div>
<div class="column is-narrow control"> <div class="control">
<button class="button is-link" type="submit"> <button class="button is-link" type="submit">
<span class="icon icon-spinner" aria-hidden="true"></span> <span class="icon icon-spinner" aria-hidden="true"></span>
<span>{% trans "Post" %}</span> <span>{% trans "Post" %}</span>
</button> </button>
</div> </div>
</div> </div>

View file

@ -53,12 +53,12 @@
id="id_password_register" id="id_password_register"
aria-describedby="desc_password_register" aria-describedby="desc_password_register"
> >
{% include 'snippets/form_errors.html' with errors_list=register_form.password.errors id="desc_password_register" %} {% include 'snippets/form_errors.html' with errors_list=register_form.password.errors id="desc_password_register" %}
</div> </div>
</div> </div>
<div class="field is-grouped"> <div class="field">
<div class="control"> <div class="control">
<button class="button is-primary" type="submit"> <button class="button is-primary" type="submit">
{% trans "Sign Up" %} {% trans "Sign Up" %}

View file

@ -48,10 +48,10 @@
{% block modal-footer %} {% block modal-footer %}
<div class="buttons is-right is-flex-grow-1">
<button class="button is-success" type="submit">{% trans "Submit" %}</button> <button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button> <button class="button is-success" type="submit">{% trans "Submit" %}</button>
</div>
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %} {% block modal-form-close %}</form>{% endblock %}

View file

@ -71,7 +71,9 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ user_shelf.id }}"> <input type="hidden" name="shelf" value="{{ user_shelf.id }}">
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove from" %} {{ user_shelf.name }}</button> <button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">
{% blocktrans with name=user_shelf|translate_shelf_name %}Remove from {{ name }}{% endblocktrans %}
</button>
</form> </form>
</li> </li>
{% endif %} {% endif %}

Some files were not shown because too many files have changed in this diff Show more