Merge branch 'master' into stable
This commit is contained in:
commit
79da1115aa
54 changed files with 728 additions and 248 deletions
3
ctl
3
ctl
|
|
@ -30,9 +30,6 @@ if [ "$action" = "init" ]; then
|
||||||
$SUDO git clone -b $branch https://code.0x2620.org/0x2620/oxjs.git static/oxjs
|
$SUDO git clone -b $branch https://code.0x2620.org/0x2620/oxjs.git static/oxjs
|
||||||
fi
|
fi
|
||||||
$SUDO mkdir -p src
|
$SUDO mkdir -p src
|
||||||
if [ ! -d src/oxtimelines ]; then
|
|
||||||
$SUDO git clone -b $branch https://code.0x2620.org/0x2620/oxtimelines.git src/oxtimelines
|
|
||||||
fi
|
|
||||||
for package in oxtimelines python-ox; do
|
for package in oxtimelines python-ox; do
|
||||||
cd ${BASE}
|
cd ${BASE}
|
||||||
if [ ! -d src/${package} ]; then
|
if [ ! -d src/${package} ]; then
|
||||||
|
|
|
||||||
36
pandora/app/oidc.py
Normal file
36
pandora/app/oidc.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
import mozilla_django_oidc.auth
|
||||||
|
|
||||||
|
from user.utils import prepare_user
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class OIDCAuthenticationBackend(mozilla_django_oidc.auth.OIDCAuthenticationBackend):
|
||||||
|
def create_user(self, claims):
|
||||||
|
user = super(OIDCAuthenticationBackend, self).create_user(claims)
|
||||||
|
username = None
|
||||||
|
for key in ('preferred_username', 'name'):
|
||||||
|
if claims.get(key):
|
||||||
|
username = claims[key]
|
||||||
|
break
|
||||||
|
n = 1
|
||||||
|
if username and username != user.username:
|
||||||
|
uname = username
|
||||||
|
while User.objects.filter(username=uname).exclude(id=user.id).exists():
|
||||||
|
n += 1
|
||||||
|
uname = '%s (%s)' % (username, n)
|
||||||
|
user.username = uname
|
||||||
|
user.save()
|
||||||
|
prepare_user(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def update_user(self, user, claims):
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def generate_username(email):
|
||||||
|
return unicodedata.normalize('NFKC', email)[:150]
|
||||||
|
|
@ -184,6 +184,7 @@ def init(request, data):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
config['site']['oidc'] = bool(getattr(settings, 'OIDC_RP_CLIENT_ID', False))
|
||||||
response['data']['site'] = config
|
response['data']['site'] = config
|
||||||
response['data']['user'] = init_user(request.user, request)
|
response['data']['user'] = init_user(request.user, request)
|
||||||
request.session['last_init'] = str(datetime.now())
|
request.session['last_init'] = str(datetime.now())
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,12 @@ info_key_map = {
|
||||||
'display_id': 'id',
|
'display_id': 'id',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
YT_DLP = ['yt-dlp']
|
||||||
|
if settings.YT_DLP_EXTRA:
|
||||||
|
YT_DLP += settings.YT_DLP_EXTRA
|
||||||
|
|
||||||
def get_info(url, referer=None):
|
def get_info(url, referer=None):
|
||||||
cmd = ['yt-dlp', '-j', '--all-subs', url]
|
cmd = YT_DLP + ['-j', '--all-subs', url]
|
||||||
if referer:
|
if referer:
|
||||||
cmd += ['--referer', referer]
|
cmd += ['--referer', referer]
|
||||||
p = subprocess.Popen(cmd,
|
p = subprocess.Popen(cmd,
|
||||||
|
|
@ -93,7 +97,7 @@ def add_subtitles(item, media, tmp):
|
||||||
sub.save()
|
sub.save()
|
||||||
|
|
||||||
def load_formats(url):
|
def load_formats(url):
|
||||||
cmd = ['yt-dlp', '-q', url, '-j', '-F']
|
cmd = YT_DLP + ['-q', url, '-j', '-F']
|
||||||
p = subprocess.Popen(cmd,
|
p = subprocess.Popen(cmd,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE, close_fds=True)
|
stderr=subprocess.PIPE, close_fds=True)
|
||||||
|
|
@ -112,7 +116,7 @@ def download(item_id, url, referer=None):
|
||||||
if isinstance(tmp, bytes):
|
if isinstance(tmp, bytes):
|
||||||
tmp = tmp.decode('utf-8')
|
tmp = tmp.decode('utf-8')
|
||||||
os.chdir(tmp)
|
os.chdir(tmp)
|
||||||
cmd = ['yt-dlp', '-q', media['url']]
|
cmd = YT_DLP + ['-q', media['url']]
|
||||||
if referer:
|
if referer:
|
||||||
cmd += ['--referer', referer]
|
cmd += ['--referer', referer]
|
||||||
elif 'referer' in media:
|
elif 'referer' in media:
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,13 @@ import ox.image
|
||||||
from ox.utils import json
|
from ox.utils import json
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
|
import pillow_avif
|
||||||
|
from pillow_heif import register_heif_opener
|
||||||
|
|
||||||
from .chop import Chop, make_keyframe_index
|
from .chop import Chop, make_keyframe_index
|
||||||
|
|
||||||
|
|
||||||
|
register_heif_opener()
|
||||||
logger = logging.getLogger('pandora.' + __name__)
|
logger = logging.getLogger('pandora.' + __name__)
|
||||||
|
|
||||||
img_extension = 'jpg'
|
img_extension = 'jpg'
|
||||||
|
|
|
||||||
32
pandora/archive/migrations/0008_file_filename_file_folder.py
Normal file
32
pandora/archive/migrations/0008_file_filename_file_folder.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 4.2.7 on 2025-01-23 10:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
def update_path(apps, schema_editor):
|
||||||
|
File = apps.get_model("archive", "File")
|
||||||
|
for file in File.objects.all():
|
||||||
|
if file.path:
|
||||||
|
parts = file.path.split('/')
|
||||||
|
file.filename = parts.pop()
|
||||||
|
file.folder = '/'.join(parts)
|
||||||
|
file.save()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('archive', '0007_stream_archive_str_file_id_69a542_idx'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='file',
|
||||||
|
name='filename',
|
||||||
|
field=models.CharField(default='', max_length=2048),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='file',
|
||||||
|
name='folder',
|
||||||
|
field=models.CharField(default='', max_length=2048),
|
||||||
|
),
|
||||||
|
migrations.RunPython(update_path),
|
||||||
|
]
|
||||||
|
|
@ -53,6 +53,9 @@ class File(models.Model):
|
||||||
path = models.CharField(max_length=2048, default="") # canoncial path/file
|
path = models.CharField(max_length=2048, default="") # canoncial path/file
|
||||||
sort_path = models.CharField(max_length=2048, default="") # sort name
|
sort_path = models.CharField(max_length=2048, default="") # sort name
|
||||||
|
|
||||||
|
folder = models.CharField(max_length=2048, default="")
|
||||||
|
filename = models.CharField(max_length=2048, default="")
|
||||||
|
|
||||||
type = models.CharField(default="", max_length=255)
|
type = models.CharField(default="", max_length=255)
|
||||||
|
|
||||||
# editable
|
# editable
|
||||||
|
|
@ -194,6 +197,13 @@ class File(models.Model):
|
||||||
data['part'] = str(data['part'])
|
data['part'] = str(data['part'])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def update_path(self):
|
||||||
|
path = self.normalize_path()
|
||||||
|
parts = path.split('/')
|
||||||
|
self.filename = parts.pop()
|
||||||
|
self.folder = '/'.join(parts)
|
||||||
|
return path
|
||||||
|
|
||||||
def normalize_path(self):
|
def normalize_path(self):
|
||||||
# FIXME: always use format_path
|
# FIXME: always use format_path
|
||||||
if settings.CONFIG['site']['folderdepth'] == 4:
|
if settings.CONFIG['site']['folderdepth'] == 4:
|
||||||
|
|
@ -257,7 +267,7 @@ class File(models.Model):
|
||||||
update_path = False
|
update_path = False
|
||||||
if self.info:
|
if self.info:
|
||||||
if self.id:
|
if self.id:
|
||||||
self.path = self.normalize_path()
|
self.path = self.update_path()
|
||||||
else:
|
else:
|
||||||
update_path = True
|
update_path = True
|
||||||
if self.item:
|
if self.item:
|
||||||
|
|
@ -290,7 +300,7 @@ class File(models.Model):
|
||||||
self.streams.filter(source=None, available=True).count()
|
self.streams.filter(source=None, available=True).count()
|
||||||
super(File, self).save(*args, **kwargs)
|
super(File, self).save(*args, **kwargs)
|
||||||
if update_path:
|
if update_path:
|
||||||
self.path = self.normalize_path()
|
self.path = self.update_path()
|
||||||
super(File, self).save(*args, **kwargs)
|
super(File, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def get_path(self, name):
|
def get_path(self, name):
|
||||||
|
|
@ -474,6 +484,9 @@ class File(models.Model):
|
||||||
'videoCodec': self.video_codec,
|
'videoCodec': self.video_codec,
|
||||||
'wanted': self.wanted,
|
'wanted': self.wanted,
|
||||||
}
|
}
|
||||||
|
for key in ('folder', 'filename'):
|
||||||
|
if keys and key in keys:
|
||||||
|
data[key] = getattr(self, key)
|
||||||
if error:
|
if error:
|
||||||
data['error'] = error
|
data['error'] = error
|
||||||
for key in self.PATH_INFO:
|
for key in self.PATH_INFO:
|
||||||
|
|
@ -540,7 +553,7 @@ class File(models.Model):
|
||||||
|
|
||||||
def process_stream(self):
|
def process_stream(self):
|
||||||
'''
|
'''
|
||||||
extract derivatives from webm upload
|
extract derivatives from stream upload
|
||||||
'''
|
'''
|
||||||
from . import tasks
|
from . import tasks
|
||||||
return tasks.process_stream.delay(self.id)
|
return tasks.process_stream.delay(self.id)
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
"canExportAnnotations": {"friend": true, "staff": true, "admin": true},
|
"canExportAnnotations": {"friend": true, "staff": true, "admin": true},
|
||||||
"canImportAnnotations": {"staff": true, "admin": true},
|
"canImportAnnotations": {"staff": true, "admin": true},
|
||||||
"canImportItems": {},
|
"canImportItems": {},
|
||||||
|
"canTranscribeAudio": {},
|
||||||
"canManageDocuments": {"staff": true, "admin": true},
|
"canManageDocuments": {"staff": true, "admin": true},
|
||||||
"canManageEntities": {"staff": true, "admin": true},
|
"canManageEntities": {"staff": true, "admin": true},
|
||||||
"canManageHome": {},
|
"canManageHome": {},
|
||||||
|
|
@ -1401,7 +1402,7 @@
|
||||||
240, 288, 360, 432, 480, 720 and 1080.
|
240, 288, 360, 432, 480, 720 and 1080.
|
||||||
*/
|
*/
|
||||||
"video": {
|
"video": {
|
||||||
"formats": ["webm", "mp4"],
|
"formats": ["mp4"],
|
||||||
// fixme: this should be named "ratio" or "defaultRatio",
|
// fixme: this should be named "ratio" or "defaultRatio",
|
||||||
// as it also applies to clip lists (on load)
|
// as it also applies to clip lists (on load)
|
||||||
"previewRatio": 1.7777777778,
|
"previewRatio": 1.7777777778,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
"canImportAnnotations": {"researcher": true, "staff": true, "admin": true},
|
"canImportAnnotations": {"researcher": true, "staff": true, "admin": true},
|
||||||
// import needs to handle itemRequiresVideo=false first
|
// import needs to handle itemRequiresVideo=false first
|
||||||
"canImportItems": {},
|
"canImportItems": {},
|
||||||
|
"canTranscribeAudio": {},
|
||||||
"canManageDocuments": {"member": true, "researcher": true, "staff": true, "admin": true},
|
"canManageDocuments": {"member": true, "researcher": true, "staff": true, "admin": true},
|
||||||
"canManageEntities": {"member": true, "researcher": true, "staff": true, "admin": true},
|
"canManageEntities": {"member": true, "researcher": true, "staff": true, "admin": true},
|
||||||
"canManageHome": {"staff": true, "admin": true},
|
"canManageHome": {"staff": true, "admin": true},
|
||||||
|
|
@ -1887,7 +1888,7 @@
|
||||||
240, 288, 360, 432, 480, 720 and 1080.
|
240, 288, 360, 432, 480, 720 and 1080.
|
||||||
*/
|
*/
|
||||||
"video": {
|
"video": {
|
||||||
"formats": ["webm", "mp4"],
|
"formats": ["mp4"],
|
||||||
"previewRatio": 1.375,
|
"previewRatio": 1.375,
|
||||||
"resolutions": [240, 480]
|
"resolutions": [240, 480]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
"canExportAnnotations": {"member": true, "staff": true, "admin": true},
|
"canExportAnnotations": {"member": true, "staff": true, "admin": true},
|
||||||
"canImportAnnotations": {"member": true, "staff": true, "admin": true},
|
"canImportAnnotations": {"member": true, "staff": true, "admin": true},
|
||||||
"canImportItems": {"member": true, "staff": true, "admin": true},
|
"canImportItems": {"member": true, "staff": true, "admin": true},
|
||||||
|
"canTranscribeAudio": {"staff": true, "admin": true},
|
||||||
"canManageDocuments": {"member": true, "staff": true, "admin": true},
|
"canManageDocuments": {"member": true, "staff": true, "admin": true},
|
||||||
"canManageEntities": {"member": true, "staff": true, "admin": true},
|
"canManageEntities": {"member": true, "staff": true, "admin": true},
|
||||||
"canManageHome": {"staff": true, "admin": true},
|
"canManageHome": {"staff": true, "admin": true},
|
||||||
|
|
@ -1391,7 +1392,7 @@
|
||||||
240, 288, 360, 432, 480, 720 and 1080.
|
240, 288, 360, 432, 480, 720 and 1080.
|
||||||
*/
|
*/
|
||||||
"video": {
|
"video": {
|
||||||
"formats": ["webm", "mp4"],
|
"formats": ["mp4"],
|
||||||
"previewRatio": 1.3333333333,
|
"previewRatio": 1.3333333333,
|
||||||
//supported resolutions are
|
//supported resolutions are
|
||||||
//1080, 720, 480, 432, 360, 288, 240, 144, 96
|
//1080, 720, 480, 432, 360, 288, 240, 144, 96
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution.
|
||||||
"canExportAnnotations": {"member": true, "staff": true, "admin": true},
|
"canExportAnnotations": {"member": true, "staff": true, "admin": true},
|
||||||
"canImportAnnotations": {"member": true, "staff": true, "admin": true},
|
"canImportAnnotations": {"member": true, "staff": true, "admin": true},
|
||||||
"canImportItems": {"member": true, "staff": true, "admin": true},
|
"canImportItems": {"member": true, "staff": true, "admin": true},
|
||||||
|
"canTranscribeAudio": {},
|
||||||
"canManageDocuments": {"member": true, "staff": true, "admin": true},
|
"canManageDocuments": {"member": true, "staff": true, "admin": true},
|
||||||
"canManageEntities": {"member": true, "staff": true, "admin": true},
|
"canManageEntities": {"member": true, "staff": true, "admin": true},
|
||||||
"canManageHome": {"staff": true, "admin": true},
|
"canManageHome": {"staff": true, "admin": true},
|
||||||
|
|
@ -1280,8 +1281,8 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution.
|
||||||
240, 288, 360, 432, 480, 720 and 1080.
|
240, 288, 360, 432, 480, 720 and 1080.
|
||||||
*/
|
*/
|
||||||
"video": {
|
"video": {
|
||||||
"downloadFormat": "webm",
|
"downloadFormat": "mp4",
|
||||||
"formats": ["webm", "mp4"],
|
"formats": ["mp4"],
|
||||||
"previewRatio": 1.3333333333,
|
"previewRatio": 1.3333333333,
|
||||||
"resolutions": [240, 480]
|
"resolutions": [240, 480]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from django.conf import settings
|
||||||
|
|
||||||
logger = logging.getLogger('pandora.' + __name__)
|
logger = logging.getLogger('pandora.' + __name__)
|
||||||
|
|
||||||
|
IMAGE_EXTENSIONS = ('png', 'jpg', 'webp', 'heic', 'heif')
|
||||||
|
|
||||||
def extract_text(pdf, page=None):
|
def extract_text(pdf, page=None):
|
||||||
if page is not None:
|
if page is not None:
|
||||||
|
|
@ -53,7 +54,7 @@ class FulltextMixin:
|
||||||
if self.file:
|
if self.file:
|
||||||
if self.extension == 'pdf':
|
if self.extension == 'pdf':
|
||||||
return extract_text(self.file.path)
|
return extract_text(self.file.path)
|
||||||
elif self.extension in ('png', 'jpg'):
|
elif self.extension in IMAGE_EXTENSIONS:
|
||||||
return ocr_image(self.file.path)
|
return ocr_image(self.file.path)
|
||||||
elif self.extension == 'html':
|
elif self.extension == 'html':
|
||||||
return self.data.get('text', '')
|
return self.data.get('text', '')
|
||||||
|
|
@ -180,7 +181,7 @@ class FulltextPageMixin(FulltextMixin):
|
||||||
if self.document.file:
|
if self.document.file:
|
||||||
if self.document.extension == 'pdf':
|
if self.document.extension == 'pdf':
|
||||||
return extract_text(self.document.file.path, self.page)
|
return extract_text(self.document.file.path, self.page)
|
||||||
elif self.extension in ('png', 'jpg'):
|
elif self.extension in IMAGE_EXTENSIONS:
|
||||||
return ocr_image(self.document.file.path)
|
return ocr_image(self.document.file.path)
|
||||||
elif self.extension == 'html':
|
elif self.extension == 'html':
|
||||||
# FIXME: is there a nice way to split that into pages
|
# FIXME: is there a nice way to split that into pages
|
||||||
|
|
|
||||||
|
|
@ -521,14 +521,17 @@ class Document(models.Model, FulltextMixin):
|
||||||
return save_chunk(self, self.file, chunk, offset, name, done_cb)
|
return save_chunk(self, self.file, chunk, offset, name, done_cb)
|
||||||
return False, 0
|
return False, 0
|
||||||
|
|
||||||
def thumbnail(self, size=None, page=None):
|
def thumbnail(self, size=None, page=None, accept=None):
|
||||||
if not self.file:
|
if not self.file:
|
||||||
return os.path.join(settings.STATIC_ROOT, 'png/document.png')
|
return os.path.join(settings.STATIC_ROOT, 'png/document.png')
|
||||||
src = self.file.path
|
src = self.file.path
|
||||||
folder = os.path.dirname(src)
|
folder = os.path.dirname(src)
|
||||||
if size:
|
if size:
|
||||||
size = int(size)
|
size = int(size)
|
||||||
path = os.path.join(folder, '%d.jpg' % size)
|
ext = 'jpg'
|
||||||
|
if accept and 'image/avif' in accept and size > 512:
|
||||||
|
ext = 'avif'
|
||||||
|
path = os.path.join(folder, '%d.%s' % (size, ext))
|
||||||
else:
|
else:
|
||||||
path = src
|
path = src
|
||||||
if self.extension == 'pdf':
|
if self.extension == 'pdf':
|
||||||
|
|
@ -561,7 +564,7 @@ class Document(models.Model, FulltextMixin):
|
||||||
path = os.path.join(folder, '%dp%d,%s.jpg' % (size, page, ','.join(map(str, crop))))
|
path = os.path.join(folder, '%dp%d,%s.jpg' % (size, page, ','.join(map(str, crop))))
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
resize_image(src, path, size=size)
|
resize_image(src, path, size=size)
|
||||||
elif self.extension in ('jpg', 'png', 'gif'):
|
elif self.extension in ('jpg', 'png', 'gif', 'webp', 'heic', 'heif'):
|
||||||
if os.path.exists(src):
|
if os.path.exists(src):
|
||||||
if size and page:
|
if size and page:
|
||||||
crop = list(map(int, page.split(',')))
|
crop = list(map(int, page.split(',')))
|
||||||
|
|
@ -574,7 +577,7 @@ class Document(models.Model, FulltextMixin):
|
||||||
img = open_image_rgb(path)
|
img = open_image_rgb(path)
|
||||||
src = path
|
src = path
|
||||||
if size < max(img.size):
|
if size < max(img.size):
|
||||||
path = os.path.join(folder, '%sp%s.jpg' % (size, ','.join(map(str, crop))))
|
path = os.path.join(folder, '%sp%s.%s' % (size, ','.join(map(str, crop)), ext))
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
resize_image(src, path, size=size)
|
resize_image(src, path, size=size)
|
||||||
if os.path.exists(src) and not os.path.exists(path):
|
if os.path.exists(src) and not os.path.exists(path):
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from glob import glob
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from glob import glob
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
import ox
|
import ox
|
||||||
|
|
@ -378,15 +379,24 @@ actions.register(sortDocuments, cache=False)
|
||||||
|
|
||||||
def file(request, id, name=None):
|
def file(request, id, name=None):
|
||||||
document = get_document_or_404_json(request, id)
|
document = get_document_or_404_json(request, id)
|
||||||
|
accept = request.headers.get("Accept")
|
||||||
|
mime_type = mimetypes.guess_type(document.file.path)[0]
|
||||||
|
mime_type = 'image/%s' % document.extension
|
||||||
|
if accept and 'image/' in accept and document.extension in (
|
||||||
|
'webp', 'heif', 'heic', 'avif', 'tiff'
|
||||||
|
) and mime_type not in accept:
|
||||||
|
image_size = max(document.width, document.height)
|
||||||
|
return HttpFileResponse(document.thumbnail(image_size, accept=accept))
|
||||||
return HttpFileResponse(document.file.path)
|
return HttpFileResponse(document.file.path)
|
||||||
|
|
||||||
def thumbnail(request, id, size=256, page=None):
|
def thumbnail(request, id, size=256, page=None):
|
||||||
size = int(size)
|
size = int(size)
|
||||||
document = get_document_or_404_json(request, id)
|
document = get_document_or_404_json(request, id)
|
||||||
|
accept = request.headers.get("Accept")
|
||||||
if "q" in request.GET and page:
|
if "q" in request.GET and page:
|
||||||
img = document.highlight_page(page, request.GET["q"], size)
|
img = document.highlight_page(page, request.GET["q"], size)
|
||||||
return HttpResponse(img, content_type="image/jpeg")
|
return HttpResponse(img, content_type="image/jpeg")
|
||||||
return HttpFileResponse(document.thumbnail(size, page=page))
|
return HttpFileResponse(document.thumbnail(size, page=page, accept=accept))
|
||||||
|
|
||||||
|
|
||||||
@login_required_json
|
@login_required_json
|
||||||
|
|
|
||||||
|
|
@ -495,6 +495,9 @@ class Clip(models.Model):
|
||||||
'id': self.get_id(),
|
'id': self.get_id(),
|
||||||
'index': self.index,
|
'index': self.index,
|
||||||
'volume': self.volume,
|
'volume': self.volume,
|
||||||
|
'hue': self.hue,
|
||||||
|
'saturation': self.saturation,
|
||||||
|
'lightness': self.lightness,
|
||||||
}
|
}
|
||||||
if self.annotation:
|
if self.annotation:
|
||||||
data['annotation'] = self.annotation.public_id
|
data['annotation'] = self.annotation.public_id
|
||||||
|
|
|
||||||
|
|
@ -1375,7 +1375,7 @@ class Item(models.Model):
|
||||||
self.poster_height = self.poster.height
|
self.poster_height = self.poster.height
|
||||||
self.poster_width = self.poster.width
|
self.poster_width = self.poster.width
|
||||||
self.clear_poster_cache(self.poster.path)
|
self.clear_poster_cache(self.poster.path)
|
||||||
if self.cache.get('posterRatio') != self.poster_width / self.poster_height:
|
if self.poster_width and self.cache.get('posterRatio') != self.poster_width / self.poster_height:
|
||||||
self.update_cache(poster_width=self.poster_width,
|
self.update_cache(poster_width=self.poster_width,
|
||||||
poster_height=self.poster_height)
|
poster_height=self.poster_height)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1004,7 +1004,9 @@ def download_source(request, id, part=None):
|
||||||
response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % quote(filename.encode('utf-8'))
|
response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % quote(filename.encode('utf-8'))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def download(request, id, resolution=None, format='webm', part=None):
|
def download(request, id, resolution=None, format=None, part=None):
|
||||||
|
if format is None:
|
||||||
|
format = settings.CONFIG['video']['formats'][0]
|
||||||
item = get_object_or_404(models.Item, public_id=id)
|
item = get_object_or_404(models.Item, public_id=id)
|
||||||
if not resolution or int(resolution) not in settings.CONFIG['video']['resolutions']:
|
if not resolution or int(resolution) not in settings.CONFIG['video']['resolutions']:
|
||||||
resolution = max(settings.CONFIG['video']['resolutions'])
|
resolution = max(settings.CONFIG['video']['resolutions'])
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,13 @@ def api(request):
|
||||||
return response
|
return response
|
||||||
if request.META.get('CONTENT_TYPE') == 'application/json':
|
if request.META.get('CONTENT_TYPE') == 'application/json':
|
||||||
r = json.loads(request.body.decode('utf-8'))
|
r = json.loads(request.body.decode('utf-8'))
|
||||||
|
if 'action' not in r:
|
||||||
|
logger.error("invalid api request: %s", r)
|
||||||
|
response = render_to_json_response(json_response(status=400,
|
||||||
|
text='Invalid request'))
|
||||||
|
response['Access-Control-Allow-Origin'] = '*'
|
||||||
|
return response
|
||||||
|
else:
|
||||||
action = r['action']
|
action = r['action']
|
||||||
data = r.get('data', {})
|
data = r.get('data', {})
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ ROOT_URLCONF = 'urls'
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
|
'mozilla_django_oidc',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
|
|
@ -158,6 +159,27 @@ INSTALLED_APPS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'system.User'
|
AUTH_USER_MODEL = 'system.User'
|
||||||
|
AUTH_PROFILE_MODULE = 'user.UserProfile'
|
||||||
|
AUTH_CHECK_USERNAME = True
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
)
|
||||||
|
|
||||||
|
# OpenID Connect login support
|
||||||
|
LOGIN_REDIRECT_URL = "/grid"
|
||||||
|
LOGOUT_REDIRECT_URL = "/grid"
|
||||||
|
OIDC_USERNAME_ALGO = "app.oidc.generate_username"
|
||||||
|
OIDC_RP_CLIENT_ID = None
|
||||||
|
|
||||||
|
# define those in local_settings to enable OCID based login
|
||||||
|
#OIDC_RP_CLIENT_ID = '<client id>'
|
||||||
|
#OIDC_RP_CLIENT_SECRET = '<client secret>'
|
||||||
|
#OIDC_RP_SIGN_ALGO = "RS256"
|
||||||
|
#OIDC_OP_JWKS_ENDPOINT = "<jwks endpoint>"
|
||||||
|
#OIDC_OP_AUTHORIZATION_ENDPOINT = "<authorization endpoint>"
|
||||||
|
#OIDC_OP_TOKEN_ENDPOINT = "<token endpoint>"
|
||||||
|
#OIDC_OP_USER_ENDPOINT = "<user endpoint>"
|
||||||
|
|
||||||
# Log errors into db
|
# Log errors into db
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
|
|
@ -193,8 +215,6 @@ CACHES = {
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
|
||||||
AUTH_PROFILE_MODULE = 'user.UserProfile'
|
|
||||||
AUTH_CHECK_USERNAME = True
|
|
||||||
FFMPEG = 'ffmpeg'
|
FFMPEG = 'ffmpeg'
|
||||||
FFPROBE = 'ffprobe'
|
FFPROBE = 'ffprobe'
|
||||||
USE_VP9 = True
|
USE_VP9 = True
|
||||||
|
|
@ -291,6 +311,8 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 32 * 1024 * 1024
|
||||||
|
|
||||||
EMPTY_CLIPS = True
|
EMPTY_CLIPS = True
|
||||||
|
|
||||||
|
YT_DLP_EXTRA = []
|
||||||
|
|
||||||
#you can ignore things below this line
|
#you can ignore things below this line
|
||||||
#=========================================================================
|
#=========================================================================
|
||||||
LOCAL_APPS = []
|
LOCAL_APPS = []
|
||||||
|
|
@ -321,3 +343,7 @@ except NameError:
|
||||||
|
|
||||||
INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS)
|
INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS)
|
||||||
|
|
||||||
|
if OIDC_RP_CLIENT_ID:
|
||||||
|
AUTHENTICATION_BACKENDS = list(AUTHENTICATION_BACKENDS) + [
|
||||||
|
'app.oidc.OIDCAuthenticationBackend'
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,9 @@ class Task(models.Model):
|
||||||
except Item.DoesNotExist:
|
except Item.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if self.status == 'transcribing':
|
||||||
|
return False
|
||||||
|
|
||||||
if self.item.files.filter(wanted=True, available=False).count():
|
if self.item.files.filter(wanted=True, available=False).count():
|
||||||
status = 'pending'
|
status = 'pending'
|
||||||
elif self.item.files.filter(uploading=True).count():
|
elif self.item.files.filter(uploading=True).count():
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<meta property="og:site_name" content="{{ settings.SITENAME }}"/>
|
<meta property="og:site_name" content="{{ settings.SITENAME }}"/>
|
||||||
{% compress css file m %}
|
{% compress css file m %}
|
||||||
<link rel="stylesheet" href="{% static 'mobile/css/reset.css' %}"></link>
|
<link rel="stylesheet" href="{% static 'mobile/css/reset.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'mobile/css/style.css' %}"></link>
|
<link rel="stylesheet" href="{% static 'mobile/css/style.css' %}">
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
<meta name="google" value="notranslate"/>
|
<meta name="google" value="notranslate"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import os
|
import os
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path, include
|
||||||
from oxdjango.http import HttpFileResponse
|
from oxdjango.http import HttpFileResponse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -33,9 +33,15 @@ import urlalias.views
|
||||||
def serve_static_file(path, location, content_type):
|
def serve_static_file(path, location, content_type):
|
||||||
return HttpFileResponse(location, content_type=content_type)
|
return HttpFileResponse(location, content_type=content_type)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
#path('admin/', admin.site.urls),
|
#path('admin/', admin.site.urls),
|
||||||
|
]
|
||||||
|
if settings.OIDC_RP_CLIENT_ID:
|
||||||
|
urlpatterns += [
|
||||||
|
path('oidc/', include('mozilla_django_oidc.urls')),
|
||||||
|
]
|
||||||
|
urlpatterns += [
|
||||||
re_path(r'^api/locale.(?P<lang>.*).json$', translation.views.locale_json),
|
re_path(r'^api/locale.(?P<lang>.*).json$', translation.views.locale_json),
|
||||||
re_path(r'^api/upload/text/?$', text.views.upload),
|
re_path(r'^api/upload/text/?$', text.views.upload),
|
||||||
re_path(r'^api/upload/document/?$', document.views.upload),
|
re_path(r'^api/upload/document/?$', document.views.upload),
|
||||||
|
|
@ -47,7 +53,7 @@ urlpatterns = [
|
||||||
re_path(r'^resetUI$', user.views.reset_ui),
|
re_path(r'^resetUI$', user.views.reset_ui),
|
||||||
re_path(r'^collection/(?P<id>.*?)/icon(?P<size>\d*).jpg$', documentcollection.views.icon),
|
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<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<id>[A-Z0-9]+)/(?P<name>.*?\.[^\d]{3,4})$', document.views.file),
|
||||||
re_path(r'^documents/(?P<fragment>.*?)$', document.views.document),
|
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'^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'^list/(?P<id>.*?)/icon(?P<size>\d*).jpg$', itemlist.views.icon),
|
||||||
|
|
|
||||||
|
|
@ -436,8 +436,7 @@ def has_capability(user, capability):
|
||||||
level = 'guest'
|
level = 'guest'
|
||||||
else:
|
else:
|
||||||
level = user.profile.get_level()
|
level = user.profile.get_level()
|
||||||
return level in settings.CONFIG['capabilities'][capability] \
|
return settings.CONFIG['capabilities'].get(capability, {}).get(level)
|
||||||
and settings.CONFIG['capabilities'][capability][level]
|
|
||||||
|
|
||||||
def merge_users(old, new):
|
def merge_users(old, new):
|
||||||
old.annotations.all().update(user=new)
|
old.annotations.all().update(user=new)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,38 @@ from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
|
||||||
import ox
|
import ox
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_user(user):
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.conf import settings
|
||||||
|
from itemlist.models import List, Position
|
||||||
|
from django.db.models import Max
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
first_user_qs = User.objects.all()
|
||||||
|
if user.id:
|
||||||
|
first_user_qs = first_user_qs.exclude(id=user.id)
|
||||||
|
if first_user_qs.count() == 0:
|
||||||
|
user.is_superuser = True
|
||||||
|
user.is_staff = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
for l in settings.CONFIG['personalLists']:
|
||||||
|
list = List(name=l['title'], user=user)
|
||||||
|
for key in ('query', 'public', 'featured'):
|
||||||
|
if key in l:
|
||||||
|
setattr(list, key, l[key])
|
||||||
|
if key == 'query':
|
||||||
|
for c in list.query['conditions']:
|
||||||
|
if c['key'] == 'user':
|
||||||
|
c['value'] = c['value'].format(username=user.username)
|
||||||
|
list.save()
|
||||||
|
pos = Position(list=list, section='personal', user=user)
|
||||||
|
qs = Position.objects.filter(user=user, section='personal')
|
||||||
|
pos.position = (qs.aggregate(Max('position'))['position__max'] or 0) + 1
|
||||||
|
pos.save()
|
||||||
|
|
||||||
|
|
||||||
def get_ip(request):
|
def get_ip(request):
|
||||||
if 'HTTP_X_FORWARDED_FOR' in request.META:
|
if 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||||
ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0]
|
ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0]
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ from user.models import Group
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from .decorators import capability_required_json
|
from .decorators import capability_required_json
|
||||||
from .utils import rename_user
|
from .utils import rename_user, prepare_user
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
@ -177,28 +177,10 @@ def signup(request, data):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
first_user = User.objects.count() == 0
|
|
||||||
user = User(username=data['username'], email=data['email'])
|
user = User(username=data['username'], email=data['email'])
|
||||||
user.set_password(data['password'])
|
user.set_password(data['password'])
|
||||||
#make first user admin
|
|
||||||
user.is_superuser = first_user
|
|
||||||
user.is_staff = first_user
|
|
||||||
user.save()
|
user.save()
|
||||||
#create default user lists:
|
prepare_user(user)
|
||||||
for l in settings.CONFIG['personalLists']:
|
|
||||||
list = List(name=l['title'], user=user)
|
|
||||||
for key in ('query', 'public', 'featured'):
|
|
||||||
if key in l:
|
|
||||||
setattr(list, key, l[key])
|
|
||||||
if key == 'query':
|
|
||||||
for c in list.query['conditions']:
|
|
||||||
if c['key'] == 'user':
|
|
||||||
c['value'] = c['value'].format(username=user.username)
|
|
||||||
list.save()
|
|
||||||
pos = Position(list=list, section='personal', user=user)
|
|
||||||
qs = Position.objects.filter(user=user, section='personal')
|
|
||||||
pos.position = (qs.aggregate(Max('position'))['position__max'] or 0) + 1
|
|
||||||
pos.save()
|
|
||||||
if request.session.session_key:
|
if request.session.session_key:
|
||||||
models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user)
|
models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user)
|
||||||
ui = json.loads(request.session.get('ui', 'null'))
|
ui = json.loads(request.session.get('ui', 'null'))
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,6 @@ future
|
||||||
pytz
|
pytz
|
||||||
pypdfium2
|
pypdfium2
|
||||||
Pillow>=10
|
Pillow>=10
|
||||||
|
pillow-heif
|
||||||
|
pillow-avif-plugin
|
||||||
|
mozilla-django-oidc==4.0.1
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,13 @@ pandora.ui.addFilesDialog = function(options) {
|
||||||
});
|
});
|
||||||
|
|
||||||
var selectItems = [];
|
var selectItems = [];
|
||||||
|
selectItems.push({
|
||||||
|
id: 'one',
|
||||||
|
title: Ox._(
|
||||||
|
options.items.length > 1 ? 'Create new {0} with multiple parts' : 'Create new {0}',
|
||||||
|
[pandora.site.itemName.singular.toLowerCase()]
|
||||||
|
)
|
||||||
|
});
|
||||||
if (pandora.user.ui.item && options.editable) {
|
if (pandora.user.ui.item && options.editable) {
|
||||||
selectItems.push({
|
selectItems.push({
|
||||||
id: 'add',
|
id: 'add',
|
||||||
|
|
@ -124,13 +131,6 @@ pandora.ui.addFilesDialog = function(options) {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
selectItems.push({
|
|
||||||
id: 'one',
|
|
||||||
title: Ox._(
|
|
||||||
options.items.length > 1 ? 'Create new {0} with multiple parts' : 'Create new {0}',
|
|
||||||
[pandora.site.itemName.singular.toLowerCase()]
|
|
||||||
)
|
|
||||||
});
|
|
||||||
var $select = Ox.Select({
|
var $select = Ox.Select({
|
||||||
items: selectItems,
|
items: selectItems,
|
||||||
width: 256
|
width: 256
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,10 @@ pandora.ui.appPanel = function() {
|
||||||
pandora.$ui.siteDialog = pandora.ui.siteDialog(page).open();
|
pandora.$ui.siteDialog = pandora.ui.siteDialog(page).open();
|
||||||
}
|
}
|
||||||
} else if (['signup', 'signin'].indexOf(page) > -1) {
|
} else if (['signup', 'signin'].indexOf(page) > -1) {
|
||||||
|
if (pandora.site.site.oidc) {
|
||||||
|
pandora.oidcLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (pandora.user.level == 'guest') {
|
if (pandora.user.level == 'guest') {
|
||||||
if (pandora.$ui.accountDialog && pandora.$ui.accountDialog.is(':visible')) {
|
if (pandora.$ui.accountDialog && pandora.$ui.accountDialog.is(':visible')) {
|
||||||
pandora.$ui.accountDialog.options(pandora.ui.accountDialogOptions(page));
|
pandora.$ui.accountDialog.options(pandora.ui.accountDialogOptions(page));
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,13 @@ pandora.ui.editor = function(data) {
|
||||||
censoredIcon: pandora.site.cantPlay.icon,
|
censoredIcon: pandora.site.cantPlay.icon,
|
||||||
censoredTooltip: Ox._(pandora.site.cantPlay.text),
|
censoredTooltip: Ox._(pandora.site.cantPlay.text),
|
||||||
clickLink: pandora.clickLink,
|
clickLink: pandora.clickLink,
|
||||||
|
confirmDeleteDialog: confirmDeleteDialog,
|
||||||
cuts: data.cuts || [],
|
cuts: data.cuts || [],
|
||||||
duration: data.duration,
|
duration: data.duration,
|
||||||
enableDownload: pandora.hasCapability('canDownloadVideo') >= data.rightslevel || data.editable,
|
enableDownload: pandora.hasCapability('canDownloadVideo') >= data.rightslevel || data.editable,
|
||||||
enableExport: pandora.hasCapability('canExportAnnotations') || data.editable,
|
enableExport: pandora.hasCapability('canExportAnnotations') || data.editable,
|
||||||
enableImport: pandora.hasCapability('canImportAnnotations') || data.editable,
|
enableImport: pandora.hasCapability('canImportAnnotations') || data.editable,
|
||||||
|
enableTranscribe: pandora.hasCapability('canTranscribeAudio') && data.editable,
|
||||||
enableSetPosterFrame: !pandora.site.media.importFrames && data.editable,
|
enableSetPosterFrame: !pandora.site.media.importFrames && data.editable,
|
||||||
enableSubtitles: ui.videoSubtitles,
|
enableSubtitles: ui.videoSubtitles,
|
||||||
find: ui.itemFind,
|
find: ui.itemFind,
|
||||||
|
|
@ -220,7 +222,9 @@ pandora.ui.editor = function(data) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
that.updateAnnotation(data.id, result.data);
|
that.updateAnnotation(data.id, result.data);
|
||||||
|
if (result.data.id) {
|
||||||
pandora.UI.set('videoPoints.' + ui.item + '.annotation', result.data.id.split('/')[1] || '');
|
pandora.UI.set('videoPoints.' + ui.item + '.annotation', result.data.id.split('/')[1] || '');
|
||||||
|
}
|
||||||
Ox.Request.clearCache();
|
Ox.Request.clearCache();
|
||||||
};
|
};
|
||||||
var edit = {
|
var edit = {
|
||||||
|
|
@ -391,7 +395,21 @@ pandora.ui.editor = function(data) {
|
||||||
pandora.UI.set({videoResolution: data.resolution});
|
pandora.UI.set({videoResolution: data.resolution});
|
||||||
},
|
},
|
||||||
select: function(data) {
|
select: function(data) {
|
||||||
|
if (Ox.isArray(data.id)) {
|
||||||
|
var range = data.id.map(id => {
|
||||||
|
return getAnnotationById(id)
|
||||||
|
})
|
||||||
|
data['in'] = Ox.min(range.map(annotation => { return annotation["in"]; }))
|
||||||
|
data['out'] = Ox.max(range.map(annotation => { return annotation["out"]; }))
|
||||||
|
pandora.UI.set('videoPoints.' + ui.item, {
|
||||||
|
annotation: '',
|
||||||
|
'in': data['in'],
|
||||||
|
out: data.out,
|
||||||
|
position: ui.videoPoints[ui.item].position
|
||||||
|
})
|
||||||
|
} else {
|
||||||
pandora.UI.set('videoPoints.' + ui.item + '.annotation', data.id.split('/')[1] || '');
|
pandora.UI.set('videoPoints.' + ui.item + '.annotation', data.id.split('/')[1] || '');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
showentityinfo: function(data) {
|
showentityinfo: function(data) {
|
||||||
pandora.URL.push('/entities/' + data.id)
|
pandora.URL.push('/entities/' + data.id)
|
||||||
|
|
@ -417,6 +435,54 @@ pandora.ui.editor = function(data) {
|
||||||
togglesize: function(data) {
|
togglesize: function(data) {
|
||||||
pandora.UI.set({videoSize: data.size});
|
pandora.UI.set({videoSize: data.size});
|
||||||
},
|
},
|
||||||
|
transcribeaudio: function() {
|
||||||
|
const $dialog = pandora.ui.iconDialog({
|
||||||
|
buttons: [
|
||||||
|
Ox.Button({
|
||||||
|
id: 'cancel',
|
||||||
|
title: Ox._('Cancel')
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
$dialog.close();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Ox.Button({
|
||||||
|
id: 'transcribe',
|
||||||
|
title: Ox._('Transcribe Audio')
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
$dialog.close();
|
||||||
|
pandora.api.transcribeAudio({
|
||||||
|
item: pandora.user.ui.item
|
||||||
|
}, function(result) {
|
||||||
|
if (result.data.taskId) {
|
||||||
|
pandora.wait(result.data.taskId, function(result) {
|
||||||
|
Ox.Request.clearCache();
|
||||||
|
if (ui.item == data.id && ui.itemView == 'editor') {
|
||||||
|
pandora.$ui.contentPanel.replaceElement(
|
||||||
|
1, pandora.$ui.item = pandora.ui.item()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pandora.ui.tasksDialog().open();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
content: Ox._(
|
||||||
|
'Are you sure you want to add automated transcription to "{0}"',
|
||||||
|
[data.title]
|
||||||
|
),
|
||||||
|
keys: {enter: 'transcribe', escape: 'cancel'},
|
||||||
|
title: Ox._(
|
||||||
|
'Transcribe {0} {1}', [data.title]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
$dialog.open()
|
||||||
|
},
|
||||||
pandora_showannotations: function(data) {
|
pandora_showannotations: function(data) {
|
||||||
that.options({showAnnotations: data.value});
|
that.options({showAnnotations: data.value});
|
||||||
},
|
},
|
||||||
|
|
@ -428,6 +494,60 @@ pandora.ui.editor = function(data) {
|
||||||
|
|
||||||
pandora._dontSelectResult = false;
|
pandora._dontSelectResult = false;
|
||||||
|
|
||||||
|
function confirmDeleteDialog(options, callback) {
|
||||||
|
const title = Ox.getObjectById(
|
||||||
|
pandora.site.layers,
|
||||||
|
getAnnotationById(options.items[0]).layer
|
||||||
|
).item
|
||||||
|
const subject = options.items.length == 1 ? title : title + 's'
|
||||||
|
const $dialog = pandora.ui.iconDialog({
|
||||||
|
buttons: [
|
||||||
|
Ox.Button({
|
||||||
|
id: 'cancel',
|
||||||
|
title: Ox._('Keep {0}', [Ox._(subject)])
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
$dialog.close();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Ox.Button({
|
||||||
|
id: 'delete',
|
||||||
|
title: Ox._('Delete {0}', [Ox._(subject)])
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
$dialog.close();
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
content: Ox._(
|
||||||
|
'Are you sure you want delete {0} {1}?<br><br>All data will be removed.',
|
||||||
|
[options.items.length, Ox._(subject.toLowerCase())]
|
||||||
|
),
|
||||||
|
height: 96,
|
||||||
|
keys: {enter: 'delete', escape: 'cancel'},
|
||||||
|
title: Ox._(
|
||||||
|
'Delete {0}', [Ox._(subject)]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
$dialog.open()
|
||||||
|
return $dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnnotationById(id) {
|
||||||
|
var annotation
|
||||||
|
data.annotations.forEach(layer => {
|
||||||
|
layer.items.forEach(a => {
|
||||||
|
if (a.id == id) {
|
||||||
|
annotation = a
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
function updateBrowser() {
|
function updateBrowser() {
|
||||||
pandora.$ui.browser.find('img[src*="/' + ui.item + '/"]').each(function() {
|
pandora.$ui.browser.find('img[src*="/' + ui.item + '/"]').each(function() {
|
||||||
$(this).attr({
|
$(this).attr({
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,13 @@ pandora.ui.exportAnnotationsDialog = function(options) {
|
||||||
$button.wrap($('<a>'));
|
$button.wrap($('<a>'));
|
||||||
// On wrap, a reference to the link would *not* be the link in the DOM
|
// On wrap, a reference to the link would *not* be the link in the DOM
|
||||||
$link = $($button.parent());
|
$link = $($button.parent());
|
||||||
|
$link.on({
|
||||||
|
click: function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
that.close()
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
})
|
||||||
updateLink();
|
updateLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -424,18 +424,26 @@ pandora.ui.folders = function(section) {
|
||||||
}).bindEvent({
|
}).bindEvent({
|
||||||
click: function() {
|
click: function() {
|
||||||
var $dialog = pandora.ui.iconDialog({
|
var $dialog = pandora.ui.iconDialog({
|
||||||
buttons: title != Ox._('Featured ' + folderItems) ? [
|
buttons: title != Ox._('Featured ' + folderItems) ? [].concat(
|
||||||
|
pandora.site.site.oidc ? []
|
||||||
|
: [
|
||||||
Ox.Button({title: Ox._('Sign Up...')}).bindEvent({
|
Ox.Button({title: Ox._('Sign Up...')}).bindEvent({
|
||||||
click: function() {
|
click: function() {
|
||||||
$dialog.close();
|
$dialog.close();
|
||||||
pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open();
|
pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open();
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
],
|
||||||
|
[
|
||||||
Ox.Button({title: Ox._('Sign In...')}).bindEvent({
|
Ox.Button({title: Ox._('Sign In...')}).bindEvent({
|
||||||
click: function() {
|
click: function() {
|
||||||
$dialog.close();
|
$dialog.close();
|
||||||
|
if (pandora.site.site.oidc) {
|
||||||
|
pandora.oidcLogin()
|
||||||
|
} else {
|
||||||
pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open();
|
pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
Ox.Button({title: Ox._('Not Now')}).bindEvent({
|
Ox.Button({title: Ox._('Not Now')}).bindEvent({
|
||||||
|
|
@ -443,7 +451,8 @@ pandora.ui.folders = function(section) {
|
||||||
$dialog.close();
|
$dialog.close();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
] : [
|
]
|
||||||
|
): [
|
||||||
Ox.Button({title: Ox._('Close')}).bindEvent({
|
Ox.Button({title: Ox._('Close')}).bindEvent({
|
||||||
click: function() {
|
click: function() {
|
||||||
$dialog.close();
|
$dialog.close();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
pandora.getItemTitle = function(itemData, includeYear) {
|
pandora.getItemTitle = function(itemData, includeYear) {
|
||||||
|
var director = itemData.director
|
||||||
|
if (!Ox.isArray(director)) {
|
||||||
|
director = [director]
|
||||||
|
}
|
||||||
return (itemData.title || Ox._('Untitled')) + (
|
return (itemData.title || Ox._('Untitled')) + (
|
||||||
Ox.len(itemData.director) || (includeYear && itemData.year)
|
Ox.len(itemData.director) || (includeYear && itemData.year)
|
||||||
? ' (' + (
|
? ' (' + (
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ pandora.ui.helpDialog = function() {
|
||||||
|
|
||||||
that.select = function(id) {
|
that.select = function(id) {
|
||||||
var img, $img;
|
var img, $img;
|
||||||
|
if ($text) {
|
||||||
$text.html(text[id || 'help']).scrollTop(0);
|
$text.html(text[id || 'help']).scrollTop(0);
|
||||||
img = $text.find('img');
|
img = $text.find('img');
|
||||||
if (img) {
|
if (img) {
|
||||||
|
|
@ -155,6 +156,7 @@ pandora.ui.helpDialog = function() {
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return that;
|
return that;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,13 +171,13 @@ pandora.ui.home = function() {
|
||||||
}),
|
}),
|
||||||
$signinButton = Ox.Button({
|
$signinButton = Ox.Button({
|
||||||
title: Ox._('Sign In'),
|
title: Ox._('Sign In'),
|
||||||
width: 74
|
width: pandora.site.site.oidc ? 156 : 74
|
||||||
})
|
})
|
||||||
.css({
|
.css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
top: '112px',
|
top: '112px',
|
||||||
right: '82px',
|
right: pandora.site.site.oidc ? '164px' : '82px',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
opacity: 0
|
opacity: 0
|
||||||
|
|
@ -248,7 +248,13 @@ pandora.ui.home = function() {
|
||||||
adjustRatio();
|
adjustRatio();
|
||||||
|
|
||||||
if (pandora.user.level == 'guest') {
|
if (pandora.user.level == 'guest') {
|
||||||
|
if (pandora.site.site.oidc) {
|
||||||
|
$signinButton.options({
|
||||||
|
width: 156
|
||||||
|
})
|
||||||
|
} else {
|
||||||
$signupButton.appendTo(that);
|
$signupButton.appendTo(that);
|
||||||
|
}
|
||||||
$signinButton.appendTo(that);
|
$signinButton.appendTo(that);
|
||||||
} else {
|
} else {
|
||||||
$preferencesButton.appendTo(that);
|
$preferencesButton.appendTo(that);
|
||||||
|
|
|
||||||
|
|
@ -201,10 +201,11 @@ pandora.ui.importAnnotationsDialog = function(options) {
|
||||||
pandora.$ui.contentPanel.replaceElement(
|
pandora.$ui.contentPanel.replaceElement(
|
||||||
1, pandora.$ui.item = pandora.ui.item()
|
1, pandora.$ui.item = pandora.ui.item()
|
||||||
);
|
);
|
||||||
|
that.close();
|
||||||
} else {
|
} else {
|
||||||
$status.html(Ox._('Import failed.'));
|
$status.html(Ox._('Import failed.'));
|
||||||
}
|
|
||||||
enableButtons();
|
enableButtons();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$status.html(Ox._('Import failed.'));
|
$status.html(Ox._('Import failed.'));
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ pandora.ui.item = function() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!Ox.isEmpty(set)) {
|
if (!Ox.isEmpty(set)) {
|
||||||
pandora.UI.set('videoPoints.' + item, Ox.extend(videoPoints[point], set[point]))
|
pandora.UI.set('videoPoints.' + item, Ox.extend(videoPoints, set))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,9 +63,9 @@ pandora.ui.item = function() {
|
||||||
var tasks = result_.data.items.filter(function(task) { return task.item == item})
|
var tasks = result_.data.items.filter(function(task) { return task.item == item})
|
||||||
if (tasks.length > 0) {
|
if (tasks.length > 0) {
|
||||||
html = Ox._(
|
html = Ox._(
|
||||||
'<i>{0}</i> is currently processed. '
|
'<i>{0}</i> is currently being processed. '
|
||||||
+ '{1} view will be available in a moment.',
|
+ '{1} view will be available in a moment.',
|
||||||
[result.data.title, Ox._(pandora.user.ui.itemView)]
|
[result.data.title, Ox.toTitleCase(Ox._(pandora.user.ui.itemView))]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
note.html(html)
|
note.html(html)
|
||||||
|
|
@ -202,6 +202,10 @@ pandora.ui.item = function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// avoid loop while selecting multiple annotations
|
||||||
|
if (options.selected == '' && Ox.isArray(pandora.$ui[pandora.user.ui.itemView].options('selected'))) {
|
||||||
|
delete options.selected
|
||||||
|
}
|
||||||
pandora.$ui[pandora.user.ui.itemView].options(options);
|
pandora.$ui[pandora.user.ui.itemView].options(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ pandora.ui.list = function() {
|
||||||
item: function(data, sort, size) {
|
item: function(data, sort, size) {
|
||||||
size = 128;
|
size = 128;
|
||||||
var clipsQuery = pandora.getClipsQuery(),
|
var clipsQuery = pandora.getClipsQuery(),
|
||||||
isClipsQuery = !!clipsQuery.conditions.length,
|
isClipsQuery = clipsQuery.conditions.length > 1,
|
||||||
ratio = ui.icons == 'posters'
|
ratio = ui.icons == 'posters'
|
||||||
? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1,
|
? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1,
|
||||||
url = pandora.getMediaURL('/' + data.id + '/' + (
|
url = pandora.getMediaURL('/' + data.id + '/' + (
|
||||||
|
|
@ -352,7 +352,7 @@ pandora.ui.list = function() {
|
||||||
},
|
},
|
||||||
items: function(data, callback) {
|
items: function(data, callback) {
|
||||||
var clipsQuery = pandora.getClipsQuery(),
|
var clipsQuery = pandora.getClipsQuery(),
|
||||||
isClipsQuery = !!clipsQuery.conditions.length;
|
isClipsQuery = clipsQuery.conditions.length > 1;
|
||||||
pandora.api.find(Ox.extend(data, Ox.extend({
|
pandora.api.find(Ox.extend(data, Ox.extend({
|
||||||
query: ui.find
|
query: ui.find
|
||||||
}, isClipsQuery ? {clips: {
|
}, isClipsQuery ? {clips: {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ pandora.ui.mainMenu = function() {
|
||||||
{ id: 'tasks', title: Ox._('Tasks...'), disabled: isGuest },
|
{ id: 'tasks', title: Ox._('Tasks...'), disabled: isGuest },
|
||||||
{ id: 'archives', title: Ox._('Archives...'), disabled: /*isGuest*/ true },
|
{ id: 'archives', title: Ox._('Archives...'), disabled: /*isGuest*/ true },
|
||||||
{},
|
{},
|
||||||
{ id: 'signup', title: Ox._('Sign Up...'), disabled: !isGuest },
|
!pandora.site.site.oidc
|
||||||
|
? { id: 'signup', title: Ox._('Sign Up...'), disabled: !isGuest }
|
||||||
|
: [],
|
||||||
isGuest ? { id: 'signin', title: Ox._('Sign In...')}
|
isGuest ? { id: 'signin', title: Ox._('Sign In...')}
|
||||||
: { id: 'signout', title: Ox._('Sign Out...')}
|
: { id: 'signout', title: Ox._('Sign Out...')}
|
||||||
] },
|
] },
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,22 @@ pandora.ui.mediaView = function(options) {
|
||||||
visible: true,
|
visible: true,
|
||||||
width: 360
|
width: 360
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
align: 'left',
|
||||||
|
id: 'folder',
|
||||||
|
operator: '+',
|
||||||
|
title: Ox._('Folder'),
|
||||||
|
visible: false,
|
||||||
|
width: 360
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'left',
|
||||||
|
id: 'filename',
|
||||||
|
operator: '+',
|
||||||
|
title: Ox._('Filename'),
|
||||||
|
visible: false,
|
||||||
|
width: 360
|
||||||
|
},
|
||||||
{
|
{
|
||||||
editable: true,
|
editable: true,
|
||||||
id: 'version',
|
id: 'version',
|
||||||
|
|
@ -441,6 +457,8 @@ pandora.ui.mediaView = function(options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}).on({
|
||||||
|
keyup: updateForm
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -663,7 +681,9 @@ pandora.ui.mediaView = function(options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.$moveButton.options({
|
self.$moveButton.options({
|
||||||
disabled: self.selected.length == 0
|
disabled: self.selected.length == 0 || (
|
||||||
|
self.$idInput.value().length + self.$titleInput.value().length
|
||||||
|
) == 0
|
||||||
});
|
});
|
||||||
self.$menu[
|
self.$menu[
|
||||||
self.selected.length == 0 ? 'disableItem' : 'enableItem'
|
self.selected.length == 0 ? 'disableItem' : 'enableItem'
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ pandora.ui.metadataDialog = function(data) {
|
||||||
'To update the metadata for this {0}, please enter its IMDb ID.',
|
'To update the metadata for this {0}, please enter its IMDb ID.',
|
||||||
[pandora.site.itemName.singular.toLowerCase()]
|
[pandora.site.itemName.singular.toLowerCase()]
|
||||||
),
|
),
|
||||||
keyboard: {enter: 'update', escape: 'close'},
|
keys: {enter: 'update', escape: 'close'},
|
||||||
title: Ox._('Update Metadata')
|
title: Ox._('Update Metadata')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,10 @@ appPanel
|
||||||
if (pandora.user.ui.itemView == 'video') {
|
if (pandora.user.ui.itemView == 'video') {
|
||||||
pandora.user.ui.itemView = 'player';
|
pandora.user.ui.itemView = 'player';
|
||||||
}
|
}
|
||||||
|
// item find accross relodas breaks links to in/out
|
||||||
|
if (pandora.user.ui.itemFind) {
|
||||||
|
pandora.user.ui.itemFind = "";
|
||||||
|
}
|
||||||
|
|
||||||
Ox.extend(pandora.site, {
|
Ox.extend(pandora.site, {
|
||||||
calendar: data.site.layers.some(function(layer) {
|
calendar: data.site.layers.some(function(layer) {
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@ pandora.ui.tasksDialog = function(options) {
|
||||||
'processing': 'Processing',
|
'processing': 'Processing',
|
||||||
'canceled': 'Canceled',
|
'canceled': 'Canceled',
|
||||||
'failed': 'Failed',
|
'failed': 'Failed',
|
||||||
'finished': 'Finished'
|
'finished': 'Finished',
|
||||||
|
'transcribing': 'Transcribing'
|
||||||
}[value] || value;
|
}[value] || value;
|
||||||
},
|
},
|
||||||
id: 'status',
|
id: 'status',
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ pandora.ui.uploadDocumentDialog = function(options, callback) {
|
||||||
existingFiles = [],
|
existingFiles = [],
|
||||||
uploadFiles = [],
|
uploadFiles = [],
|
||||||
|
|
||||||
supportedExtensions = ['gif', 'jpg', 'jpeg', 'pdf', 'png'],
|
|
||||||
|
|
||||||
filename,
|
filename,
|
||||||
|
|
||||||
ids = [],
|
ids = [],
|
||||||
|
|
@ -72,7 +70,7 @@ pandora.ui.uploadDocumentDialog = function(options, callback) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!Ox.every(extensions, function(extension) {
|
if (!Ox.every(extensions, function(extension) {
|
||||||
return Ox.contains(supportedExtensions, extension)
|
return Ox.contains(pandora.documentExtensions, extension)
|
||||||
})) {
|
})) {
|
||||||
return errorDialog(
|
return errorDialog(
|
||||||
Ox._('Supported file types are GIF, JPG, PNG and PDF.')
|
Ox._('Supported file types are GIF, JPG, PNG and PDF.')
|
||||||
|
|
|
||||||
|
|
@ -420,13 +420,28 @@ pandora.createLinks = function($element) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pandora.imageExtensions = [
|
||||||
|
'avif',
|
||||||
|
'gif',
|
||||||
|
'heic',
|
||||||
|
'heif',
|
||||||
|
'jpeg',
|
||||||
|
'jpg',
|
||||||
|
'png',
|
||||||
|
'tiff',
|
||||||
|
'webp'
|
||||||
|
];
|
||||||
|
|
||||||
|
pandora.documentExtensions = [
|
||||||
|
'pdf', /* 'epub', 'txt', */
|
||||||
|
].concat(pandora.imageExtensions);
|
||||||
|
|
||||||
pandora.uploadDroppedFiles = function(files) {
|
pandora.uploadDroppedFiles = function(files) {
|
||||||
var documentExtensions = ['pdf', /* 'epub', 'txt', */ 'png', 'gif', 'jpg', 'jpeg'];
|
|
||||||
files = Ox.map(files, function(file) { return file });
|
files = Ox.map(files, function(file) { return file });
|
||||||
|
|
||||||
if (files.every(function(file) {
|
if (files.every(function(file) {
|
||||||
var extension = file.name.split('.').pop().toLowerCase()
|
var extension = file.name.split('.').pop().toLowerCase()
|
||||||
return Ox.contains(documentExtensions, extension)
|
return Ox.contains(pandora.documentExtensions, extension)
|
||||||
})) {
|
})) {
|
||||||
pandora.ui.uploadDocumentDialog({
|
pandora.ui.uploadDocumentDialog({
|
||||||
files: files
|
files: files
|
||||||
|
|
@ -2132,7 +2147,7 @@ pandora.getSpan = function(state, val, callback) {
|
||||||
} else {
|
} else {
|
||||||
state.span = val;
|
state.span = val;
|
||||||
}
|
}
|
||||||
} else if (Ox.contains(['gif', 'jpg', 'png'], extension)) {
|
} else if (Ox.contains(pandora.imageExtensions, extension)) {
|
||||||
values = val.split(',');
|
values = val.split(',');
|
||||||
if (values.length == 4) {
|
if (values.length == 4) {
|
||||||
state.span = values.map(function(number, index) {
|
state.span = values.map(function(number, index) {
|
||||||
|
|
@ -2650,6 +2665,11 @@ pandora.logEvent = function(data, event, element) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pandora.oidcLogin = function() {
|
||||||
|
Ox.LoadingScreen().css({zIndex: 100}).addClass('OxScreen').appendTo(document.body).show().start()
|
||||||
|
document.location.href = '/oidc/authenticate/';
|
||||||
|
};
|
||||||
|
|
||||||
pandora.openLicenseDialog = function() {
|
pandora.openLicenseDialog = function() {
|
||||||
if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) {
|
if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) {
|
||||||
pandora.ui.licenseDialog().open().bindEvent({
|
pandora.ui.licenseDialog().open().bindEvent({
|
||||||
|
|
|
||||||
|
|
@ -201,3 +201,75 @@ ol li {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.seekbar {
|
||||||
|
padding: 12px 22px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.fullscreen .seekbar {
|
||||||
|
padding: 28px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seekbar-progress {
|
||||||
|
height: 10px;
|
||||||
|
border: solid 1px #B1B1B1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seekbar-progress [role="progressbar"] {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
background-color: #B1B1B180;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seekbar-progress [role="progressbar"]:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -10px;
|
||||||
|
background-color: #B1B1B1;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seekbar input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: transparent;
|
||||||
|
outline: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seekbar input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seekbar input[type="range"]::-moz-range-thumb {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seekbar input[type="range"]::-moz-range-track {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seekbar input[type="range"]::-moz-focus-outer {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ VideoElement <f> VideoElement Object
|
||||||
loop <b|false> loop playback
|
loop <b|false> loop playback
|
||||||
playbackRate <n|1> playback rate
|
playbackRate <n|1> playback rate
|
||||||
position <n|0> start position
|
position <n|0> start position
|
||||||
|
in <n|0> start offset
|
||||||
self <o> Shared private variable
|
self <o> Shared private variable
|
||||||
([options[, self]]) -> <o:Element> VideoElement Object
|
([options[, self]]) -> <o:Element> VideoElement Object
|
||||||
loadedmetadata <!> loadedmetadata
|
loadedmetadata <!> loadedmetadata
|
||||||
|
|
@ -38,6 +39,7 @@ window.VideoElement = function(options) {
|
||||||
muted: false,
|
muted: false,
|
||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
position: 0,
|
position: 0,
|
||||||
|
"in": 0,
|
||||||
volume: 1
|
volume: 1
|
||||||
}
|
}
|
||||||
Object.assign(self.options, options);
|
Object.assign(self.options, options);
|
||||||
|
|
@ -166,9 +168,10 @@ window.VideoElement = function(options) {
|
||||||
|
|
||||||
function getCurrentTime() {
|
function getCurrentTime() {
|
||||||
var item = self.items[self.currentItem];
|
var item = self.items[self.currentItem];
|
||||||
return self.seeking || self.loading
|
var currentTime = self.seeking || self.loading
|
||||||
? self.currentTime
|
? self.currentTime
|
||||||
: item ? item.position + self.video.currentTime - item['in'] : 0;
|
: item ? item.position + self.video.currentTime - item['in'] - self.options["in"] : 0;
|
||||||
|
return currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
function getset(key, value) {
|
function getset(key, value) {
|
||||||
|
|
@ -528,6 +531,10 @@ window.VideoElement = function(options) {
|
||||||
|
|
||||||
function setCurrentTime(time) {
|
function setCurrentTime(time) {
|
||||||
debug('Video', 'sCT', time);
|
debug('Video', 'sCT', time);
|
||||||
|
if (self.options["in"]) {
|
||||||
|
debug('Video', 'sCT shift time by', self.options["in"], 'to', time);
|
||||||
|
time += self.options["in"]
|
||||||
|
}
|
||||||
var currentTime, currentItem;
|
var currentTime, currentItem;
|
||||||
self.items.forEach(function(item, i) {
|
self.items.forEach(function(item, i) {
|
||||||
if (time >= item.position
|
if (time >= item.position
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ window.VideoPlayer = function(options) {
|
||||||
loop: false,
|
loop: false,
|
||||||
muted: false,
|
muted: false,
|
||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
|
"in": 0,
|
||||||
position: 0,
|
position: 0,
|
||||||
volume: 1
|
volume: 1
|
||||||
}
|
}
|
||||||
|
|
@ -153,9 +154,11 @@ window.VideoPlayer = function(options) {
|
||||||
${icon.mute}
|
${icon.mute}
|
||||||
</div>
|
</div>
|
||||||
<div class="position">
|
<div class="position">
|
||||||
<div class="bar">
|
<div class="seekbar">
|
||||||
<div class="progress"></div>
|
<input type="range" value="0" min='0' max='100' step='.25' />
|
||||||
|
<div class="seekbar-progress">
|
||||||
|
<div role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="38" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="time">
|
<div class="time">
|
||||||
|
|
@ -223,15 +226,31 @@ window.VideoPlayer = function(options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var showControls
|
var showControls
|
||||||
|
function hideControlsLater() {
|
||||||
|
if (showControls) {
|
||||||
|
clearTimeout(showControls)
|
||||||
|
}
|
||||||
|
showControls = setTimeout(() => {
|
||||||
|
if (touching) {
|
||||||
|
hideControlsLater()
|
||||||
|
} else {
|
||||||
|
self.controls.style.opacity = that.paused ? '1' : '0'
|
||||||
|
showControls = null
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
var toggleControls = event => {
|
var toggleControls = event => {
|
||||||
|
if (event.target.tagName == "INPUT") {
|
||||||
|
if (showControls) {
|
||||||
|
clearTimeout(showControls)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (self.controls.style.opacity == '0') {
|
if (self.controls.style.opacity == '0') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
self.controls.style.opacity = '1'
|
self.controls.style.opacity = '1'
|
||||||
showControls = setTimeout(() => {
|
hideControlsLater()
|
||||||
self.controls.style.opacity = that.paused ? '1' : '0'
|
|
||||||
showControls = null
|
|
||||||
}, 3000)
|
|
||||||
} else {
|
} else {
|
||||||
self.controls.style.opacity = '0'
|
self.controls.style.opacity = '0'
|
||||||
}
|
}
|
||||||
|
|
@ -241,10 +260,7 @@ window.VideoPlayer = function(options) {
|
||||||
clearTimeout(showControls)
|
clearTimeout(showControls)
|
||||||
}
|
}
|
||||||
self.controls.style.opacity = '1'
|
self.controls.style.opacity = '1'
|
||||||
showControls = setTimeout(() => {
|
hideControlsLater()
|
||||||
self.controls.style.opacity = that.paused ? '1' : '0'
|
|
||||||
showControls = null
|
|
||||||
}, 3000)
|
|
||||||
})
|
})
|
||||||
self.controls.addEventListener("mouseleave", event => {
|
self.controls.addEventListener("mouseleave", event => {
|
||||||
if (showControls) {
|
if (showControls) {
|
||||||
|
|
@ -253,7 +269,13 @@ window.VideoPlayer = function(options) {
|
||||||
self.controls.style.opacity = that.paused ? '1' : '0'
|
self.controls.style.opacity = that.paused ? '1' : '0'
|
||||||
showControls = null
|
showControls = null
|
||||||
})
|
})
|
||||||
|
self.controls.addEventListener("touchstart", event => {
|
||||||
|
touching = true
|
||||||
|
})
|
||||||
self.controls.addEventListener("touchstart", toggleControls)
|
self.controls.addEventListener("touchstart", toggleControls)
|
||||||
|
self.controls.addEventListener("touchend", event => {
|
||||||
|
touching = false
|
||||||
|
})
|
||||||
self.controls.querySelector('.toggle').addEventListener("click", toggleVideo)
|
self.controls.querySelector('.toggle').addEventListener("click", toggleVideo)
|
||||||
self.controls.querySelector('.volume').addEventListener("click", toggleSound)
|
self.controls.querySelector('.volume').addEventListener("click", toggleSound)
|
||||||
self.controls.querySelector('.fullscreen-btn').addEventListener("click", toggleFullscreen)
|
self.controls.querySelector('.fullscreen-btn').addEventListener("click", toggleFullscreen)
|
||||||
|
|
@ -310,6 +332,7 @@ window.VideoPlayer = function(options) {
|
||||||
that.append(unblock)
|
that.append(unblock)
|
||||||
})
|
})
|
||||||
var loading = true
|
var loading = true
|
||||||
|
var touching = false
|
||||||
that.brightness(0)
|
that.brightness(0)
|
||||||
that.addEventListener("loadedmetadata", event => {
|
that.addEventListener("loadedmetadata", event => {
|
||||||
//
|
//
|
||||||
|
|
@ -331,42 +354,40 @@ window.VideoPlayer = function(options) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
var time = that.querySelector('.controls .time div'),
|
var time = that.querySelector('.controls .time div');
|
||||||
progress = that.querySelector('.controls .position .progress')
|
const progressbar = that.querySelector('.seekbar div[role="progressbar"]');
|
||||||
that.querySelector('.controls .position').addEventListener("click", event => {
|
function setProgressPosition(value) {
|
||||||
var bar = event.target
|
progressbar.style.width = value + '%';
|
||||||
while (bar && !bar.classList.contains('bar')) {
|
progressbar.setAttribute('aria-valuenow', value);
|
||||||
bar = bar.parentElement
|
|
||||||
}
|
}
|
||||||
if (bar && bar.classList.contains('bar')) {
|
that.querySelector('.controls .position input').addEventListener('input', event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
var rect = bar.getBoundingClientRect()
|
setProgressPosition(event.target.value)
|
||||||
var x = event.clientX - rect.x
|
var position = event.target.value/100 * self.options.duration
|
||||||
var percent = x / rect.width
|
displayTime(position)
|
||||||
var position = percent * self.options.duration
|
|
||||||
if (self.options.position) {
|
|
||||||
position += self.options.position
|
|
||||||
}
|
|
||||||
progress.style.width = (100 * percent) + '%'
|
|
||||||
that.currentTime(position)
|
that.currentTime(position)
|
||||||
}
|
hideControlsLater()
|
||||||
})
|
})
|
||||||
that.addEventListener("timeupdate", event => {
|
function displayTime(currentTime) {
|
||||||
var currentTime = that.currentTime(),
|
duration = formatDuration(self.options.duration)
|
||||||
duration = self.options.duration
|
|
||||||
if (self.options.position) {
|
|
||||||
currentTime -= self.options.position
|
|
||||||
}
|
|
||||||
progress.style.width = (100 * currentTime / duration) + '%'
|
|
||||||
duration = formatDuration(duration)
|
|
||||||
currentTime = formatDuration(currentTime)
|
currentTime = formatDuration(currentTime)
|
||||||
while (duration && duration.startsWith('00:')) {
|
while (duration && duration.startsWith('00:')) {
|
||||||
duration = duration.slice(3)
|
duration = duration.slice(3)
|
||||||
}
|
}
|
||||||
currentTime = currentTime.slice(currentTime.length - duration.length)
|
currentTime = currentTime.slice(currentTime.length - duration.length)
|
||||||
time.innerText = `${currentTime} / ${duration}`
|
time.innerText = `${currentTime} / ${duration}`
|
||||||
|
}
|
||||||
|
|
||||||
|
that.addEventListener("timeupdate", event => {
|
||||||
|
var currentTime = that.currentTime(),
|
||||||
|
duration = self.options.duration
|
||||||
|
if (self.options.position) {
|
||||||
|
currentTime -= self.options.position
|
||||||
|
}
|
||||||
|
setProgressPosition(100 * currentTime / duration)
|
||||||
|
displayTime(currentTime)
|
||||||
})
|
})
|
||||||
|
|
||||||
that.addEventListener("play", event => {
|
that.addEventListener("play", event => {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,4 @@
|
||||||
|
|
||||||
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) {
|
const sortByKey = function(array, by) {
|
||||||
return array.sort(function(a, b) {
|
return array.sort(function(a, b) {
|
||||||
|
|
@ -122,7 +91,7 @@ async function loadEdit(id, args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.edit = response['data']
|
data.edit = response['data']
|
||||||
if (data.edit.status !== 'public') {
|
if (['public', 'featured'].indexOf(data.edit.status) == -1) {
|
||||||
return {
|
return {
|
||||||
site: data.site,
|
site: data.site,
|
||||||
error: {
|
error: {
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,10 @@ async function loadData(id, args) {
|
||||||
<span class="icon">${icon.down}</span>
|
<span class="icon">${icon.down}</span>
|
||||||
${layerData.title}
|
${layerData.title}
|
||||||
</h3>`)
|
</h3>`)
|
||||||
|
data.layers[layer] = sortBy(data.layers[layer], [
|
||||||
|
{key: "in", operator: "+"},
|
||||||
|
{key: "created", operator: "+"}
|
||||||
|
])
|
||||||
data.layers[layer].forEach(annotation => {
|
data.layers[layer].forEach(annotation => {
|
||||||
if (pandora.url) {
|
if (pandora.url) {
|
||||||
annotation.value = annotation.value.replace(
|
annotation.value = annotation.value.replace(
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ function parseURL() {
|
||||||
id = id.replace('/editor/', '/').replace('/player/', '/')
|
id = id.replace('/editor/', '/').replace('/player/', '/')
|
||||||
type = "item"
|
type = "item"
|
||||||
}
|
}
|
||||||
|
//console.log(type, id, args)
|
||||||
return [type, id, args]
|
return [type, id, args]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ function renderItem(data) {
|
||||||
var video = window.video = VideoPlayer({
|
var video = window.video = VideoPlayer({
|
||||||
items: data.videos,
|
items: data.videos,
|
||||||
poster: data.poster,
|
poster: data.poster,
|
||||||
position: data["in"] || 0,
|
"in": data["in"] || 0,
|
||||||
|
position: 0,
|
||||||
duration: data.duration,
|
duration: data.duration,
|
||||||
aspectratio: data.aspectratio
|
aspectratio: data.aspectratio
|
||||||
})
|
})
|
||||||
|
|
@ -85,16 +86,10 @@ function renderItem(data) {
|
||||||
video.addEventListener("loadedmetadata", event => {
|
video.addEventListener("loadedmetadata", event => {
|
||||||
//
|
//
|
||||||
})
|
})
|
||||||
video.addEventListener("timeupdate", event => {
|
|
||||||
var currentTime = video.currentTime()
|
function updateAnnotations(currentTime) {
|
||||||
if (currentTime >= data['out']) {
|
|
||||||
if (!video.paused) {
|
|
||||||
video.pause()
|
|
||||||
}
|
|
||||||
video.currentTime(data['in'])
|
|
||||||
}
|
|
||||||
div.querySelectorAll('.annotation').forEach(annot => {
|
div.querySelectorAll('.annotation').forEach(annot => {
|
||||||
var now = currentTime
|
var now = currentTime + (data["in"] || 0)
|
||||||
var start = parseFloat(annot.dataset.in)
|
var start = parseFloat(annot.dataset.in)
|
||||||
var end = parseFloat(annot.dataset.out)
|
var end = parseFloat(annot.dataset.out)
|
||||||
if (now >= start && now <= end) {
|
if (now >= start && now <= end) {
|
||||||
|
|
@ -107,8 +102,18 @@ function renderItem(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
video.addEventListener("timeupdate", event => {
|
||||||
|
var currentTime = video.currentTime()
|
||||||
|
if ((currentTime + (data["in"] || 0)) >= data['out']) {
|
||||||
|
if (!video.paused) {
|
||||||
|
video.pause()
|
||||||
|
}
|
||||||
|
video.currentTime(0)
|
||||||
|
}
|
||||||
|
updateAnnotations(currentTime)
|
||||||
})
|
})
|
||||||
|
updateAnnotations(data["position"] || 0)
|
||||||
if (item.next || item.previous) {
|
if (item.next || item.previous) {
|
||||||
var nav = document.createElement('nav')
|
var nav = document.createElement('nav')
|
||||||
nav.classList.add('items')
|
nav.classList.add('items')
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,10 @@ const clickLink = function(event) {
|
||||||
}
|
}
|
||||||
document.location.hash = '#' + link.slice(1)
|
document.location.hash = '#' + link.slice(1)
|
||||||
} else {
|
} else {
|
||||||
if (!link.startsWith('/m')) {
|
if (link.includes('/download/')) {
|
||||||
|
document.location.href = link
|
||||||
|
return
|
||||||
|
} else if (!link.startsWith('/m')) {
|
||||||
link = '/m' + link
|
link = '/m' + link
|
||||||
}
|
}
|
||||||
history.pushState({}, '', link);
|
history.pushState({}, '', link);
|
||||||
|
|
@ -161,3 +164,59 @@ const getVideoURL = function(id, resolution, part, track, streamId) {
|
||||||
return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId);
|
return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sortBy(array, by, map) {
|
||||||
|
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(
|
||||||
|
map && map[key] ? map[key](a[key], a) : a[key]
|
||||||
|
);
|
||||||
|
bValue = getSortValue(
|
||||||
|
map && map[key] ? map[key](b[key], b) : 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,10 @@ if __name__ == "__main__":
|
||||||
if old <= 6581:
|
if old <= 6581:
|
||||||
run('./bin/pip', 'install', '-U', 'pip')
|
run('./bin/pip', 'install', '-U', 'pip')
|
||||||
run('./bin/pip', 'install', '-r', 'requirements.txt')
|
run('./bin/pip', 'install', '-r', 'requirements.txt')
|
||||||
|
if old <= 6659:
|
||||||
|
run('./bin/pip', 'install', '-r', 'requirements.txt')
|
||||||
|
if old <= 6688:
|
||||||
|
run('./bin/pip', 'install', '-r', 'requirements.txt')
|
||||||
else:
|
else:
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
branch = get_branch()
|
branch = get_branch()
|
||||||
|
|
@ -322,6 +326,7 @@ if __name__ == "__main__":
|
||||||
current_branch = get_branch(path)
|
current_branch = get_branch(path)
|
||||||
revno = get_version(path)
|
revno = get_version(path)
|
||||||
if repo == 'pandora':
|
if repo == 'pandora':
|
||||||
|
print("Pandora Version pre update: ", revno)
|
||||||
pandora_old_revno = revno
|
pandora_old_revno = revno
|
||||||
current += revno
|
current += revno
|
||||||
if current_branch != branch:
|
if current_branch != branch:
|
||||||
|
|
@ -345,6 +350,7 @@ if __name__ == "__main__":
|
||||||
new += '+'
|
new += '+'
|
||||||
os.chdir(join(base, 'pandora'))
|
os.chdir(join(base, 'pandora'))
|
||||||
if pandora_old_revno != pandora_new_revno:
|
if pandora_old_revno != pandora_new_revno:
|
||||||
|
print("Pandora Version post update: ", pandora_new_revno)
|
||||||
os.chdir(base)
|
os.chdir(base)
|
||||||
run('./update.py', 'postupdate', pandora_old_revno, pandora_new_revno)
|
run('./update.py', 'postupdate', pandora_old_revno, pandora_new_revno)
|
||||||
os.chdir(join(base, 'pandora'))
|
os.chdir(join(base, 'pandora'))
|
||||||
|
|
@ -361,7 +367,7 @@ if __name__ == "__main__":
|
||||||
and row not in ['BEGIN;', 'COMMIT;']
|
and row not in ['BEGIN;', 'COMMIT;']
|
||||||
]
|
]
|
||||||
if diff:
|
if diff:
|
||||||
print('Database has changed, please make a backup and run %s db' % sys.argv[0])
|
print('Database has changed, please make a backup and run: sudo pandoractl update db')
|
||||||
elif branch != 'master':
|
elif branch != 'master':
|
||||||
print('pan.do/ra is at the latest release,\nyou can run "%s switch master" to switch to the development version' % sys.argv[0])
|
print('pan.do/ra is at the latest release,\nyou can run "%s switch master" to switch to the development version' % sys.argv[0])
|
||||||
elif current != new:
|
elif current != new:
|
||||||
|
|
|
||||||
|
|
@ -25,53 +25,20 @@ LXC=`grep -q lxc /proc/1/environ && echo 'yes' || echo 'no'`
|
||||||
if [ -e /etc/os-release ]; then
|
if [ -e /etc/os-release ]; then
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
fi
|
fi
|
||||||
if [ -z "$UBUNTU_CODENAME" ]; then
|
|
||||||
UBUNTU_CODENAME=bionic
|
|
||||||
fi
|
|
||||||
if [ "$VERSION_CODENAME" = "bullseye" ]; then
|
|
||||||
UBUNTU_CODENAME=focal
|
|
||||||
fi
|
|
||||||
if [ "$VERSION_CODENAME" = "bookworm" ]; then
|
|
||||||
UBUNTU_CODENAME=lunar
|
|
||||||
fi
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
echo "deb http://ppa.launchpad.net/j/pandora/ubuntu ${UBUNTU_CODENAME} main" > /etc/apt/sources.list.d/j-pandora.list
|
|
||||||
|
|
||||||
apt-get install -y gnupg
|
apt-get install -y gnupg curl
|
||||||
|
|
||||||
if [ -e /etc/apt/trusted.gpg.d ]; then
|
distribution=bookworm
|
||||||
gpg --dearmor > /etc/apt/trusted.gpg.d/j-pandora.gpg <<EOF
|
for version in bookworm trixie bionic focal jammy noble; do
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
if [ "$VERSION_CODENAME" = $version ]; then
|
||||||
Version: GnuPG v1
|
distribution=$VERSION_CODENAME
|
||||||
|
|
||||||
mI0ESXYhEgEEALl9jDTdmgpApPbjN+7b85dC92HisPUp56ifEkKJOBj0X5HhRqxs
|
|
||||||
Wjx/zlP4/XJGrHnxJyrdPxjSwAXz7bNdeggkN4JWdusTkr5GOXvggQnng0X7f/rX
|
|
||||||
oJwoEGtYOCODLPs6PC0qjh5yPzJVeiRsKUOZ7YVNnwNwdfS4D8RZvtCrABEBAAG0
|
|
||||||
FExhdW5jaHBhZCBQUEEgZm9yIGpeiLYEEwECACAFAkl2IRICGwMGCwkIBwMCBBUC
|
|
||||||
CAMEFgIDAQIeAQIXgAAKCRAohRM8AZde82FfA/9OB/64/YLaCpizHZ8f6DK3rGgF
|
|
||||||
e6mX3rFK8yOKGGL06316VhDzfzMiZSauUZ0t+lKHR/KZYeSaFwEoUoblTG/s4IIo
|
|
||||||
9aBMHWhVXJW6eifKUmTGqEn2/0UxoWQq2C3F6njMkCaP+ALOD5uzaSYGdjqAUAwS
|
|
||||||
pAAGSEQ4uz6bYSeM4Q==
|
|
||||||
=SM2a
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
apt-key add - <<EOF
|
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
Version: GnuPG v1
|
|
||||||
|
|
||||||
mI0ESXYhEgEEALl9jDTdmgpApPbjN+7b85dC92HisPUp56ifEkKJOBj0X5HhRqxs
|
|
||||||
Wjx/zlP4/XJGrHnxJyrdPxjSwAXz7bNdeggkN4JWdusTkr5GOXvggQnng0X7f/rX
|
|
||||||
oJwoEGtYOCODLPs6PC0qjh5yPzJVeiRsKUOZ7YVNnwNwdfS4D8RZvtCrABEBAAG0
|
|
||||||
FExhdW5jaHBhZCBQUEEgZm9yIGpeiLYEEwECACAFAkl2IRICGwMGCwkIBwMCBBUC
|
|
||||||
CAMEFgIDAQIeAQIXgAAKCRAohRM8AZde82FfA/9OB/64/YLaCpizHZ8f6DK3rGgF
|
|
||||||
e6mX3rFK8yOKGGL06316VhDzfzMiZSauUZ0t+lKHR/KZYeSaFwEoUoblTG/s4IIo
|
|
||||||
9aBMHWhVXJW6eifKUmTGqEn2/0UxoWQq2C3F6njMkCaP+ALOD5uzaSYGdjqAUAwS
|
|
||||||
pAAGSEQ4uz6bYSeM4Q==
|
|
||||||
=SM2a
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
EOF
|
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
curl -Ls https://code.0x2620.org/api/packages/0x2620/debian/repository.key -o /etc/apt/keyrings/pandora.asc
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/pandora.asc] https://code.0x2620.org/api/packages/0x2620/debian $distribution main" > /etc/apt/sources.list.d/pandora.list
|
||||||
|
|
||||||
echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/99languages
|
echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/99languages
|
||||||
|
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue