embeded pandora mobile view
This commit is contained in:
commit
b420bf43b7
45 changed files with 3437 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
venv
|
||||||
|
*.swp
|
||||||
|
__pycache__
|
||||||
|
db.sqlite3
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
16
app/asgi.py
Normal file
16
app/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for app project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
0
app/item/__init__.py
Normal file
0
app/item/__init__.py
Normal file
23
app/item/admin.py
Normal file
23
app/item/admin.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class ItemAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ['title', 'description', 'url']
|
||||||
|
list_display = ['__str__', 'id', 'published']
|
||||||
|
list_filter = (
|
||||||
|
("published", admin.EmptyFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.site.register(models.Item, ItemAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ['item__title', 'item__url', 'text', 'name', 'email']
|
||||||
|
list_display = ['__str__', 'published']
|
||||||
|
list_filter = (
|
||||||
|
("published", admin.EmptyFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.site.register(models.Comment, CommentAdmin)
|
6
app/item/apps.py
Normal file
6
app/item/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ItemConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "app.item"
|
74
app/item/migrations/0001_initial.py
Normal file
74
app/item/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# Generated by Django 4.2.3 on 2023-07-11 16:06
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Comment",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("modified", models.DateTimeField(auto_now=True)),
|
||||||
|
("name", models.CharField(max_length=1024)),
|
||||||
|
("email", models.CharField(max_length=1024)),
|
||||||
|
("text", models.TextField(blank=True, default="")),
|
||||||
|
("data", models.JSONField(default=dict, editable=False)),
|
||||||
|
("published", models.DateTimeField(default=None, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Item",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("modified", models.DateTimeField(auto_now=True)),
|
||||||
|
("url", models.CharField(max_length=1024, unique=True)),
|
||||||
|
("title", models.CharField(max_length=1024)),
|
||||||
|
("description", models.TextField(blank=True, default="")),
|
||||||
|
("published", models.DateTimeField(default=datetime.datetime.now)),
|
||||||
|
("announced", models.DateTimeField(default=None, null=True)),
|
||||||
|
("data", models.JSONField(default=dict, editable=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Settings",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("modified", models.DateTimeField(auto_now=True)),
|
||||||
|
("key", models.CharField(max_length=1024, unique=True)),
|
||||||
|
("value", models.JSONField(default=dict, editable=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
46
app/item/migrations/0002_initial.py
Normal file
46
app/item/migrations/0002_initial.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Generated by Django 4.2.3 on 2023-07-11 16:06
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("item", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="item",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="items",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="comment",
|
||||||
|
name="item",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="comments",
|
||||||
|
to="item.item",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="comment",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="comments",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
0
app/item/migrations/__init__.py
Normal file
0
app/item/migrations/__init__.py
Normal file
104
app/item/models.py
Normal file
104
app/item/models.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
from django.utils.timezone import datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(models.Model):
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
key = models.CharField(max_length=1024, unique=True)
|
||||||
|
value = models.JSONField(default=dict, editable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Item(models.Model):
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, null=True, related_name='items', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
url = models.CharField(max_length=1024, unique=True)
|
||||||
|
title = models.CharField(max_length=1024)
|
||||||
|
description = models.TextField(default="", blank=True)
|
||||||
|
published = models.DateTimeField(default=timezone.now, null=True, blank=True)
|
||||||
|
announced = models.DateTimeField(null=True, default=None, blank=True)
|
||||||
|
data = models.JSONField(default=dict, editable=False)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s (%s)' % (self.title, self.url)
|
||||||
|
|
||||||
|
def public_comments(self):
|
||||||
|
return self.comments.exclude(published=None)
|
||||||
|
|
||||||
|
def public_comments_json(self):
|
||||||
|
comments = []
|
||||||
|
for comment in self.public_comments():
|
||||||
|
comments.append({
|
||||||
|
"name": comment.name,
|
||||||
|
"date": comment.date,
|
||||||
|
"text": comment.text,
|
||||||
|
})
|
||||||
|
return json.dumps(comments)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def public(cls):
|
||||||
|
now = timezone.now()
|
||||||
|
qs = cls.objects.exclude(published=None).filter(published__lte=now).order_by('-published')
|
||||||
|
cal = now.date().isocalendar()
|
||||||
|
monday = now.date() - timedelta(days=now.date().isocalendar().weekday - 1)
|
||||||
|
monday = timezone.datetime(monday.year, monday.month, monday.day, tzinfo=now.tzinfo)
|
||||||
|
print(now.tzinfo)
|
||||||
|
first_post = qs.filter(published__gt=monday).first()
|
||||||
|
print('!!', first_post.published, now, first_post.published > now)
|
||||||
|
if first_post.published < now:
|
||||||
|
print('only this week')
|
||||||
|
week = qs.filter(published__gt=monday)
|
||||||
|
else:
|
||||||
|
print('only last week')
|
||||||
|
last_monday = monday - timedelta(days=7)
|
||||||
|
week = qs.filter(published__gt=last_monday)
|
||||||
|
archive = qs.exclude(id__in=week)
|
||||||
|
return week, archive
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('item', kwargs={'id': self.id})
|
||||||
|
|
||||||
|
class Comment(models.Model):
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
item = models.ForeignKey(Item, related_name='comments', on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, null=True, related_name='comments', on_delete=models.CASCADE, blank=True)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=1024)
|
||||||
|
email = models.CharField(max_length=1024)
|
||||||
|
text = models.TextField(default="", blank=True)
|
||||||
|
data = models.JSONField(default=dict, editable=False)
|
||||||
|
published = models.DateTimeField(null=True, default=None)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s: %s' % (self.item, self.user)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.user:
|
||||||
|
self.name = self.user.username
|
||||||
|
self.email = self.user.email
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
return self.created.strftime('%Y-%m-%d %H:%M')
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
data = {}
|
||||||
|
data['name'] = self.name
|
||||||
|
data['date'] = self.date
|
||||||
|
data['text'] = self.text
|
||||||
|
return data
|
3
app/item/tests.py
Normal file
3
app/item/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
51
app/item/views.py
Normal file
51
app/item/views.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
context = {}
|
||||||
|
week, archive = models.Item.public()
|
||||||
|
context['items'] = week
|
||||||
|
context['archive'] = archive.exists()
|
||||||
|
return render(request, 'index.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
def archive(request):
|
||||||
|
context = {}
|
||||||
|
qs = models.Item.public()
|
||||||
|
week, archive = models.Item.public()
|
||||||
|
context['items'] = archive
|
||||||
|
return render(request, 'archive.html', context)
|
||||||
|
|
||||||
|
def item(request, id):
|
||||||
|
context = {}
|
||||||
|
item = models.Item.objects.get(id=id)
|
||||||
|
context['item'] = item
|
||||||
|
return render(request, 'item.html', context)
|
||||||
|
|
||||||
|
import json
|
||||||
|
from django.http import Http404, HttpResponse
|
||||||
|
|
||||||
|
def render_to_json(response):
|
||||||
|
content = json.dumps(response)
|
||||||
|
return HttpResponse(content, 'application/json; charset=utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def comment(request):
|
||||||
|
response = {}
|
||||||
|
data = json.loads(request.body)
|
||||||
|
print(data)
|
||||||
|
comment = models.Comment()
|
||||||
|
comment.item = models.Item.objects.get(id=data['item'])
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
comment.user = request.user
|
||||||
|
comment.published = datetime.now()
|
||||||
|
else:
|
||||||
|
comment.name = data['name']
|
||||||
|
comment.email = data['email']
|
||||||
|
comment.text = data['text']
|
||||||
|
comment.save()
|
||||||
|
response = comment.json()
|
||||||
|
return render_to_json(response)
|
128
app/settings.py
Normal file
128
app/settings.py
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
"""
|
||||||
|
Django settings for app project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.2.3.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = "django-insecure-()6&rdheil=iyz%36dl-fnb)a+*7*^cb%isz6x%fi+ong5#*zz"
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
|
||||||
|
"app",
|
||||||
|
"app.user",
|
||||||
|
"app.item",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "app.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "app.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = 'Asia/Kolkata'
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'user.User'
|
46
app/static/css/reset.css
Normal file
46
app/static/css/reset.css
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/* 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,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
u, i, center,
|
||||||
|
ol,
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
171
app/static/css/style.css
Normal file
171
app/static/css/style.css
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
//background: rgb(240, 240, 240);
|
||||||
|
//background: rgb(144, 144, 144);
|
||||||
|
//color: rgb(0, 0, 0);
|
||||||
|
background: rgb(16, 16, 16);
|
||||||
|
color: rgb(240, 240, 240);
|
||||||
|
font-family: "Noto Sans", "Lucida Grande", "Segoe UI", "DejaVu Sans", "Lucida Sans Unicode", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(128, 128, 255)
|
||||||
|
}
|
||||||
|
iframe {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
video, .poster {
|
||||||
|
border-bottom: 1px solid yellow;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid pink;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
.byline {
|
||||||
|
padding: 0;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
.player {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
@media(max-width:768px) {
|
||||||
|
.title,
|
||||||
|
.byline {
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
.player {
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.player video {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
padding: 4px;
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
@media(max-width:768px) {
|
||||||
|
.value {
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.more {
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
||||||
|
.layer.active {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.layer.active:first-child {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation {
|
||||||
|
padding: 4px;
|
||||||
|
border-bottom: 1px solid blueviolet;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.annotation.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.annotation.active.place,
|
||||||
|
.annotation.active.string {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid blueviolet;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
@media(max-width:768px) {
|
||||||
|
.annotation a img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer h3,
|
||||||
|
.comments h3 {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 0;
|
||||||
|
margin: 0;
|
||||||
|
//display: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.layer.active h3 {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.layer .icon svg,
|
||||||
|
.comments .icon svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
.layer.collapsed .annotation.active {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.rotate {
|
||||||
|
transform: rotate(90deg) translateY(-100%);
|
||||||
|
transform-origin:bottom left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document.text {
|
||||||
|
line-height: 1.5;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
.document.text p {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
.document.text img {
|
||||||
|
max-width: 100vw;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-right: -4px;
|
||||||
|
}
|
||||||
|
figure {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
figure img {
|
||||||
|
max-width: 100vw;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-right: -4px;
|
||||||
|
}
|
||||||
|
@media(max-width:768px) {
|
||||||
|
.document.text {
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ol li {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked svg {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
730
app/static/js/VideoElement.js
Normal file
730
app/static/js/VideoElement.js
Normal file
|
@ -0,0 +1,730 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*@
|
||||||
|
VideoElement <f> VideoElement Object
|
||||||
|
options <o> Options object
|
||||||
|
autoplay <b|false> autoplay
|
||||||
|
items <a|[]> array of objects with src,in,out,duration
|
||||||
|
loop <b|false> loop playback
|
||||||
|
playbackRate <n|1> playback rate
|
||||||
|
position <n|0> start position
|
||||||
|
self <o> Shared private variable
|
||||||
|
([options[, self]]) -> <o:Element> VideoElement Object
|
||||||
|
loadedmetadata <!> loadedmetadata
|
||||||
|
itemchange <!> itemchange
|
||||||
|
seeked <!> seeked
|
||||||
|
seeking <!> seeking
|
||||||
|
sizechange <!> sizechange
|
||||||
|
ended <!> ended
|
||||||
|
@*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var queue = [],
|
||||||
|
queueSize = 100,
|
||||||
|
restrictedElements = [],
|
||||||
|
requiresUserGesture = mediaPlaybackRequiresUserGesture(),
|
||||||
|
unblock = [];
|
||||||
|
|
||||||
|
|
||||||
|
window.VideoElement = function(options) {
|
||||||
|
|
||||||
|
var self = {},
|
||||||
|
that = document.createElement("div");
|
||||||
|
|
||||||
|
self.options = {
|
||||||
|
autoplay: false,
|
||||||
|
items: [],
|
||||||
|
loop: false,
|
||||||
|
muted: false,
|
||||||
|
playbackRate: 1,
|
||||||
|
position: 0,
|
||||||
|
volume: 1
|
||||||
|
}
|
||||||
|
Object.assign(self.options, options);
|
||||||
|
debug(self.options)
|
||||||
|
|
||||||
|
that.style.position = "relative"
|
||||||
|
that.style.width = "100%"
|
||||||
|
that.style.height = "100%"
|
||||||
|
that.style.maxHeight = "100vh"
|
||||||
|
that.style.margin = 'auto'
|
||||||
|
if (self.options.aspectratio) {
|
||||||
|
that.style.aspectRatio = self.options.aspectratio
|
||||||
|
} else {
|
||||||
|
that.style.height = '128px'
|
||||||
|
}
|
||||||
|
that.triggerEvent = function(event, data) {
|
||||||
|
if (event != 'timeupdate') {
|
||||||
|
debug('Video', 'triggerEvent', event, data);
|
||||||
|
}
|
||||||
|
event = new Event(event)
|
||||||
|
event.data = data
|
||||||
|
that.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
.update({
|
||||||
|
items: function() {
|
||||||
|
self.loadedMetadata = false;
|
||||||
|
loadItems(function() {
|
||||||
|
self.loadedMetadata = true;
|
||||||
|
var update = true;
|
||||||
|
if (self.currentItem >= self.numberOfItems) {
|
||||||
|
self.currentItem = 0;
|
||||||
|
}
|
||||||
|
if (!self.numberOfItems) {
|
||||||
|
self.video.src = '';
|
||||||
|
that.triggerEvent('durationchange', {
|
||||||
|
duration: that.duration()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (self.currentItemId != self.items[self.currentItem].id) {
|
||||||
|
// check if current item is in new items
|
||||||
|
self.items.some(function(item, i) {
|
||||||
|
if (item.id == self.currentItemId) {
|
||||||
|
self.currentItem = i;
|
||||||
|
loadNextVideo();
|
||||||
|
update = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (update) {
|
||||||
|
self.currentItem = 0;
|
||||||
|
self.currentItemId = self.items[self.currentItem].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!update) {
|
||||||
|
that.triggerEvent('seeked');
|
||||||
|
that.triggerEvent('durationchange', {
|
||||||
|
duration: that.duration()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCurrentVideo(function() {
|
||||||
|
that.triggerEvent('seeked');
|
||||||
|
that.triggerEvent('durationchange', {
|
||||||
|
duration: that.duration()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
playbackRate: function() {
|
||||||
|
self.video.playbackRate = self.options.playbackRate;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.css({width: '100%', height: '100%'});
|
||||||
|
*/
|
||||||
|
|
||||||
|
debug('Video', 'VIDEO ELEMENT OPTIONS', self.options);
|
||||||
|
|
||||||
|
self.currentItem = -1;
|
||||||
|
self.currentTime = 0;
|
||||||
|
self.currentVideo = 0;
|
||||||
|
self.items = [];
|
||||||
|
self.loadedMetadata = false;
|
||||||
|
that.paused = self.paused = true;
|
||||||
|
self.seeking = false;
|
||||||
|
self.loading = true;
|
||||||
|
self.buffering = true;
|
||||||
|
self.videos = [getVideo(), getVideo()];
|
||||||
|
self.video = self.videos[self.currentVideo];
|
||||||
|
self.video.classList.add("active")
|
||||||
|
self.volume = self.options.volume;
|
||||||
|
self.muted = self.options.muted;
|
||||||
|
self.brightness = document.createElement('div')
|
||||||
|
self.brightness.style.top = '0'
|
||||||
|
self.brightness.style.left = '0'
|
||||||
|
self.brightness.style.width = '100%'
|
||||||
|
self.brightness.style.height = '100%'
|
||||||
|
self.brightness.style.background = 'rgb(0, 0, 0)'
|
||||||
|
self.brightness.style.opacity = '0'
|
||||||
|
self.brightness.style.position = "absolute"
|
||||||
|
that.append(self.brightness)
|
||||||
|
|
||||||
|
self.timeupdate = setInterval(function() {
|
||||||
|
if (!self.paused
|
||||||
|
&& !self.loading
|
||||||
|
&& self.loadedMetadata
|
||||||
|
&& self.items[self.currentItem]
|
||||||
|
&& self.items[self.currentItem].out
|
||||||
|
&& self.video.currentTime >= self.items[self.currentItem].out) {
|
||||||
|
setCurrentItem(self.currentItem + 1);
|
||||||
|
}
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
// mobile browsers only allow playing media elements after user interaction
|
||||||
|
if (restrictedElements.length > 0) {
|
||||||
|
unblock.push(setSource)
|
||||||
|
setTimeout(function() {
|
||||||
|
that.triggerEvent('requiresusergesture');
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTime() {
|
||||||
|
var item = self.items[self.currentItem];
|
||||||
|
return self.seeking || self.loading
|
||||||
|
? self.currentTime
|
||||||
|
: item ? item.position + self.video.currentTime - item['in'] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getset(key, value) {
|
||||||
|
var ret;
|
||||||
|
if (isUndefined(value)) {
|
||||||
|
ret = self.video[key];
|
||||||
|
} else {
|
||||||
|
self.video[key] = value;
|
||||||
|
ret = that;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideo() {
|
||||||
|
var video = getVideoElement()
|
||||||
|
video.style.display = "none"
|
||||||
|
video.style.width = "100%"
|
||||||
|
video.style.height = "100%"
|
||||||
|
video.style.margin = "auto"
|
||||||
|
video.style.background = '#000'
|
||||||
|
if (self.options.aspectratio) {
|
||||||
|
video.style.aspectRatio = self.options.aspectratio
|
||||||
|
} else {
|
||||||
|
video.style.height = '128px'
|
||||||
|
}
|
||||||
|
video.style.top = 0
|
||||||
|
video.style.left = 0
|
||||||
|
video.style.position = "absolute"
|
||||||
|
video.preload = "metadata"
|
||||||
|
video.addEventListener("ended", event => {
|
||||||
|
if (self.video == video) {
|
||||||
|
setCurrentItem(self.currentItem + 1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
video.addEventListener("loadedmetadata", event => {
|
||||||
|
//console.log("!!", video.src, "loaded", 'current?', video == self.video)
|
||||||
|
})
|
||||||
|
video.addEventListener("progress", event => {
|
||||||
|
// stop buffering if buffered to end point
|
||||||
|
var item = self.items[self.currentItem],
|
||||||
|
nextItem = mod(self.currentItem + 1, self.numberOfItems),
|
||||||
|
next = self.items[nextItem],
|
||||||
|
nextVideo = self.videos[mod(self.currentVideo + 1, self.videos.length)];
|
||||||
|
if (self.video == video && (video.preload != 'none' || self.buffering)) {
|
||||||
|
if (clipCached(video, item)) {
|
||||||
|
video.preload = 'none';
|
||||||
|
self.buffering = false;
|
||||||
|
if (nextItem != self.currentItem) {
|
||||||
|
nextVideo.preload = 'auto';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (nextItem != self.currentItem && nextVideo.preload != 'none' && nextVideo.src) {
|
||||||
|
nextVideo.preload = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (nextVideo == video && video.preload != 'none' && nextVideo.src) {
|
||||||
|
if (clipCached(video, next)) {
|
||||||
|
video.preload = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipCached(video, item) {
|
||||||
|
var cached = false
|
||||||
|
for (var i=0; i<video.buffered.length; i++) {
|
||||||
|
if (video.buffered.start(i) <= item['in']
|
||||||
|
&& video.buffered.end(i) >= item.out) {
|
||||||
|
cached = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
})
|
||||||
|
video.addEventListener("volumechange", event => {
|
||||||
|
if (self.video == video) {
|
||||||
|
that.triggerEvent('volumechange')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
video.addEventListener("play", event => {
|
||||||
|
/*
|
||||||
|
if (self.video == video) {
|
||||||
|
that.triggerEvent('play')
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
video.addEventListener("pause", event => {
|
||||||
|
/*
|
||||||
|
if (self.video == video) {
|
||||||
|
that.triggerEvent('pause')
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
video.addEventListener("timeupdate", event => {
|
||||||
|
if (self.video == video) {
|
||||||
|
/*
|
||||||
|
var box = self.video.getBoundingClientRect()
|
||||||
|
if (box.width && box.height) {
|
||||||
|
that.style.width = box.width + 'px'
|
||||||
|
that.style.height = box.height + 'px'
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
that.triggerEvent('timeupdate', {
|
||||||
|
currentTime: getCurrentTime()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
video.addEventListener("seeking", event => {
|
||||||
|
if (self.video == video) {
|
||||||
|
that.triggerEvent('seeking')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
video.addEventListener("stop", event => {
|
||||||
|
if (self.video == video) {
|
||||||
|
//self.video.pause();
|
||||||
|
that.triggerEvent('ended');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
that.append(video)
|
||||||
|
return video
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideoElement() {
|
||||||
|
var video;
|
||||||
|
if (requiresUserGesture) {
|
||||||
|
if (queue.length) {
|
||||||
|
video = queue.pop();
|
||||||
|
} else {
|
||||||
|
video = document.createElement('video');
|
||||||
|
restrictedElements.push(video);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
video = document.createElement('video');
|
||||||
|
}
|
||||||
|
video.playsinline = true
|
||||||
|
video.setAttribute('playsinline', 'playsinline')
|
||||||
|
video.setAttribute('webkit-playsinline', 'webkit-playsinline')
|
||||||
|
video.WebKitPlaysInline = true
|
||||||
|
return video
|
||||||
|
};
|
||||||
|
|
||||||
|
function getVolume() {
|
||||||
|
var volume = 1;
|
||||||
|
if (self.items[self.currentItem] && isNumber(self.items[self.currentItem].volume)) {
|
||||||
|
volume = self.items[self.currentItem].volume;
|
||||||
|
}
|
||||||
|
return self.volume * volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function isReady(video, callback) {
|
||||||
|
if (video.seeking && !self.paused && !self.seeking) {
|
||||||
|
that.triggerEvent('seeking');
|
||||||
|
debug('Video', 'isReady', 'seeking');
|
||||||
|
video.addEventListener('seeked', function(event) {
|
||||||
|
debug('Video', 'isReady', 'seeked');
|
||||||
|
that.triggerEvent('seeked');
|
||||||
|
callback(video);
|
||||||
|
}, {once: true})
|
||||||
|
} else if (video.readyState) {
|
||||||
|
callback(video);
|
||||||
|
} else {
|
||||||
|
that.triggerEvent('seeking');
|
||||||
|
video.addEventListener('loadedmetadata', function(event) {
|
||||||
|
callback(video);
|
||||||
|
}, {once: true});
|
||||||
|
video.addEventListener('seeked', event => {
|
||||||
|
that.triggerEvent('seeked');
|
||||||
|
}, {once: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadItems(callback) {
|
||||||
|
debug('loadItems')
|
||||||
|
var currentTime = 0,
|
||||||
|
items = self.options.items.map(function(item) {
|
||||||
|
return isObject(item) ? {...item} : {src: item};
|
||||||
|
});
|
||||||
|
|
||||||
|
self.items = items;
|
||||||
|
self.numberOfItems = self.items.length;
|
||||||
|
items.forEach(item => {
|
||||||
|
item['in'] = item['in'] || 0;
|
||||||
|
item.position = currentTime;
|
||||||
|
if (item.out) {
|
||||||
|
item.duration = item.out - item['in'];
|
||||||
|
}
|
||||||
|
if (item.duration) {
|
||||||
|
if (!item.out) {
|
||||||
|
item.out = item.duration;
|
||||||
|
}
|
||||||
|
currentTime += item.duration;
|
||||||
|
item.id = getId(item);
|
||||||
|
} else {
|
||||||
|
getVideoInfo(item.src, function(info) {
|
||||||
|
item.duration = info.duration;
|
||||||
|
if (!item.out) {
|
||||||
|
item.out = item.duration;
|
||||||
|
}
|
||||||
|
currentTime += item.duration;
|
||||||
|
item.id = getId(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
debug('loadItems done')
|
||||||
|
callback && callback();
|
||||||
|
|
||||||
|
function getId(item) {
|
||||||
|
return item.id || item.src + '/' + item['in'] + '-' + item.out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadNextVideo() {
|
||||||
|
if (self.numberOfItems <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var item = self.items[self.currentItem],
|
||||||
|
nextItem = mod(self.currentItem + 1, self.numberOfItems),
|
||||||
|
next = self.items[nextItem],
|
||||||
|
nextVideo = self.videos[mod(self.currentVideo + 1, self.videos.length)];
|
||||||
|
nextVideo.addEventListener('loadedmetadata', function() {
|
||||||
|
if (self.video != nextVideo) {
|
||||||
|
nextVideo.currentTime = next['in'] || 0;
|
||||||
|
}
|
||||||
|
}, {once: true});
|
||||||
|
nextVideo.src = next.src;
|
||||||
|
nextVideo.preload = 'metadata';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentItem(item) {
|
||||||
|
debug('Video', 'sCI', item, self.numberOfItems);
|
||||||
|
var interval;
|
||||||
|
if (item >= self.numberOfItems || item < 0) {
|
||||||
|
if (self.options.loop) {
|
||||||
|
item = mod(item, self.numberOfItems);
|
||||||
|
} else {
|
||||||
|
self.seeking = false;
|
||||||
|
self.ended = true;
|
||||||
|
that.paused = self.paused = true;
|
||||||
|
self.video && self.video.pause();
|
||||||
|
that.triggerEvent('ended');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.video && self.video.pause();
|
||||||
|
self.currentItem = item;
|
||||||
|
self.currentItemId = self.items[self.currentItem].id;
|
||||||
|
setCurrentVideo(function() {
|
||||||
|
if (!self.loadedMetadata) {
|
||||||
|
self.loadedMetadata = true;
|
||||||
|
that.triggerEvent('loadedmetadata');
|
||||||
|
}
|
||||||
|
debug('Video', 'sCI', 'trigger itemchange',
|
||||||
|
self.items[self.currentItem]['in'], self.video.currentTime, self.video.seeking);
|
||||||
|
that.triggerEvent('sizechange');
|
||||||
|
that.triggerEvent('itemchange', {
|
||||||
|
item: self.currentItem
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentVideo(callback) {
|
||||||
|
var css = {},
|
||||||
|
muted = self.muted,
|
||||||
|
item = self.items[self.currentItem],
|
||||||
|
next;
|
||||||
|
debug('Video', 'sCV', JSON.stringify(item));
|
||||||
|
|
||||||
|
['left', 'top', 'width', 'height'].forEach(function(key) {
|
||||||
|
css[key] = self.videos[self.currentVideo].style[key];
|
||||||
|
});
|
||||||
|
self.currentTime = item.position;
|
||||||
|
self.loading = true;
|
||||||
|
if (self.video) {
|
||||||
|
self.videos[self.currentVideo].style.display = "none"
|
||||||
|
self.videos[self.currentVideo].classList.remove("active")
|
||||||
|
self.video.pause();
|
||||||
|
}
|
||||||
|
self.currentVideo = mod(self.currentVideo + 1, self.videos.length);
|
||||||
|
self.video = self.videos[self.currentVideo];
|
||||||
|
self.video.classList.add("active")
|
||||||
|
self.video.muted = true; // avoid sound glitch during load
|
||||||
|
if (!self.video.attributes.src || self.video.attributes.src.value != item.src) {
|
||||||
|
self.loadedMetadata && debug('Video', 'caching next item failed, reset src');
|
||||||
|
self.video.src = item.src;
|
||||||
|
}
|
||||||
|
self.video.preload = 'metadata';
|
||||||
|
self.video.volume = getVolume();
|
||||||
|
self.video.playbackRate = self.options.playbackRate;
|
||||||
|
Object.keys(css).forEach(key => {
|
||||||
|
self.video.style[key] = css[key]
|
||||||
|
})
|
||||||
|
self.buffering = true;
|
||||||
|
debug('Video', 'sCV', self.video.src, item['in'],
|
||||||
|
self.video.currentTime, self.video.seeking);
|
||||||
|
isReady(self.video, function(video) {
|
||||||
|
var in_ = item['in'] || 0;
|
||||||
|
|
||||||
|
function ready() {
|
||||||
|
debug('Video', 'sCV', 'ready');
|
||||||
|
self.seeking = false;
|
||||||
|
self.loading = false;
|
||||||
|
self.video.muted = muted;
|
||||||
|
!self.paused && self.video.play();
|
||||||
|
self.video.style.display = 'block'
|
||||||
|
callback && callback();
|
||||||
|
loadNextVideo();
|
||||||
|
}
|
||||||
|
if (video.currentTime == in_) {
|
||||||
|
debug('Video', 'sCV', 'already at position', item.id, in_, video.currentTime);
|
||||||
|
ready();
|
||||||
|
} else {
|
||||||
|
self.video.addEventListener("seeked", event => {
|
||||||
|
debug('Video', 'sCV', 'seeked callback');
|
||||||
|
ready();
|
||||||
|
}, {once: true})
|
||||||
|
if (!self.seeking) {
|
||||||
|
debug('Video', 'sCV set in', video.src, in_, video.currentTime, video.seeking);
|
||||||
|
self.seeking = true;
|
||||||
|
video.currentTime = in_;
|
||||||
|
if (self.paused) {
|
||||||
|
var promise = self.video.play();
|
||||||
|
if (promise !== undefined) {
|
||||||
|
promise.then(function() {
|
||||||
|
self.video.pause();
|
||||||
|
self.video.muted = muted;
|
||||||
|
}).catch(function() {
|
||||||
|
self.video.pause();
|
||||||
|
self.video.muted = muted;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.video.pause();
|
||||||
|
self.video.muted = muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentItemTime(currentTime) {
|
||||||
|
debug('Video', 'sCIT', currentTime, self.video.currentTime,
|
||||||
|
'delta', currentTime - self.video.currentTime);
|
||||||
|
isReady(self.video, function(video) {
|
||||||
|
if (self.video == video) {
|
||||||
|
if(self.video.seeking) {
|
||||||
|
self.video.addEventListener("seeked", event => {
|
||||||
|
that.triggerEvent('seeked');
|
||||||
|
self.seeking = false;
|
||||||
|
}, {once: true})
|
||||||
|
} else if (self.seeking) {
|
||||||
|
that.triggerEvent('seeked');
|
||||||
|
self.seeking = false;
|
||||||
|
}
|
||||||
|
video.currentTime = currentTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentTime(time) {
|
||||||
|
debug('Video', 'sCT', time);
|
||||||
|
var currentTime, currentItem;
|
||||||
|
self.items.forEach(function(item, i) {
|
||||||
|
if (time >= item.position
|
||||||
|
&& time < item.position + item.duration) {
|
||||||
|
currentItem = i;
|
||||||
|
currentTime = time - item.position + item['in'];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (self.items.length) {
|
||||||
|
// Set to end of items if time > duration
|
||||||
|
if (isUndefined(currentItem) && isUndefined(currentTime)) {
|
||||||
|
currentItem = self.items.length - 1;
|
||||||
|
currentTime = self.items[currentItem].duration + self.items[currentItem]['in'];
|
||||||
|
}
|
||||||
|
debug('Video', 'sCT', time, '=>', currentItem, currentTime);
|
||||||
|
if (currentItem != self.currentItem) {
|
||||||
|
setCurrentItem(currentItem);
|
||||||
|
}
|
||||||
|
self.seeking = true;
|
||||||
|
self.currentTime = time;
|
||||||
|
that.triggerEvent('seeking');
|
||||||
|
setCurrentItemTime(currentTime);
|
||||||
|
} else {
|
||||||
|
self.currentTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSource() {
|
||||||
|
self.loadedMetadata = false;
|
||||||
|
loadItems(function() {
|
||||||
|
setCurrentTime(self.options.position);
|
||||||
|
self.options.autoplay && setTimeout(function() {
|
||||||
|
that.play();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*@
|
||||||
|
brightness <f> get/set brightness
|
||||||
|
@*/
|
||||||
|
that.brightness = function() {
|
||||||
|
var ret;
|
||||||
|
if (arguments.length == 0) {
|
||||||
|
ret = 1 - parseFloat(self.brightness.style.opacity);
|
||||||
|
} else {
|
||||||
|
self.brightness.style.opacity = 1 - arguments[0]
|
||||||
|
ret = that;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
buffered <f> buffered
|
||||||
|
@*/
|
||||||
|
that.buffered = function() {
|
||||||
|
return self.video.buffered;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
currentTime <f> get/set currentTime
|
||||||
|
@*/
|
||||||
|
that.currentTime = function() {
|
||||||
|
var ret;
|
||||||
|
if (arguments.length == 0) {
|
||||||
|
ret = getCurrentTime();
|
||||||
|
} else {
|
||||||
|
self.ended = false;
|
||||||
|
setCurrentTime(arguments[0]);
|
||||||
|
ret = that;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
duration <f> duration
|
||||||
|
@*/
|
||||||
|
that.duration = function() {
|
||||||
|
return self.items ? self.items.reduce((duration, item) => {
|
||||||
|
return duration + item.duration;
|
||||||
|
}, 0) : NaN;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
muted <f> get/set muted
|
||||||
|
@*/
|
||||||
|
that.muted = function(value) {
|
||||||
|
if (!isUndefined(value)) {
|
||||||
|
self.muted = value;
|
||||||
|
}
|
||||||
|
return getset('muted', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
pause <f> pause
|
||||||
|
@*/
|
||||||
|
that.pause = function() {
|
||||||
|
that.paused = self.paused = true;
|
||||||
|
self.video.pause();
|
||||||
|
that.paused && that.triggerEvent('pause')
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
play <f> play
|
||||||
|
@*/
|
||||||
|
that.play = function() {
|
||||||
|
if (self.ended) {
|
||||||
|
that.currentTime(0);
|
||||||
|
}
|
||||||
|
isReady(self.video, function(video) {
|
||||||
|
self.ended = false;
|
||||||
|
that.paused = self.paused = false;
|
||||||
|
self.seeking = false;
|
||||||
|
video.play();
|
||||||
|
that.triggerEvent('play')
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
that.removeElement = function() {
|
||||||
|
self.currentTime = getCurrentTime();
|
||||||
|
self.loading = true;
|
||||||
|
clearInterval(self.timeupdate);
|
||||||
|
//Chrome does not properly release resources, reset manually
|
||||||
|
//http://code.google.com/p/chromium/issues/detail?id=31014
|
||||||
|
self.videos.forEach(function(video) {
|
||||||
|
video.src = ''
|
||||||
|
});
|
||||||
|
return Ox.Element.prototype.removeElement.apply(that, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
videoHeight <f> get videoHeight
|
||||||
|
@*/
|
||||||
|
that.videoHeight = function() {
|
||||||
|
return self.video.videoHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
videoWidth <f> get videoWidth
|
||||||
|
@*/
|
||||||
|
that.videoWidth = function() {
|
||||||
|
return self.video.videoWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
volume <f> get/set volume
|
||||||
|
@*/
|
||||||
|
that.volume = function(value) {
|
||||||
|
if (isUndefined(value)) {
|
||||||
|
value = self.volume
|
||||||
|
} else {
|
||||||
|
self.volume = value;
|
||||||
|
self.video.volume = getVolume();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// mobile browsers only allow playing media elements after user interaction
|
||||||
|
|
||||||
|
function mediaPlaybackRequiresUserGesture() {
|
||||||
|
// test if play() is ignored when not called from an input event handler
|
||||||
|
var video = document.createElement('video');
|
||||||
|
video.muted = true
|
||||||
|
video.play();
|
||||||
|
return video.paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function removeBehaviorsRestrictions() {
|
||||||
|
debug('Video', 'remove restrictions on video', self.video, restrictedElements.length, queue.length);
|
||||||
|
if (restrictedElements.length > 0) {
|
||||||
|
var rElements = restrictedElements;
|
||||||
|
restrictedElements = [];
|
||||||
|
rElements.forEach(async function(video) {
|
||||||
|
await video.load();
|
||||||
|
});
|
||||||
|
setTimeout(function() {
|
||||||
|
var u = unblock;
|
||||||
|
unblock = [];
|
||||||
|
u.forEach(function(callback) { callback(); });
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
while (queue.length < queueSize) {
|
||||||
|
var video = document.createElement('video');
|
||||||
|
video.load();
|
||||||
|
queue.push(video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresUserGesture) {
|
||||||
|
window.addEventListener('keydown', removeBehaviorsRestrictions);
|
||||||
|
window.addEventListener('mousedown', removeBehaviorsRestrictions);
|
||||||
|
window.addEventListener('touchstart', removeBehaviorsRestrictions);
|
||||||
|
}
|
||||||
|
})();
|
400
app/static/js/VideoPlayer.js
Normal file
400
app/static/js/VideoPlayer.js
Normal file
|
@ -0,0 +1,400 @@
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
window.VideoPlayer = function(options) {
|
||||||
|
|
||||||
|
var self = {}, that;
|
||||||
|
self.options = {
|
||||||
|
autoplay: false,
|
||||||
|
controls: true,
|
||||||
|
items: [],
|
||||||
|
loop: false,
|
||||||
|
muted: false,
|
||||||
|
playbackRate: 1,
|
||||||
|
position: 0,
|
||||||
|
volume: 1
|
||||||
|
}
|
||||||
|
Object.assign(self.options, options);
|
||||||
|
that = VideoElement(options);
|
||||||
|
|
||||||
|
self.controls = document.createElement('div')
|
||||||
|
self.controls.classList.add('mx-controls')
|
||||||
|
//self.controls.style.display = "none"
|
||||||
|
if (self.options.controls) {
|
||||||
|
var ratio = `aspect-ratio: ${self.options.aspectratio};`
|
||||||
|
if (!self.options.aspectratio) {
|
||||||
|
ratio = 'height: 128px;'
|
||||||
|
}
|
||||||
|
self.controls.innerHTML = `
|
||||||
|
<style>
|
||||||
|
.fullscreen video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.mx-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
${ratio}
|
||||||
|
max-height: 100vh;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
color: white;
|
||||||
|
z-index: 1;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-controls .toggle {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.mx-controls .volume:hover,
|
||||||
|
.mx-controls .fullscreen-btn:hover,
|
||||||
|
.mx-controls .toggle:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mx-controls .toggle div {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.mx-controls .controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.mx-controls .controls .position {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.mx-controls .toggle svg {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.mx-controls .toggle .loading svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.mx-controls .controls .volume svg,
|
||||||
|
.mx-controls .controls .fullscreen-btn svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
.mx-controls .controls .volume {
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
.mx-controls.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.fullscreen .mx-controls {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.fullscreen .mx-controls video {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.fullscreen .mx-controls .controls {
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.fullscreen .mx-controls .fullscreen-btn {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.fullscreen .mx-controls .volume {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.mx-controls {
|
||||||
|
transition: opacity 0.3s linear;
|
||||||
|
}
|
||||||
|
.mx-controls .controls .position {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.mx-controls .controls .position .bar {
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
height: 4px;
|
||||||
|
border: solid 1px #B1B1B1;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.fullscreen .mx-controls .controls .position .bar {
|
||||||
|
}
|
||||||
|
.mx-controls .controls .position .progress {
|
||||||
|
width: 0;
|
||||||
|
background: #B1B1B180;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
.mx-controls .controls .time {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.mx-controls .controls .time div {
|
||||||
|
margin: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="toggle" title="Play">
|
||||||
|
<div>${icon.play}</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="volume" title="Mute">
|
||||||
|
${icon.mute}
|
||||||
|
</div>
|
||||||
|
<div class="position">
|
||||||
|
<div class="bar">
|
||||||
|
<div class="progress"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="time">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div class="fullscreen-btn" title="Fullscreen">
|
||||||
|
${isIOS || !self.options.aspectratio ? "" : icon.enterFullscreen}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
var toggleVideo = event => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (that.paused) {
|
||||||
|
that.play()
|
||||||
|
} else {
|
||||||
|
that.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function toggleFullscreen(event) {
|
||||||
|
if (isIOS) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
that.classList.add('fullscreen')
|
||||||
|
if (that.webkitRequestFullscreen) {
|
||||||
|
await that.webkitRequestFullscreen()
|
||||||
|
} else {
|
||||||
|
await that.requestFullscreen()
|
||||||
|
}
|
||||||
|
console.log('entered fullscreen')
|
||||||
|
var failed = false
|
||||||
|
if (!screen.orientation.type.startsWith("landscape")) {
|
||||||
|
await screen.orientation.lock("landscape").catch(err => {
|
||||||
|
console.log('no luck with lock', err)
|
||||||
|
/*
|
||||||
|
document.querySelector('.error').innerHTML = '' + err
|
||||||
|
that.classList.remove('fullscreen')
|
||||||
|
document.exitFullscreen();
|
||||||
|
screen.orientation.unlock()
|
||||||
|
failed = true
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (that.paused && !failed) {
|
||||||
|
that.play()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
that.classList.remove('fullscreen')
|
||||||
|
document.exitFullscreen();
|
||||||
|
screen.orientation.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var toggleSound = event => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
if (that.muted()) {
|
||||||
|
that.muted(false)
|
||||||
|
} else {
|
||||||
|
that.muted(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var showControls
|
||||||
|
var toggleControls = event => {
|
||||||
|
if (self.controls.style.opacity == '0') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
self.controls.style.opacity = '1'
|
||||||
|
showControls = setTimeout(() => {
|
||||||
|
self.controls.style.opacity = that.paused ? '1' : '0'
|
||||||
|
showControls = null
|
||||||
|
}, 3000)
|
||||||
|
} else {
|
||||||
|
self.controls.style.opacity = '0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.controls.addEventListener("mousemove", event => {
|
||||||
|
if (showControls) {
|
||||||
|
clearTimeout(showControls)
|
||||||
|
}
|
||||||
|
self.controls.style.opacity = '1'
|
||||||
|
showControls = setTimeout(() => {
|
||||||
|
self.controls.style.opacity = that.paused ? '1' : '0'
|
||||||
|
showControls = null
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
self.controls.addEventListener("mouseleave", event => {
|
||||||
|
if (showControls) {
|
||||||
|
clearTimeout(showControls)
|
||||||
|
}
|
||||||
|
self.controls.style.opacity = that.paused ? '1' : '0'
|
||||||
|
showControls = null
|
||||||
|
})
|
||||||
|
self.controls.addEventListener("touchstart", toggleControls)
|
||||||
|
self.controls.querySelector('.toggle').addEventListener("click", toggleVideo)
|
||||||
|
self.controls.querySelector('.volume').addEventListener("click", toggleSound)
|
||||||
|
self.controls.querySelector('.fullscreen-btn').addEventListener("click", toggleFullscreen)
|
||||||
|
document.addEventListener('fullscreenchange', event => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
screen.orientation.unlock()
|
||||||
|
that.classList.remove('fullscreen')
|
||||||
|
that.querySelector('.fullscreen-btn').innerHTML = icon.enterFullscreen
|
||||||
|
} else {
|
||||||
|
self.controls.querySelector('.fullscreen-btn').innerHTML = icon.exitFullscreen
|
||||||
|
}
|
||||||
|
})
|
||||||
|
that.append(self.controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVideoWidth() {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
var av = that.querySelector('video.active')
|
||||||
|
return av ? av.getBoundingClientRect().width + 'px' : '100%'
|
||||||
|
}
|
||||||
|
|
||||||
|
var playOnLoad = false
|
||||||
|
var unblock = document.createElement("div")
|
||||||
|
|
||||||
|
that.addEventListener("requiresusergesture", event => {
|
||||||
|
unblock.style.position = "absolute"
|
||||||
|
unblock.style.width = '100%'
|
||||||
|
unblock.style.height = '100%'
|
||||||
|
unblock.style.backgroundImage = `url(${self.options.poster})`
|
||||||
|
unblock.style.zIndex = '1000'
|
||||||
|
unblock.style.backgroundPosition = "top left"
|
||||||
|
unblock.style.backgroundRepeat = "no-repeat"
|
||||||
|
unblock.style.backgroundSize = "cover"
|
||||||
|
unblock.style.display = 'flex'
|
||||||
|
unblock.classList.add('mx-controls')
|
||||||
|
unblock.classList.add('poster')
|
||||||
|
unblock.innerHTML = `
|
||||||
|
<div class="toggle">
|
||||||
|
<div style="margin: auto">${icon.play}</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls" style="height: 37px;"></div>
|
||||||
|
`
|
||||||
|
self.controls.style.opacity = '0'
|
||||||
|
unblock.addEventListener("click", event => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
playOnLoad = true
|
||||||
|
unblock.querySelector('.toggle').innerHTML = `
|
||||||
|
<div style="margin: auto" class="loading">${icon.loading}</div>
|
||||||
|
`
|
||||||
|
}, {once: true})
|
||||||
|
that.append(unblock)
|
||||||
|
})
|
||||||
|
var loading = true
|
||||||
|
that.brightness(0)
|
||||||
|
that.addEventListener("loadedmetadata", event => {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
that.addEventListener("seeked", event => {
|
||||||
|
if (loading) {
|
||||||
|
that.brightness(1)
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
if (playOnLoad) {
|
||||||
|
playOnLoad = false
|
||||||
|
var toggle = self.controls.querySelector('.toggle')
|
||||||
|
toggle.title = 'Pause'
|
||||||
|
toggle.querySelector('div').innerHTML = icon.pause
|
||||||
|
self.controls.style.opacity = '0'
|
||||||
|
unblock.remove()
|
||||||
|
that.play()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var time = that.querySelector('.controls .time div'),
|
||||||
|
progress = that.querySelector('.controls .position .progress')
|
||||||
|
that.querySelector('.controls .position').addEventListener("click", event => {
|
||||||
|
var bar = event.target
|
||||||
|
while (bar && !bar.classList.contains('bar')) {
|
||||||
|
bar = bar.parentElement
|
||||||
|
}
|
||||||
|
if (bar && bar.classList.contains('bar')) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
var rect = bar.getBoundingClientRect()
|
||||||
|
var x = event.clientX - rect.x
|
||||||
|
var percent = x / rect.width
|
||||||
|
var position = percent * self.options.duration
|
||||||
|
if (self.options.position) {
|
||||||
|
position += self.options.position
|
||||||
|
}
|
||||||
|
progress.style.width = (100 * percent) + '%'
|
||||||
|
that.currentTime(position)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
that.addEventListener("timeupdate", event => {
|
||||||
|
var currentTime = that.currentTime(),
|
||||||
|
duration = self.options.duration
|
||||||
|
if (self.options.position) {
|
||||||
|
currentTime -= self.options.position
|
||||||
|
}
|
||||||
|
progress.style.width = (100 * currentTime / duration) + '%'
|
||||||
|
duration = formatDuration(duration)
|
||||||
|
currentTime = formatDuration(currentTime)
|
||||||
|
while (duration && duration.startsWith('00:')) {
|
||||||
|
duration = duration.slice(3)
|
||||||
|
}
|
||||||
|
currentTime = currentTime.slice(currentTime.length - duration.length)
|
||||||
|
time.innerText = `${currentTime} / ${duration}`
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
that.addEventListener("play", event => {
|
||||||
|
var toggle = self.controls.querySelector('.toggle')
|
||||||
|
toggle.title = 'Pause'
|
||||||
|
toggle.querySelector('div').innerHTML = icon.pause
|
||||||
|
self.controls.style.opacity = '0'
|
||||||
|
})
|
||||||
|
that.addEventListener("pause", event => {
|
||||||
|
var toggle = self.controls.querySelector('.toggle')
|
||||||
|
toggle.title = 'Play'
|
||||||
|
toggle.querySelector('div').innerHTML = icon.play
|
||||||
|
self.controls.style.opacity = '1'
|
||||||
|
})
|
||||||
|
that.addEventListener("ended", event => {
|
||||||
|
var toggle = self.controls.querySelector('.toggle')
|
||||||
|
toggle.title = 'Play'
|
||||||
|
toggle.querySelector('div').innerHTML = icon.play
|
||||||
|
self.controls.style.opacity = '1'
|
||||||
|
})
|
||||||
|
that.addEventListener("seeking", event => {
|
||||||
|
//console.log("seeking")
|
||||||
|
|
||||||
|
})
|
||||||
|
that.addEventListener("seeked", event => {
|
||||||
|
//console.log("seeked")
|
||||||
|
})
|
||||||
|
that.addEventListener("volumechange", event => {
|
||||||
|
var volume = self.controls.querySelector('.volume')
|
||||||
|
if (that.muted()) {
|
||||||
|
volume.innerHTML = icon.unmute
|
||||||
|
volume.title = "Unmute"
|
||||||
|
} else {
|
||||||
|
volume.innerHTML = icon.mute
|
||||||
|
volume.title = "Mute"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.addEventListener('resize', event => {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
return that
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
25
app/static/js/api.js
Normal file
25
app/static/js/api.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
var pandora = {
|
||||||
|
format: getFormat(),
|
||||||
|
hostname: document.location.hostname || 'pad.ma'
|
||||||
|
}
|
||||||
|
|
||||||
|
var pandoraURL = document.location.hostname ? "" : `https://${pandora.hostname}`
|
||||||
|
var cache = cache || {}
|
||||||
|
|
||||||
|
async function pandoraAPI(action, data) {
|
||||||
|
var url = pandoraURL + '/api/'
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
66
app/static/js/comments.js
Normal file
66
app/static/js/comments.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
function renderComments(data) {
|
||||||
|
var cdiv = div.querySelector('.comments')
|
||||||
|
cdiv.innerHTML = `
|
||||||
|
<h3>
|
||||||
|
<span class="icon">${icon.down}</span>
|
||||||
|
Comments
|
||||||
|
</h3>
|
||||||
|
`
|
||||||
|
comments.forEach(comment => {
|
||||||
|
var c = document.createElement('div')
|
||||||
|
c.className = 'comment'
|
||||||
|
c.innerHTML = `
|
||||||
|
<div class="comment">
|
||||||
|
<div class="name">${comment.name}</div>
|
||||||
|
<div class="date">${comment.date}</div>
|
||||||
|
<div class="text">${comment.text}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
cdiv.append(c)
|
||||||
|
})
|
||||||
|
var add = document.querySelector('.add-comment')
|
||||||
|
add.style.display = 'block'
|
||||||
|
cdiv.append(add)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('button#add-comment').addEventListener('click', event => {
|
||||||
|
var data = {}, csrf;
|
||||||
|
document.querySelector('.add-comment').querySelectorAll('input,textarea').forEach(input => {
|
||||||
|
if (input.name == 'csrfmiddlewaretoken') {
|
||||||
|
csrf = input.value.trim()
|
||||||
|
} else {
|
||||||
|
data[input.name] = input.value.trim()
|
||||||
|
if (!data[input.name]) {
|
||||||
|
delete data[input.name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
data.item = pandora.comment
|
||||||
|
fetch("/comment/", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': csrf
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}).then(response => {
|
||||||
|
return response.json()
|
||||||
|
}).then(response => {
|
||||||
|
var comment= document.createElement('div')
|
||||||
|
comment.classList.add('comment')
|
||||||
|
var name = document.createElement('div')
|
||||||
|
name.classList.add('name')
|
||||||
|
name.innerText = response.name
|
||||||
|
comment.append(name)
|
||||||
|
var date = document.createElement('div')
|
||||||
|
date.classList.add('date')
|
||||||
|
date.innerText = response.date
|
||||||
|
comment.append(date)
|
||||||
|
var text = document.createElement('div')
|
||||||
|
text.classList.add('name')
|
||||||
|
text.innerText = response.text
|
||||||
|
comment.append(text)
|
||||||
|
document.querySelector('.comments').append(comment)
|
||||||
|
document.querySelector('.add-comment textarea').value = ''
|
||||||
|
})
|
||||||
|
})
|
112
app/static/js/documents.js
Normal file
112
app/static/js/documents.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
|
||||||
|
async function loadDocument(id, args) {
|
||||||
|
var data = window.data = {}
|
||||||
|
var parts = id.split('/')
|
||||||
|
data.id = parts.shift()
|
||||||
|
data.site = pandora.hostname
|
||||||
|
|
||||||
|
if (parts.length == 2) {
|
||||||
|
data.page = parts.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length == 1) {
|
||||||
|
var rect = parts[0].split(',')
|
||||||
|
if (rect.length == 1) {
|
||||||
|
data.page = parts[0]
|
||||||
|
} else {
|
||||||
|
data.crop = rect
|
||||||
|
}
|
||||||
|
} else if (parts.length == 2) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await pandoraAPI('getDocument', {
|
||||||
|
id: data.id,
|
||||||
|
keys: [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"extension",
|
||||||
|
"text",
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (response.status.code != 200) {
|
||||||
|
return {
|
||||||
|
site: data.site,
|
||||||
|
error: response.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.document = response['data']
|
||||||
|
data.title = data.document.name
|
||||||
|
data.link = `${pandora.proto}://${data.site}/documents/${data.document.id}`
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderDocument(data) {
|
||||||
|
if (data.error) {
|
||||||
|
return renderError(data)
|
||||||
|
}
|
||||||
|
div = document.createElement('div')
|
||||||
|
div.className = "content"
|
||||||
|
if (!data.document) {
|
||||||
|
div.innerHTML = `<div style="display: flex;height: 100vh; width:100%"><div style="margin: auto">document not found</div></div>`
|
||||||
|
} else if (data.document.extension == "html") {
|
||||||
|
div.innerHTML = `
|
||||||
|
<h1 class="title">${data.document.title}</h1>
|
||||||
|
<div class="text document" lang="en">
|
||||||
|
${data.document.text}
|
||||||
|
</div>
|
||||||
|
<div class="more">
|
||||||
|
<a href="${data.link}">Open on ${data.site}</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
div.querySelectorAll('.text a').forEach(a => {
|
||||||
|
a.addEventListener("click", clickLink)
|
||||||
|
|
||||||
|
})
|
||||||
|
} else if (data.document.extension == "pdf" && data.page && data.crop) {
|
||||||
|
var img = `${pandora.proto}://${data.site}/documents/${data.document.id}/1024p${data.page},${data.crop.join(',')}.jpg`
|
||||||
|
data.link = getLink(`documents/${data.document.id}/${data.page}`)
|
||||||
|
div.innerHTML = `
|
||||||
|
<h1 class="title">${data.document.title}</h1>
|
||||||
|
<div class="text document" style="display: flex; width: 100%">
|
||||||
|
<img src="${img}" style="margin: auto;max-width: 100%">
|
||||||
|
</div>
|
||||||
|
<div class="more">
|
||||||
|
<a href="${data.link}">Open pdf page</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
} else if (data.document.extension == "pdf") {
|
||||||
|
var page = data.page || 1,
|
||||||
|
file = encodeURIComponent(`/documents/${data.document.id}/${safeDocumentName(data.document.title)}.pdf`)
|
||||||
|
div.innerHTML = `
|
||||||
|
<h1 class="title">${data.document.title}</h1>
|
||||||
|
<div class="text document">
|
||||||
|
<iframe src="${pandora.proto}://${data.site}/static/pdf.js/?file=${file}#page=${page}" frameborder="0" style="width: 100%; height: calc(100vh - 8px);"></iframe>
|
||||||
|
</div>
|
||||||
|
<div class="more">
|
||||||
|
<a href="${data.link}">Open on ${data.site}</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
} else if (data.document.extension == "jpg" || data.document.extension == "png") {
|
||||||
|
var img = `${pandora.proto}://${data.site}/documents/${data.document.id}/${safeDocumentName(data.document.title)}.${data.document.extension}`
|
||||||
|
var open_text = `Open on ${data.site}`
|
||||||
|
if (data.crop) {
|
||||||
|
img = `${pandora.proto}://${data.site}/documents/${data.document.id}/1024p${data.crop.join(',')}.jpg`
|
||||||
|
data.link = getLink(`documents/${data.document.id}`)
|
||||||
|
open_text = `Open image`
|
||||||
|
}
|
||||||
|
div.innerHTML = `
|
||||||
|
<h1 class="title">${data.document.title}</h1>
|
||||||
|
<div class="text" style="display: flex; width: 100%">
|
||||||
|
<img src="${img}" style="margin: auto;max-width: 100%">
|
||||||
|
</div>
|
||||||
|
<div class="more">
|
||||||
|
<a href="${data.link}">${open_text}</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
} else {
|
||||||
|
div.innerHTML = `unsupported document type`
|
||||||
|
}
|
||||||
|
document.querySelector(".content").replaceWith(div)
|
||||||
|
}
|
230
app/static/js/edits.js
Normal file
230
app/static/js/edits.js
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
|
||||||
|
const getSortValue = function(value) {
|
||||||
|
var sortValue = value;
|
||||||
|
function trim(value) {
|
||||||
|
return value.replace(/^\W+(?=\w)/, '');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isEmpty(value)
|
||||||
|
|| isNull(value)
|
||||||
|
|| isUndefined(value)
|
||||||
|
) {
|
||||||
|
sortValue = null;
|
||||||
|
} else if (isString(value)) {
|
||||||
|
// make lowercase and remove leading non-word characters
|
||||||
|
sortValue = trim(value.toLowerCase());
|
||||||
|
// move leading articles to the end
|
||||||
|
// and remove leading non-word characters
|
||||||
|
['a', 'an', 'the'].forEach(function(article) {
|
||||||
|
if (new RegExp('^' + article + ' ').test(sortValue)) {
|
||||||
|
sortValue = trim(sortValue.slice(article.length + 1))
|
||||||
|
+ ', ' + sortValue.slice(0, article.length);
|
||||||
|
return false; // break
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// remove thousand separators and pad numbers
|
||||||
|
sortValue = sortValue.replace(/(\d),(?=(\d{3}))/g, '$1')
|
||||||
|
.replace(/\d+/g, function(match) {
|
||||||
|
return match.padStart(64, '0')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sortValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortByKey = function(array, by) {
|
||||||
|
return array.sort(function(a, b) {
|
||||||
|
var aValue, bValue, index = 0, key, ret = 0;
|
||||||
|
while (ret == 0 && index < by.length) {
|
||||||
|
key = by[index].key;
|
||||||
|
aValue = getSortValue(a[key])
|
||||||
|
bValue = getSortValue(b[key])
|
||||||
|
if ((aValue === null) != (bValue === null)) {
|
||||||
|
ret = aValue === null ? 1 : -1;
|
||||||
|
} else if (aValue < bValue) {
|
||||||
|
ret = by[index].operator == '+' ? -1 : 1;
|
||||||
|
} else if (aValue > bValue) {
|
||||||
|
ret = by[index].operator == '+' ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function sortClips(edit, sort) {
|
||||||
|
var key = sort.key, index;
|
||||||
|
if (key == 'position') {
|
||||||
|
key = 'in';
|
||||||
|
}
|
||||||
|
if ([
|
||||||
|
'id', 'index', 'in', 'out', 'duration',
|
||||||
|
'title', 'director', 'year', 'videoRatio'
|
||||||
|
].indexOf(key) > -1) {
|
||||||
|
sortBy(sort);
|
||||||
|
index = 0;
|
||||||
|
edit.clips.forEach(function(clip) {
|
||||||
|
clip.sort = index++;
|
||||||
|
if (sort.operator == '-') {
|
||||||
|
clip.sort = -clip.sort;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var response = await pandoraAPI('sortClips', {
|
||||||
|
edit: edit.id,
|
||||||
|
sort: [sort]
|
||||||
|
})
|
||||||
|
edit.clips.forEach(function(clip) {
|
||||||
|
clip.sort = response.data.clips.indexOf(clip.id);
|
||||||
|
if (sort.operator == '-') {
|
||||||
|
clip.sort = -clip.sort;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sortBy({
|
||||||
|
key: 'sort',
|
||||||
|
operator: '+'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function sortBy(key) {
|
||||||
|
edit.clips = sortByKey(edit.clips, [key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEdit(id, args) {
|
||||||
|
var data = window.data = {}
|
||||||
|
data.id = id
|
||||||
|
data.site = pandora.hostname
|
||||||
|
|
||||||
|
var response = await pandoraAPI('getEdit', {
|
||||||
|
id: data.id,
|
||||||
|
keys: [
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (response.status.code != 200) {
|
||||||
|
return {
|
||||||
|
site: data.site,
|
||||||
|
error: response.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.edit = response['data']
|
||||||
|
if (data.edit.status !== 'public') {
|
||||||
|
return {
|
||||||
|
site: data.site,
|
||||||
|
error: {
|
||||||
|
code: 403,
|
||||||
|
text: 'permission denied'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.layers = {}
|
||||||
|
data.videos = []
|
||||||
|
|
||||||
|
if (args.sort) {
|
||||||
|
await sortClips(data.edit, args.sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.edit.duration = 0;
|
||||||
|
data.edit.clips.forEach(function(clip) {
|
||||||
|
clip.position = data.edit.duration;
|
||||||
|
data.edit.duration += clip.duration;
|
||||||
|
});
|
||||||
|
|
||||||
|
data.edit.clips.forEach(clip => {
|
||||||
|
var start = clip['in'] || 0, end = clip.out, position = 0;
|
||||||
|
clip.durations.forEach((duration, idx) => {
|
||||||
|
if (!duration) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (position + duration <= start || position > end) {
|
||||||
|
// pass
|
||||||
|
} else {
|
||||||
|
var video = {}
|
||||||
|
var oshash = clip.streams[idx]
|
||||||
|
video.src = getVideoURL(clip.item, 480, idx+1, '', oshash)
|
||||||
|
/*
|
||||||
|
if (clip['in'] && clip.out) {
|
||||||
|
video.src += `#t=${clip['in']},${clip.out}`
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (isNumber(clip.volume)) {
|
||||||
|
video.volume = clip.volume;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
position <= start
|
||||||
|
&& position + duration > start
|
||||||
|
) {
|
||||||
|
video['in'] = start - position;
|
||||||
|
}
|
||||||
|
if (position + duration >= end) {
|
||||||
|
video.out = end - position;
|
||||||
|
}
|
||||||
|
if (video['in'] && video.out) {
|
||||||
|
video.duration = video.out - video['in']
|
||||||
|
} else if (video.out) {
|
||||||
|
video.duration = video.out;
|
||||||
|
} else if (!isUndefined(video['in'])) {
|
||||||
|
video.duration = duration - video['in'];
|
||||||
|
video.out = duration;
|
||||||
|
} else {
|
||||||
|
video.duration = duration;
|
||||||
|
video['in'] = 0;
|
||||||
|
video.out = video.duration;
|
||||||
|
}
|
||||||
|
data.videos.push(video)
|
||||||
|
}
|
||||||
|
position += duration
|
||||||
|
})
|
||||||
|
Object.keys(clip.layers).forEach(layer => {
|
||||||
|
data.layers[layer] = data.layers[layer] || []
|
||||||
|
clip.layers[layer].forEach(annotation => {
|
||||||
|
if (args.users && !args.users.includes(annotation.user)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (args.layers && !args.layers.includes(layer)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var a = {...annotation}
|
||||||
|
a['id'] = clip['id'] + '/' + a['id'];
|
||||||
|
a['in'] = Math.max(
|
||||||
|
clip['position'],
|
||||||
|
a['in'] - clip['in'] + clip['position']
|
||||||
|
);
|
||||||
|
a.out = Math.min(
|
||||||
|
clip['position'] + clip['duration'],
|
||||||
|
a.out - clip['in'] + clip['position']
|
||||||
|
);
|
||||||
|
data.layers[layer].push(a)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
var value = []
|
||||||
|
pandora.layerKeys.forEach(layer => {
|
||||||
|
if (!data.layers[layer]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var html = []
|
||||||
|
var layerData = getObjectById(pandora.site.layers, layer)
|
||||||
|
html.push(`<h3>
|
||||||
|
<span class="icon">${icon.down}</span>
|
||||||
|
${layerData.title}
|
||||||
|
</h3>`)
|
||||||
|
data.layers[layer].forEach(annotation => {
|
||||||
|
html.push(`
|
||||||
|
<div class="annotation ${layerData.type}" data-in="${annotation.in}" data-out="${annotation.out}">
|
||||||
|
${annotation.value}
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
value.push('<div class="layer">' + html.join('\n') + '</div>')
|
||||||
|
})
|
||||||
|
data.value = value.join('\n')
|
||||||
|
|
||||||
|
data.title = data.edit.name
|
||||||
|
data.byline = data.edit.description
|
||||||
|
data.link = `${pandora.proto}://${data.site}/edits/${data.edit.id}`
|
||||||
|
data.poster = data.videos[0].src.split('/48')[0] + `/480p${data.videos[0].in}.jpg`
|
||||||
|
data.aspectratio = data.edit.clips[0].videoRatio
|
||||||
|
data.duration = data.edit.duration
|
||||||
|
return data
|
||||||
|
|
||||||
|
}
|
182
app/static/js/icons.js
Normal file
182
app/static/js/icons.js
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
var icon = {}
|
||||||
|
icon.enterFullscreen = `
|
||||||
|
<svg
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="#B1B1B1"
|
||||||
|
stroke-width="8">
|
||||||
|
<path
|
||||||
|
d="M 44,112 l 0,-68 l 68,0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 144,44 l 68,0 l 0,68"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 212,144 l 0,68 l -68,0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 112,212 l -68,0 l 0,-68"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
icon.exitFullscreen = `
|
||||||
|
<svg
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="#B1B1B1"
|
||||||
|
stroke-width="8">
|
||||||
|
<path
|
||||||
|
d="m 100,32 v 68 H 32"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 224,100 H 156 V 32"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 156,224 v -68 h 68"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 32,156 h 68 v 68"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
icon.mute = `
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
viewBox="0 0 256 256">
|
||||||
|
<path
|
||||||
|
d="M 160,96 a 48,48 0 0,1 0,64"
|
||||||
|
fill="none"
|
||||||
|
stroke="#808080"
|
||||||
|
stroke-width="16"
|
||||||
|
id="path2275"
|
||||||
|
style="opacity:1;stroke-width:8;stroke-dasharray:none;stroke:#B1B1B1" />
|
||||||
|
<path
|
||||||
|
d="M 176,64 a 88,88 0 0,1 0,128"
|
||||||
|
fill="none"
|
||||||
|
stroke="#808080"
|
||||||
|
stroke-width="16"
|
||||||
|
id="path2277"
|
||||||
|
style="stroke-width:8;stroke-dasharray:none;stroke:#B1B1B1" />
|
||||||
|
<path
|
||||||
|
d="M 192,32 a 128,128 0 0,1 0,192"
|
||||||
|
fill="none"
|
||||||
|
stroke="#808080"
|
||||||
|
stroke-width="16"
|
||||||
|
id="path2279"
|
||||||
|
style="stroke-width:8;stroke-dasharray:none;stroke:#B1B1B1" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-rule:evenodd;stroke:#B1B1B1;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
|
||||||
|
d="m 15.73147,87.829137 64.658698,0.143482 64.118622,-63.901556 -0.0618,208.474357 -64.560837,-65.01777 -63.594727,0.48342 z"
|
||||||
|
id="path4946" />
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
icon.unmute = `
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
viewBox="0 0 256 256">
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-rule:evenodd;stroke:#B1B1B1;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
|
||||||
|
d="m 15.73147,87.829137 64.658698,0.143482 64.118622,-63.901556 -0.0618,208.474357 -64.560837,-65.01777 -63.594727,0.48342 z"
|
||||||
|
id="path4946" />
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
icon.play = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
|
||||||
|
<polygon points="56,32 248,128 56,224" style="fill:none;fill-rule:evenodd;stroke:#B1B1B1;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
icon.pause = `
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
viewBox="0 0 256 256">
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-rule:evenodd;stroke:#B1B1B1;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
|
||||||
|
d="m 55.561915,31.764828 47.856395,0.122789 0.79397,192.070763 -48.048909,-0.50781 z"
|
||||||
|
id="path6254" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-rule:evenodd;stroke:#B1B1B1;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
|
||||||
|
d="m 150.7755,32.038558 47.85639,0.122789 0.79397,192.070763 -48.04891,-0.50781 z"
|
||||||
|
id="path6254-6" />
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
icon.loading = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
|
||||||
|
<g transform="translate(128, 128)" stroke="#404040" stroke-linecap="round" stroke-width="28">
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(0)" opacity="1">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="0s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(30)" opacity="0.083333">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.916667s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(60)" opacity="0.166667">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.833333s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(90)" opacity="0.25">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.75s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(120)" opacity="0.333333">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.666667s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(150)" opacity="0.416667">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.583333s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(180)" opacity="0.5">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.5s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(210)" opacity="0.583333">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.416667" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(240)" opacity="0.666667">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.333333s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(270)" opacity="0.75">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.25s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(300)" opacity="0.833333">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.166667s" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(330)" opacity="0.916667">
|
||||||
|
<animate attributeName="opacity" from="1" to="0" begin="-0.083333" dur="1s" repeatCount="indefinite"></animate>
|
||||||
|
</line>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
icon.right = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
|
||||||
|
<polygon points="56,32 248,128 56,224" fill="#808080"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
icon.left = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
|
||||||
|
<polygon points="8,128 200,32 200,224" fill="#808080"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
icon.down = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
|
||||||
|
<polygon points="32,56 224,56 128,248" fill="#808080"/>
|
||||||
|
</svg>
|
||||||
|
`
|
133
app/static/js/item.js
Normal file
133
app/static/js/item.js
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
|
||||||
|
async function loadData(id, args) {
|
||||||
|
var data = window.data = {}
|
||||||
|
data.id = id
|
||||||
|
data.site = pandora.hostname
|
||||||
|
|
||||||
|
var response = await pandoraAPI('get', {
|
||||||
|
id: data.id.split('/')[0],
|
||||||
|
keys: [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"director",
|
||||||
|
"summary",
|
||||||
|
"streams",
|
||||||
|
"duration",
|
||||||
|
"durations",
|
||||||
|
"layers",
|
||||||
|
"rightslevel",
|
||||||
|
"videoRatio"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status.code != 200) {
|
||||||
|
return {
|
||||||
|
site: data.site,
|
||||||
|
error: response.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.item = response['data']
|
||||||
|
if (data.item.rightslevel > pandora.site.capabilities.canPlayClips['guest']) {
|
||||||
|
return {
|
||||||
|
site: data.site,
|
||||||
|
error: {
|
||||||
|
code: 403,
|
||||||
|
text: 'permission denied'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (id.split('/').length == 1 || id.split('/')[1] == 'info') {
|
||||||
|
data.view = 'info'
|
||||||
|
data.title = data.item.title
|
||||||
|
data.byline = data.item.director ? data.item.director.join(', ') : ''
|
||||||
|
data.link = `${pandora.proto}://${data.site}/${data.item.id}/info`
|
||||||
|
let poster = pandora.site.user.ui.icons == 'posters' ? 'poster' : 'icon'
|
||||||
|
data.icon = `${pandora.proto}://${data.site}/${data.item.id}/${poster}.jpg`
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.includes('-') || id.includes(',')) {
|
||||||
|
var inout = id.split('/')[1].split(id.includes('-') ? '-' : ',').map(parseDuration)
|
||||||
|
data.out = inout.pop()
|
||||||
|
data['in'] = inout.pop()
|
||||||
|
} else if (args.full) {
|
||||||
|
data.out = data.item.duration
|
||||||
|
data['in'] = 0
|
||||||
|
} else {
|
||||||
|
var annotation = await pandoraAPI('getAnnotation', {
|
||||||
|
id: data.id,
|
||||||
|
})
|
||||||
|
if (annotation.status.code != 200) {
|
||||||
|
return {
|
||||||
|
site: data.site,
|
||||||
|
error: annotation.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.annotation = annotation['data']
|
||||||
|
data['in'] = data.annotation['in']
|
||||||
|
data.out = data.annotation['out']
|
||||||
|
}
|
||||||
|
|
||||||
|
data.layers = {}
|
||||||
|
|
||||||
|
pandora.layerKeys.forEach(layer => {
|
||||||
|
data.item.layers[layer].forEach(annot => {
|
||||||
|
if (data.annotation) {
|
||||||
|
if (annot.id == data.annotation.id) {
|
||||||
|
data.layers[layer] = data.layers[layer] || []
|
||||||
|
data["layers"][layer].push(annot)
|
||||||
|
}
|
||||||
|
} else if (annot['out'] > data['in'] && annot['in'] < data['out']) {
|
||||||
|
if (args.users && !args.users.includes(annot.user)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (args.layers && !args.layers.includes(layer)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.layers[layer] = data.layers[layer] || []
|
||||||
|
//annot['in'] = Math.max([annot['in'], data['in']])
|
||||||
|
//annot['out'] = Math.min([annot['out'], data['out']])
|
||||||
|
data["layers"][layer].push(annot)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
data.videos = []
|
||||||
|
data.item.durations.forEach((duration, idx) => {
|
||||||
|
var oshash = data.item.streams[idx]
|
||||||
|
var url = getVideoURL(data.item.id, 480, idx+1, '', oshash)
|
||||||
|
data.videos.push({
|
||||||
|
src: url,
|
||||||
|
duration: duration
|
||||||
|
})
|
||||||
|
})
|
||||||
|
var value = []
|
||||||
|
Object.keys(data.layers).forEach(layer => {
|
||||||
|
var html = []
|
||||||
|
var layerData = getObjectById(pandora.site.layers, layer)
|
||||||
|
html.push(`<h3>
|
||||||
|
<span class="icon">${icon.down}</span>
|
||||||
|
${layerData.title}
|
||||||
|
</h3>`)
|
||||||
|
data.layers[layer].forEach(annotation => {
|
||||||
|
html.push(`
|
||||||
|
<div class="annotation ${layerData.type}" data-in="${annotation.in}" data-out="${annotation.out}">
|
||||||
|
${annotation.value}
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
value.push('<div class="layer">' + html.join('\n') + '</div>')
|
||||||
|
})
|
||||||
|
data.value = value.join('\n')
|
||||||
|
|
||||||
|
data.title = data.item.title
|
||||||
|
data.byline = data.item.director ? data.item.director.join(', ') : ''
|
||||||
|
data.link = `${pandora.proto}://${data.site}/${data.item.id}/${data["in"]},${data.out}`
|
||||||
|
data.poster = `${pandora.proto}://${data.site}/${data.item.id}/480p${data["in"]}.jpg`
|
||||||
|
data.aspectratio = data.item.videoRatio
|
||||||
|
if (data['in'] == data['out']) {
|
||||||
|
data['out'] += 0.04
|
||||||
|
}
|
||||||
|
data.duration = data.out - data['in']
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
130
app/static/js/main.js
Normal file
130
app/static/js/main.js
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
|
||||||
|
|
||||||
|
function parseURL() {
|
||||||
|
var url = pandora.url ? pandora.url : document.location,
|
||||||
|
fragment = url.hash.slice(1)
|
||||||
|
if (!fragment && url.pathname.startsWith('/m/')) {
|
||||||
|
var prefix = url.protocol + '//' + url.hostname + '/m/'
|
||||||
|
fragment = url.href.slice(prefix.length)
|
||||||
|
}
|
||||||
|
var args = fragment.split('?')
|
||||||
|
var id = args.shift()
|
||||||
|
if (args) {
|
||||||
|
args = args.join('?').split('&').map(arg => {
|
||||||
|
var kv = arg.split('=')
|
||||||
|
k = kv.shift()
|
||||||
|
v = kv.join('=')
|
||||||
|
if (['users', 'layers'].includes(k)) {
|
||||||
|
v = v.split(',')
|
||||||
|
}
|
||||||
|
return [k, v]
|
||||||
|
}).filter(kv => {
|
||||||
|
return kv[0].length
|
||||||
|
})
|
||||||
|
if (args) {
|
||||||
|
args = Object.fromEntries(args);
|
||||||
|
} else {
|
||||||
|
args = {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = {}
|
||||||
|
}
|
||||||
|
var type = "item"
|
||||||
|
if (id.startsWith('document')) {
|
||||||
|
id = id.split('/')
|
||||||
|
id.shift()
|
||||||
|
id = id.join('/')
|
||||||
|
type = "document"
|
||||||
|
} else if (id.startsWith('edits/')) {
|
||||||
|
var parts = id.split('/')
|
||||||
|
parts.shift()
|
||||||
|
id = parts.shift().replace(/_/g, ' ')
|
||||||
|
type = "edit"
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
args.sort = parts[1]
|
||||||
|
if (args.sort[0] == '-') {
|
||||||
|
args.sort = {
|
||||||
|
key: args.sort.slice(1),
|
||||||
|
operator: '-'
|
||||||
|
}
|
||||||
|
} else if (args.sort[0] == '+') {
|
||||||
|
args.sort = {
|
||||||
|
key: args.sort.slice(1),
|
||||||
|
operator: '+'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.sort = {
|
||||||
|
key: args.sort,
|
||||||
|
operator: '+'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.parts = parts
|
||||||
|
} else {
|
||||||
|
if (id.endsWith('/player') || id.endsWith('/editor')) {
|
||||||
|
args.full = true
|
||||||
|
}
|
||||||
|
id = id.replace('/editor/', '/').replace('/player/', '/')
|
||||||
|
type = "item"
|
||||||
|
}
|
||||||
|
return [type, id, args]
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
var type, id, args;
|
||||||
|
[type, id, args] = parseURL()
|
||||||
|
document.querySelector(".content").innerHTML = loadingScreen
|
||||||
|
if (type == "document") {
|
||||||
|
loadDocument(id, args).then(renderDocument)
|
||||||
|
} else if (type == "edit") {
|
||||||
|
loadEdit(id, args).then(renderItem)
|
||||||
|
} else {
|
||||||
|
loadData(id, args).then(renderItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
var loadingScreen = `
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div style="margin: auto;width: 64px;height: 64px;">${icon.loading}</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
document.querySelector(".content").innerHTML = loadingScreen
|
||||||
|
pandoraAPI("init").then(response => {
|
||||||
|
pandora = {
|
||||||
|
...pandora,
|
||||||
|
...response.data
|
||||||
|
}
|
||||||
|
pandora.proto = pandora.site.site.https ? 'https' : 'http'
|
||||||
|
if (pandora.site.site.videoprefix.startsWith('//')) {
|
||||||
|
pandora.site.site.videoprefix = pandora.proto + ':' + pandora.site.site.videoprefix
|
||||||
|
}
|
||||||
|
var layerKeys = []
|
||||||
|
var subtitleLayer = pandora.site.layers.filter(layer => {return layer.isSubtitles})[0]
|
||||||
|
if (subtitleLayer) {
|
||||||
|
layerKeys.push(subtitleLayer.id)
|
||||||
|
}
|
||||||
|
pandora.site.layers.map(layer => {
|
||||||
|
return layer.id
|
||||||
|
}).filter(layer => {
|
||||||
|
return !subtitleLayer || layer != subtitleLayer.id
|
||||||
|
}).forEach(layer => {
|
||||||
|
layerKeys.push(layer)
|
||||||
|
})
|
||||||
|
pandora.layerKeys = layerKeys
|
||||||
|
id = document.location.hash.slice(1)
|
||||||
|
window.addEventListener("hashchange", event => {
|
||||||
|
render()
|
||||||
|
})
|
||||||
|
window.addEventListener("popstate", event => {
|
||||||
|
console.log("popsatte")
|
||||||
|
render()
|
||||||
|
})
|
||||||
|
window.addEventListener('resize', event => {
|
||||||
|
})
|
||||||
|
render()
|
||||||
|
})
|
0
app/static/js/overwrite.js
Normal file
0
app/static/js/overwrite.js
Normal file
121
app/static/js/render.js
Normal file
121
app/static/js/render.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
|
||||||
|
function renderItemInfo(data) {
|
||||||
|
div = document.createElement('div')
|
||||||
|
div.className = "content"
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="title">${data.title}</div>
|
||||||
|
<div class="byline">${data.byline}</div>
|
||||||
|
<figure>
|
||||||
|
<img src="${data.icon}">
|
||||||
|
</figure>
|
||||||
|
<div class="more">
|
||||||
|
<a href="${data.link}">Open on ${data.site}</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
document.querySelector(".content").replaceWith(div)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem(data) {
|
||||||
|
if (data.error) {
|
||||||
|
return renderError(data)
|
||||||
|
}
|
||||||
|
if (data.view == "info") {
|
||||||
|
return renderItemInfo(data)
|
||||||
|
}
|
||||||
|
div = document.createElement('div')
|
||||||
|
div.className = "content"
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="title">${data.title}</div>
|
||||||
|
<div class="byline">${data.byline}</div>
|
||||||
|
<div class="player">
|
||||||
|
<div class="video"></div>
|
||||||
|
</div>
|
||||||
|
<div class="value">${data.value}</div>
|
||||||
|
<div class="comments"></div>
|
||||||
|
<div class="more">
|
||||||
|
<a href="${data.link}">Open on ${data.site}</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
var comments = div.querySelector('.comments')
|
||||||
|
if (window.renderComments) {
|
||||||
|
renderComments(comments, data)
|
||||||
|
} else {
|
||||||
|
comments.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
div.querySelectorAll('.layer a').forEach(a => {
|
||||||
|
a.addEventListener("click", clickLink)
|
||||||
|
})
|
||||||
|
|
||||||
|
div.querySelectorAll('.layer').forEach(layer => {
|
||||||
|
layer.querySelector('h3').addEventListener("click", event => {
|
||||||
|
var img = layer.querySelector('h3 .icon')
|
||||||
|
if (layer.classList.contains("collapsed")) {
|
||||||
|
layer.classList.remove("collapsed")
|
||||||
|
img.innerHTML = icon.down
|
||||||
|
} else {
|
||||||
|
layer.classList.add("collapsed")
|
||||||
|
img.innerHTML = icon.right
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
var video = window.video = VideoPlayer({
|
||||||
|
items: data.videos,
|
||||||
|
poster: data.poster,
|
||||||
|
position: data["in"] || 0,
|
||||||
|
duration: data.duration,
|
||||||
|
aspectratio: data.aspectratio
|
||||||
|
})
|
||||||
|
div.querySelector('.video').replaceWith(video)
|
||||||
|
video.classList.add('video')
|
||||||
|
|
||||||
|
video.addEventListener("loadedmetadata", event => {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
video.addEventListener("timeupdate", event => {
|
||||||
|
var currentTime = video.currentTime()
|
||||||
|
if (currentTime >= data['out']) {
|
||||||
|
if (!video.paused) {
|
||||||
|
video.pause()
|
||||||
|
}
|
||||||
|
video.currentTime(data['in'])
|
||||||
|
}
|
||||||
|
div.querySelectorAll('.annotation').forEach(annot => {
|
||||||
|
var now = currentTime
|
||||||
|
var start = parseFloat(annot.dataset.in)
|
||||||
|
var end = parseFloat(annot.dataset.out)
|
||||||
|
if (now >= start && now <= end) {
|
||||||
|
annot.classList.add("active")
|
||||||
|
annot.parentElement.classList.add('active')
|
||||||
|
} else {
|
||||||
|
annot.classList.remove("active")
|
||||||
|
if (!annot.parentElement.querySelector('.active')) {
|
||||||
|
annot.parentElement.classList.remove('active')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
document.querySelector(".content").replaceWith(div)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderError(data) {
|
||||||
|
var link = '/' + document.location.hash.slice(1)
|
||||||
|
div = document.createElement('div')
|
||||||
|
div.className = "content"
|
||||||
|
div.innerHTML = `
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div style="margin: auto">
|
||||||
|
Page not found<br>
|
||||||
|
<a href="${link}">Open on ${data.site}</a>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
document.querySelector(".content").replaceWith(div)
|
||||||
|
}
|
160
app/static/js/utils.js
Normal file
160
app/static/js/utils.js
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
|
||||||
|
const parseDuration = function(string) {
|
||||||
|
return string.split(':').reverse().slice(0, 4).reduce(function(p, c, i) {
|
||||||
|
return p + (parseFloat(c) || 0) * (i == 3 ? 86400 : Math.pow(60, i));
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDuration = function(seconds) {
|
||||||
|
var parts = [
|
||||||
|
parseInt(seconds / 86400),
|
||||||
|
parseInt(seconds % 86400 / 3600),
|
||||||
|
parseInt(seconds % 3600 / 60),
|
||||||
|
s = parseInt(seconds % 60)
|
||||||
|
]
|
||||||
|
return parts.map(p => { return p.toString().padStart(2, '0')}).join(':')
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeOf = function(value) {
|
||||||
|
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
|
||||||
|
};
|
||||||
|
const isUndefined = function(value) {
|
||||||
|
return typeOf(value) == 'undefined';
|
||||||
|
}
|
||||||
|
const isNumber = function(value) {
|
||||||
|
return typeOf(value) == 'number';
|
||||||
|
};
|
||||||
|
const isObject = function(value) {
|
||||||
|
return typeOf(value) == 'object';
|
||||||
|
};
|
||||||
|
const isNull = function(value) {
|
||||||
|
return typeOf(value) == 'null';
|
||||||
|
};
|
||||||
|
const isString = function(value) {
|
||||||
|
return typeOf(value) == 'string';
|
||||||
|
};
|
||||||
|
const isEmpty = function(value) {
|
||||||
|
var type = typeOf(value)
|
||||||
|
if (['arguments', 'array', 'nodelist', 'string'].includes(value)) {
|
||||||
|
return value.length == 0
|
||||||
|
}
|
||||||
|
if (['object', 'storage'].includes(type)) {
|
||||||
|
return Object.keys(value).length;
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
};
|
||||||
|
const mod = function(number, by) {
|
||||||
|
return (number % by + by) % by;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getObjectById = function(array, id) {
|
||||||
|
return array.filter(obj => { return obj.id == id})[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const debug = function() {
|
||||||
|
if (localStorage.debug) {
|
||||||
|
console.log.apply(null, arguments)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canPlayMP4 = function() {
|
||||||
|
var video = document.createElement('video');
|
||||||
|
if (video.canPlayType && video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace('no', '')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
};
|
||||||
|
|
||||||
|
const canPlayWebm = function() {
|
||||||
|
var video = document.createElement('video');
|
||||||
|
if (video.canPlayType && video.canPlayType('video/webm; codecs="vp8, vorbis"').replace('no', '')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFormat = function() {
|
||||||
|
//var format = canPlayWebm() ? "webm" : "mp4"
|
||||||
|
var format = canPlayMP4() ? "mp4" : "webm"
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeDocumentName = function(name) {
|
||||||
|
['\\?', '#', '%', '/'].forEach(function(c) {
|
||||||
|
var r = new RegExp(c, 'g')
|
||||||
|
name = name.replace(r, '_');
|
||||||
|
})
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVideoInfo = function() {
|
||||||
|
console.log("FIXME implement getvideoInfo")
|
||||||
|
}
|
||||||
|
|
||||||
|
const isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
|
||||||
|
|
||||||
|
|
||||||
|
const getLink = function(fragment) {
|
||||||
|
if (document.location.hash.length > 2) {
|
||||||
|
return '#' + fragment
|
||||||
|
} else {
|
||||||
|
return '/m/' + fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickLink = function(event) {
|
||||||
|
var a = event.target
|
||||||
|
while (a && a.tagName != 'A') {
|
||||||
|
a = a.parentElement
|
||||||
|
}
|
||||||
|
if (!a) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var href = a.attributes.href.value
|
||||||
|
var prefix = document.location.protocol + '//' + document.location.hostname
|
||||||
|
if (href.startsWith(prefix)) {
|
||||||
|
href = href.slice(prefix.length)
|
||||||
|
}
|
||||||
|
if (href.startsWith('/')) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
var link = href.split('#embed')[0]
|
||||||
|
if (document.location.hash.length > 2) {
|
||||||
|
if (link.startsWith('/m')) {
|
||||||
|
link = link.slice(2)
|
||||||
|
}
|
||||||
|
document.location.hash = '#' + link.slice(1)
|
||||||
|
} else {
|
||||||
|
if (!link.startsWith('/m')) {
|
||||||
|
link = '/m' + link
|
||||||
|
}
|
||||||
|
history.pushState({}, '', link);
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUid = (function() {
|
||||||
|
var uid = 0;
|
||||||
|
return function() {
|
||||||
|
return ++uid;
|
||||||
|
};
|
||||||
|
}());
|
||||||
|
|
||||||
|
|
||||||
|
const getVideoURLName = function(id, resolution, part, track, streamId) {
|
||||||
|
return id + '/' + resolution + 'p' + part + (track ? '.' + track : '')
|
||||||
|
+ '.' + pandora.format + (streamId ? '?' + streamId : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVideoURL = function(id, resolution, part, track, streamId) {
|
||||||
|
var uid = getUid(),
|
||||||
|
prefix = pandora.site.site.videoprefix
|
||||||
|
.replace('{id}', id)
|
||||||
|
.replace('{part}', part)
|
||||||
|
.replace('{resolution}', resolution)
|
||||||
|
.replace('{uid}', uid)
|
||||||
|
.replace('{uid42}', uid % 42);
|
||||||
|
return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId);
|
||||||
|
};
|
||||||
|
|
77
app/templates/:u
Normal file
77
app/templates/:u
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="css/reset.css"></link>
|
||||||
|
<link rel="stylesheet" href="css/style.css"></link>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: turquoise;
|
||||||
|
}
|
||||||
|
.add-comment {
|
||||||
|
width: 100%;
|
||||||
|
height: 30vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.add-comment input {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.add-comment textarea {
|
||||||
|
width: 80vw;
|
||||||
|
display: block;
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.add-comment button {
|
||||||
|
background: red;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment .text {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="item">
|
||||||
|
<iframe src="{{ item.url }}" frameborder="0" allowfullscreen style="border:0;padding:0;margin:0;width:100%;height:100vh"></iframe>
|
||||||
|
</div>
|
||||||
|
<div class="comments">
|
||||||
|
{% for comment in item.public_comments %}
|
||||||
|
<div class="comment">
|
||||||
|
<div class="name">{{ comment.name }}</div>
|
||||||
|
<div class="date">{{ comment.date }}</div>
|
||||||
|
<div class="text">{{ comment.text }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="add-comment">
|
||||||
|
{% if request.user.is_anonymous %}
|
||||||
|
<input name="name" type="text" placeholder="your name"></input>
|
||||||
|
<input name="email" type="email" placeholder="your email"></input>
|
||||||
|
<br>
|
||||||
|
{% endif %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<textarea name="text" placeholder="your comment"></textarea>
|
||||||
|
<button id="add-comment">Add comment</button>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block end %}
|
||||||
|
<script src="/static/js/utils./static/js"></script>
|
||||||
|
<script src="/static/js/api./static/js"></script>
|
||||||
|
<script src="/static/js/icons./static/js"></script>
|
||||||
|
<script src="/static/js/VideoElement./static/js"></script>
|
||||||
|
<script src="/static/js/VideoPlayer./static/js"></script>
|
||||||
|
<script src="/static/js/documents./static/js"></script>
|
||||||
|
<script src="/static/js/edits./static/js"></script>
|
||||||
|
<script src="/static/js/item./static/js"></script>
|
||||||
|
<script src="/static/js/render./static/js"></script>
|
||||||
|
<script src="/static/js/main./static/js"></script>
|
||||||
|
|
||||||
|
<script src="/static/js/comments./static/js"></script>
|
||||||
|
{% endblock %}
|
25
app/templates/archive.html
Normal file
25
app/templates/archive.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
{% for item in items %}
|
||||||
|
<div class="item">
|
||||||
|
<a href="{{ item.url }}">
|
||||||
|
<h1>{{ item.title }}</h1>
|
||||||
|
<figure>
|
||||||
|
<img src={{ item.icon }}>
|
||||||
|
<figcaption>
|
||||||
|
{{ item.data.title }}
|
||||||
|
{% if item.data.description %}
|
||||||
|
<br>
|
||||||
|
{{ item.data.description }}
|
||||||
|
{% endif %}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
{{ item.description | safe}}
|
||||||
|
</div>
|
||||||
|
<a href="{{ item.get_absolute_url }}">{{ item.public_comments.count }} comments</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
15
app/templates/base.html
Normal file
15
app/templates/base.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
{% block head %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="content">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block end %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
44
app/templates/index.html
Normal file
44
app/templates/index.html
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% for item in items %}
|
||||||
|
<div class="item">
|
||||||
|
<a href="{{ item.get_absolute_url }}">
|
||||||
|
<h1>{{ item.title }}</h1>
|
||||||
|
<figure>
|
||||||
|
<img src={{ item.icon }}>
|
||||||
|
<figcaption>
|
||||||
|
{{ item.data.title }}
|
||||||
|
{% if item.data.description %}
|
||||||
|
<br>
|
||||||
|
{{ item.data.description }}
|
||||||
|
{% endif %}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
{{ item.description | safe }}
|
||||||
|
</div>
|
||||||
|
<a href="{{ item.get_absolute_url }}">{{ item.public_comments.count }} comments</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% if archive %}
|
||||||
|
<a href="/archive/">older items</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
77
app/templates/item.html
Normal file
77
app/templates/item.html
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: turquoise;
|
||||||
|
}
|
||||||
|
.add-comment {
|
||||||
|
width: 100%;
|
||||||
|
height: 30vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.add-comment input {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.add-comment textarea {
|
||||||
|
width: 80%;
|
||||||
|
display: block;
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.add-comment button {
|
||||||
|
background: red;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment .text {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="/static/css/reset.css"></link>
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css"></link>
|
||||||
|
{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="content">
|
||||||
|
</div>
|
||||||
|
<div class="add-comment" style="display: none">
|
||||||
|
{% if request.user.is_anonymous %}
|
||||||
|
<input name="name" type="text" placeholder="your name"></input>
|
||||||
|
<input name="email" type="email" placeholder="your email"></input>
|
||||||
|
<br>
|
||||||
|
{% endif %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<textarea name="text" placeholder="your comment"></textarea>
|
||||||
|
<button id="add-comment">Add comment</button>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block end %}
|
||||||
|
<script>
|
||||||
|
var comments = {{ item.public_comments_json|safe }};
|
||||||
|
</script>
|
||||||
|
<script src="/static/js/utils.js"></script>
|
||||||
|
<script src="/static/js/api.js"></script>
|
||||||
|
<script src="/static/js/icons.js"></script>
|
||||||
|
<script src="/static/js/VideoElement.js"></script>
|
||||||
|
<script src="/static/js/VideoPlayer.js"></script>
|
||||||
|
<script src="/static/js/documents.js"></script>
|
||||||
|
<script src="/static/js/edits.js"></script>
|
||||||
|
<script src="/static/js/item.js"></script>
|
||||||
|
<script src="/static/js/render.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
pandora.url = new URL('{{ item.url|escapejs }}');
|
||||||
|
pandora.comment = '{{ item.id | escapejs }}';
|
||||||
|
pandora.hostname = pandora.url.hostname
|
||||||
|
pandoraURL = `https://${pandora.hostname}`
|
||||||
|
</script>
|
||||||
|
<script src="/static/js/overwrite.js"></script>
|
||||||
|
|
||||||
|
<script src="/static/js/main.js"></script>
|
||||||
|
<script src="/static/js/comments.js"></script>
|
||||||
|
{% endblock %}
|
16
app/templates/item2.html
Normal file
16
app/templates/item2.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<div class="content"></div>
|
||||||
|
<link rel="stylesheet" href="css/reset.css"></link>
|
||||||
|
<link rel="stylesheet" href="css/style.css"></link>
|
||||||
|
<script src="js/utils.js"></script>
|
||||||
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/icons.js"></script>
|
||||||
|
<script src="js/VideoElement.js"></script>
|
||||||
|
<script src="js/VideoPlayer.js"></script>
|
||||||
|
<script src="js/documents.js"></script>
|
||||||
|
<script src="js/edits.js"></script>
|
||||||
|
<script src="js/item.js"></script>
|
||||||
|
<script src="js/render.js"></script>
|
||||||
|
<script src="js/main.js"></script>
|
28
app/urls.py
Normal file
28
app/urls.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"""
|
||||||
|
URL configuration for app project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, re_path
|
||||||
|
|
||||||
|
from .item import views as item_views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path('archive/', item_views.archive, name='archive'),
|
||||||
|
path('comment/', item_views.comment, name='comment'),
|
||||||
|
path('<int:id>/', item_views.item, name='item'),
|
||||||
|
path('', item_views.index, name='index'),
|
||||||
|
]
|
0
app/user/__init__.py
Normal file
0
app/user/__init__.py
Normal file
3
app/user/admin.py
Normal file
3
app/user/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
app/user/apps.py
Normal file
6
app/user/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UserConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "app.user"
|
133
app/user/migrations/0001_initial.py
Normal file
133
app/user/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
# Generated by Django 4.2.3 on 2023-07-11 16:06
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0012_alter_user_first_name_max_length"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="User",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||||
|
(
|
||||||
|
"last_login",
|
||||||
|
models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="last login"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_superuser",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||||
|
verbose_name="superuser status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
models.CharField(
|
||||||
|
error_messages={
|
||||||
|
"unique": "A user with that username already exists."
|
||||||
|
},
|
||||||
|
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||||
|
max_length=150,
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||||
|
],
|
||||||
|
verbose_name="username",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"first_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"last_name",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=150, verbose_name="last name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email",
|
||||||
|
models.EmailField(
|
||||||
|
blank=True, max_length=254, verbose_name="email address"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_staff",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates whether the user can log into this admin site.",
|
||||||
|
verbose_name="staff status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||||
|
verbose_name="active",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date_joined",
|
||||||
|
models.DateTimeField(
|
||||||
|
default=django.utils.timezone.now, verbose_name="date joined"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("modified", models.DateTimeField(auto_now=True)),
|
||||||
|
("data", models.JSONField(default=dict)),
|
||||||
|
(
|
||||||
|
"groups",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.group",
|
||||||
|
verbose_name="groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user_permissions",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Specific permissions for this user.",
|
||||||
|
related_name="user_set",
|
||||||
|
related_query_name="user",
|
||||||
|
to="auth.permission",
|
||||||
|
verbose_name="user permissions",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "user",
|
||||||
|
"verbose_name_plural": "users",
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
("objects", django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
app/user/migrations/__init__.py
Normal file
0
app/user/migrations/__init__.py
Normal file
7
app/user/models.py
Normal file
7
app/user/models.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
data = models.JSONField(default=dict)
|
3
app/user/tests.py
Normal file
3
app/user/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
app/user/views.py
Normal file
3
app/user/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
16
app/wsgi.py
Normal file
16
app/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for app project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
22
manage.py
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
django
|
Loading…
Reference in a new issue