embeded pandora mobile view

This commit is contained in:
j 2023-07-15 13:00:36 +05:30
commit b420bf43b7
45 changed files with 3437 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
venv
*.swp
__pycache__
db.sqlite3

0
app/__init__.py Normal file
View file

16
app/asgi.py Normal file
View 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
View file

23
app/item/admin.py Normal file
View 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
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ItemConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "app.item"

View 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)),
],
),
]

View 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,
),
),
]

View file

104
app/item/models.py Normal file
View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

51
app/item/views.py Normal file
View 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
View 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
View 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
View 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;
}

View 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);
}
})();

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
})

View file

121
app/static/js/render.js Normal file
View 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
View 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
View 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 %}

View 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
View 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
View 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
View 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
View 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
View 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
View file

3
app/user/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
app/user/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class UserConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "app.user"

View 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()),
],
),
]

View file

7
app/user/models.py Normal file
View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
app/user/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

16
app/wsgi.py Normal file
View 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
View 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
View file

@ -0,0 +1 @@
django