diff --git a/README.md b/README.md new file mode 100644 index 0000000..6213db9 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ + +Getting started: +``` +python3 -m venv venv +./venv/bin/pip install -r requirements.txt +./manage.py migrate +./manage.py load_titles +./mange.py runserver +``` diff --git a/app/settings.py b/app/settings.py index e45e806..4a2f6c6 100644 --- a/app/settings.py +++ b/app/settings.py @@ -143,3 +143,6 @@ STATICFILES_FINDERS = [ # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +TIMELINE_PREFIX = "https://aab21.pad.ma/" diff --git a/app/static/css/partials/_ascroll.scss b/app/static/css/partials/_ascroll.scss new file mode 100644 index 0000000..dab8c7d --- /dev/null +++ b/app/static/css/partials/_ascroll.scss @@ -0,0 +1,34 @@ +#ascroll { + font-family: "Noton Sans"; + width: 100vw; + + h1 { + margin: 4px; + margin-top: 32px; + margin-bottom: 64px; + font-size: 24px; + letter-spacing: 1px; + font-weight: bold; + } + .player { + position: absolute; + display: none; + width: 100vw; + height: auto; + //transition: opacity 0.4s; + + } + .annotation { + .frame { + img { + width: 100vw; + height: auto; + } + } + .text { + margin: 20px 20px; + font-size: 22px; + line-height: 26px; + } + } +} diff --git a/app/static/css/partials/_layout.scss b/app/static/css/partials/_layout.scss new file mode 100644 index 0000000..51dbe86 --- /dev/null +++ b/app/static/css/partials/_layout.scss @@ -0,0 +1,18 @@ +body { + background: #d3d; + color: #eee; + font-family: "Noto Sans"; + font-size: 20px; + overflow-x: hidden; +} + +main { + line-height: 1.2em; + +} + +nav { + a { + color: #fff; + } +} diff --git a/app/static/css/partials/_reset.scss b/app/static/css/partials/_reset.scss new file mode 100644 index 0000000..d9f27b5 --- /dev/null +++ b/app/static/css/partials/_reset.scss @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/app/static/css/partials/burger.scss b/app/static/css/partials/burger.scss new file mode 100644 index 0000000..18db412 --- /dev/null +++ b/app/static/css/partials/burger.scss @@ -0,0 +1,53 @@ + +.topnav { + background-color: #333; + position: relative; + height: 72px; + padding-left: 16px; + padding-top: 16px; + @media screen and (max-width: 799px) { + overflow: hidden; + z-index: 100; + height: initial; + padding-left: initial; + padding-top: initial; + } + + nav { + display: block; + text-align: right; + margin-right: 16px; + @media screen and (max-width: 799px) { + display: none; + text-align: initial; + margin-right: initial; + } + } + + a { + color: white; + text-decoration: none; + font-size: 17px; + @media screen and (max-width: 799px) { + padding: 14px 16px; + display: block; + } + + &.icon { + display: none; + @media screen and (max-width: 799px) { + display: block; + position: absolute; + right: 0; + top: 0; + } + } + + &:hover { + background-color: #ddd; + color: black; + } + } + + +} diff --git a/app/static/css/style.scss b/app/static/css/style.scss index 438b94c..b53e26c 100644 --- a/app/static/css/style.scss +++ b/app/static/css/style.scss @@ -1,12 +1,4 @@ -body { - background: #000; - color: #fff; - font-family: "Noto Sans"; - font-size: 20px; -} - -nav { - a { - color: #fff; - } -} +@import "partials/reset"; +@import "partials/layout"; +@import "partials/burger"; +@import "partials/ascroll"; diff --git a/app/static/js/ascroll.js b/app/static/js/ascroll.js new file mode 100644 index 0000000..f4b123c --- /dev/null +++ b/app/static/js/ascroll.js @@ -0,0 +1,121 @@ +var cache = cache || {} +var layer = 'descriptions' +var baseURL = 'https://pad.ma' +var imageResolution = 480 + +let lastKnownScrollPosition = 0 +let ticking = false; + + +async function pandoraAPI(action, data) { + var url = baseURL + '/api/' + //var url = '/pandoraAPI/' + var key = JSON.stringify([action, data]) + if (!cache[key]) { + var response = await fetch(url, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + action: action, + data: data + }) + }) + cache[key] = await response.json() + } + return cache[key] +} + +function updatePlayerPosition(video, lastKnownScrollPosition) { + console.log('update', lastKnownScrollPosition) + video.style.top = lastKnownScrollPosition + 'px' + video.style.display = 'block'; +} + +function updatePlayer(video, frame, currentTime) { + var rect = frame.getBoundingClientRect(); + video.style.opacity = 0 + console.log('update player', rect) + video.style.top = (rect.top + window.scrollY) + 'px' + video.style.display = 'block'; + //video.poster = frame.querySelector('img').src + video.currentTime = currentTime + video.controls = true + video.play() + video.style.opacity = 1 +} + +function isElementInViewport (el) { + var rect = el.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */ + rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ + ); +} + +function onVisibilityChange(el, callback) { + var old_visible; + return function () { + var visible = isElementInViewport(el); + if (visible != old_visible) { + old_visible = visible; + if (typeof callback == 'function') { + callback(visible); + } + } + } +} + + +pandoraAPI('get', {id: film.id, keys: ['id', 'title', 'layers']}).then(response => { + var ascroll = document.querySelector('#ascroll') + var loaded = false + + var video = document.createElement('video') + video.classList.add('player') + video.muted = true + video.src = `${baseURL}/${film.id}/480p.webm` + ascroll.appendChild(video) + + var h1 = document.createElement('h1') + h1.innerHTML = response.data.title + ascroll.appendChild(h1) + + response.data.layers[layer].forEach(annotation => { + var div = document.createElement('div') + div.classList.add('annotation') + div.innerHTML = ` +
+
${annotation.value}
+ + ` + ascroll.appendChild(div) + var frame = div.querySelector('.frame') + document.addEventListener('scroll', onVisibilityChange(div.querySelector('.frame'), function(visible) { + if (loaded && visible) + updatePlayer(video, frame, annotation['in']) + + })) + }) + loaded = true + let frame = ascroll.querySelector('.annotation .frame') + if (frame) { + updatePlayer(video, frame, 0) + } + + /* + document.addEventListener('scroll', function(e) { + lastKnownScrollPosition = window.scrollY; + + if (!ticking) { + window.requestAnimationFrame(function() { + updatePlayerPosition(video, lastKnownScrollPosition); + ticking = false; + }); + + ticking = true; + } + }) + */ +}) diff --git a/app/templates/base.html b/app/templates/base.html index c16fdf7..189bf59 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -12,21 +12,36 @@ {% block head %}{% endblock head %} -

- phtantasmapolis: Looking Back to the Future -
- 未竟之城:回顧未來 -

- +
+ + phtantasmapolis: Looking Back to the Future +
+ 未竟之城:回顧未來 +
+ + + + +
{% block main %}{% endblock main %} -
+ {% block end %}{% endblock end %} + diff --git a/app/templates/film.html b/app/templates/film.html index ebaaea3..edc9c1d 100644 --- a/app/templates/film.html +++ b/app/templates/film.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block main %} - +

{{ film.data.title | safe }}

{{ film.data.director|join:", "|safe }}

diff --git a/app/templates/film_play.html b/app/templates/film_play.html index a508050..a3f94cf 100644 --- a/app/templates/film_play.html +++ b/app/templates/film_play.html @@ -1,7 +1,12 @@ {% extends "base.html" %} {% block main %} -

{{ film.data.title }}

-{% if film.vimeo_id %} - -{% endif %} +
+{% endblock %} +{% block end %} + + {% endblock %} diff --git a/app/templates/films.html b/app/templates/films.html index 67cb2f2..c40b174 100644 --- a/app/templates/films.html +++ b/app/templates/films.html @@ -1,9 +1,8 @@ {% extends "base.html" %} {% block main %} -some overview page with stuff {% for film in films %}
- {{ film.data.title | safe }} + {{ film.data.title | safe }} {{ film.data.director|join:", "|safe }}
{% endfor %} diff --git a/app/text/views.py b/app/text/views.py index 8f33951..0f6eb92 100644 --- a/app/text/views.py +++ b/app/text/views.py @@ -15,7 +15,7 @@ def essays(request): context['essays'] = models.Essay.objects.filter(public=True).order_by('created') return render(request, 'essays.html', context) -def essay(request, slug): +def essay(request, slug, lang): context = {} context['essay'] = get_object_or_404(models.Essay, slug=slug) return render(request, 'essay.html', context) diff --git a/app/urls.py b/app/urls.py index 9e0d008..c81e92f 100644 --- a/app/urls.py +++ b/app/urls.py @@ -21,15 +21,17 @@ from .text import views as text urlpatterns = [ path('admin/', admin.site.urls), + #path('pandoraAPI/', video.pandoraAPI, name='pandoraAPI'), path('films/', video.films, name='films'), - path('film//play', video.film_play, name='film_play'), + path('film//play/', video.film_play, name='film_play'), path('film//', video.film, name='film'), path('edits/', video.edits, name='edits'), - path('edit//play', video.edit_play, name='edit_play'), + path('edit//play/', video.edit_play, name='edit_play'), path('edit//', video.edit, name='edit'), path('tv/', video.tv, name='tv'), path('essays/', text.essays, name='essays'), - path('essay//', text.essay, name='essay'), + path('essay//', text.essay, name='essay'), path('about/', text.about, name='about'), + path('', text.index, name='index'), ] diff --git a/app/user/migrations/0002_alter_user_data.py b/app/user/migrations/0002_alter_user_data.py new file mode 100644 index 0000000..9f5aa2f --- /dev/null +++ b/app/user/migrations/0002_alter_user_data.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.7 on 2021-09-30 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='data', + field=models.JSONField(blank=True, default=dict, editable=False), + ), + ] diff --git a/app/video/migrations/0002_auto_20210930_1527.py b/app/video/migrations/0002_auto_20210930_1527.py new file mode 100644 index 0000000..18e2ac9 --- /dev/null +++ b/app/video/migrations/0002_auto_20210930_1527.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.7 on 2021-09-30 15:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('video', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='edit', + name='data', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='film', + name='data', + field=models.JSONField(blank=True, default=dict), + ), + ] diff --git a/app/video/utils.py b/app/video/utils.py new file mode 100644 index 0000000..9bcbef6 --- /dev/null +++ b/app/video/utils.py @@ -0,0 +1,39 @@ +import datetime +from django.utils import datetime_safe +from django.http import HttpResponse, Http404 +import json +from django.conf import settings + +def json_response(data=None, status=200, text='ok'): + if not data: + data = {} + return {'status': {'code': status, 'text': text}, 'data': data} + +def _to_json(python_object): + if isinstance(python_object, datetime.datetime): + if python_object.year < 1900: + tt = python_object.timetuple() + return '%d-%02d-%02dT%02d:%02d%02dZ' % tuple(list(tt)[:6]) + return python_object.strftime('%Y-%m-%dT%H:%M:%SZ') + if isinstance(python_object, datetime_safe.datetime): + return python_object.strftime('%Y-%m-%dT%H:%M:%SZ') + raise TypeError('%s %s is not JSON serializable' % (repr(python_object), type(python_object))) + +def json_dump(data, fp, indent=4): + return json.dump(data, fp, indent=indent, default=_to_json, ensure_ascii=False) + +def json_dumps(data, indent=4): + return json.dumps(data, indent=indent, default=_to_json, ensure_ascii=False) + +def render_to_json_response(dictionary, content_type="application/json", status=200): + indent = None + if settings.DEBUG: + content_type = "text/javascript" + indent = 2 + if getattr(settings, 'JSON_DEBUG', False): + print(json_dumps(dictionary, indent=2).encode('utf-8')) + response = json_dumps(dictionary, indent=indent) + if not isinstance(response, bytes): + response = response.encode('utf-8') + return HttpResponse(response, content_type=content_type, status=status) + diff --git a/app/video/views.py b/app/video/views.py index b6872c2..353f6a1 100644 --- a/app/video/views.py +++ b/app/video/views.py @@ -1,10 +1,12 @@ from django.shortcuts import render, redirect, get_object_or_404 +from django.views.decorators.csrf import csrf_exempt + from . import models def films(request): context = {} - context['films'] = models.Film.objects.filter(public=True).order_by('created') + context['films'] = models.Film.objects.filter(public=True).order_by('data__title') return render(request, 'films.html', context) def film(request, slug): @@ -12,9 +14,10 @@ def film(request, slug): context['film'] = get_object_or_404(models.Film, slug=slug) return render(request, 'film.html', context) -def film_play(request, slug): +def film_play(request, slug, lang): context = {} context['film'] = get_object_or_404(models.Film, slug=slug) + context['lang'] = lang return render(request, 'film_play.html', context) def edits(request): @@ -27,11 +30,26 @@ def edit(request, slug): context['edit'] = get_object_or_404(models.Edit, slug=slug) return render(request, 'edit.html', context) -def edit_play(request, slug): +def edit_play(request, slug, lang): context = {} context['edit'] = get_object_or_404(models.Edit, slug=slug) + context['lang'] = lang return render(request, 'edit_play.html', context) def tv(request): context = {} return render(request, 'tv.html', context) + + +@csrf_exempt +def pandoraAPI(request): + import ox + from .utils import render_to_json_response + import json + data = json.loads(request.body.decode()) + print('pandora request', data) + api = ox.api.signin('https://pad.ma/api/') + data = getattr(api, data['action'])(**data['data']) + print('response', data) + return render_to_json_response(data) + diff --git a/manage.py b/manage.py index a787340..118d972 100755 --- a/manage.py +++ b/manage.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """Django's command-line utility for administrative tasks.""" import os import sys diff --git a/requirements.txt b/requirements.txt index 8e59a6d..eb04912 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ Django -psycopg2 libsass -django-sass-processor +django-compressor django-sass-processor ox