Initial bookmarks.
authorRadek Czajka <rczajka@rczajka.pl>
Wed, 5 Jun 2024 11:44:54 +0000 (13:44 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Wed, 5 Jun 2024 11:44:54 +0000 (13:44 +0200)
42 files changed:
src/bookmarks/__init__.py [new file with mode: 0644]
src/bookmarks/admin.py [new file with mode: 0644]
src/bookmarks/apps.py [new file with mode: 0644]
src/bookmarks/migrations/0001_initial.py [new file with mode: 0644]
src/bookmarks/migrations/0002_quote.py [new file with mode: 0644]
src/bookmarks/migrations/__init__.py [new file with mode: 0644]
src/bookmarks/models.py [new file with mode: 0644]
src/bookmarks/templates/bookmarks/quote_detail.html [new file with mode: 0644]
src/bookmarks/tests.py [new file with mode: 0644]
src/bookmarks/urls.py [new file with mode: 0644]
src/bookmarks/views.py [new file with mode: 0644]
src/catalogue/models/book.py
src/catalogue/static/player/player.js
src/catalogue/templates/catalogue/book_text.html
src/catalogue/views.py
src/wolnelektury/settings/apps.py
src/wolnelektury/settings/custom.py
src/wolnelektury/settings/static.py
src/wolnelektury/static/2022/images/add-icon.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/add-note-icon.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/notka-saved.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/play-now-icon.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/tool-copy.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/tool-link.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/tool-quote.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/zakladka-full.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/zakladka-note.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/zakladka-usun.svg [new file with mode: 0644]
src/wolnelektury/static/2022/images/zakladka.svg [new file with mode: 0644]
src/wolnelektury/static/2022/styles/layout/_author.scss
src/wolnelektury/static/2022/styles/layout/_bookmarks.scss [new file with mode: 0644]
src/wolnelektury/static/2022/styles/layout/_module.scss
src/wolnelektury/static/2022/styles/layout/_text.scss
src/wolnelektury/static/2022/styles/reader_player.scss
src/wolnelektury/static/js/book_text/marker.js [new file with mode: 0644]
src/wolnelektury/static/js/book_text/note.js
src/wolnelektury/static/js/book_text/pbox-items.js [new file with mode: 0644]
src/wolnelektury/static/js/book_text/pbox.js [new file with mode: 0644]
src/wolnelektury/static/js/book_text/progress.js
src/wolnelektury/static/js/book_text/references.js
src/wolnelektury/static/js/contrib/jquery.scrollto.js
src/wolnelektury/urls.py

diff --git a/src/bookmarks/__init__.py b/src/bookmarks/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/bookmarks/admin.py b/src/bookmarks/admin.py
new file mode 100644 (file)
index 0000000..956f233
--- /dev/null
@@ -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 (file)
index 0000000..c7fc019
--- /dev/null
@@ -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 (file)
index 0000000..eb5f7ff
--- /dev/null
@@ -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 (file)
index 0000000..f413c51
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/src/bookmarks/models.py b/src/bookmarks/models.py
new file mode 100644 (file)
index 0000000..67a4fa5
--- /dev/null
@@ -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 (file)
index 0000000..46181dd
--- /dev/null
@@ -0,0 +1,83 @@
+{% extends 'base.html' %}
+{% load i18n static %}
+
+
+{% block breadcrumbs %}
+  <a><span>Cytaty użytkowników</span></a>
+
+{% endblock %}
+
+{% block main %}
+
+{% with book=object.book %}
+  <main class="l-main page-book">
+    <section class="l-section lay-s-col-rev">
+      {% with first_text=book.get_first_text %}
+        <aside class="l-aside">
+          <figure class="only-l">
+            <a href="{{ book.get_absolute_url }}">
+              <img src="{% if book.cover_clean %}{{ book.cover_clean.url }}{% endif %}" alt="{{ book.pretty_title }}" width="240">
+            </a>
+          </figure>
+          </ul>
+        </aside>
+        <div class="l-content">
+          <header class="l-header">
+            <div class="l-header__content">
+              <p>{% for author in book.authors %}<a href="{{ author.get_absolute_url }}">{{ author.name }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}
+              </p>
+              <h1><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></h1>
+              {% if book.translators.exists %}
+                <p class="l-header__translators">
+                  {% for translator in book.translators.all %}
+                    {% if forloop.first and translator.name != 'tłumacz nieznany' %}
+                      {% trans "tłum." %}
+                    {% endif %}
+                    <a href="{{ translator.get_absolute_url }}">
+                    {{ translator }}</a>{% if not forloop.last %}, {% endif %}
+                  {% endfor %}
+                </p>
+              {% endif %}
+            </div>
+
+
+
+          </header>
+          <article class="">
+
+            <div class="c-media">
+              <div class="lay-row lay-l-block lay-spread">
+                <figure class="only-s book-cover-small">
+                  <a href="{{ book.get_absolute_url }}">
+                    <img src="{% if book.cover_clean %}{{ book.cover_clean.url }}{% endif %}" alt="{{ book.pretty_title }}">
+                  </a>
+                </figure>
+              </div>
+            </div>
+
+              <div class="row">
+                <div class="l-author__quotes">
+                    <div class="l-author__quotes__slider__item">
+                      <em>
+                        <p>
+                          {{ object.text|linebreaksbr }}
+                        </p>
+                      </em>
+                      <p>
+                        <a class="text-link" href="{% url 'book_text' object.book.slug %}#{{ object.start_elem }}">zobacz w treści</a>
+                      </p>
+                  </div>
+                </div>
+              </div>
+          </article>
+        </div>
+      {% endwith %}
+    </section>
+
+  </main>
+
+
+
+
+  {% endwith %}
+{% endblock %}
diff --git a/src/bookmarks/tests.py b/src/bookmarks/tests.py
new file mode 100644 (file)
index 0000000..7ce503c
--- /dev/null
@@ -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 (file)
index 0000000..333afd7
--- /dev/null
@@ -0,0 +1,12 @@
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+    path('zakladki/', views.bookmarks, name='bookmarks'),
+    path('zakladki/<uuid:uuid>/', views.bookmark, name='bookmark'),
+    path('zakladki/<uuid:uuid>/delete/', views.bookmark_delete, name='bookmark_delete'),
+
+    path('cytaty/', views.quotes, name='quotes'),
+    path('cytaty/<uuid:uuid>/', views.quote, name='quote'),
+]
diff --git a/src/bookmarks/views.py b/src/bookmarks/views.py
new file mode 100644 (file)
index 0000000..7d83818
--- /dev/null
@@ -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,
+        })
+
+
index 86010df..ac8c02b 100644 (file)
@@ -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:
index dfac096..cbf9026 100644 (file)
@@ -4,10 +4,13 @@
         $(".book-right-column").remove();
 
         if ($("#player-bar").length) {
-            $("h1").first().after($("<div class='dynamic-insert'><a href='#' class='enable-player-bar'><div class='text'><i class='icon icon-play' style='border: 1px solid black; border-radius: 100%; padding: 1em; margin-right: 1em;'></i> Możesz jednocześnie czytać i słuchać tej lektury!</div></a></div>"));
+            $("#book-text-buttons").append(
+                $("<a class='enable-player-bar'><i class='icon icon-play'></i> zacznij słuchać</a>")
+            ).show();
         }
         
         $(".enable-player-bar").click(function() {
+            
             $('body').addClass('with-player-bar');
             $('.jp-play').click();
             return false;
 
             // 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<smil.length; ++i) {
                     if (smil[i][0] == id) {
index 52f281b..22106b4 100644 (file)
@@ -6,7 +6,7 @@
 {% load chunks %}
 {% load thumbnail %}
 {% load static %}
-{% load annoy_banners from annoy %}
+{% load annoy %}
 
 
 {% block title %}{{ book.pretty_title }}{% endblock %}
         {% content_warning book %}
       </div>
 
+      {#% annoy_banner 'book-start' %#}
+
+      <div id="book-text-buttons">
+      </div>
+
     </div>
   </header>
 
 
-  <article id="main-text">
+  <article id="main-text" {% if book.has_sync_file %}class="has-sync"{% endif %}>
     {% with next=book.get_next_text prev=book.get_prev_text %}
       {% if next %}
         <a class="text_next-book" href="{% url 'book_text' next.slug %}">{{ next.title }}&nbsp;&rarr;</a>
       {% if prev %}
         <a class="text_prev-book" href="{% url 'book_text' prev.slug %}">&larr;&nbsp;{{ prev.title }}</a>
       {% endif %}
-      {{ book_text|safe }}
+      <div class="main-text-body">
+        {{ book_text|safe }}
+      </div>
+
+      <div class="l-checkout__box" style="margin: 2em 0;">
+        {% include 'club/donation_step1_form.html' with form=donation_form %}
+      </div>
     {% endwith %}
 
 
       <a id="reference-link" target="_blank"></a>
     </div>
 
+    <div id="annotation-box"
+         data-default-leftoffset="40"
+    >
+      <div id="annotation">
+        <div id="annotation-content"></div>
+        <a id="footnote-link">Czytaj w przypisie dolnym</a>
+      </div>
+      <div class="pointer pointer-bottom"></div>
+      <div class="pointer pointer-top"></div>
+    </div>
 
-    {% if book.other_versions.exists %}
-      <div class="box" id="other">
-        <h2>{% trans "Inne wersje utworu" %}</h2>
-        <a class="other-text-close" href="#">{% trans "Zamknij drugą wersję" %}</a>
-        <ul>
-          {% spaceless %}
-            {% for other_version in book.other_versions %}
-              <li>
-                <a class="display-other"
-                   data-other="{{ other_version.html_url }}"
-                   href="{% url 'book_text' other_version.slug %}">
-                  {{ other_version.mini_box_nolink }}
-                </a>
-              </li>
-            {% endfor %}
-          {% endspaceless %}
-        </ul>
+    <div id="qbox" class="qbox"
+         data-default-leftoffset="105"
+         data-attach-bottom="true"
+    >
+      <div class="content">
+        <a class="qbox-t-link" href="" title="Skopiuj link"><img src="{% static '2022/images/tool-link.svg' %}" alt="Skopiuj link"></a>
+        <a class="qbox-t-copy" href="" title="Skopiuj cytat"><img src="{% static '2022/images/tool-copy.svg' %}" alt="Skopiuj cytat"></a>
+        {% if request.user.is_authenticated %}
+          <a class="qbox-t-quote" href="" title="Zapisz cytat"><img src="{% static '2022/images/tool-quote.svg' %}" alt="Zapisz cytat"></a>
+        {% endif %}
       </div>
-    {% endif %}
+      <div class="pointer pointer-bottom"></div>
+      <div class="pointer pointer-top"></div>
+    </div>
 
-    <div id="annoy-stubs">
-      {% annoy_banners 'book-text-intermission' %}
+    <div id="zakladka" class="zakladka">
+      <div class="icon">
+        <img style="height: 30px;" class="icon-empty" src="{% static '2022/images/zakladka.svg' %}" alt="Zakładka">
+        <img class="icon-exists" src="{% static '2022/images/zakladka-full.svg' %}" alt="Istniejąca zakładka">
+        <img class="icon-note" src="{% static '2022/images/zakladka-note.svg' %}" alt="Notka">
+      </div>
+      <div id="zakladka-box">
+        <div class="content">
+          {% if request.user.is_authenticated %}
+            <div class="zakladka-tool zakladka-tool_zakladka_delete">
+              Usuń zakładkę
+            </div>
+            <div class="zakladka-tool zakladka-tool_zakladka">
+              Dodaj zakładkę
+            </div>
+            <div class="zakladka-tool zakladka-tool_sluchaj">
+              Słuchaj od tego miejsca
+            </div>
+            <div class="zakladka-tool_notka_text">
+              <textarea placeholder="Notatka"></textarea>
+              <svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" id="notka-save">
+                <circle r="7" cx="8" cy="8" fill="none" stroke="black" stroke-width="2px" stroke-dasharray="1,1" />
+              </svg>
+              <img src="{% static '2022/images/notka-saved.svg' %}" id="notka-saved">
+            </div>
+
+          {% else %}
+            <div class="zakladka-tool zakladka-tool_login">
+              <a href="{% url 'login' %}?next={{ request.path }}">
+                Zaloguj się </a>
+              /
+              <a href="{% url 'register' %}?next={{ request.path }}"> Załóż konto
+              </a>
+          {% endif %}
 
-      {% for insert in inserts %}
-        {% include 'annoy/dynamic_insert.html' %}
-      {% endfor %}
-    </div>
+            </div>
+
+            <div class="pointer pointer-bottom"></div>
+            <div class="pointer pointer-top"></div>
+        </div>
+      </div>
+
+
+      {% if book.other_versions.exists %}
+        <div class="box" id="other">
+          <h2>{% trans "Inne wersje utworu" %}</h2>
+          <a class="other-text-close" href="#">{% trans "Zamknij drugą wersję" %}</a>
+          <ul>
+            {% spaceless %}
+              {% for other_version in book.other_versions %}
+                <li>
+                  <a class="display-other"
+                     data-other="{{ other_version.html_url }}"
+                     href="{% url 'book_text' other_version.slug %}">
+                    {{ other_version.mini_box_nolink }}
+                  </a>
+                </li>
+              {% endfor %}
+            {% endspaceless %}
+          </ul>
+        </div>
+      {% endif %}
+
+      <div id="annoy-stubs">
+        {% annoy_banners 'book-text-intermission' %}
+
+        {% for insert in inserts %}
+          {% include 'annoy/dynamic_insert.html' %}
+        {% endfor %}
+      </div>
 
   </article>
 
index 588cdb0..fc01ad6 100644 (file)
@@ -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(),
     })
 
 
index 769565d..d4faa8b 100644 (file)
@@ -8,6 +8,7 @@ INSTALLED_APPS_OUR = [
     'ajaxable',
     'annoy',
     'api',
+    'bookmarks',
     'catalogue',
     'chunks',
     'dictionary',
index a9ae731..16b5e0a 100644 (file)
@@ -74,3 +74,5 @@ WIDGETS = {}
 SEARCH_CONFIG = 'english'
 SEARCH_CONFIG_SIMPLE = 'simple'
 SEARCH_USE_UNACCENT = False
+
+FEATURE_SYNCHRO = False
index b3edb72..b83691b 100644 (file)
@@ -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 (file)
index 0000000..006ee63
--- /dev/null
@@ -0,0 +1,5 @@
+<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="30" height="30" rx="15" fill="#74BDC2"/>
+<rect x="8" y="14" width="14" height="2" fill="#E1F1F2"/>
+<rect x="16" y="8" width="14" height="2" transform="rotate(90 16 8)" fill="#E1F1F2"/>
+</svg>
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 (file)
index 0000000..ba6a5ff
--- /dev/null
@@ -0,0 +1,4 @@
+<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="30" height="30" rx="15" fill="#74BDC2"/>
+<path d="M15 6L17.0206 12.2188H23.5595L18.2694 16.0623L20.2901 22.2812L15 18.4377L9.70993 22.2812L11.7306 16.0623L6.44049 12.2188H12.9794L15 6Z" fill="#E1F1F2"/>
+</svg>
diff --git a/src/wolnelektury/static/2022/images/notka-saved.svg b/src/wolnelektury/static/2022/images/notka-saved.svg
new file mode 100644 (file)
index 0000000..79eebd1
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+
+<svg
+   width="800px"
+   height="800px"
+   viewBox="0 0 24 24"
+   fill="none"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="notka-saved.svg"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     id="namedview6"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="0.89875"
+     inkscape:cx="299.86092"
+     inkscape:cy="199.16551"
+     inkscape:window-width="1920"
+     inkscape:window-height="1011"
+     inkscape:window-x="0"
+     inkscape:window-y="32"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4" />
+  <path
+     fill-rule="evenodd"
+     clip-rule="evenodd"
+     d="M19.7071 6.29289C20.0976 6.68342 20.0976 7.31658 19.7071 7.70711L10.4142 17C9.63316 17.7811 8.36683 17.781 7.58579 17L3.29289 12.7071C2.90237 12.3166 2.90237 11.6834 3.29289 11.2929C3.68342 10.9024 4.31658 10.9024 4.70711 11.2929L9 15.5858L18.2929 6.29289C18.6834 5.90237 19.3166 5.90237 19.7071 6.29289Z"
+     fill="#0F1729"
+     id="path2" />
+</svg>
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 (file)
index 0000000..4aeace3
--- /dev/null
@@ -0,0 +1,4 @@
+<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="30" height="30" rx="15" fill="#74BDC2"/>
+<path d="M22 15L11.5 21.0622L11.5 8.93782L22 15Z" fill="#E1F1F2"/>
+</svg>
diff --git a/src/wolnelektury/static/2022/images/tool-copy.svg b/src/wolnelektury/static/2022/images/tool-copy.svg
new file mode 100644 (file)
index 0000000..cf26b5a
--- /dev/null
@@ -0,0 +1,6 @@
+<svg width="16" height="22" viewBox="0 0 16 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Group 582">
+<rect id="Rectangle 158" x="3.5" y="1" width="12" height="17" rx="1.5" fill="white" stroke="#007880"/>
+<rect id="Rectangle 159" x="0.5" y="4" width="12" height="17" rx="1.5" fill="white" stroke="#007880"/>
+</g>
+</svg>
diff --git a/src/wolnelektury/static/2022/images/tool-link.svg b/src/wolnelektury/static/2022/images/tool-link.svg
new file mode 100644 (file)
index 0000000..241bd68
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+
+<svg
+   width="800px"
+   height="800px"
+   viewBox="0 0 24 24"
+   fill="none"
+   version="1.1"
+   id="svg830"
+   sodipodi:docname="tool-link.svg"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs834" />
+  <sodipodi:namedview
+     id="namedview832"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="0.89875"
+     inkscape:cx="-45.062587"
+     inkscape:cy="399.44367"
+     inkscape:window-width="1920"
+     inkscape:window-height="1011"
+     inkscape:window-x="0"
+     inkscape:window-y="32"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg830" />
+  <path
+     d="M10.0464 14C8.54044 12.4882 8.67609 9.90087 10.3494 8.22108L15.197 3.35462C16.8703 1.67483 19.4476 1.53865 20.9536 3.05046C22.4596 4.56228 22.3239 7.14956 20.6506 8.82935L18.2268 11.2626"
+     stroke="#007880"
+     stroke-width="1.5"
+     stroke-linecap="round"
+     id="path826" />
+  <path
+     d="M13.9536 10C15.4596 11.5118 15.3239 14.0991 13.6506 15.7789L11.2268 18.2121L8.80299 20.6454C7.12969 22.3252 4.55237 22.4613 3.0464 20.9495C1.54043 19.4377 1.67609 16.8504 3.34939 15.1706L5.77323 12.7373"
+     stroke="#007880"
+     stroke-width="1.5"
+     stroke-linecap="round"
+     id="path828" />
+</svg>
diff --git a/src/wolnelektury/static/2022/images/tool-quote.svg b/src/wolnelektury/static/2022/images/tool-quote.svg
new file mode 100644 (file)
index 0000000..0c8670a
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+
+<svg
+   width="800px"
+   height="800px"
+   viewBox="0 0 24 24"
+   fill="none"
+   version="1.1"
+   id="svg895"
+   sodipodi:docname="tool-quote.svg"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs899" />
+  <sodipodi:namedview
+     id="namedview897"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="0.41533122"
+     inkscape:cx="49.358196"
+     inkscape:cy="469.5048"
+     inkscape:window-width="1920"
+     inkscape:window-height="1011"
+     inkscape:window-x="0"
+     inkscape:window-y="32"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg895" />
+  <path
+     fill-rule="evenodd"
+     clip-rule="evenodd"
+     d="M12 1.25C12.4142 1.25 12.75 1.58579 12.75 2V12.9726L14.4306 11.0119C14.7001 10.6974 15.1736 10.661 15.4881 10.9306C15.8026 11.2001 15.839 11.6736 15.5694 11.9881L12.5694 15.4881C12.427 15.6543 12.2189 15.75 12 15.75C11.7811 15.75 11.573 15.6543 11.4306 15.4881L8.43056 11.9881C8.16099 11.6736 8.19741 11.2001 8.51191 10.9306C8.8264 10.661 9.29988 10.6974 9.56944 11.0119L11.25 12.9726V2C11.25 1.58579 11.5858 1.25 12 1.25ZM6.99583 8.25196C7.41003 8.24966 7.74768 8.58357 7.74999 8.99778C7.7523 9.41199 7.41838 9.74964 7.00418 9.75194C5.91068 9.75803 5.1356 9.78643 4.54735 9.89448C3.98054 9.99859 3.65246 10.1658 3.40901 10.4092C3.13225 10.686 2.9518 11.0746 2.85315 11.8083C2.75159 12.5637 2.75 13.5648 2.75 15.0002V16.0002C2.75 17.4356 2.75159 18.4367 2.85315 19.1921C2.9518 19.9259 3.13225 20.3144 3.40901 20.5912C3.68577 20.868 4.07435 21.0484 4.80812 21.1471C5.56347 21.2486 6.56458 21.2502 8 21.2502H16C17.4354 21.2502 18.4365 21.2486 19.1919 21.1471C19.9257 21.0484 20.3142 20.868 20.591 20.5912C20.8678 20.3144 21.0482 19.9259 21.1469 19.1921C21.2484 18.4367 21.25 17.4356 21.25 16.0002V15.0002C21.25 13.5648 21.2484 12.5637 21.1469 11.8083C21.0482 11.0746 20.8678 10.686 20.591 10.4092C20.3475 10.1658 20.0195 9.99859 19.4527 9.89448C18.8644 9.78643 18.0893 9.75803 16.9958 9.75194C16.5816 9.74964 16.2477 9.41199 16.25 8.99778C16.2523 8.58357 16.59 8.24966 17.0042 8.25196C18.0857 8.25799 18.9871 8.28387 19.7236 8.41916C20.4816 8.55839 21.1267 8.82364 21.6517 9.34857C22.2536 9.95048 22.5125 10.7084 22.6335 11.6085C22.75 12.4754 22.75 13.5778 22.75 14.9453V16.0551C22.75 17.4227 22.75 18.525 22.6335 19.392C22.5125 20.2921 22.2536 21.0499 21.6517 21.6519C21.0497 22.2538 20.2919 22.5127 19.3918 22.6337C18.5248 22.7503 17.4225 22.7502 16.0549 22.7502H7.94513C6.57754 22.7502 5.47522 22.7503 4.60825 22.6337C3.70814 22.5127 2.95027 22.2538 2.34835 21.6519C1.74643 21.0499 1.48754 20.2921 1.36652 19.392C1.24996 18.525 1.24998 17.4227 1.25 16.0551V14.9453C1.24998 13.5778 1.24996 12.4754 1.36652 11.6085C1.48754 10.7084 1.74643 9.95048 2.34835 9.34857C2.87328 8.82363 3.51835 8.55839 4.27635 8.41916C5.01291 8.28387 5.9143 8.25798 6.99583 8.25196Z"
+     fill="#007880"
+     id="path893" />
+</svg>
diff --git a/src/wolnelektury/static/2022/images/zakladka-full.svg b/src/wolnelektury/static/2022/images/zakladka-full.svg
new file mode 100644 (file)
index 0000000..4fccefe
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="18" height="30" viewBox="0 0 18 30" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5547 23.1679L9 22.7981L8.4453 23.1679L1 28.1315V3C1 1.81856 1.86597 1 2.76923 1H15.2308C16.134 1 17 1.81856 17 3L17 28.1315L9.5547 23.1679Z" fill="#007880" stroke="#007880" stroke-width="2"/>
+</svg>
diff --git a/src/wolnelektury/static/2022/images/zakladka-note.svg b/src/wolnelektury/static/2022/images/zakladka-note.svg
new file mode 100644 (file)
index 0000000..1c4d3ea
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="44"
+   height="44"
+   viewBox="0 0 44 44"
+   fill="none"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="zakladka-note.svg"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs12" />
+  <sodipodi:namedview
+     id="namedview10"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="16.340909"
+     inkscape:cx="22"
+     inkscape:cy="22.030598"
+     inkscape:window-width="1920"
+     inkscape:window-height="1011"
+     inkscape:window-x="0"
+     inkscape:window-y="32"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg8" />
+  <path
+     d="M22.5547 30.1679L22 29.7981L21.4453 30.1679L14 35.1315V10C14 8.81856 14.866 8 15.7692 8H28.2308C29.134 8 30 8.81856 30 10L30 35.1315L22.5547 30.1679Z"
+     fill="#007880"
+     stroke="#007880"
+     stroke-width="2"
+     id="path4" />
+  <path
+     d="M22 12L23.3471 16.1459H27.7063L24.1796 18.7082L25.5267 22.8541L22 20.2918L18.4733 22.8541L19.8204 18.7082L16.2937 16.1459H20.6529L22 12Z"
+     fill="#F7BA00"
+     id="path6" />
+</svg>
diff --git a/src/wolnelektury/static/2022/images/zakladka-usun.svg b/src/wolnelektury/static/2022/images/zakladka-usun.svg
new file mode 100644 (file)
index 0000000..628efb1
--- /dev/null
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="30"
+   height="30"
+   viewBox="0 0 30 30"
+   fill="none"
+   version="1.1"
+   id="svg9"
+   sodipodi:docname="zakladka-usun.svg"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs13" />
+  <sodipodi:namedview
+     id="namedview11"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="7.7834598"
+     inkscape:cx="-27.558439"
+     inkscape:cy="5.1391028"
+     inkscape:window-width="1920"
+     inkscape:window-height="1011"
+     inkscape:window-x="0"
+     inkscape:window-y="32"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg9" />
+  <g
+     id="Group 579"
+     transform="matrix(1.2364596,0,0,1.2364596,6.9630126,4.7992083)">
+    <path
+       id="Rectangle 578"
+       d="M 4.5,2.5 C 4.5,1.67157 5.17157,1 6,1 H 7 C 7.82843,1 8.5,1.67157 8.5,2.5 V 4 h -4 z"
+       stroke="#007880" />
+    <rect
+       id="Rectangle 579"
+       x="0.25"
+       y="3.75"
+       width="12.5"
+       height="0.5"
+       stroke="#007880"
+       stroke-width="0.5" />
+    <path
+       id="Rectangle 158"
+       d="m 2,5.5 v 9 c 0,0.5523 0.44772,1 1,1 h 7 c 0.5523,0 1,-0.4477 1,-1 v -9"
+       stroke="#007880" />
+    <rect
+       id="Rectangle 574"
+       x="4"
+       y="5.5"
+       width="1"
+       height="8"
+       fill="#007880" />
+    <rect
+       id="Rectangle 575"
+       x="6"
+       y="5.5"
+       width="1"
+       height="8"
+       fill="#007880" />
+    <rect
+       id="Rectangle 576"
+       x="8"
+       y="5.5"
+       width="1"
+       height="8"
+       fill="#007880" />
+  </g>
+</svg>
diff --git a/src/wolnelektury/static/2022/images/zakladka.svg b/src/wolnelektury/static/2022/images/zakladka.svg
new file mode 100644 (file)
index 0000000..55aceaa
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="14" height="24" viewBox="0 0 14 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path id="Union" d="M7.56553 18.3753L7 17.9875L6.43447 18.3753L1 22.1018V2.4C1 1.52228 1.61562 1 2.15385 1H11.8462C12.3844 1 13 1.52228 13 2.4L13 22.1018L7.56553 18.3753Z" stroke="#D9D9D9" stroke-width="2"/>
+</svg>
index 618b19a..30796dd 100644 (file)
 }
 
 .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 (file)
index 0000000..2293476
--- /dev/null
@@ -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;
+        }
+    }
+}
index fcefb2f..4e4a35f 100644 (file)
@@ -5,6 +5,7 @@
 /*!*/
 
 @import "annoy";
+@import "bookmarks";
 @import "navigation";
 @import "header";
 @import "main";
index dd48d4f..1c617c7 100644 (file)
         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;
 
     
 
 #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%;
+            }
+        }
+    }
+}
index 28cc2d0..bf0fb2b 100644 (file)
@@ -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;
 }
   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%;
     }
 
     .icon {
-      color: white;
+      color: #083F4D;
       font-size: 16px;
     }
   }
 
   .icon {
     font-size: 21px;
-    color: white;
+    color: #083F4D;
     margin-right: 8px;
     cursor: pointer;
   }
 
     &.up:after { 
         content: '▲';
-        color: white;
+        color: #083F4D;
         font-size: .8em;
         position: absolute;
         top: 0px;
     }
     &.down:after { 
         content: '▼';
-        color: white;
+        color: #083F4D;
         font-size: .8em;
         position: absolute;
         bottom: 2px;
     }
 }
 
+}
 
 
 #menu {
 
 
 .with-player-bar .playing-highlight {
-    background: #D5ECED;
+    background: #FFE694;
 }
 
 
     #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 (file)
index 0000000..5db4d7e
--- /dev/null
@@ -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);
index 2f0feaa..277f3d4 100644 (file)
@@ -1,9 +1,10 @@
 (function($){$(function(){
 
 
-if ($('#nota_red li').length > 0) {
-    $("#menu-nota_red").show();
-}
+    if ($('#nota_red p').length > 0) {
+        $("#info").prepend($("<hr>"));
+        $("#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 (file)
index 0000000..2f9a502
--- /dev/null
@@ -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 (file)
index 0000000..816938c
--- /dev/null
@@ -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 = $("<div>");
+            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);
index d57a32f..ae5b14a 100644 (file)
@@ -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;
index cdfc1f0..1460675 100644 (file)
@@ -1,4 +1,5 @@
 (function($){$(function(){
+    let csrf = $("[name='csrfmiddlewaretoken']").val();
 
     var interestingReferences = $("#interesting-references").text();
     if (interestingReferences) {
 
         _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 = $("<div class='zakladka'>");
+                $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);
index c403ab9..0717e0a 100644 (file)
-/**
- * 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;
+});
index afee7f1..cfecb50 100644 (file)
@@ -57,6 +57,7 @@ urlpatterns += [
     path('towarzystwo/', RedirectView.as_view(url='/pomagam/', permanent=False, query_string=True)),
     path('towarzystwo/<path:path>', RedirectView.as_view(
         url='/pomagam/%(path)s', permanent=False)),
+    path('', include('bookmarks.urls')),
 
     path('chunks/', include('chunks.urls')),