From f812cdbfdb99f51ba98c13d673dd0da1180c8dc2 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 19 Mar 2024 11:51:51 +0100 Subject: [PATCH 01/16] fix toc --- src/wolnelektury/static/2022/styles/layout/_text.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wolnelektury/static/2022/styles/layout/_text.scss b/src/wolnelektury/static/2022/styles/layout/_text.scss index 0a0e2a5a2..22ad16c9f 100644 --- a/src/wolnelektury/static/2022/styles/layout/_text.scss +++ b/src/wolnelektury/static/2022/styles/layout/_text.scss @@ -89,6 +89,11 @@ } + +#heretoc { + margin-top: .5em; +} + #menu .box { display: none; position: absolute; @@ -99,7 +104,7 @@ height: 300px; padding: 30px; overflow: auto; - background: #F7BA00; + background: #FFE79E; box-shadow: 0px 0px 20px rgba(1, 129, 137, 0.2); border-radius: 10px; } @@ -185,7 +190,6 @@ height: 300px; padding: 0px 30px 30px; overflow: auto; - background: #F7BA00; box-shadow: 0px 0px 20px rgba(1, 129, 137, 0.2); border-radius: 10px; -- 2.20.1 From 7a27a3fbc14b2049b8d110db15855a625e4c2455 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 19 Mar 2024 12:10:46 +0100 Subject: [PATCH 02/16] fix --- src/wolnelektury/static/2022/styles/layout/_text.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/wolnelektury/static/2022/styles/layout/_text.scss b/src/wolnelektury/static/2022/styles/layout/_text.scss index 22ad16c9f..dd48d4f25 100644 --- a/src/wolnelektury/static/2022/styles/layout/_text.scss +++ b/src/wolnelektury/static/2022/styles/layout/_text.scss @@ -92,6 +92,11 @@ #heretoc { margin-top: .5em; + + // Workaround for missing li's. + ol a { + line-height: 30px; + } } #menu .box { -- 2.20.1 From dee22de65cab11aa05b68313e22e0201308de826 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 10 Apr 2024 12:56:20 +0200 Subject: [PATCH 03/16] Funding: help texts for Spent objects. --- src/funding/admin.py | 1 + ...nt_annotation_alter_spent_book_and_more.py | 30 +++++++++++++++++++ src/funding/models.py | 22 +++++++++++--- 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 src/funding/migrations/0014_alter_spent_annotation_alter_spent_book_and_more.py diff --git a/src/funding/admin.py b/src/funding/admin.py index bb44cd451..7402c2a95 100644 --- a/src/funding/admin.py +++ b/src/funding/admin.py @@ -73,6 +73,7 @@ class SpentAdmin(admin.ModelAdmin): model = Spent list_display = ['book', 'amount', 'timestamp'] search_fields = ['book__title'] + autocomplete_fields = ['book'] admin.site.register(Offer, OfferAdmin) diff --git a/src/funding/migrations/0014_alter_spent_annotation_alter_spent_book_and_more.py b/src/funding/migrations/0014_alter_spent_annotation_alter_spent_book_and_more.py new file mode 100644 index 000000000..0e6ea3bb1 --- /dev/null +++ b/src/funding/migrations/0014_alter_spent_annotation_alter_spent_book_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.0.8 on 2024-04-10 10:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0046_alter_book_options_alter_bookmedia_options_and_more'), + ('funding', '0013_missing_spent'), + ] + + operations = [ + migrations.AlterField( + model_name='spent', + name='annotation', + field=models.CharField(blank=True, help_text='Adnotacja pojawi się w nawiasie w rozliczeniu, by wyjaśnić sytuację w której do tej samej książki może być przypisany więcej niż jeden wydatek. Np. osobny wydatek na audiobook może mieć adnotację „audiobook”.', max_length=255, verbose_name='adnotacja'), + ), + migrations.AlterField( + model_name='spent', + name='book', + field=models.ForeignKey(blank=True, help_text='Książka, na którą zostały wydatkowane środki. Powinny tu być uwzględnione zarówno książki na które zbierano środki, jak i dodatkowe książki sfinansowane z nadwyżek ze zbiórek.', null=True, on_delete=django.db.models.deletion.PROTECT, to='catalogue.book', verbose_name='książka'), + ), + migrations.AlterField( + model_name='spent', + name='link', + field=models.URLField(blank=True, help_text='Jeśli wydatek nie dotyczy pojedynczej książki, to zamiast pola „Książka” powinien zostać uzupełniony link do sfinansowanego obiektu (np. kolekcji).'), + ), + ] diff --git a/src/funding/models.py b/src/funding/models.py index 12fa0ca3e..5afdc2259 100644 --- a/src/funding/models.py +++ b/src/funding/models.py @@ -372,11 +372,25 @@ class PayUNotification(club.payu.models.Notification): class Spent(models.Model): """ Some of the remaining money spent on a book. """ - book = models.ForeignKey(Book, models.PROTECT, null=True, blank=True) - link = models.URLField(blank=True, help_text='zamiast książki, np. kolekcja') + book = models.ForeignKey( + Book, models.PROTECT, null=True, blank=True, + verbose_name='książka', + help_text='Książka, na którą zostały wydatkowane środki. ' + 'Powinny tu być uwzględnione zarówno książki na które zbierano środki, jak i dodatkowe książki ' + 'sfinansowane z nadwyżek ze zbiórek.' + ) + link = models.URLField( + blank=True, + help_text="Jeśli wydatek nie dotyczy pojedynczej książki, to zamiast pola „Książka” " + "powinien zostać uzupełniony link do sfinansowanego obiektu (np. kolekcji)." + ) amount = models.DecimalField('kwota', decimal_places=2, max_digits=10) timestamp = models.DateField('kiedy') - annotation = models.CharField('adnotacja', max_length=255, blank=True, help_text="np. 'audiobook'") + annotation = models.CharField( + 'adnotacja', max_length=255, blank=True, + help_text="Adnotacja pojawi się w nawiasie w rozliczeniu, by wyjaśnić sytuację w której " + "do tej samej książki może być przypisany więcej niż jeden wydatek. " + "Np. osobny wydatek na audiobook może mieć adnotację „audiobook”.") class Meta: verbose_name = 'pieniądze wydane na książkę' @@ -384,5 +398,5 @@ class Spent(models.Model): ordering = ['-timestamp'] def __str__(self): - return "Spent: %s" % str(self.book) + return "Wydane na: %s" % str(self.book or self.link) -- 2.20.1 From 1959361810afbb5f1de88b7d717da32979c7235d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 10 Apr 2024 13:34:15 +0200 Subject: [PATCH 04/16] Fundraising in PDF. --- requirements/requirements.txt | 2 +- src/annoy/models.py | 1 + src/catalogue/fields.py | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0fa614370..eede89479 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -39,7 +39,7 @@ mutagen==1.45.1 sorl-thumbnail==12.8.0 # home-brewed & dependencies -librarian==24.1 +librarian==24.4.1 # celery tasks celery[redis]==5.2.7 diff --git a/src/annoy/models.py b/src/annoy/models.py index da371fc43..e60e3c024 100644 --- a/src/annoy/models.py +++ b/src/annoy/models.py @@ -116,6 +116,7 @@ class MediaInsertSet(models.Model): file_format = models.CharField(max_length=8, choices=[ ('epub', 'epub'), ('mobi', 'mobi'), + ('pdf', 'pdf'), ]) etag = models.CharField(max_length=64, blank=True) diff --git a/src/catalogue/fields.py b/src/catalogue/fields.py index 94bdb6097..c4dec7e3f 100644 --- a/src/catalogue/fields.py +++ b/src/catalogue/fields.py @@ -251,10 +251,13 @@ class PdfField(EbookField): @staticmethod def transform(wldoc, book): + MediaInsertSet = apps.get_model('annoy', 'MediaInsertSet') return wldoc.as_pdf( morefloats=settings.LIBRARIAN_PDF_MOREFLOATS, cover=get_make_cover(book), - base_url=absolute_url(gallery_url(wldoc.book_info.url.slug)), customizations=['notoc']) + base_url=absolute_url(gallery_url(wldoc.book_info.url.slug)), customizations=['notoc'], + fundraising=MediaInsertSet.get_texts_for('pdf'), + ) def build(self, fieldfile): super().build(fieldfile) -- 2.20.1 From f3ee4c1bfbf4e3856268fa79c63465f591f3499e Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 20 May 2024 15:19:37 +0200 Subject: [PATCH 05/16] Translators as authors. --- .../migrations/0047_book_translators.py | 18 +++++++++++++++ src/catalogue/models/book.py | 14 +++++------- src/catalogue/models/tag.py | 22 +++++++++++-------- .../templates/catalogue/book_detail.html | 15 ++++++------- .../templates/catalogue/book_list.html | 11 ++++++++++ .../templates/catalogue/book_text.html | 19 ++++++++-------- src/catalogue/views.py | 2 ++ src/picture/models.py | 2 +- 8 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 src/catalogue/migrations/0047_book_translators.py diff --git a/src/catalogue/migrations/0047_book_translators.py b/src/catalogue/migrations/0047_book_translators.py new file mode 100644 index 000000000..6146f6495 --- /dev/null +++ b/src/catalogue/migrations/0047_book_translators.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.8 on 2024-05-20 12:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0046_alter_book_options_alter_bookmedia_options_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='translators', + field=models.ManyToManyField(to='catalogue.tag'), + ), + ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 10e9d22e3..9e0ec50ff 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -91,6 +91,7 @@ class Book(models.Model): tagged = managers.ModelTaggedItemManager(Tag) tags = managers.TagDescriptor(Tag) tag_relations = GenericRelation(Tag.intermediary_table_model) + translators = models.ManyToManyField(Tag) html_built = django.dispatch.Signal() published = django.dispatch.Signal() @@ -154,12 +155,6 @@ class Book(models.Model): def genre_unicode(self): return self.tag_unicode('genre') - def translators(self): - translators = self.get_extra_info_json().get('translators') or [] - return [ - '\xa0'.join(reversed(translator.split(', ', 1))) for translator in translators - ] - def translator(self): translators = self.get_extra_info_json().get('translators') if not translators: @@ -658,14 +653,17 @@ class Book(models.Model): meta_tags = Tag.tags_from_info(book_info) - for tag in meta_tags: + for tag, relationship in meta_tags: if not tag.for_books: tag.for_books = True tag.save() - book.tags = set(meta_tags + book_shelves) + just_tags = [t for (t, rel) in meta_tags if not rel] + book.tags = set(just_tags + book_shelves) book.save() # update sort_key_author + book.translators.set([t for (t, rel) in meta_tags if rel == 'translator']) + cover_changed = old_cover != book.cover_info() obsolete_children = set(b for b in book.children.all() if b not in children) diff --git a/src/catalogue/models/tag.py b/src/catalogue/models/tag.py index cdc1dc8a2..a5c96d542 100644 --- a/src/catalogue/models/tag.py +++ b/src/catalogue/models/tag.py @@ -213,16 +213,20 @@ class Tag(models.Model): from slugify import slugify from sortify import sortify meta_tags = [] - categories = (('kinds', 'kind'), ('genres', 'genre'), ('authors', 'author'), ('epochs', 'epoch')) - for field_name, category in categories: + categories = ( + # BookInfo field names, Tag category, relationship + ('kinds', 'kind', None), + ('genres', 'genre', None), + ('epochs', 'epoch', None), + ('authors', 'author', None), + ('translators', 'author', 'translator'), + ) + for field_name, category, relationship in categories: try: tag_names = getattr(info, field_name) except (AttributeError, KeyError): # TODO: shouldn't be KeyError here at all. - try: - tag_names = [getattr(info, category)] - except KeyError: - # For instance, Pictures do not have 'genre' field. - continue + # For instance, Pictures do not have 'genre' field. + continue for tag_name in tag_names: lang = getattr(tag_name, 'lang', None) or settings.LANGUAGE_CODE tag_sort_key = tag_name @@ -243,9 +247,9 @@ class Tag(models.Model): tag.sort_key = sortify(tag_sort_key.lower()) tag.save() - meta_tags.append(tag) + meta_tags.append((tag, relationship)) else: - meta_tags.append(tag) + meta_tags.append((tag, relationship)) return meta_tags diff --git a/src/catalogue/templates/catalogue/book_detail.html b/src/catalogue/templates/catalogue/book_detail.html index 6586970dd..eb1ab8a7d 100644 --- a/src/catalogue/templates/catalogue/book_detail.html +++ b/src/catalogue/templates/catalogue/book_detail.html @@ -90,18 +90,17 @@

{% for author in book.authors %}{{ author.name }}{% if not forloop.last %}, {% endif %}{% endfor %}

{{ book.title }}

- {% with translators=book.translators %} - {% if translators %} + {% if book.translators.exists %}

- {% if translators.0 != 'tłumacz nieznany' %} - {% trans "tłum." %} - {% endif %} - {% for translator in translators %} - {{ translator }}{% if not forloop.last %}, {% endif %} + {% for translator in book.translators.all %} + {% if forloop.first and translator.name != 'tłumacz nieznany' %} + {% trans "tłum." %} + {% endif %} + + {{ translator }}{% if not forloop.last %}, {% endif %} {% endfor %}

{% endif %} - {% endwith %} diff --git a/src/catalogue/templates/catalogue/book_list.html b/src/catalogue/templates/catalogue/book_list.html index dabb0ca48..e8debd975 100644 --- a/src/catalogue/templates/catalogue/book_list.html +++ b/src/catalogue/templates/catalogue/book_list.html @@ -117,6 +117,17 @@ {% paginate %} + {% if translation_list %} +
+

Tłumaczenia

+
+ {% for book in translation_list %} + {% include "catalogue/book_box.html" %} + {% endfor %} +
+
+ {% endif %} + {% if main_tag %}
diff --git a/src/catalogue/templates/catalogue/book_text.html b/src/catalogue/templates/catalogue/book_text.html index 421d439fe..52f281b05 100644 --- a/src/catalogue/templates/catalogue/book_text.html +++ b/src/catalogue/templates/catalogue/book_text.html @@ -151,18 +151,17 @@

{% for author in book.authors %}{{ author.name }}{% if not forloop.last %}, {% endif %}{% endfor %}

{{ book.title }}

- {% with translators=book.translators %} - {% if translators %} -

- {% if translators.0 != 'tłumacz nieznany' %} + {% if book.translators.exists %} +

+ {% for translator in book.translators.all %} + {% if forloop.first and translator.name != 'tłumacz nieznany' %} {% trans "tłum." %} {% endif %} - {% for translator in translators %} - {{ translator }}{% if not forloop.last %}, {% endif %} - {% endfor %} -

- {% endif %} - {% endwith %} + + {{ translator }}{% if not forloop.last %}, {% endif %} + {% endfor %} +

+ {% endif %}
{% content_warning book %}
diff --git a/src/catalogue/views.py b/src/catalogue/views.py index af4d1e3b5..588cdb073 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -253,6 +253,8 @@ class TaggedObjectList(BookList): t for t in self.ctx['tags'] if t is not self.ctx['main_tag'] ] + if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author': + self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all() def get_queryset(self): qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True) diff --git a/src/picture/models.py b/src/picture/models.py index dec9960cb..98fd382f3 100644 --- a/src/picture/models.py +++ b/src/picture/models.py @@ -202,7 +202,7 @@ class Picture(models.Model): picture.title = str(picture_xml.picture_info.title) picture.extra_info = json.dumps(picture_xml.picture_info.to_dict()) - picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info)) + picture_tags = set([t for (t, rel) in catalogue.models.Tag.tags_from_info(picture_xml.picture_info)]) for tag in picture_tags: if not tag.for_pictures: tag.for_pictures = True -- 2.20.1 From 2c101e78aee8cd3ccf3f24a0ecaa22fa77593c5c Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 20 May 2024 15:21:30 +0200 Subject: [PATCH 06/16] remove the banner --- src/wolnelektury/templates/base.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wolnelektury/templates/base.html b/src/wolnelektury/templates/base.html index 170a0797b..f1a7bf685 100644 --- a/src/wolnelektury/templates/base.html +++ b/src/wolnelektury/templates/base.html @@ -30,8 +30,6 @@ {% block under-menu %}{% endblock %} - {% include 'banner_procent.html' %} - {% if not funding_no_show_current %}
{% cache 120 funding_top_bar LANGUAGE_CODE %} -- 2.20.1 From af196fd22bf74f7a7e228f8528cff79b863a7351 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 22 May 2024 11:48:43 +0200 Subject: [PATCH 07/16] Librarian. --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index eede89479..8c508540a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -39,7 +39,7 @@ mutagen==1.45.1 sorl-thumbnail==12.8.0 # home-brewed & dependencies -librarian==24.4.1 +librarian==24.5 # celery tasks celery[redis]==5.2.7 -- 2.20.1 From d9c51b94bfe6b1cda1790d57a9108b83c6e1643b Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Tue, 4 Jun 2024 11:18:37 +0200 Subject: [PATCH 08/16] minor fix --- src/catalogue/models/book.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 9e0ec50ff..86010df1a 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -91,7 +91,7 @@ class Book(models.Model): tagged = managers.ModelTaggedItemManager(Tag) tags = managers.TagDescriptor(Tag) tag_relations = GenericRelation(Tag.intermediary_table_model) - translators = models.ManyToManyField(Tag) + translators = models.ManyToManyField(Tag, blank=True) html_built = django.dispatch.Signal() published = django.dispatch.Signal() -- 2.20.1 From f7d8acded4a58d423035c5759f6dee9a34396959 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Jun 2024 13:44:54 +0200 Subject: [PATCH 09/16] Initial bookmarks. --- src/bookmarks/__init__.py | 0 src/bookmarks/admin.py | 16 + src/bookmarks/apps.py | 6 + src/bookmarks/migrations/0001_initial.py | 31 + src/bookmarks/migrations/0002_quote.py | 33 + src/bookmarks/migrations/__init__.py | 0 src/bookmarks/models.py | 63 ++ .../templates/bookmarks/quote_detail.html | 83 +++ src/bookmarks/tests.py | 3 + src/bookmarks/urls.py | 12 + src/bookmarks/views.py | 137 +++++ src/catalogue/models/book.py | 2 +- src/catalogue/static/player/player.js | 13 +- .../templates/catalogue/book_text.html | 133 +++- src/catalogue/views.py | 5 +- src/wolnelektury/settings/apps.py | 1 + src/wolnelektury/settings/custom.py | 2 + src/wolnelektury/settings/static.py | 8 + .../static/2022/images/add-icon.svg | 5 + .../static/2022/images/add-note-icon.svg | 4 + .../static/2022/images/notka-saved.svg | 43 ++ .../static/2022/images/play-now-icon.svg | 4 + .../static/2022/images/tool-copy.svg | 6 + .../static/2022/images/tool-link.svg | 49 ++ .../static/2022/images/tool-quote.svg | 43 ++ .../static/2022/images/zakladka-full.svg | 3 + .../static/2022/images/zakladka-note.svg | 45 ++ .../static/2022/images/zakladka-usun.svg | 76 +++ .../static/2022/images/zakladka.svg | 3 + .../static/2022/styles/layout/_author.scss | 4 +- .../static/2022/styles/layout/_bookmarks.scss | 24 + .../static/2022/styles/layout/_module.scss | 1 + .../static/2022/styles/layout/_text.scss | 396 +++++++++++- .../static/2022/styles/reader_player.scss | 26 +- .../static/js/book_text/marker.js | 36 ++ src/wolnelektury/static/js/book_text/note.js | 7 +- .../static/js/book_text/pbox-items.js | 38 ++ src/wolnelektury/static/js/book_text/pbox.js | 46 ++ .../static/js/book_text/progress.js | 3 +- .../static/js/book_text/references.js | 576 ++++++++++++++++++ .../static/js/contrib/jquery.scrollto.js | 301 ++++----- src/wolnelektury/urls.py | 1 + 42 files changed, 2087 insertions(+), 201 deletions(-) create mode 100644 src/bookmarks/__init__.py create mode 100644 src/bookmarks/admin.py create mode 100644 src/bookmarks/apps.py create mode 100644 src/bookmarks/migrations/0001_initial.py create mode 100644 src/bookmarks/migrations/0002_quote.py create mode 100644 src/bookmarks/migrations/__init__.py create mode 100644 src/bookmarks/models.py create mode 100644 src/bookmarks/templates/bookmarks/quote_detail.html create mode 100644 src/bookmarks/tests.py create mode 100644 src/bookmarks/urls.py create mode 100644 src/bookmarks/views.py create mode 100644 src/wolnelektury/static/2022/images/add-icon.svg create mode 100644 src/wolnelektury/static/2022/images/add-note-icon.svg create mode 100644 src/wolnelektury/static/2022/images/notka-saved.svg create mode 100644 src/wolnelektury/static/2022/images/play-now-icon.svg create mode 100644 src/wolnelektury/static/2022/images/tool-copy.svg create mode 100644 src/wolnelektury/static/2022/images/tool-link.svg create mode 100644 src/wolnelektury/static/2022/images/tool-quote.svg create mode 100644 src/wolnelektury/static/2022/images/zakladka-full.svg create mode 100644 src/wolnelektury/static/2022/images/zakladka-note.svg create mode 100644 src/wolnelektury/static/2022/images/zakladka-usun.svg create mode 100644 src/wolnelektury/static/2022/images/zakladka.svg create mode 100644 src/wolnelektury/static/2022/styles/layout/_bookmarks.scss create mode 100644 src/wolnelektury/static/js/book_text/marker.js create mode 100644 src/wolnelektury/static/js/book_text/pbox-items.js create mode 100644 src/wolnelektury/static/js/book_text/pbox.js diff --git a/src/bookmarks/__init__.py b/src/bookmarks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/bookmarks/admin.py b/src/bookmarks/admin.py new file mode 100644 index 000000000..956f233c2 --- /dev/null +++ b/src/bookmarks/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin +from . import models + + +@admin.register(models.Bookmark) +class BookmarkAdmin(admin.ModelAdmin): + date_hierarchy = 'created_at' + list_display = ['uuid', 'created_at', 'user', 'book', 'anchor'] + raw_id_fields = ['book', 'user'] + + +@admin.register(models.Quote) +class BookmarkAdmin(admin.ModelAdmin): + date_hierarchy = 'created_at' + list_display = ['uuid', 'created_at', 'user', 'book', 'start_elem'] + raw_id_fields = ['book', 'user'] diff --git a/src/bookmarks/apps.py b/src/bookmarks/apps.py new file mode 100644 index 000000000..c7fc01929 --- /dev/null +++ b/src/bookmarks/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BookmarksConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'bookmarks' diff --git a/src/bookmarks/migrations/0001_initial.py b/src/bookmarks/migrations/0001_initial.py new file mode 100644 index 000000000..eb5f7ff71 --- /dev/null +++ b/src/bookmarks/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 4.0.8 on 2024-02-28 11:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('catalogue', '0046_alter_book_options_alter_bookmedia_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Bookmark', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('anchor', models.CharField(blank=True, max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('note', models.TextField(blank=True)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalogue.book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/bookmarks/migrations/0002_quote.py b/src/bookmarks/migrations/0002_quote.py new file mode 100644 index 000000000..f413c5104 --- /dev/null +++ b/src/bookmarks/migrations/0002_quote.py @@ -0,0 +1,33 @@ +# Generated by Django 4.0.8 on 2024-03-06 11:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalogue', '0046_alter_book_options_alter_bookmedia_options_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('bookmarks', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Quote', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('start_elem', models.CharField(blank=True, max_length=100)), + ('end_elem', models.CharField(blank=True, max_length=100)), + ('start_offset', models.IntegerField(blank=True, null=True)), + ('end_offset', models.IntegerField(blank=True, null=True)), + ('text', models.TextField(blank=True)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalogue.book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/bookmarks/migrations/__init__.py b/src/bookmarks/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/bookmarks/models.py b/src/bookmarks/models.py new file mode 100644 index 000000000..67a4fa5e6 --- /dev/null +++ b/src/bookmarks/models.py @@ -0,0 +1,63 @@ +import uuid +from django.db import models + + +class Bookmark(models.Model): + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey('auth.User', models.CASCADE) + book = models.ForeignKey('catalogue.Book', models.CASCADE) + anchor = models.CharField(max_length=100, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + note = models.TextField(blank=True) + + def __str__(self): + return str(self.uuid) + + def get_for_json(self): + return { + 'uuid': self.uuid, + 'anchor': self.anchor, + 'note': self.note, + 'created_at': self.created_at, + } + + +class Quote(models.Model): + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey('auth.User', models.CASCADE) + book = models.ForeignKey('catalogue.Book', models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + start_elem = models.CharField(max_length=100, blank=True) + end_elem = models.CharField(max_length=100, blank=True) + start_offset = models.IntegerField(null=True, blank=True) + end_offset = models.IntegerField(null=True, blank=True) + text = models.TextField(blank=True) + + def __str__(self): + return str(self.uuid) + + def get_for_json(self): + return { + 'uuid': self.uuid, + 'startElem': self.start_elem, + 'endElem': self.end_elem, + 'startOffset': self.start_offset, + 'startOffset': self.end_offset, + 'created_at': self.created_at, + } + +def from_path(elem, path): + def child_nodes(e): + if e.text: yield (e, 'text') + for child in e: + if child.attrib.get('id') != 'toc': + yield (child, None) + if child.tail: + yield (child, 'tail') + while len(path) > 1: + n = path.pop(0) + elem = list(child_nodes(elem))[n] + return elem + + + diff --git a/src/bookmarks/templates/bookmarks/quote_detail.html b/src/bookmarks/templates/bookmarks/quote_detail.html new file mode 100644 index 000000000..46181dd22 --- /dev/null +++ b/src/bookmarks/templates/bookmarks/quote_detail.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% load i18n static %} + + +{% block breadcrumbs %} + Cytaty użytkowników + +{% endblock %} + +{% block main %} + +{% with book=object.book %} +
+
+ {% with first_text=book.get_first_text %} + +
+ + +
+ {% endwith %} +
+ +
+ + + + + {% endwith %} +{% endblock %} diff --git a/src/bookmarks/tests.py b/src/bookmarks/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/src/bookmarks/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/bookmarks/urls.py b/src/bookmarks/urls.py new file mode 100644 index 000000000..333afd783 --- /dev/null +++ b/src/bookmarks/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + + +urlpatterns = [ + path('zakladki/', views.bookmarks, name='bookmarks'), + path('zakladki//', views.bookmark, name='bookmark'), + path('zakladki//delete/', views.bookmark_delete, name='bookmark_delete'), + + path('cytaty/', views.quotes, name='quotes'), + path('cytaty//', views.quote, name='quote'), +] diff --git a/src/bookmarks/views.py b/src/bookmarks/views.py new file mode 100644 index 000000000..7d83818c1 --- /dev/null +++ b/src/bookmarks/views.py @@ -0,0 +1,137 @@ +from django.http import Http404, JsonResponse +from django.shortcuts import render, get_object_or_404 +from django.views.decorators import cache +import catalogue.models +from wolnelektury.utils import is_ajax +from . import models +from lxml import html +import re + + +# login required + +@cache.never_cache +def bookmarks(request): + try: + slug = request.headers['Referer'].rsplit('.', 1)[0].rsplit('/', 1)[-1] + except: + slug = 'w-80-dni-dookola-swiata' +# raise Http404() + try: + book = catalogue.models.Book.objects.get(slug=slug) + except catalogue.models.Book.DoesNotExist: + raise Http404() + + if request.method == 'POST': + # TODO test + bm, created = models.Bookmark.objects.update_or_create( + user=request.user, + book=book, + anchor=request.POST.get('anchor', ''), + defaults={ + 'note': request.POST.get('note', ''), + } + ) + return JsonResponse(bm.get_for_json()) + else: + return JsonResponse({ + bm.anchor: bm.get_for_json() + for bm in models.Bookmark.objects.filter( + user=request.user, + book=book, + ) + }) + + +def bookmark(request, uuid): + bm = get_object_or_404(models.Bookmark, user=request.user, uuid=uuid) + if request.method == 'POST': + bm.note = request.POST.get('note', '') + bm.save() + return JsonResponse(bm.get_for_json()) + + +def bookmark_delete(request, uuid): + models.Bookmark.objects.filter(user=request.user, uuid=uuid).delete() + return JsonResponse({}) + + + + +@cache.never_cache +def quotes(request): + try: + slug = request.headers['Referer'].rsplit('.', 1)[0].rsplit('/', 1)[-1] + except: + slug = 'w-80-dni-dookola-swiata' +# raise Http404() + try: + book = catalogue.models.Book.objects.get(slug=slug) + except catalogue.models.Book.DoesNotExist: + raise Http404() + + if request.method == 'POST': + # TODO test + # ensure unique? or no? + + text = request.POST.get('text', '') + text = text.strip() + + stext = re.sub(r'\s+', ' ', text) + ## verify + print(text) + + + # find out + with book.html_file.open('r') as f: + ht = f.read() + tree = html.fromstring(ht) + # TODO: clear + for sel in ('.//a[@class="theme-begin"]', + './/a[@class="anchor"]', + ): + for e in tree.xpath(sel): + e.clear(keep_tail=True) + htext = html.tostring(tree, encoding='unicode', method='text') + htext = re.sub(r'\s+', ' ', htext) + + print(htext) + + otext = stext + if stext not in htext: + # raise 401 + raise Http404() + + # paths? + # start elem? + q = models.Quote.objects.create( + user=request.user, + book=book, + start_elem=request.POST.get('startElem', ''), + end_elem=request.POST.get('startElem', ''), + start_offset=request.POST.get('startOffset', None), + end_offset=request.POST.get('startOffset', None), + text=text, + ) + return JsonResponse(q.get_for_json()) + else: + return JsonResponse({ + q.start_elem: q.get_for_json() + for q in models.Quote.objects.filter( + user=request.user, + book=book, + ) + }) + + + +def quote(request, uuid): + q = get_object_or_404(models.Quote, user=request.user, uuid=uuid) + if is_ajax(request): + return JsonResponse(q.get_for_json()) + else: + return render(request, 'bookmarks/quote_detail.html', { + 'object': q, + }) + + diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 86010df1a..ac8c02b9e 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -414,7 +414,7 @@ class Book(models.Model): has_daisy_file.boolean = True def has_sync_file(self): - return self.has_media("sync") + return settings.FEATURE_SYNCHRO and self.has_media("sync") def get_sync(self): with self.get_media('sync').first().file.open('r') as f: diff --git a/src/catalogue/static/player/player.js b/src/catalogue/static/player/player.js index dfac09615..cbf90261b 100644 --- a/src/catalogue/static/player/player.js +++ b/src/catalogue/static/player/player.js @@ -4,10 +4,13 @@ $(".book-right-column").remove(); if ($("#player-bar").length) { - $("h1").first().after($("")); + $("#book-text-buttons").append( + $(" zacznij słuchać") + ).show(); } $(".enable-player-bar").click(function() { + $('body').addClass('with-player-bar'); $('.jp-play').click(); return false; @@ -67,9 +70,11 @@ // TODO: will need class for attach // may be added from sync data - $(".syncable").click(function() { - if (!$('body').hasClass('with-player-bar')) return; - let id = $(this).attr('id'); + + + $(".zakladka-tool_sluchaj").click(function() { + $('body').addClass('with-player-bar'); + let id = $(this).data('sync'); if (!id) return; for (let i=0; i + {#% annoy_banner 'book-start' %#} + +
+
+
-
+
{% with next=book.get_next_text prev=book.get_prev_text %} {% if next %} {{ next.title }} → @@ -178,7 +183,13 @@ {% if prev %} ← {{ prev.title }} {% endif %} - {{ book_text|safe }} +
+ {{ book_text|safe }} +
+ +
+ {% include 'club/donation_step1_form.html' with form=donation_form %} +
{% endwith %} @@ -200,34 +211,102 @@
+ - {% if book.other_versions.exists %} -
-

{% trans "Inne wersje utworu" %}

- {% trans "Zamknij drugą wersję" %} - +
+
+ Skopiuj link + Skopiuj cytat + {% if request.user.is_authenticated %} + Zapisz cytat + {% endif %}
- {% endif %} +
+
+
-
- {% annoy_banners 'book-text-intermission' %} +
+
+ Zakładka + Istniejąca zakładka + Notka +
+
+
+ {% if request.user.is_authenticated %} +
+ Usuń zakładkę +
+
+ Dodaj zakładkę +
+
+ Słuchaj od tego miejsca +
+
+ + + + + +
+ + {% else %} + +
+ +
+
+
+
+ + + {% if book.other_versions.exists %} +
+

{% trans "Inne wersje utworu" %}

+ {% trans "Zamknij drugą wersję" %} + +
+ {% endif %} + +
+ {% annoy_banners 'book-text-intermission' %} + + {% for insert in inserts %} + {% include 'annoy/dynamic_insert.html' %} + {% endfor %} +
diff --git a/src/catalogue/views.py b/src/catalogue/views.py index 588cdb073..fc01ad65e 100644 --- a/src/catalogue/views.py +++ b/src/catalogue/views.py @@ -550,7 +550,10 @@ def book_text(request, slug): 'book': book, 'extra_info': book.get_extra_info_json(), 'book_text': book_text, - 'inserts': DynamicTextInsert.get_all(request) + 'inserts': DynamicTextInsert.get_all(request), + + 'club': Club.objects.first(), + 'donation_form': DonationStep1Form(), }) diff --git a/src/wolnelektury/settings/apps.py b/src/wolnelektury/settings/apps.py index 769565d03..d4faa8bfb 100644 --- a/src/wolnelektury/settings/apps.py +++ b/src/wolnelektury/settings/apps.py @@ -8,6 +8,7 @@ INSTALLED_APPS_OUR = [ 'ajaxable', 'annoy', 'api', + 'bookmarks', 'catalogue', 'chunks', 'dictionary', diff --git a/src/wolnelektury/settings/custom.py b/src/wolnelektury/settings/custom.py index a9ae731eb..16b5e0a4f 100644 --- a/src/wolnelektury/settings/custom.py +++ b/src/wolnelektury/settings/custom.py @@ -74,3 +74,5 @@ WIDGETS = {} SEARCH_CONFIG = 'english' SEARCH_CONFIG_SIMPLE = 'simple' SEARCH_USE_UNACCENT = False + +FEATURE_SYNCHRO = False diff --git a/src/wolnelektury/settings/static.py b/src/wolnelektury/settings/static.py index b3edb72cb..b83691b24 100644 --- a/src/wolnelektury/settings/static.py +++ b/src/wolnelektury/settings/static.py @@ -31,6 +31,7 @@ PIPELINE = { 'chunks/edit.scss', 'scss/text.scss', + '2022/styles/reader_player.scss', ], 'output_filename': 'css/compressed/main.css', }, @@ -59,6 +60,7 @@ PIPELINE = { 'source_filenames': [ '2022/scripts/vendor.js', 'contrib/jquery-ui-1.13.1.custom/jquery-ui.js', + #'js/contrib/jquery.scrollto.js', 'js/search.js', 'js/header.js', @@ -78,6 +80,9 @@ PIPELINE = { 'js/book_text/toc.js', 'js/book_text/progress.js', + 'js/book_text/pbox.js', + 'js/book_text/pbox-items.js', + 'js/contrib/jquery.countdown.js', 'js/contrib/jquery.countdown-pl.js', 'js/contrib/jquery.countdown-de.js', 'js/contrib/jquery.countdown-uk.js', 'js/contrib/jquery.countdown-es.js', 'js/contrib/jquery.countdown-lt.js', @@ -99,6 +104,9 @@ PIPELINE = { 'source_filenames': [ 'js/contrib/jquery.form.js', 'js/contrib/jquery.jqmodal.js', + + 'js/contrib/jquery.scrollto.js', + 'js/book_text/info.js', 'js/book_text/menu.js', 'js/book_text/note.js', diff --git a/src/wolnelektury/static/2022/images/add-icon.svg b/src/wolnelektury/static/2022/images/add-icon.svg new file mode 100644 index 000000000..006ee63f2 --- /dev/null +++ b/src/wolnelektury/static/2022/images/add-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/wolnelektury/static/2022/images/add-note-icon.svg b/src/wolnelektury/static/2022/images/add-note-icon.svg new file mode 100644 index 000000000..ba6a5ff10 --- /dev/null +++ b/src/wolnelektury/static/2022/images/add-note-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/wolnelektury/static/2022/images/notka-saved.svg b/src/wolnelektury/static/2022/images/notka-saved.svg new file mode 100644 index 000000000..79eebd136 --- /dev/null +++ b/src/wolnelektury/static/2022/images/notka-saved.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/src/wolnelektury/static/2022/images/play-now-icon.svg b/src/wolnelektury/static/2022/images/play-now-icon.svg new file mode 100644 index 000000000..4aeace3fb --- /dev/null +++ b/src/wolnelektury/static/2022/images/play-now-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/wolnelektury/static/2022/images/tool-copy.svg b/src/wolnelektury/static/2022/images/tool-copy.svg new file mode 100644 index 000000000..cf26b5ae1 --- /dev/null +++ b/src/wolnelektury/static/2022/images/tool-copy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/wolnelektury/static/2022/images/tool-link.svg b/src/wolnelektury/static/2022/images/tool-link.svg new file mode 100644 index 000000000..241bd68c4 --- /dev/null +++ b/src/wolnelektury/static/2022/images/tool-link.svg @@ -0,0 +1,49 @@ + + + + + + + + + diff --git a/src/wolnelektury/static/2022/images/tool-quote.svg b/src/wolnelektury/static/2022/images/tool-quote.svg new file mode 100644 index 000000000..0c8670a1e --- /dev/null +++ b/src/wolnelektury/static/2022/images/tool-quote.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/src/wolnelektury/static/2022/images/zakladka-full.svg b/src/wolnelektury/static/2022/images/zakladka-full.svg new file mode 100644 index 000000000..4fccefe99 --- /dev/null +++ b/src/wolnelektury/static/2022/images/zakladka-full.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/wolnelektury/static/2022/images/zakladka-note.svg b/src/wolnelektury/static/2022/images/zakladka-note.svg new file mode 100644 index 000000000..1c4d3eaef --- /dev/null +++ b/src/wolnelektury/static/2022/images/zakladka-note.svg @@ -0,0 +1,45 @@ + + + + + + + diff --git a/src/wolnelektury/static/2022/images/zakladka-usun.svg b/src/wolnelektury/static/2022/images/zakladka-usun.svg new file mode 100644 index 000000000..628efb12c --- /dev/null +++ b/src/wolnelektury/static/2022/images/zakladka-usun.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + diff --git a/src/wolnelektury/static/2022/images/zakladka.svg b/src/wolnelektury/static/2022/images/zakladka.svg new file mode 100644 index 000000000..55aceaa5a --- /dev/null +++ b/src/wolnelektury/static/2022/images/zakladka.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/wolnelektury/static/2022/styles/layout/_author.scss b/src/wolnelektury/static/2022/styles/layout/_author.scss index 618b19ac5..30796dd8b 100644 --- a/src/wolnelektury/static/2022/styles/layout/_author.scss +++ b/src/wolnelektury/static/2022/styles/layout/_author.scss @@ -180,7 +180,9 @@ } .l-author__quotes__slider { - display: flex; + display: flex; +} +.l-author__quotes { div { em { font-style: italic; diff --git a/src/wolnelektury/static/2022/styles/layout/_bookmarks.scss b/src/wolnelektury/static/2022/styles/layout/_bookmarks.scss new file mode 100644 index 000000000..229347684 --- /dev/null +++ b/src/wolnelektury/static/2022/styles/layout/_bookmarks.scss @@ -0,0 +1,24 @@ +.bookmarks-quote-box { + padding: 30px ; + background: #E6F0F1; + border-radius: 20px; + margin-bottom: 30px; + display: flex; + justify-content: center; + align-items: center; + gap: 30px; + + .bookmarks-quote { + + } + + .bookmarks-quote-content { + padding: 30px; + } + + .bookmarks-quote-book { + img { + width: 170px; + } + } +} diff --git a/src/wolnelektury/static/2022/styles/layout/_module.scss b/src/wolnelektury/static/2022/styles/layout/_module.scss index fcefb2f3e..4e4a35f37 100644 --- a/src/wolnelektury/static/2022/styles/layout/_module.scss +++ b/src/wolnelektury/static/2022/styles/layout/_module.scss @@ -5,6 +5,7 @@ /*!*/ @import "annoy"; +@import "bookmarks"; @import "navigation"; @import "header"; @import "main"; diff --git a/src/wolnelektury/static/2022/styles/layout/_text.scss b/src/wolnelektury/static/2022/styles/layout/_text.scss index dd48d4f25..1c617c7ee 100644 --- a/src/wolnelektury/static/2022/styles/layout/_text.scss +++ b/src/wolnelektury/static/2022/styles/layout/_text.scss @@ -67,13 +67,12 @@ left: 0; top: 0; } - #book-text { - padding: 5px; + .main-text-body { + margin: 16px; @include rwd($break-wide) { width: 717px; - padding: 0; + margin: 0 auto; } - margin: 0 auto; color: #333333; @@ -244,12 +243,13 @@ #main-text #book-text { + body { - font-size: 16px; - font-family: Gelasio, Georgia, "Times New Roman", serif; - line-height: 1.5em; - margin: 0; -} + font-size: 16px; + font-family: Gelasio, Georgia, "Times New Roman", serif; + line-height: 1.5em; + margin: 0; + } a { color: blue; @@ -366,6 +366,7 @@ blockquote { /* ============= */ .verse, .paragraph { position:relative; + padding: 0 48px 0 0; } /*.anchor { position: absolute; @@ -467,7 +468,7 @@ div.kwestia div.stanza { p.paragraph { text-align: justify; - margin: 0; + margin: 0 0 0 44px; text-indent: 1.5em; } @@ -562,8 +563,9 @@ table.border td, table.border th { .anchor { /* Show line numbers. */ float: left; + clear: left; font-size: .8em; - margin-left: -40px; + margin-left: 0; width: 36px; height: auto; padding: 2px; @@ -573,7 +575,6 @@ table.border td, table.border th { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; - } @@ -586,7 +587,6 @@ table.border td, table.border th { clear: both; line-height: 1.5em; text-align: left; - z-index: 60; font-style: normal; font-weight: normal; @@ -652,3 +652,373 @@ a.reference.interesting:after { margin-bottom: 1em; } } + + +#main-text { + #annotation-box { + display: none; + position: fixed; + + .pointer-bottom { + transform: rotate(45deg); + left: 27.5px; + bottom: -6px; + width: 12px; + height: 12px; + position: absolute; + + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 1; + border-radius: 0px 0px 2px 0px; + background: var(--white-100, #FFF); + border: 1px solid var(--teal-700, #007880); + display: block; + } + .pointer-top { + left: 27.5px; + bottom: -5px; + transform: rotate(45deg); + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 3; + border-radius: 0px 0px 2px 0px; + background: #fff; + display: block; + } + + #annotation { + max-width: 470px; + position: relative; + z-index: 2; + background: white; + + padding: 20px; + box-shadow: 0px 0px 20px rgba(1, 129, 137, 0.2); + border: 1px solid #007880; + border-radius: 6px; + + @include rwd($break-wide) { + } + } + + + #annotation-content { + max-height: 138px; + overflow-y: scroll; + color: var(--black-900, #333); + leading-trim: both; + text-edge: cap; + font-variant-numeric: oldstyle-nums proportional-nums; + /* Czytnik/Desktop/p read */ + font-family: "Source Serif Pro"; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 28px; /* 155.556% */ + } + + #footnote-link { + display: block; + margin-top: 16px; + text-align: right; + } + } + + + #qbox { + display: none; + position: fixed; + padding: 4px; + border: 1px solid #007880; + border-radius: 22px; + background: white; + box-shadow: 6px 6px 10px 0px rgba(0, 120, 128, 0.35); + overflow: hidden; + + &.copied { + .content:after { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + content: "skopiowane"; + display: flex; + background: white; + color: black; + justify-content: center; + align-items: center; + } + } + + .content { + overflow: hidden; + display: flex; + a { + width: 70px; + line-height: 44px; + text-align: center; + border-left: 1px solid #BBF6FA; + + &:nth-child(1) { + border-left: none; + } + + img { + vertical-align: middle; + height: 22px; + } + } + } + + .pointer-bottom { + transform: rotate(45deg); + left: 128px; + top: -6px; + width: 12px; + height: 12px; + position: absolute; + + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 1; + border-radius: 0px 0px 2px 0px; + background: var(--white-100, #FFF); + border: 1px solid var(--teal-700, #007880); + display: block; + } + .pointer-top { + left: 128px; + top: -5px; + transform: rotate(45deg); + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 3; + border-radius: 0px 0px 2px 0px; + background: #fff; + display: block; + } + + } +} + + + +.zakladka { + display: none; + position: absolute; + //z-index: 70; + width: 40px; + height: 40px; + .icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + .icon-exists, .icon-note {display: none;} + img { + } + } + + #zakladka-box { + padding: 20px; + width: 270px; + border: 1px solid #007880; + border-radius: 8px; + box-shadow: 6px 6px 10px 0px rgba(0, 120, 128, 0.35); + background: #E1F1F2; + position: relative; + left: -225px; + z-index: 3; + @include rwd($break-wide) { + left: -12px; + } + + .pointer-bottom { + transform: rotate(45deg); + left: 238px; + top: -7px; + width: 12px; + height: 12px; + position: absolute; + box-sizing: border-box; + + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 1; + border-radius: 0px 0px 2px 0px; + background: var(--white-100, #E1F1F2); + border: 1px solid var(--teal-700, #007880); + display: block; + + @include rwd($break-wide) { + left: 25px; + } + } + .pointer-top { + left: 238px; + top: -5px; + transform: rotate(45deg); + display: block; + width: 12px; + height: 12px; + position: absolute; + z-index: 3; + border-radius: 0px 0px 2px 0px; + background: #E1F1F2; + display: block; + @include rwd($break-wide) { + left: 25px; + } + } + + + .content { + display: flex; + align-items: flex-start; + flex-direction: column; + gap: 12px; + .zakladka-tool { + color: #333; + line-height: 30px; + font-family: "Source Sans Pro"; + font-size: 18px; + font-style: normal; + font-weight: 400; + cursor: pointer; + padding-left: 38px; + + &:before { + height: 30px; + width: 30px; + display: inline-block; + vertical-align: bottom; + margin-left: -38px; + margin-right: 8px; + } + + &.zakladka-tool_zakladka_delete:before { + content: url('/static/2022/images/zakladka-usun.svg'); + } + &.zakladka-tool_zakladka:before { + content: url('/static/2022/images/add-icon.svg'); + } + &.zakladka-tool_sluchaj:before { + content: url('/static/2022/images/play-now-icon.svg'); + } + &.zakladka-tool_notka:before { + content: url('/static/2022/images/add-note-icon.svg'); + } + } + .zakladka-tool_notka_text { + display: none; + width: 100%; + position: relative; + textarea { + width: 100%; + +display: flex; +padding: 16px; +justify-content: center; +align-items: center; +gap: 8px; +flex: 1 0 0; +border-radius: 6px; +border: 1px solid #007880; +background: #fff; + } + #notka-save, #notka-saved { + position: absolute; + top: 4px; + right: 4px; + display: none; + width: 16px; + height: 16px; + } + #notka-saved { + } + } + .zakladka-tool_zakladka_delete { + display: none; +// position: absolute; +// bottom: 40px; +// right: 3px; +// width: 29px; +// height: 29px; +// cursor: pointer; +// align-items: center; +// justify-content: center; + } + } + } + + + + &.zakladka-exists { + .icon-empty {display: none;} + .icon-exists {display: flex;} + #zakladka-box .content { + .zakladka-tool_notka_text {display: block;} + .zakladka-tool_zakladka_delete {display: block;} + } + } + &.zakladka-note { + .icon-empty {display: none;} + .icon-note {display: flex;} + #zakladka-box .content { + .zakladka-tool_notka_text {display: block;} + .zakladka-tool_zakladka_delete {display: block;} + } + } +} + + + +/* +.zakladka-tool_sluchaj {display: none;} +.has-sync .zakladka-tool_sluchaj {display: block;} +*/ + +#book-text-buttons { + display: none; + margin-top: 16px; + border: 1px solid #D5ECED; + border-radius: 6px; + padding: 8px; + max-width: 717px; + + + a { + display: inline-block; + padding: 20px 30px; + font-weight: bold; + cursor: pointer; + border-radius: 6px; + &:hover { + background: #E1F1F2; + } + i { + margin-right: 10px; + &.icon-play { + color: white; + background: #018189; + display: inline-block; + padding: 10px 8px 10px 12px; + border-radius: 100%; + } + } + } +} diff --git a/src/wolnelektury/static/2022/styles/reader_player.scss b/src/wolnelektury/static/2022/styles/reader_player.scss index 28cc2d04b..bf0fb2b29 100644 --- a/src/wolnelektury/static/2022/styles/reader_player.scss +++ b/src/wolnelektury/static/2022/styles/reader_player.scss @@ -3,6 +3,11 @@ @import "components/select"; + + + +#player-bar { + // copied from local, move to base .jp-state-playing .icon-play { &:before { @@ -18,6 +23,8 @@ // * + + .c-media { margin: 0 auto; } @@ -106,12 +113,13 @@ width: 100%; align-items: center; position: relative; - background-color: black; + color: #083F4D; + background-color: #D5ECED; padding: 0 34px 0 14px; } .c-player__btn { - background: white; + background: #083F4D; border: 0; outline: 0; border-radius: 50%; @@ -196,7 +204,7 @@ } .icon { - color: white; + color: #083F4D; font-size: 16px; } } @@ -352,7 +360,7 @@ .icon { font-size: 21px; - color: white; + color: #083F4D; margin-right: 8px; cursor: pointer; } @@ -416,7 +424,7 @@ &.up:after { content: '▲'; - color: white; + color: #083F4D; font-size: .8em; position: absolute; top: 0px; @@ -426,7 +434,7 @@ } &.down:after { content: '▼'; - color: white; + color: #083F4D; font-size: .8em; position: absolute; bottom: 2px; @@ -436,6 +444,7 @@ } } +} #menu { @@ -448,7 +457,7 @@ .with-player-bar .playing-highlight { - background: #D5ECED; + background: #FFE694; } @@ -460,9 +469,6 @@ #player-bar { display: block; } - .syncable { - cursor: pointer; - } } .annoy-banner-on_blackout { diff --git a/src/wolnelektury/static/js/book_text/marker.js b/src/wolnelektury/static/js/book_text/marker.js new file mode 100644 index 000000000..5db4d7e7a --- /dev/null +++ b/src/wolnelektury/static/js/book_text/marker.js @@ -0,0 +1,36 @@ +(function($){$(function(){ + + class PMarker { + putBox(box) { + + let $z = $(this).closest('.zakladka'); + let $box = $("#zakladka-box"); + $z.append($box); + $box.data('z', $z); + + anchor = $z.data('anchor'); + let note = anchor in zakladki ? zakladki[anchor].note : ''; + $('textarea', $box).val(note); + + // TODO update note content here. + // And/or delete buttons. + $box.toggle(); + + + } + + + + showForAnchor(anchor) { + } + + showForP(p) { + } + } + + $.PMarker = PMarker; + + // There can be more than one marker. + // Some markers + +})})(jQuery); diff --git a/src/wolnelektury/static/js/book_text/note.js b/src/wolnelektury/static/js/book_text/note.js index 2f0feaaea..277f3d48e 100644 --- a/src/wolnelektury/static/js/book_text/note.js +++ b/src/wolnelektury/static/js/book_text/note.js @@ -1,9 +1,10 @@ (function($){$(function(){ -if ($('#nota_red li').length > 0) { - $("#menu-nota_red").show(); -} + if ($('#nota_red p').length > 0) { + $("#info").prepend($("
")); + $("#info").prepend($('#nota_red *')); + } diff --git a/src/wolnelektury/static/js/book_text/pbox-items.js b/src/wolnelektury/static/js/book_text/pbox-items.js new file mode 100644 index 000000000..2f9a502e8 --- /dev/null +++ b/src/wolnelektury/static/js/book_text/pbox-items.js @@ -0,0 +1,38 @@ +// i18n for labels? +// maybe move labels to templates after all? + +(function($){$(function(){ + + class PBoxItem { + update(pbox) { + if (this.isAvailable(pbox)) { + pbox.showButton(this, this.label, this.pboxClass); + } + } + } + + + class LoginPBI extends PBoxItem { + label = 'ZALOGUJ'; + pboxClass = 'zakladka-tool_login'; + + isAvailable(pbox) { + return true; + } + + action() { + alert('akcja'); + } + } + + + class BookmarkPBI extends PBoxItem { + label = 'DODAJ ZAKŁADKĘ' + + } + + + $.pbox.addItem(new LoginPBI()); + + +})})(jQuery); diff --git a/src/wolnelektury/static/js/book_text/pbox.js b/src/wolnelektury/static/js/book_text/pbox.js new file mode 100644 index 000000000..816938cd1 --- /dev/null +++ b/src/wolnelektury/static/js/book_text/pbox.js @@ -0,0 +1,46 @@ +(function($){$(function(){ + + class PBox { + items = []; + + constructor(element) { + this.$element = element; + } + + addItem(item) { + this.items.unshift(item); + } + + clear() { + $("div", this.$element).remove(); + } + + showButton(item, text, cls) { + let btn = $("
"); + btn.addClass('zakladka-tool'); + btn.addClass('cls'); + btn.text(text); + btn.on('click', item.action); + this.$element.append(btn); + } + + // What's a p? + // We should open at a *marker*. + // And it's the marker that should know its context. + openAt(marker) { + this.marker = marker; + this.clear(); + $.each(this.items, (i, item) => { + item.update(this); + }); + } + + close() { + } + } + + $.pbox = new PBox($('#zakladka-box')); // TODO: rename id + + + +})})(jQuery); diff --git a/src/wolnelektury/static/js/book_text/progress.js b/src/wolnelektury/static/js/book_text/progress.js index d57a32fbd..ae5b14ac4 100644 --- a/src/wolnelektury/static/js/book_text/progress.js +++ b/src/wolnelektury/static/js/book_text/progress.js @@ -1,6 +1,7 @@ (function($){$(function(){ - t = $('#global-progress').data('t'); + let t = $('#global-progress').data('t'); + function upd_t() { $text = $('#main-text #book-text'); texttop = $text.offset().top; diff --git a/src/wolnelektury/static/js/book_text/references.js b/src/wolnelektury/static/js/book_text/references.js index cdfc1f06f..146067523 100644 --- a/src/wolnelektury/static/js/book_text/references.js +++ b/src/wolnelektury/static/js/book_text/references.js @@ -1,4 +1,5 @@ (function($){$(function(){ + let csrf = $("[name='csrfmiddlewaretoken']").val(); var interestingReferences = $("#interesting-references").text(); if (interestingReferences) { @@ -104,4 +105,579 @@ _paq.push(['trackEvent', 'html', 'reference']); }); + + + function putNoteAt($elem, anchor, side) { + $elem.data('anchoredTo', anchor); + updateNote($elem, side); + } + + function updateNote($elem, side) { + anchor = $elem.data('anchoredTo') + if (!anchor) return; + let anchorRect = anchor.getBoundingClientRect(); + + let x = anchorRect.x + anchorRect.width / 2; + let y = anchorRect.y; + if ($elem.data('attach-bottom')) { + y += anchorRect.height; + } + minx = $("#book-text").position().left; + maxx = minx + $("#book-text").width(); + + margin = 20; + minx += margin; + maxx -= margin; + maxx += 10000; + + //boxwidth = 470; + boxwidth = $elem.width(); + + if (maxx - minx <= boxwidth) { + nx = margin; + right = margin; + leftoffset = x - margin; + } else { + right = ''; + + // default position. + leftoffset = 40; + leftoffset = $elem.data('default-leftoffset'); + + nx = x - leftoffset; + + $elem.css({right: ''}); + + // Do we need to move away from the left? + if (nx < minx) { + let d = minx - nx; + nx += d; + leftoffset -= d; + } + + // Do we need to move away from the right? + if (nx + boxwidth > maxx) { + // ACTUALLY CALCULATE STUFF + // if maxx - minx < 470 px -- daj z lewej do prawej i już! + + right = ''; + let d = nx + boxwidth - maxx; + //if (leftoffset + d > $elem.width() - 10) d = $elem.width() - leftoffset - 10; + nx -= d; + leftoffset += d; + } + } + $elem.css({ + left: nx, + right: right + }); + if (!$elem.data('attach-bottom')) { + ny = y - $elem.height() - 10; + } else { + ny = y + 10; + } + $elem.css({ + top: ny + }); + $('.pointer', $elem).css({ + left: leftoffset - 6 + }); + + $elem.css({ + display: "block" + }); + } + + function closeNoteBox() { + $('#annotation-box').data('anchoredTo', null).fadeOut(); + } + $(document).on('click', function(event) { + let t = $(event.target); + if (t.parents('#annotation-box').length && !t.is('#footnote-link')) { + return; + } + closeNoteBox(); + }); + $(window).on('resize', closeNoteBox); + + function getPositionInBookText($e) { + let x = 0, y = 0; + + // Ok dla Y, nie ok dla X + + while ($e.attr('id') != 'book-text') { + let p = $e.position(); + x += p.left; + y += p.top; + $e = $e.offsetParent(); + break; + } + return {"x": x, "y": y} + } + + $('#book-text .annotation').on('click', function(event) { + if ($(this).parents('#footnotes').length) return; + event.preventDefault(); + + + + let x = $(this).width() / 2, y = 0; + let elem = $(this); + while (elem.attr('id') != 'book-text') { + let p = $(elem).position(); + x += p.left; + y += p.top; + elem = elem.parent(); + } + href = $(this).attr('href').substr(1); + content = $("[name='" + href + "']").next().next().html(); + if (!content) return; + $("#annotation-content").html(content); + $("#footnote-link").attr('href', '#' + href) + + + putNoteAt($('#annotation-box'), this); + event.stopPropagation(); + }); + + + + let zakladki = {}; + $.get({ + url: '/zakladki/', + success: function(data) { + zakladki = data; + $.each(zakladki, (i, e) => { + zakladkaUpdateFor( + // TODO: not just paragraphs. + $('[href="#' + e.anchor + '"]').nextAll('.paragraph').first() + ); + }); + } + }); + + // TODO: create bookmarks on init + // We need to do that from anchors. + + function zakladkaUpdateFor($item) { + + let anchor = $item.prevAll('.target').first().attr('name'); + + if (anchor in zakladki) { + let $booktag = $item.data('booktag'); + if (!$booktag) { + + // TODO: only copy without the dialog. + $booktag = $("
"); + $booktag.append($('.icon', $zakladka).clone()); + + $item.data('booktag', $booktag); + $booktag.data('p', $item); + $booktag.data('anchor', anchor); + $zakladka.after($booktag); + + zakladkaSetPosition($booktag); + $booktag.show(); + } + + $z = $booktag; + if (zakladki[anchor].note) { + $z.removeClass('zakladka-exists'); + $z.addClass('zakladka-note'); + } else { + $z.removeClass('zakladka-note'); + $z.addClass('zakladka-exists'); + } + } else { + let $booktag = $item.data('booktag'); + if ($booktag) { + $item.data('booktag', null); + $zakladka.append($("#zakladka-box")); + $booktag.remove(); + } + } + } + + function zakladkaSetPosition($z) { + $item = $z.data('p'); + pos = getPositionInBookText($item); + $z.css({ + display: 'block', + top: pos.y, + right: ($('#main-text').width() - $('#book-text').width()) / 2, + }); + } + + let $zakladka = $('#zakladka'); + $('#book-text .paragraph').on('mouseover', function() {showMarker($(this));}); + $('#book-text .verse').on('mouseover', function() {showMarker($(this));}); + //$.PMarker.showForP(this); + + + function showMarker(p) { + + // Close the currently tag box when moving to another one. + // TBD: Do we want to keep the box open and prevent moving? + $("#zakladka-box").hide(); + + let anchor = p.prevAll('.target').first().attr('name'); + // Don't bother if there's no anchor to use. + if (!anchor) return; + + // Only show tag if there is not already a tag for this p. + if (p.data('booktag')) { + $zakladka.hide(); + } else { + $zakladka.data('p', p); + $zakladka.data('anchor', anchor); + + // (not needed) zakladkaUpdateClass(); + // TODO: UPDATE THIS ON OPEN? + //let note = anchor in zakladki ? zakladki[anchor].note : ''; + //$('textarea', $zakladka).val(note); + + zakladkaSetPosition($zakladka); + $zakladka.show(); + } + } + + $(".zakladka-tool_zakladka").on('click', function() { + let $z = $("#zakladka-box").data('z'); + let anchor = $z.data('anchor'); + let $p = $z.data('p'); + $.post({ + url: '/zakladki/', + data: { + csrfmiddlewaretoken: csrf, + anchor: anchor + }, + success: function(data) { + zakladki[data.anchor] = data; + $("#zakladka-box").hide(); + + // Just hide, and create new .zakladka if not already exists? + // In general no hiding 'classed' .zakladka. + // So the 'cursor' .zakladka doesn't ever need class update. + //zakladkaUpdateClass(); + zakladkaUpdateFor($p); + + } + }); + }); + + $(".zakladka-tool_notka_text textarea").on('input', function() { + // FIXME: no use const $zakladka here, check which .zakladka are we attached to. + let $z = $(this).closest('.zakladka'); + let anchor = $z.data('anchor'); + + $("#notka-saved").hide(); + //$("#notka-save").show(); + $.post({ + url: '/zakladki/' + zakladki[anchor].uuid + '/', + data: { + csrfmiddlewaretoken: csrf, + note: $(this).val() + }, + success: function(data) { + zakladki[anchor] = data; + zakladkaUpdateFor($z.data('p')); + $("#notka-save").hide(); + $("#notka-saved").fadeIn(); + } + }); + }); + + $(".zakladka-tool_zakladka_delete").on('click', function() { + let $z = $(this).closest('.zakladka'); + let anchor = $z.data('anchor'); + $.post({ + url: '/zakladki/' + zakladki[anchor].uuid + '/delete/', + data: { + csrfmiddlewaretoken: csrf, + }, + success: function(data) { + delete zakladki[anchor]; + $("#zakladka-box").hide(); + zakladkaUpdateFor($z.data('p')); + } + }); + }); + + $("#main-text").on("click", ".zakladka .icon", function() { + let $z = $(this).closest('.zakladka'); + let $box = $("#zakladka-box"); + $z.append($box); + $box.data('z', $z); + + let $p = $z.data('p'); + let anchor = $z.data('anchor'); + let note = anchor in zakladki ? zakladki[anchor].note : ''; + + $('.zakladka-tool_zakladka', $box).toggle(!(anchor in zakladki)); + $('.zakladka-tool_sluchaj', $box).toggle($p.hasClass('syncable')).data('sync', $p.attr('id')); + $('textarea', $box).val(note); + + $box.toggle(); + }); + + + class QBox { + constructor(qbox) { + this.qbox = qbox; + } + showForSelection(sel) { + // TODO: only consider ranges inside text.? + this.selection = sel; + + // TODO: multiple ranges. + let range = sel.getRangeAt(0); + let rect = range.getBoundingClientRect(); + + putNoteAt(this.qbox, range) + } + showForBlock(b) { + let rect = b.getBoundingClientRect(); + + putNoteAt(this.qbox, b, 'left') + } + hide() { + this.qbox.data('anchoredTo', null); + this.qbox.fadeOut(); + } + hideCopied() { + this.qbox.data('anchoredTo', null); + this.qbox.addClass('copied').fadeOut(1500, () => { + this.qbox.removeClass('copied'); + }); + } + + copyText() { + // TODO: only consider ranges inside text.? + let range = this.selection.getRangeAt(0); + let e = range.startContainer; + let anchor = getIdForElem(e); + let text = window.location.protocol + '//' + + window.location.host + + window.location.pathname; + + navigator.clipboard.writeText( + this.selection.toString() + + '\n\nCałość czytaj na: ' + text + ); + this.hideCopied(); + } + copyLink() { + // TODO: only consider ranges inside text.? + let range = this.selection.getRangeAt(0); + let e = range.startContainer; + let anchor = getIdForElem(e); + let text = window.location.protocol + '//' + + window.location.host + + window.location.pathname; + if (anchor) text += anchor; + navigator.clipboard.writeText(text); + + this.hideCopied(); + } + quote() { + // What aboot non-contiguous selections? + let sel = this.selection; + let textContent = sel.toString(); + let anchor = getIdForElem(sel.getRangeAt(0).startContainer); + let paths = getSelectionPaths(sel); + $.post({ + url: '/cytaty/', + data: { + csrfmiddlewaretoken: csrf, + text: textContent, + startElem: anchor, + //endElem: endElem, + //startOffset: 0, + //endOffset: 0, + paths: paths, + }, + success: function (data) { + var win = window.open('/cytaty/' + data.uuid + '/', '_blank'); + } + }); + + } + + } + let qbox = new QBox($("#qbox")); + + + function getPathToNode(elem) { + // Need normalize? + let path = []; + while (elem.id != 'book-text') { + let p = elem.parentElement; + path.unshift([...p.childNodes].indexOf(elem)) + elem = p; + } + return path; + } + function getSelectionPaths(selection) { + // does it work? + let range1 = selection.getRangeAt(0); + let range2 = selection.getRangeAt(selection.rangeCount - 1); + let paths = [ + getPathToNode(range1.startContainer) + [range1.startOffset], + getPathToNode(range2.endContainer) + [range2.endOffset] + ] + return paths; + } + + + function getIdForElem(elem) { + // is it used? + let $elem = $(elem); + // check if inside book-text? + + while (true) { + if ($elem.hasClass('target')) { + return $elem.attr('name'); + } + $p = $elem.prev(); + if ($p.length) { + $elem = $p; + } else { + // Gdy wychodzimy w górę -- to jest ten moment, w którym znajdujemy element od którego wychodzimy i zliczamy znaki. + + + $p = $elem.parent() + if ($p.length) { + // is there text? + $elem = $p; + } else { + return undefined; + } + } + } + } + + function getIdForElem(elem) { + // is it used? + // check if inside book-text? + $elem = $(elem); + while (true) { + if ($elem.hasClass('target')) { + return $elem.attr('name'); + } + $p = $elem.prev(); + if ($p.length) { + $elem = $p; + } else { + $p = $elem.parent() + if ($p.length) { + // is there text? + $elem = $p; + } else { + return undefined; + } + } + } + } + + + function positionToIIDOffset(container, offset) { + // Container and offset follow Range rules. + // If container is a text node, offset is text offset. + // If container is an element node, offset is number of child nodes from container start. + // (containers of type Comment, CDATASection - ignore)z + } + + + function updateQBox() { + sel = document.getSelection(); + let goodS = true; + if (sel.isCollapsed || sel.rangeCount < 1) { + goodS = false; + } + + if (!goodS) { + qbox.hide(); + } else { + qbox.showForSelection(sel); + } + }; + $(document).on('selectionchange', updateQBox); + + function updateBoxes() { + updateNote(qbox.qbox); + updateNote($('#annotation-box')); + + } + $(window).on('scroll', updateBoxes); + $(window).on('resize', updateBoxes); + + + $(window).on('resize', function() { + $('.zakladka').each(function() { + zakladkaSetPosition($(this)); + }); + }); + + $('a.anchor').on('click', function(e) { + e.preventDefault(); + + let sel = window.getSelection(); + sel.removeAllRanges(); + let range = document.createRange(); + + let $p = $(this).nextAll('.paragraph').first() + range.selectNode($p[0]); + sel.addRange(range); + + qbox.showForSelection(sel); + + showMarker($p); + }); + + + + $('.qbox-t-copy').on('click', function(e) { + e.preventDefault(); + qbox.copyText(); + }); + $('.qbox-t-link').on('click', function(e) { + e.preventDefault(); + qbox.copyLink(); + }); + $('.qbox-t-quote').on('click', function(e) { + e.preventDefault(); + qbox.quote(); + }); + + + /* + $(".paragraph").on('click', function(e) { + qbox.showForBlock(this); + }); + */ + + + function scrollToAnchor(anchor) { + if (anchor) { + var anchor_name = anchor.slice(1); + var element = $('a[name="' + anchor_name + '"]'); + if (element.length > 0) { + $("html").animate({ + scrollTop: element.offset().top - 55 + }, { + duration: 500, + done: function() { + history.pushState({}, '', anchor); + }, + }); + } + } + } + scrollToAnchor(window.location.hash) + $('#toc, #themes, #book-text, #annotation').on('click', 'a', function(event) { + event.preventDefault(); + scrollToAnchor($(this).attr('href')); + }); + + })})(jQuery); diff --git a/src/wolnelektury/static/js/contrib/jquery.scrollto.js b/src/wolnelektury/static/js/contrib/jquery.scrollto.js index c403ab9df..0717e0aa6 100644 --- a/src/wolnelektury/static/js/contrib/jquery.scrollto.js +++ b/src/wolnelektury/static/js/contrib/jquery.scrollto.js @@ -1,194 +1,215 @@ -/** - * jQuery.ScrollTo - * Copyright (c) 2007-2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com - * Dual licensed under MIT and GPL. - * Date: 9/11/2008 - * - * @projectDescription Easy element scrolling using jQuery. - * http://flesler.blogspot.com/2007/10/jqueryscrollto.html - * Tested with jQuery 1.2.6. On FF 2/3, IE 6/7, Opera 9.2/5 and Safari 3. on Windows. - * +/*! + * jQuery.scrollTo + * Copyright (c) 2007 Ariel Flesler - aflesler ○ gmail • com | https://github.com/flesler + * Licensed under MIT + * https://github.com/flesler/jquery.scrollTo + * @projectDescription Lightweight, cross-browser and highly customizable animated scrolling with jQuery * @author Ariel Flesler - * @version 1.4 - * - * @id jQuery.scrollTo - * @id jQuery.fn.scrollTo - * @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements. - * The different options for target are: - * - A number position (will be applied to all axes). - * - A string position ('44', '100px', '+=90', etc ) will be applied to all axes - * - A jQuery/DOM element ( logically, child of the element to scroll ) - * - A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc ) - * - A hash { top:x, left:y }, x and y can be any kind of number/string like above. - * @param {Number} duration The OVERALL length of the animation, this argument can be the settings object instead. - * @param {Object,Function} settings Optional set of settings or the onAfter callback. - * @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'. - * @option {Number} duration The OVERALL length of the animation. - * @option {String} easing The easing method for the animation. - * @option {Boolean} margin If true, the margin of the target element will be deducted from the final position. - * @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }. - * @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes. - * @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends. - * @option {Function} onAfter Function to be called after the scrolling ends. - * @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends. - * @return {jQuery} Returns the same jQuery object, for chaining. - * - * @desc Scroll to a fixed position - * @example $('div').scrollTo( 340 ); - * - * @desc Scroll relatively to the actual position - * @example $('div').scrollTo( '+=340px', { axis:'y' } ); - * - * @dec Scroll using a selector (relative to the scrolled element) - * @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } ); - * - * @ Scroll to a DOM element (same for jQuery object) - * @example var second_child = document.getElementById('container').firstChild.nextSibling; - * $('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){ - * alert('scrolled!!'); - * }}); - * - * @desc Scroll on both axes, to different values - * @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } ); + * @version 2.1.3 */ -;(function( $ ){ - - var $scrollTo = $.scrollTo = function( target, duration, settings ){ - $(window).scrollTo( target, duration, settings ); +;(function(factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module !== 'undefined' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Global + factory(jQuery); + } +})(function($) { + 'use strict'; + + var $scrollTo = $.scrollTo = function(target, duration, settings) { + return $(window).scrollTo(target, duration, settings); }; $scrollTo.defaults = { - axis:'y', - duration:1 + axis:'xy', + duration: 0, + limit:true }; - // Returns the element that needs to be animated to scroll the window. - // Kept for backwards compatibility (specially for localScroll & serialScroll) - $scrollTo.window = function( scope ){ - return $(window).scrollable(); - }; + function isWin(elem) { + return !elem.nodeName || + $.inArray(elem.nodeName.toLowerCase(), ['iframe','#document','html','body']) !== -1; + } - // Hack, hack, hack... stay away! - // Returns the real elements to scroll (supports window/iframes, documents and regular nodes) - $.fn.scrollable = function(){ - return this.map(function(){ - // Just store it, we might need it - var win = this.parentWindow || this.defaultView, - // If it's a document, get its iframe or the window if it's THE document - elem = this.nodeName == '#document' ? win.frameElement || win : this, - // Get the corresponding document - doc = elem.contentDocument || (elem.contentWindow || elem).document, - isWin = elem.setInterval; - - return elem.nodeName == 'IFRAME' || isWin && $.browser.safari ? doc.body - : isWin ? doc.documentElement - : this; - }); - }; + function isFunction(obj) { + // Brought from jQuery since it's deprecated + return typeof obj === 'function' + } - $.fn.scrollTo = function( target, duration, settings ){ - if( typeof duration == 'object' ){ + $.fn.scrollTo = function(target, duration, settings) { + if (typeof duration === 'object') { settings = duration; duration = 0; } - if( typeof settings == 'function' ) + if (typeof settings === 'function') { settings = { onAfter:settings }; + } + if (target === 'max') { + target = 9e9; + } - settings = $.extend( {}, $scrollTo.defaults, settings ); + settings = $.extend({}, $scrollTo.defaults, settings); // Speed is still recognized for backwards compatibility - duration = duration || settings.speed || settings.duration; + duration = duration || settings.duration; // Make sure the settings are given right - settings.queue = settings.queue && settings.axis.length > 1; - - if( settings.queue ) + var queue = settings.queue && settings.axis.length > 1; + if (queue) { // Let's keep the overall duration duration /= 2; - settings.offset = both( settings.offset ); - settings.over = both( settings.over ); + } + settings.offset = both(settings.offset); + settings.over = both(settings.over); + + return this.each(function() { + // Null target yields nothing, just like jQuery does + if (target === null) return; - return this.scrollable().each(function(){ - var elem = this, + var win = isWin(this), + elem = win ? this.contentWindow || window : this, $elem = $(elem), - targ = target, toff, attr = {}, - win = $elem.is('html,body'); + targ = target, + attr = {}, + toff; - switch( typeof targ ){ + switch (typeof targ) { // A number will pass the regex case 'number': case 'string': - if( /^([+-]=)?\d+(px)?$/.test(targ) ){ - targ = both( targ ); + if (/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)) { + targ = both(targ); // We are done break; } - // Relative selector, no break! - targ = $(targ,this); + // Relative/Absolute selector + targ = win ? $(targ) : $(targ, elem); + /* falls through */ case 'object': + if (targ.length === 0) return; // DOMElement / jQuery - if( targ.is || targ.style ) + if (targ.is || targ.style) { // Get the real position of the target toff = (targ = $(targ)).offset(); + } } - $.each( settings.axis.split(''), function( i, axis ){ - var Pos = axis == 'x' ? 'Left' : 'Top', + + var offset = isFunction(settings.offset) && settings.offset(elem, targ) || settings.offset; + + $.each(settings.axis.split(''), function(i, axis) { + var Pos = axis === 'x' ? 'Left' : 'Top', pos = Pos.toLowerCase(), key = 'scroll' + Pos, - old = elem[key], - Dim = axis == 'x' ? 'Width' : 'Height', - dim = Dim.toLowerCase(); + prev = $elem[key](), + max = $scrollTo.max(elem, axis); - if( toff ){// jQuery / DOMElement - attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] ); + if (toff) {// jQuery / DOMElement + attr[key] = toff[pos] + (win ? 0 : prev - $elem.offset()[pos]); // If it's a dom element, reduce the margin - if( settings.margin ){ - attr[key] -= parseInt(targ.css('margin'+Pos)) || 0; - attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0; + if (settings.margin) { + attr[key] -= parseInt(targ.css('margin'+Pos), 10) || 0; + attr[key] -= parseInt(targ.css('border'+Pos+'Width'), 10) || 0; } - attr[key] += settings.offset[pos] || 0; + attr[key] += offset[pos] || 0; - if( settings.over[pos] ) + if (settings.over[pos]) { // Scroll to a fraction of its width/height - attr[key] += targ[dim]() * settings.over[pos]; - }else - attr[key] = targ[pos]; + attr[key] += targ[axis === 'x'?'width':'height']() * settings.over[pos]; + } + } else { + var val = targ[pos]; + // Handle percentage values + attr[key] = val.slice && val.slice(-1) === '%' ? + parseFloat(val) / 100 * max + : val; + } // Number or 'number' - if( /^\d+$/.test(attr[key]) ) + if (settings.limit && /^\d+$/.test(attr[key])) { // Check the limits - attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max(Dim) ); + attr[key] = attr[key] <= 0 ? 0 : Math.min(attr[key], max); + } - // Queueing axes - if( !i && settings.queue ){ - // Don't waste time animating, if there's no need. - if( old != attr[key] ) + // Don't waste time animating, if there's no need. + if (!i && settings.axis.length > 1) { + if (prev === attr[key]) { + // No animation needed + attr = {}; + } else if (queue) { // Intermediate animation - animate( settings.onAfterFirst ); - // Don't animate this axis again in the next iteration. - delete attr[key]; + animate(settings.onAfterFirst); + // Don't animate this axis again in the next iteration. + attr = {}; + } } }); - animate( settings.onAfter ); - function animate( callback ){ - $elem.animate( attr, duration, settings.easing, callback && function(){ - callback.call(this, target, settings); + animate(settings.onAfter); + + function animate(callback) { + var opts = $.extend({}, settings, { + // The queue setting conflicts with animate() + // Force it to always be true + queue: true, + duration: duration, + complete: callback && function() { + callback.call(elem, targ, settings); + } }); - }; - function max( Dim ){ - var attr ='scroll'+Dim, - doc = elem.ownerDocument; - - return win - ? Math.max( doc.documentElement[attr], doc.body[attr] ) - : elem[attr]; - }; - }).end(); + $elem.animate(attr, opts); + } + }); + }; + + // Max scrolling position, works on quirks mode + // It only fails (not too badly) on IE, quirks mode. + $scrollTo.max = function(elem, axis) { + var Dim = axis === 'x' ? 'Width' : 'Height', + scroll = 'scroll'+Dim; + + if (!isWin(elem)) + return elem[scroll] - $(elem)[Dim.toLowerCase()](); + + var size = 'client' + Dim, + doc = elem.ownerDocument || elem.document, + html = doc.documentElement, + body = doc.body; + + return Math.max(html[scroll], body[scroll]) - Math.min(html[size], body[size]); }; - function both( val ){ - return typeof val == 'object' ? val : { top:val, left:val }; + function both(val) { + return isFunction(val) || $.isPlainObject(val) ? val : { top:val, left:val }; + } + + // Add special hooks so that window scroll properties can be animated + $.Tween.propHooks.scrollLeft = + $.Tween.propHooks.scrollTop = { + get: function(t) { + return $(t.elem)[t.prop](); + }, + set: function(t) { + var curr = this.get(t); + // If interrupt is true and user scrolled, stop animating + if (t.options.interrupt && t._last && t._last !== curr) { + return $(t.elem).stop(); + } + var next = Math.round(t.now); + // Don't waste CPU + // Browsers don't render floating point scroll + if (curr !== next) { + $(t.elem)[t.prop](next); + t._last = this.get(t); + } + } }; -})( jQuery ); \ No newline at end of file + // AMD requirement + return $scrollTo; +}); diff --git a/src/wolnelektury/urls.py b/src/wolnelektury/urls.py index afee7f1f9..cfecb5078 100644 --- a/src/wolnelektury/urls.py +++ b/src/wolnelektury/urls.py @@ -57,6 +57,7 @@ urlpatterns += [ path('towarzystwo/', RedirectView.as_view(url='/pomagam/', permanent=False, query_string=True)), path('towarzystwo/', RedirectView.as_view( url='/pomagam/%(path)s', permanent=False)), + path('', include('bookmarks.urls')), path('chunks/', include('chunks.urls')), -- 2.20.1 From 59b18d2848d2561f1547b78921936a501d0f0b37 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Jun 2024 15:20:10 +0200 Subject: [PATCH 10/16] quickfix --- src/bookmarks/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bookmarks/views.py b/src/bookmarks/views.py index 7d83818c1..0ff5104de 100644 --- a/src/bookmarks/views.py +++ b/src/bookmarks/views.py @@ -12,6 +12,8 @@ import re @cache.never_cache def bookmarks(request): + if not request.user.is_authenticated: + return JsonResponse({}) try: slug = request.headers['Referer'].rsplit('.', 1)[0].rsplit('/', 1)[-1] except: -- 2.20.1 From 7dcb126f47bdbc52b9ad33b24386da410ee695b5 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Jun 2024 15:22:13 +0200 Subject: [PATCH 11/16] fix --- src/wolnelektury/static/js/book_text/references.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wolnelektury/static/js/book_text/references.js b/src/wolnelektury/static/js/book_text/references.js index 146067523..87c806195 100644 --- a/src/wolnelektury/static/js/book_text/references.js +++ b/src/wolnelektury/static/js/book_text/references.js @@ -474,7 +474,7 @@ let text = window.location.protocol + '//' + window.location.host + window.location.pathname; - if (anchor) text += anchor; + if (anchor) text += '#' + anchor; navigator.clipboard.writeText(text); this.hideCopied(); -- 2.20.1 From e462859a6ffbeed55529ecaab4fb40ba67c14221 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Jun 2024 15:34:39 +0200 Subject: [PATCH 12/16] missing import --- src/funding/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funding/models.py b/src/funding/models.py index 5afdc2259..81d439df8 100644 --- a/src/funding/models.py +++ b/src/funding/models.py @@ -12,7 +12,7 @@ from django.template.loader import render_to_string from django.urls import reverse from django.utils.html import mark_safe from django.utils.timezone import utc -from django.utils.translation import override +from django.utils.translation import gettext_lazy as _, override from catalogue.models import Book from catalogue.utils import get_random_hash from polls.models import Poll -- 2.20.1 From f1875769ae80dc4d9fd63af069d85ba2631f1876 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Jun 2024 15:55:11 +0200 Subject: [PATCH 13/16] fix --- src/funding/templates/funding/email/thanks.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/funding/templates/funding/email/thanks.txt b/src/funding/templates/funding/email/thanks.txt index 065be470c..3a21a3e4a 100644 --- a/src/funding/templates/funding/email/thanks.txt +++ b/src/funding/templates/funding/email/thanks.txt @@ -3,7 +3,7 @@ {% block body %} -{% trans Dziękujemy za wsparcie - dzięki Tobie uwolnimy kolejną książkę.' %}{% if funding.name %} +{% trans 'Dziękujemy za wsparcie - dzięki Tobie uwolnimy kolejną książkę.' %}{% if funding.name %} {% trans 'Twoje imię i nazwisko lub pseudonim zostaną dodane do listy darczyńców przy opublikowanej książce.' %}{% endif %} {% if funding.perks.exists %} -- 2.20.1 From 581ffb970c3593c925dd4256eb4276c5b9625f92 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 12 Jun 2024 10:31:50 +0200 Subject: [PATCH 14/16] Remove code unneded since 2015. --- src/reporting/templatetags/reporting_stats.py | 41 +++---------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/src/reporting/templatetags/reporting_stats.py b/src/reporting/templatetags/reporting_stats.py index bca84f1d3..c4ce84cf4 100644 --- a/src/reporting/templatetags/reporting_stats.py +++ b/src/reporting/templatetags/reporting_stats.py @@ -1,7 +1,6 @@ # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Wolne Lektury. See NOTICE for more information. # -from functools import wraps from django import template from catalogue.models import Book @@ -9,52 +8,22 @@ from catalogue.models import Book register = template.Library() -class StatsNode(template.Node): - def __init__(self, value, varname=None): - self.value = value - self.varname = varname - - def render(self, context): - if self.varname: - context[self.varname] = self.value - return '' - else: - return str(self.value) - - -def register_counter(f): - """Turns a simple counting function into a registered counter tag. - - You can run a counter tag as a simple {% tag_name %} tag, or - as {% tag_name var_name %} to store the result in a variable. - - """ - @wraps(f) - def wrapped(parser, token): - try: - tag_name, args = token.contents.split(None, 1) - except ValueError: - args = None - return StatsNode(f(), args) - - return register.tag(wrapped) - - -@register_counter +@register.simple_tag def count_books_all(): return Book.objects.all().count() -@register_counter +@register.simple_tag def count_books(): + print('count', Book.objects.filter(children=None).count()) return Book.objects.filter(children=None).count() -@register_counter +@register.simple_tag def count_books_parent(): return Book.objects.exclude(children=None).count() -@register_counter +@register.simple_tag def count_books_root(): return Book.objects.filter(parent=None).count() -- 2.20.1 From 0f43b6ba435ae72565f7a1ccd942edfabc9d6d97 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 14 Jun 2024 15:51:41 +0200 Subject: [PATCH 15/16] one of many positioning fixes --- src/catalogue/templates/catalogue/book_text.html | 7 +++++-- src/wolnelektury/static/2022/styles/layout/_text.scss | 8 ++++---- src/wolnelektury/static/js/book_text/references.js | 11 ++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/catalogue/templates/catalogue/book_text.html b/src/catalogue/templates/catalogue/book_text.html index 22106b473..dbe92591a 100644 --- a/src/catalogue/templates/catalogue/book_text.html +++ b/src/catalogue/templates/catalogue/book_text.html @@ -223,7 +223,6 @@
@@ -270,9 +269,13 @@ / Załóż konto +
+
+ Słuchaj od tego miejsca +
+
{% endif %} -
diff --git a/src/wolnelektury/static/2022/styles/layout/_text.scss b/src/wolnelektury/static/2022/styles/layout/_text.scss index 1c617c7ee..d74496800 100644 --- a/src/wolnelektury/static/2022/styles/layout/_text.scss +++ b/src/wolnelektury/static/2022/styles/layout/_text.scss @@ -468,7 +468,7 @@ div.kwestia div.stanza { p.paragraph { text-align: justify; - margin: 0 0 0 44px; + margin: 0; text-indent: 1.5em; } @@ -565,7 +565,7 @@ table.border td, table.border th { float: left; clear: left; font-size: .8em; - margin-left: 0; + margin-left: -40px; width: 36px; height: auto; padding: 2px; @@ -738,7 +738,6 @@ a.reference.interesting:after { border-radius: 22px; background: white; box-shadow: 6px 6px 10px 0px rgba(0, 120, 128, 0.35); - overflow: hidden; &.copied { .content:after { @@ -753,9 +752,10 @@ a.reference.interesting:after { color: black; justify-content: center; align-items: center; + border-radius: 22px; } } - + .content { overflow: hidden; display: flex; diff --git a/src/wolnelektury/static/js/book_text/references.js b/src/wolnelektury/static/js/book_text/references.js index 87c806195..6abd103d4 100644 --- a/src/wolnelektury/static/js/book_text/references.js +++ b/src/wolnelektury/static/js/book_text/references.js @@ -139,10 +139,11 @@ leftoffset = x - margin; } else { right = ''; - - // default position. - leftoffset = 40; + leftoffset = $elem.data('default-leftoffset'); + if (leftoffset === undefined) { + leftoffset = $elem.width() / 2; + } nx = x - leftoffset; @@ -157,12 +158,8 @@ // Do we need to move away from the right? if (nx + boxwidth > maxx) { - // ACTUALLY CALCULATE STUFF - // if maxx - minx < 470 px -- daj z lewej do prawej i już! - right = ''; let d = nx + boxwidth - maxx; - //if (leftoffset + d > $elem.width() - 10) d = $elem.width() - leftoffset - 10; nx -= d; leftoffset += d; } -- 2.20.1 From f0f0f13c94ff22d0f8a105c604252c6d678a9bab Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 14 Jun 2024 16:08:05 +0200 Subject: [PATCH 16/16] other placement fix --- .../static/js/book_text/references.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/wolnelektury/static/js/book_text/references.js b/src/wolnelektury/static/js/book_text/references.js index 6abd103d4..9fd69282c 100644 --- a/src/wolnelektury/static/js/book_text/references.js +++ b/src/wolnelektury/static/js/book_text/references.js @@ -168,6 +168,9 @@ left: nx, right: right }); + $elem.css({ + display: "block" + }); if (!$elem.data('attach-bottom')) { ny = y - $elem.height() - 10; } else { @@ -180,9 +183,6 @@ left: leftoffset - 6 }); - $elem.css({ - display: "block" - }); } function closeNoteBox() { @@ -216,23 +216,12 @@ if ($(this).parents('#footnotes').length) return; event.preventDefault(); - - - let x = $(this).width() / 2, y = 0; - let elem = $(this); - while (elem.attr('id') != 'book-text') { - let p = $(elem).position(); - x += p.left; - y += p.top; - elem = elem.parent(); - } href = $(this).attr('href').substr(1); content = $("[name='" + href + "']").next().next().html(); if (!content) return; $("#annotation-content").html(content); $("#footnote-link").attr('href', '#' + href) - putNoteAt($('#annotation-box'), this); event.stopPropagation(); }); -- 2.20.1