Compare commits
24 commits
Author | SHA1 | Date | |
---|---|---|---|
dd8ea22d45 | |||
85eba0bf09 | |||
627a016515 | |||
ad2af2a257 | |||
3bc2b1bd3e | |||
5c6c7e37c7 | |||
7cfe645ab7 | |||
ff236e8828 | |||
34af2b1fab | |||
d83309d4cd | |||
59c2045ac6 | |||
a24d96b098 | |||
03daede441 | |||
9e6ecb5459 | |||
e7ede6ade0 | |||
f4bfe9294b | |||
c69ca372ee | |||
9e00dd09e3 | |||
7cc1504069 | |||
d5ace7aeca | |||
1b1442e664 | |||
18cbf0ec9c | |||
a8aa619217 | |||
ff1c929d4d |
26 changed files with 413 additions and 198 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
|
||||
fi
|
||||
$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
|
||||
cd ${BASE}
|
||||
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:
|
||||
pass
|
||||
|
||||
config['site']['oidc'] = bool(getattr(settings, 'OIDC_RP_CLIENT_ID', False))
|
||||
response['data']['site'] = config
|
||||
response['data']['user'] = init_user(request.user, request)
|
||||
request.session['last_init'] = str(datetime.now())
|
||||
|
|
|
@ -40,8 +40,12 @@ info_key_map = {
|
|||
'display_id': 'id',
|
||||
}
|
||||
|
||||
YT_DLP = ['yt-dlp']
|
||||
if settings.YT_DLP_EXTRA:
|
||||
YT_DLP += settings.YT_DLP_EXTRA
|
||||
|
||||
def get_info(url, referer=None):
|
||||
cmd = ['yt-dlp', '-j', '--all-subs', url]
|
||||
cmd = YT_DLP + ['-j', '--all-subs', url]
|
||||
if referer:
|
||||
cmd += ['--referer', referer]
|
||||
p = subprocess.Popen(cmd,
|
||||
|
@ -93,7 +97,7 @@ def add_subtitles(item, media, tmp):
|
|||
sub.save()
|
||||
|
||||
def load_formats(url):
|
||||
cmd = ['yt-dlp', '-q', url, '-j', '-F']
|
||||
cmd = YT_DLP + ['-q', url, '-j', '-F']
|
||||
p = subprocess.Popen(cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, close_fds=True)
|
||||
|
@ -112,7 +116,7 @@ def download(item_id, url, referer=None):
|
|||
if isinstance(tmp, bytes):
|
||||
tmp = tmp.decode('utf-8')
|
||||
os.chdir(tmp)
|
||||
cmd = ['yt-dlp', '-q', media['url']]
|
||||
cmd = YT_DLP + ['-q', media['url']]
|
||||
if referer:
|
||||
cmd += ['--referer', referer]
|
||||
elif 'referer' in media:
|
||||
|
|
|
@ -34,6 +34,13 @@ def api(request):
|
|||
return response
|
||||
if request.META.get('CONTENT_TYPE') == 'application/json':
|
||||
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']
|
||||
data = r.get('data', {})
|
||||
else:
|
||||
|
|
|
@ -111,6 +111,7 @@ ROOT_URLCONF = 'urls'
|
|||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'mozilla_django_oidc',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
|
@ -158,6 +159,27 @@ INSTALLED_APPS = (
|
|||
)
|
||||
|
||||
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
|
||||
LOGGING = {
|
||||
|
@ -193,8 +215,6 @@ CACHES = {
|
|||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
|
||||
AUTH_PROFILE_MODULE = 'user.UserProfile'
|
||||
AUTH_CHECK_USERNAME = True
|
||||
FFMPEG = 'ffmpeg'
|
||||
FFPROBE = 'ffprobe'
|
||||
USE_VP9 = True
|
||||
|
@ -291,6 +311,8 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 32 * 1024 * 1024
|
|||
|
||||
EMPTY_CLIPS = True
|
||||
|
||||
YT_DLP_EXTRA = []
|
||||
|
||||
#you can ignore things below this line
|
||||
#=========================================================================
|
||||
LOCAL_APPS = []
|
||||
|
@ -321,3 +343,7 @@ except NameError:
|
|||
|
||||
INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS)
|
||||
|
||||
if OIDC_RP_CLIENT_ID:
|
||||
AUTHENTICATION_BACKENDS = list(AUTHENTICATION_BACKENDS) + [
|
||||
'app.oidc.OIDCAuthenticationBackend'
|
||||
]
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
{% endif %}
|
||||
<meta property="og:site_name" content="{{ settings.SITENAME }}"/>
|
||||
{% compress css file m %}
|
||||
<link rel="stylesheet" href="{% static 'mobile/css/reset.css' %}"></link>
|
||||
<link rel="stylesheet" href="{% static 'mobile/css/style.css' %}"></link>
|
||||
<link rel="stylesheet" href="{% static 'mobile/css/reset.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'mobile/css/style.css' %}">
|
||||
{% endcompress %}
|
||||
<meta name="google" value="notranslate"/>
|
||||
</head>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import os
|
||||
import importlib
|
||||
|
||||
from django.urls import path, re_path
|
||||
from django.urls import path, re_path, include
|
||||
from oxdjango.http import HttpFileResponse
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -33,9 +33,15 @@ import urlalias.views
|
|||
def serve_static_file(path, location, content_type):
|
||||
return HttpFileResponse(location, content_type=content_type)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
#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/upload/text/?$', text.views.upload),
|
||||
re_path(r'^api/upload/document/?$', document.views.upload),
|
||||
|
|
|
@ -3,6 +3,38 @@ from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
|
|||
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):
|
||||
if 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||
ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0]
|
||||
|
|
|
@ -28,7 +28,7 @@ from user.models import Group
|
|||
|
||||
from . import models
|
||||
from .decorators import capability_required_json
|
||||
from .utils import rename_user
|
||||
from .utils import rename_user, prepare_user
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -177,28 +177,10 @@ def signup(request, data):
|
|||
}
|
||||
})
|
||||
else:
|
||||
first_user = User.objects.count() == 0
|
||||
user = User(username=data['username'], email=data['email'])
|
||||
user.set_password(data['password'])
|
||||
#make first user admin
|
||||
user.is_superuser = first_user
|
||||
user.is_staff = first_user
|
||||
user.save()
|
||||
#create default user lists:
|
||||
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()
|
||||
prepare_user(user)
|
||||
if request.session.session_key:
|
||||
models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user)
|
||||
ui = json.loads(request.session.get('ui', 'null'))
|
||||
|
|
|
@ -20,3 +20,4 @@ future
|
|||
pytz
|
||||
pypdfium2
|
||||
Pillow>=10
|
||||
mozilla-django-oidc==4.0.1
|
||||
|
|
|
@ -106,6 +106,13 @@ pandora.ui.addFilesDialog = function(options) {
|
|||
});
|
||||
|
||||
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) {
|
||||
selectItems.push({
|
||||
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({
|
||||
items: selectItems,
|
||||
width: 256
|
||||
|
|
|
@ -100,6 +100,10 @@ pandora.ui.appPanel = function() {
|
|||
pandora.$ui.siteDialog = pandora.ui.siteDialog(page).open();
|
||||
}
|
||||
} else if (['signup', 'signin'].indexOf(page) > -1) {
|
||||
if (pandora.site.site.oidc) {
|
||||
pandora.oidcLogin()
|
||||
return
|
||||
}
|
||||
if (pandora.user.level == 'guest') {
|
||||
if (pandora.$ui.accountDialog && pandora.$ui.accountDialog.is(':visible')) {
|
||||
pandora.$ui.accountDialog.options(pandora.ui.accountDialogOptions(page));
|
||||
|
|
|
@ -424,18 +424,26 @@ pandora.ui.folders = function(section) {
|
|||
}).bindEvent({
|
||||
click: function() {
|
||||
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({
|
||||
click: function() {
|
||||
$dialog.close();
|
||||
pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open();
|
||||
}
|
||||
}),
|
||||
})
|
||||
],
|
||||
[
|
||||
Ox.Button({title: Ox._('Sign In...')}).bindEvent({
|
||||
click: function() {
|
||||
$dialog.close();
|
||||
if (pandora.site.site.oidc) {
|
||||
pandora.oidcLogin()
|
||||
} else {
|
||||
pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open();
|
||||
}
|
||||
}
|
||||
}),
|
||||
{},
|
||||
Ox.Button({title: Ox._('Not Now')}).bindEvent({
|
||||
|
@ -443,7 +451,8 @@ pandora.ui.folders = function(section) {
|
|||
$dialog.close();
|
||||
}
|
||||
})
|
||||
] : [
|
||||
]
|
||||
): [
|
||||
Ox.Button({title: Ox._('Close')}).bindEvent({
|
||||
click: function() {
|
||||
$dialog.close();
|
||||
|
|
|
@ -137,6 +137,7 @@ pandora.ui.helpDialog = function() {
|
|||
|
||||
that.select = function(id) {
|
||||
var img, $img;
|
||||
if ($text) {
|
||||
$text.html(text[id || 'help']).scrollTop(0);
|
||||
img = $text.find('img');
|
||||
if (img) {
|
||||
|
@ -155,6 +156,7 @@ pandora.ui.helpDialog = function() {
|
|||
textAlign: 'right',
|
||||
whiteSpace: 'nowrap'
|
||||
});
|
||||
}
|
||||
return that;
|
||||
}
|
||||
|
||||
|
|
|
@ -171,13 +171,13 @@ pandora.ui.home = function() {
|
|||
}),
|
||||
$signinButton = Ox.Button({
|
||||
title: Ox._('Sign In'),
|
||||
width: 74
|
||||
width: pandora.site.site.oidc ? 156 : 74
|
||||
})
|
||||
.css({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: '112px',
|
||||
right: '82px',
|
||||
right: pandora.site.site.oidc ? '164px' : '82px',
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
opacity: 0
|
||||
|
@ -248,7 +248,13 @@ pandora.ui.home = function() {
|
|||
adjustRatio();
|
||||
|
||||
if (pandora.user.level == 'guest') {
|
||||
if (pandora.site.site.oidc) {
|
||||
$signinButton.options({
|
||||
width: 156
|
||||
})
|
||||
} else {
|
||||
$signupButton.appendTo(that);
|
||||
}
|
||||
$signinButton.appendTo(that);
|
||||
} else {
|
||||
$preferencesButton.appendTo(that);
|
||||
|
|
|
@ -270,7 +270,7 @@ pandora.ui.list = function() {
|
|||
item: function(data, sort, size) {
|
||||
size = 128;
|
||||
var clipsQuery = pandora.getClipsQuery(),
|
||||
isClipsQuery = !!clipsQuery.conditions.length,
|
||||
isClipsQuery = clipsQuery.conditions.length > 1,
|
||||
ratio = ui.icons == 'posters'
|
||||
? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1,
|
||||
url = pandora.getMediaURL('/' + data.id + '/' + (
|
||||
|
@ -352,7 +352,7 @@ pandora.ui.list = function() {
|
|||
},
|
||||
items: function(data, callback) {
|
||||
var clipsQuery = pandora.getClipsQuery(),
|
||||
isClipsQuery = !!clipsQuery.conditions.length;
|
||||
isClipsQuery = clipsQuery.conditions.length > 1;
|
||||
pandora.api.find(Ox.extend(data, Ox.extend({
|
||||
query: ui.find
|
||||
}, isClipsQuery ? {clips: {
|
||||
|
|
|
@ -46,7 +46,9 @@ pandora.ui.mainMenu = function() {
|
|||
{ id: 'tasks', title: Ox._('Tasks...'), disabled: isGuest },
|
||||
{ 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...')}
|
||||
: { id: 'signout', title: Ox._('Sign Out...')}
|
||||
] },
|
||||
|
|
|
@ -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() {
|
||||
if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) {
|
||||
pandora.ui.licenseDialog().open().bindEvent({
|
||||
|
|
|
@ -201,3 +201,75 @@ ol li {
|
|||
width: 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;
|
||||
}
|
||||
|
|
|
@ -153,9 +153,11 @@ window.VideoPlayer = function(options) {
|
|||
${icon.mute}
|
||||
</div>
|
||||
<div class="position">
|
||||
<div class="bar">
|
||||
<div class="progress"></div>
|
||||
|
||||
<div class="seekbar">
|
||||
<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 class="time">
|
||||
|
@ -223,15 +225,31 @@ window.VideoPlayer = function(options) {
|
|||
}
|
||||
}
|
||||
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 => {
|
||||
if (event.target.tagName == "INPUT") {
|
||||
if (showControls) {
|
||||
clearTimeout(showControls)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (self.controls.style.opacity == '0') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
self.controls.style.opacity = '1'
|
||||
showControls = setTimeout(() => {
|
||||
self.controls.style.opacity = that.paused ? '1' : '0'
|
||||
showControls = null
|
||||
}, 3000)
|
||||
hideControlsLater()
|
||||
} else {
|
||||
self.controls.style.opacity = '0'
|
||||
}
|
||||
|
@ -241,10 +259,7 @@ window.VideoPlayer = function(options) {
|
|||
clearTimeout(showControls)
|
||||
}
|
||||
self.controls.style.opacity = '1'
|
||||
showControls = setTimeout(() => {
|
||||
self.controls.style.opacity = that.paused ? '1' : '0'
|
||||
showControls = null
|
||||
}, 3000)
|
||||
hideControlsLater()
|
||||
})
|
||||
self.controls.addEventListener("mouseleave", event => {
|
||||
if (showControls) {
|
||||
|
@ -253,7 +268,13 @@ window.VideoPlayer = function(options) {
|
|||
self.controls.style.opacity = that.paused ? '1' : '0'
|
||||
showControls = null
|
||||
})
|
||||
self.controls.addEventListener("touchstart", event => {
|
||||
touching = true
|
||||
})
|
||||
self.controls.addEventListener("touchstart", toggleControls)
|
||||
self.controls.addEventListener("touchend", event => {
|
||||
touching = false
|
||||
})
|
||||
self.controls.querySelector('.toggle').addEventListener("click", toggleVideo)
|
||||
self.controls.querySelector('.volume').addEventListener("click", toggleSound)
|
||||
self.controls.querySelector('.fullscreen-btn').addEventListener("click", toggleFullscreen)
|
||||
|
@ -310,6 +331,7 @@ window.VideoPlayer = function(options) {
|
|||
that.append(unblock)
|
||||
})
|
||||
var loading = true
|
||||
var touching = false
|
||||
that.brightness(0)
|
||||
that.addEventListener("loadedmetadata", event => {
|
||||
//
|
||||
|
@ -331,42 +353,40 @@ window.VideoPlayer = function(options) {
|
|||
}
|
||||
})
|
||||
|
||||
var time = that.querySelector('.controls .time div'),
|
||||
progress = that.querySelector('.controls .position .progress')
|
||||
that.querySelector('.controls .position').addEventListener("click", event => {
|
||||
var bar = event.target
|
||||
while (bar && !bar.classList.contains('bar')) {
|
||||
bar = bar.parentElement
|
||||
var time = that.querySelector('.controls .time div');
|
||||
const progressbar = that.querySelector('.seekbar div[role="progressbar"]');
|
||||
function setProgressPosition(value) {
|
||||
progressbar.style.width = value + '%';
|
||||
progressbar.setAttribute('aria-valuenow', value);
|
||||
|
||||
}
|
||||
if (bar && bar.classList.contains('bar')) {
|
||||
that.querySelector('.controls .position input').addEventListener('input', event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
var rect = bar.getBoundingClientRect()
|
||||
var x = event.clientX - rect.x
|
||||
var percent = x / rect.width
|
||||
var position = percent * self.options.duration
|
||||
if (self.options.position) {
|
||||
position += self.options.position
|
||||
}
|
||||
progress.style.width = (100 * percent) + '%'
|
||||
setProgressPosition(event.target.value)
|
||||
var position = event.target.value/100 * self.options.duration
|
||||
displayTime(position)
|
||||
that.currentTime(position)
|
||||
}
|
||||
hideControlsLater()
|
||||
})
|
||||
that.addEventListener("timeupdate", event => {
|
||||
var currentTime = that.currentTime(),
|
||||
duration = self.options.duration
|
||||
if (self.options.position) {
|
||||
currentTime -= self.options.position
|
||||
}
|
||||
progress.style.width = (100 * currentTime / duration) + '%'
|
||||
duration = formatDuration(duration)
|
||||
function displayTime(currentTime) {
|
||||
duration = formatDuration(self.options.duration)
|
||||
currentTime = formatDuration(currentTime)
|
||||
while (duration && duration.startsWith('00:')) {
|
||||
duration = duration.slice(3)
|
||||
}
|
||||
currentTime = currentTime.slice(currentTime.length - duration.length)
|
||||
time.innerText = `${currentTime} / ${duration}`
|
||||
}
|
||||
|
||||
that.addEventListener("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 => {
|
||||
|
|
|
@ -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) {
|
||||
return array.sort(function(a, b) {
|
||||
|
|
|
@ -129,6 +129,10 @@ async function loadData(id, args) {
|
|||
<span class="icon">${icon.down}</span>
|
||||
${layerData.title}
|
||||
</h3>`)
|
||||
data.layers[layer] = sortBy(data.layers[layer], [
|
||||
{key: "in", operator: "+"},
|
||||
{key: "created", operator: "+"}
|
||||
])
|
||||
data.layers[layer].forEach(annotation => {
|
||||
if (pandora.url) {
|
||||
annotation.value = annotation.value.replace(
|
||||
|
|
|
@ -125,7 +125,10 @@ const clickLink = function(event) {
|
|||
}
|
||||
document.location.hash = '#' + link.slice(1)
|
||||
} else {
|
||||
if (!link.startsWith('/m')) {
|
||||
if (link.includes('/download/')) {
|
||||
document.location.href = link
|
||||
return
|
||||
} else if (!link.startsWith('/m')) {
|
||||
link = '/m' + link
|
||||
}
|
||||
history.pushState({}, '', link);
|
||||
|
@ -161,3 +164,59 @@ const getVideoURL = function(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,8 @@ if __name__ == "__main__":
|
|||
if old <= 6581:
|
||||
run('./bin/pip', 'install', '-U', 'pip')
|
||||
run('./bin/pip', 'install', '-r', 'requirements.txt')
|
||||
if old <= 6659:
|
||||
run('./bin/pip', 'install', '-r', 'requirements.txt')
|
||||
else:
|
||||
if len(sys.argv) == 1:
|
||||
branch = get_branch()
|
||||
|
@ -322,6 +324,7 @@ if __name__ == "__main__":
|
|||
current_branch = get_branch(path)
|
||||
revno = get_version(path)
|
||||
if repo == 'pandora':
|
||||
print("Pandora Version pre update: ", revno)
|
||||
pandora_old_revno = revno
|
||||
current += revno
|
||||
if current_branch != branch:
|
||||
|
@ -345,6 +348,7 @@ if __name__ == "__main__":
|
|||
new += '+'
|
||||
os.chdir(join(base, 'pandora'))
|
||||
if pandora_old_revno != pandora_new_revno:
|
||||
print("Pandora Version post update: ", pandora_new_revno)
|
||||
os.chdir(base)
|
||||
run('./update.py', 'postupdate', pandora_old_revno, pandora_new_revno)
|
||||
os.chdir(join(base, 'pandora'))
|
||||
|
@ -361,7 +365,7 @@ if __name__ == "__main__":
|
|||
and row not in ['BEGIN;', 'COMMIT;']
|
||||
]
|
||||
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':
|
||||
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:
|
||||
|
|
|
@ -25,53 +25,20 @@ LXC=`grep -q lxc /proc/1/environ && echo 'yes' || echo 'no'`
|
|||
if [ -e /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
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
|
||||
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
|
||||
gpg --dearmor > /etc/apt/trusted.gpg.d/j-pandora.gpg <<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
|
||||
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
|
||||
distribution=bookworm
|
||||
for version in bookworm trixie bionic focal jammy noble; do
|
||||
if [ "$VERSION_CODENAME" = $version ]; then
|
||||
distribution=$VERSION_CODENAME
|
||||
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
|
||||
|
||||
apt-get update -qq
|
||||
|
|
Loading…
Reference in a new issue