Compare commits

...

2 commits

31 changed files with 2705 additions and 5 deletions

View file

@ -190,6 +190,12 @@ class MetaClip(object):
def __str__(self):
return self.public_id
def get_first_frame(self, resolution=None):
if resolution is None:
resolution = max(settings.CONFIG['video']['resolutions'])
return '/%s/%sp%0.03f.jpg' % (self.item.public_id, resolution, float(self.start))
class Meta:
unique_together = ("item", "start", "end")

View file

@ -567,7 +567,7 @@ class Document(models.Model, FulltextMixin):
if len(crop) == 4:
path = os.path.join(folder, '%s.jpg' % ','.join(map(str, crop)))
if not os.path.exists(path):
img = Image.open(src).crop(crop)
img = Image.open(src).convert('RGB').crop(crop)
img.save(path)
else:
img = Image.open(path)

View file

@ -12,9 +12,10 @@ from oxdjango.decorators import login_required_json
from oxdjango.http import HttpFileResponse
from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response, HttpErrorJson
from django import forms
from django.db.models import Count, Sum
from django.conf import settings
from django.db.models import Count, Sum
from django.http import HttpResponse
from django.shortcuts import render
from item import utils
from item.models import Item
@ -512,3 +513,33 @@ def autocompleteDocuments(request, data):
response['data']['items'] = [i['value'] for i in qs]
return render_to_json_response(response)
actions.register(autocompleteDocuments)
def document(request, fragment):
context = {}
parts = fragment.split('/')
id = parts[0]
page = None
crop = None
if len(parts) == 2:
rect = parts[1].split(',')
if len(rect) == 1:
page = rect[0]
else:
crop = rect
document = models.Document.objects.filter(id=ox.fromAZ(id)).first()
if document and document.access(request.user):
context['title'] = document.data['title']
if document.data.get('description'):
context['description'] = document.data['description']
link = request.build_absolute_uri(document.get_absolute_url())
public_id = ox.toAZ(document.id)
preview = '/documents/%s/512p.jpg' % public_id
if page:
preview = '/documents/%s/512p%s.jpg' % (public_id, page)
if crop:
preview = '/documents/%s/512p%s.jpg' % (public_id, ','.join(crop))
context['preview'] = request.build_absolute_uri(preview)
context['url'] = request.build_absolute_uri('/documents/' + fragment)
context['settings'] = settings
return render(request, "document.html", context)

View file

3
pandora/mobile/admin.py Normal file
View file

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

5
pandora/mobile/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class MobileConfig(AppConfig):
name = 'mobile'

View file

3
pandora/mobile/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
pandora/mobile/tests.py Normal file
View file

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

75
pandora/mobile/views.py Normal file
View file

@ -0,0 +1,75 @@
from django.conf import settings
from django.shortcuts import render
import ox
def index(request, fragment):
from item.models import Item
from edit.models import Edit
from document.models import Document
context = {}
parts = fragment.split('/')
if parts[0] in ('document', 'documents'):
type = 'document'
id = parts[1]
page = None
crop = None
if len(parts) == 3:
rect = parts[2].split(',')
if len(rect) == 1:
page = rect[0]
else:
crop = rect
document = Document.objects.filter(id=ox.fromAZ(id)).first()
if document and document.access(request.user):
context['title'] = document.data['title']
link = request.build_absolute_uri(document.get_absolute_url())
# FIXME: get preview image or fragment parse from url
public_id = ox.toAZ(document.id)
preview = '/documents/%s/512p.jpg' % public_id
if page:
preview = '/documents/%s/512p%s.jpg' % (public_id, page)
if crop:
preview = '/documents/%s/512p%s.jpg' % (public_id, ','.join(crop))
context['preview'] = request.build_absolute_uri(preview)
elif parts[0] == 'edits':
type = 'edit'
id = parts[1]
id = id.split(':')
username = id[0]
name = ":".join(id[1:])
name = name.replace('_', ' ')
edit = Edit.objects.filter(user__username=username, name=name).first()
if edit and edit.accessible(request.user):
link = request.build_absolute_uri('/m' + edit.get_absolute_url())
context['title'] = name
context['description'] = edit.description.split('\n\n')[0]
# FIXME: use sort from parts if needed
context['preview'] = request.build_absolute_uri(edit.get_clips().first().get_first_frame())
else:
type = 'item'
id = parts[0]
item = Item.objects.filter(public_id=id).first()
if item and item.accessible(request.user):
link = request.build_absolute_uri(item.get_absolute_url())
if len(parts) > 1 and parts[1] in ('editor', 'player'):
parts = [parts[0]] + parts[2:]
if len(parts) > 1:
inout = parts[1]
if '-' in inout:
inout = inout.split('-')
else:
inout = inout.split(',')
inout = [ox.parse_timecode(p) for p in inout]
if len(inout) == 3:
inout.pop(1)
context['preview'] = link + '/480p%s.jpg' % inout[0]
else:
context['preview'] = link + '/480p.jpg'
context['title'] = item.get('title')
if context:
context['url'] = request.build_absolute_uri('/m/' + fragment)
context['settings'] = settings
return render(request, "mobile/index.html", context)

View file

@ -67,7 +67,8 @@ STATICFILES_DIRS = (
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
#'django.contrib.staticfiles.finders.DefaultStorageFinder',
"sass_processor.finders.CssFinder",
"compressor.finders.CompressorFinder",
)
GEOIP_PATH = normpath(join(PROJECT_ROOT, '..', 'data', 'geo'))
@ -124,6 +125,9 @@ INSTALLED_APPS = (
'django_extensions',
'django_celery_results',
'django_celery_beat',
'compressor',
'sass_processor',
'app',
'log',
'annotation',
@ -150,6 +154,7 @@ INSTALLED_APPS = (
'websocket',
'taskqueue',
'home',
'mobile',
)
AUTH_USER_MODEL = 'system.User'

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>{{head_title}}</title>
<link rel="alternate" type="application/json+oembed" href="{{base_url}}oembed?url={{current_url|urlencode}}" title="oEmbed Profile"/>
{% include "baseheader.html" %}
<meta name="title" content="{{title}}"/>
{%if description %}<meta name="description" content="{{description}}"/> {%endif%}
<meta content="{{ preview }}" name="thumbnail"/>
<meta content="{{ preview }}" name="image_src"/>
<meta property="og:title" content="{{title}}"/>
<meta property="og:url" content="{{url}}"/>
<meta property="og:image" content="{{ preview }}"/>
<meta property="og:site_name" content="{{settings.SITENAME}}"/>
{%if description %}<meta property="og:description" content="{{description}}"/>{%endif%}
</head>
<body>
<noscript>
<div style="position: fixed; left: 0; top: 0; right: 0; bottom: 0; background-color: rgb(144, 144, 144);"></div>
<div style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; text-align: center; font-family: Lucida Grande, Segoe UI, DejaVu Sans, Lucida Sans Unicode, Helvetica, Arial, sans-serif; font-size: 11px; color: rgb(0, 0, 0); -moz-user-select: none; -o-user-select: none; -webkit-user-select: none">
<img src="/{{id}}/{{icon}}128.jpg" alt="" style="padding-top: 64px; -moz-user-drag: none; -o-user-drag: none; -webkit-user-drag: none">
<div style="position: absolute; width: 512px; left: 0; right: 0; margin: auto; -moz-user-select: text; -o-user-select: text; -webkit-user-select: text">
<div style="padding: 16px 0 8px 0; font-weight: bold; font-size: 13px">{{title}}</div>
<div>{{description_html|safe}}</div>
{% for c in clips %}
<div style="clear:both; padding-top: 4px">
<img src="/{{id}}/96p{{c.in}}.jpg" alt="" style="float: left; margin-right: 8px">
{{c.annotations|safe}}
</div>
{% endfor %}
<div style="clear: both; padding-top: 16px; padding-bottom: 16px; font-style: italic">{{settings.SITENAME}} requires JavaScript.</div>
</div>
</div>
</noscript>
</body>
</html>

View file

@ -22,7 +22,7 @@
<div style="position: fixed; left: 0; top: 0; right: 0; bottom: 0; background-color: rgb(144, 144, 144);"></div>
<div style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; text-align: center; font-family: Lucida Grande, Segoe UI, DejaVu Sans, Lucida Sans Unicode, Helvetica, Arial, sans-serif; font-size: 11px; color: rgb(0, 0, 0); -moz-user-select: none; -o-user-select: none; -webkit-user-select: none">
<img src="/{{id}}/{{icon}}128.jpg" alt="" style="padding-top: 64px; -moz-user-drag: none; -o-user-drag: none; -webkit-user-drag: none">
<div style="position: absolute; width: 512px; left: 0; right: 0; margin: auto; -moz-user-select: text; -o-user-select: text; -webkit-user-select: text">
<div style="position: absolute; width: 512px; left: 0; right: 0; margin: auto; -moz-user-select: text; -o-user-select: text; -webkit-user-select: text">
<div style="padding: 16px 0 8px 0; font-weight: bold; font-size: 13px">{{title}}</div>
<div>{{description_html|safe}}</div>
{% for c in clips %}

View file

@ -0,0 +1,45 @@
<!DOCTYPE html>{% load static sass_tags compress %}
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if title %}
<title>{{title}}</title>
<meta name="title" content="{{title}}"/>
<meta property="og:title" content="{{title}}"/>
{% endif %}
{%if description %}
<meta name="description" content="{{description}}"/>
<meta property="og:description" content="{{description}}"/>
{%endif%}
{% if preview %}
<meta content="{{ preview }}" name="thumbnail"/>
<meta content="{{ preview }}" name="image_src"/>
<meta property="og:image" content="{{ preview }}"/>
{% endif %}
{% if url %}
<meta property="og:url" content="{{ url }}"/>
{% endif %}
<meta property="og:site_name" content="{{ settings.SITENAME }}"/>
{% compress css file m %}
<link rel="stylesheet" href="{% static 'mobile/css/reset.css' %}"></link>
<link rel="stylesheet" href="{% static 'mobile/css/style.css' %}"></link>
{% endcompress %}
<meta name="google" value="notranslate"/>
</head>
<body>
<div class="content"></div>
{% compress js file m %}
<script src="{% static 'mobile/js/utils.js' %}"></script>
<script src="{% static 'mobile/js/api.js' %}"></script>
<script src="{% static 'mobile/js/icons.js' %}"></script>
<script src="{% static 'mobile/js/VideoElement.js' %}"></script>
<script src="{% static 'mobile/js/VideoPlayer.js' %}"></script>
<script src="{% static 'mobile/js/documents.js' %}"></script>
<script src="{% static 'mobile/js/edits.js' %}"></script>
<script src="{% static 'mobile/js/item.js' %}"></script>
<script src="{% static 'mobile/js/render.js' %}"></script>
<script src="{% static 'mobile/js/main.js' %}"></script>
{% endcompress %}
</body>
</html>

View file

@ -26,6 +26,7 @@ import edit.views
import itemlist.views
import item.views
import item.site
import mobile.views
import translation.views
import urlalias.views
@ -47,6 +48,7 @@ urlpatterns = [
re_path(r'^collection/(?P<id>.*?)/icon(?P<size>\d*).jpg$', documentcollection.views.icon),
re_path(r'^documents/(?P<id>[A-Z0-9]+)/(?P<size>\d*)p(?P<page>[\d,]*).jpg$', document.views.thumbnail),
re_path(r'^documents/(?P<id>[A-Z0-9]+)/(?P<name>.*?\.[^\d]{3})$', document.views.file),
re_path(r'^documents/(?P<fragment>.*?)$', document.views.document),
re_path(r'^edit/(?P<id>.*?)/icon(?P<size>\d*).jpg$', edit.views.icon),
re_path(r'^list/(?P<id>.*?)/icon(?P<size>\d*).jpg$', itemlist.views.icon),
re_path(r'^text/(?P<id>.*?)/icon(?P<size>\d*).jpg$', text.views.icon),
@ -65,6 +67,7 @@ urlpatterns = [
re_path(r'^robots.txt$', app.views.robots_txt),
re_path(r'^sitemap.xml$', item.views.sitemap_xml),
re_path(r'^sitemap(?P<part>\d+).xml$', item.views.sitemap_part_xml),
re_path(r'm/(?P<fragment>.*?)$', mobile.views.index),
path(r'', item.site.urls),
]
#sould this not be enabled by default? nginx should handle those

View file

@ -5,6 +5,9 @@ celery<5.0,>4.3
django-celery-results<2
django-celery-beat
django-extensions==2.2.9
libsass
django-compressor
django-sass-processor
gunicorn==20.0.4
html5lib
requests<3.0.0,>=2.24.0

View file

@ -609,6 +609,8 @@ pandora.ui.mainMenu = function() {
pandora.$ui.player.options({fullscreen: true});
} else if (data.id == 'embed') {
pandora.$ui.embedDialog = pandora.ui.embedDialog().open();
} else if (data.id == 'share') {
pandora.$ui.shareDialog = pandora.ui.shareDialog().open();
} else if (data.id == 'advancedfind') {
pandora.$ui.filterDialog = pandora.ui.filterDialog().open();
} else if (data.id == 'clearquery') {
@ -1909,7 +1911,8 @@ pandora.ui.mainMenu = function() {
}) }
] },
{},
{ id: 'embed', title: Ox._('Embed...') }
{ id: 'embed', title: Ox._('Embed...') },
{ id: 'share', title: Ox._('Share...') }
]}
}

37
static/js/shareDialog.js Normal file
View file

@ -0,0 +1,37 @@
'use strict';
pandora.ui.shareDialog = function(/*[url, ]callback*/) {
if (arguments.length == 1) {
var url, callback = arguments[0];
} else {
var url = arguments[0], callback = arguments[1];
}
var url = document.location.href.replace(document.location.hostname, document.location.hostname + '/m'),
$content = Ox.Element().append(
Ox.Input({
height: 100,
width: 256,
placeholder: 'Share Link',
type: 'textarea',
disabled: true,
value: url
})
),
that = pandora.ui.iconDialog({
buttons: [
Ox.Button({
id: 'close',
title: Ox._('Close')
}).bindEvent({
click: function() {
that.close();
}
}),
],
keys: {enter: 'close', escape: 'close'},
content: $content,
title: "Share current view",
});
return that;
}

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
static/mobile/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;
}
.layer h3 {
font-weight: bold;
padding: 4px;
padding-left: 0;
margin: 0;
//display: none;
}
.layer.active h3 {
display: block;
}
.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 {
cursor: pointer;
}
.layer .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
static/mobile/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]
}

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
static/mobile/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
static/mobile/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
static/mobile/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
}

129
static/mobile/js/main.js Normal file
View file

@ -0,0 +1,129 @@
function parseURL() {
var fragment = document.location.hash.slice(1)
if (!fragment && document.location.pathname.startsWith('/m/')) {
var prefix = document.location.protocol + '//' + document.location.hostname + '/m/'
fragment = document.location.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()
})

120
static/mobile/js/render.js Normal file
View file

@ -0,0 +1,120 @@
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="more">
<a href="${data.link}">Open on ${data.site}</a>
</div>
`
var comments = `
<div class="comments" style="width: 100%;text-align: center;">
<textarea style="width:90%;height:45px" placeholder="Respond in style..."></textarea>
<button style="width:90%">Submit</button>
<br>
</div>
`
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
static/mobile/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);
};

View file

@ -303,6 +303,8 @@ if __name__ == "__main__":
run('./bin/pip', 'install', 'yt-dlp>=2022.3.8.2')
if old < 6465:
run('./bin/pip', 'install', '-r', 'requirements.txt')
if old < 6507:
run('./bin/pip', 'install', '-r', 'requirements.txt')
else:
if len(sys.argv) == 1:
branch = get_branch()