Compare commits

..

24 commits

Author SHA1 Message Date
j
dd8ea22d45 preview new timecode 2024-12-14 20:15:47 +00:00
j
85eba0bf09 round seek element 2024-12-14 19:46:20 +00:00
j
627a016515 better mobile seek bar 2024-12-14 19:16:41 +00:00
j
ad2af2a257 cleanup html 2024-12-14 19:01:17 +00:00
j
3bc2b1bd3e use preferred_username and fallback to name 2024-11-09 17:02:57 +00:00
j
5c6c7e37c7 remove debug 2024-11-09 16:59:47 +00:00
j
7cfe645ab7 oidc logout is same as our logout, no need for special case 2024-11-09 16:48:46 +00:00
j
ff236e8828 only add oidc urls if oidc is enabled 2024-11-08 12:47:47 +00:00
j
34af2b1fab add ocid based login 2024-11-08 12:29:35 +00:00
j
d83309d4cd ff 2024-11-07 16:28:18 +00:00
j
59c2045ac6 move first signup code into function for reuse 2024-11-07 15:57:16 +00:00
j
a24d96b098 log invalid api requests 2024-11-07 14:28:28 +00:00
j
03daede441 pass download 2024-10-18 17:49:54 +01:00
j
9e6ecb5459 avoid people accidentally adding itesm to current video 2024-10-18 15:57:10 +01:00
j
e7ede6ade0 fix sort 2024-10-13 17:20:33 +01:00
j
f4bfe9294b reuse getSortValue 2024-10-13 17:10:11 +01:00
j
c69ca372ee sort annotations 2024-10-13 17:06:15 +01:00
j
9e00dd09e3 allow adding global yt-dlp flags 2024-10-09 17:49:21 +01:00
j
7cc1504069 fix isClipsQuery check 2024-09-16 20:51:37 +01:00
j
d5ace7aeca use pandoractl to update db 2024-09-16 17:38:48 +01:00
j
1b1442e664 might not be loaded 2024-09-16 17:23:21 +01:00
j
18cbf0ec9c print pandora versions during update 2024-09-16 09:06:10 +01:00
j
a8aa619217 stop using ppa, use deb repository from code.0x2620.org instead 2024-09-11 23:01:57 +01:00
j
ff1c929d4d don't checkout oxtimelines twice 2024-09-11 15:05:21 +01:00
26 changed files with 413 additions and 198 deletions

3
ctl
View file

@ -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
View 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]

View file

@ -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())

View file

@ -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:

View file

@ -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:

View file

@ -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'
]

View file

@ -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>

View file

@ -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),

View file

@ -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]

View file

@ -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'))

View file

@ -20,3 +20,4 @@ future
pytz pytz
pypdfium2 pypdfium2
Pillow>=10 Pillow>=10
mozilla-django-oidc==4.0.1

View file

@ -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

View file

@ -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));

View file

@ -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();

View file

@ -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;
} }

View file

@ -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);

View file

@ -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: {

View file

@ -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...')}
] }, ] },

View file

@ -2650,6 +2650,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({

View file

@ -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;
}

View file

@ -153,9 +153,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 +225,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 +259,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 +268,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 +331,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 +353,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 => {

View file

@ -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) {

View file

@ -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(

View file

@ -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;
});
}

View file

@ -302,6 +302,8 @@ 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')
else: else:
if len(sys.argv) == 1: if len(sys.argv) == 1:
branch = get_branch() branch = get_branch()
@ -322,6 +324,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 +348,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 +365,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:

View file

@ -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