diff --git a/.gitignore b/.gitignore index e3a95437..09047e5d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ pandora/gunicorn_config.py .DS_Store .env overlay/ +pandora/encoding.conf +pandora/tasks.conf diff --git a/Dockerfile b/Dockerfile index a1b287ee..24e6d63c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM 0x2620/pandora-base:latest +FROM code.0x2620.org/0x2620/pandora-base:latest LABEL maintainer="0x2620@0x2620.org" diff --git a/README.md b/README.md index 7801e180..d387011a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ We recommend to run pan.do/ra inside of LXD or LXC or dedicated VM or server. You will need at least 2GB of free disk space - pan.do/ra is known to work with Ubuntu 18.04, 20.04 and Debian/10 (buster), + pan.do/ra is known to work with Debian/12 (bookworm) and Ubuntu 20.04, other distributions might also work, let us know if it works for you. Use the following commands as root to install pan.do/ra and all dependencies: @@ -16,7 +16,7 @@ cd /root curl -sL https://pan.do/ra-install > pandora_install.sh chmod +x pandora_install.sh -export BRANCH=stable # change to 'master' to get current developement version +export BRANCH=master # change to 'stable' to get the latest release (sometimes outdated) ./pandora_install.sh 2>&1 | tee pandora_install.log ``` diff --git a/ctl b/ctl index b78183ba..816b843d 100755 --- a/ctl +++ b/ctl @@ -27,25 +27,33 @@ if [ "$action" = "init" ]; then $SUDO bin/python3 -m pip install -U --ignore-installed "pip<9" fi if [ ! -d static/oxjs ]; then - $SUDO git clone --depth 1 -b $branch https://git.0x2620.org/oxjs.git static/oxjs + $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 --depth 1 -b $branch https://git.0x2620.org/oxtimelines.git src/oxtimelines + $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 - $SUDO git clone --depth 1 -b $branch https://git.0x2620.org/${package}.git src/${package} + $SUDO git clone -b $branch https://code.0x2620.org/0x2620/${package}.git src/${package} fi cd ${BASE}/src/${package} - $SUDO ${BASE}/bin/python setup.py develop + + $SUDO ${BASE}/bin/pip install -e . + done cd ${BASE} $SUDO ./bin/pip install -r requirements.txt - if [ ! -e pandora/gunicorn_config.py ]; then - $SUDO cp pandora/gunicorn_config.py.in pandora/gunicorn_config.py - fi + for template in gunicorn_config.py encoding.conf tasks.conf; do + if [ ! -e pandora/$template ]; then + $SUDO cp pandora/${template}.in pandora/$template + fi + done + exit 0 +fi +if [ "$action" = "version" ]; then + git rev-list HEAD --count exit 0 fi @@ -73,10 +81,15 @@ if [ `whoami` != 'root' ]; then exit 1 fi if [ "$action" = "install" ]; then - cd "`dirname "$0"`" + cd "`dirname "$self"`" BASE=`pwd` if [ -x /bin/systemctl ]; then if [ -d /etc/systemd/system/ ]; then + for template in gunicorn_config.py encoding.conf tasks.conf; do + if [ ! -e pandora/$template ]; then + $SUDO cp pandora/${template}.in pandora/$template + fi + done for service in $SERVICES; do if [ -e /lib/systemd/system/${service}.service ]; then rm -f /lib/systemd/system/${service}.service \ diff --git a/docker-compose.yml b/docker-compose.yml index 1360247c..532a1fda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,6 @@ services: - "127.0.0.1:2620:80" networks: - backend - - default links: - pandora - websocketd @@ -28,7 +27,7 @@ services: restart: unless-stopped db: - image: postgres:latest + image: postgres:15 networks: - backend env_file: .env diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index 727891e8..199af642 100644 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:buster +FROM debian:12 LABEL maintainer="0x2620@0x2620.org" diff --git a/docker/base/install.sh b/docker/base/install.sh index 2d016eba..c344d74c 100755 --- a/docker/base/install.sh +++ b/docker/base/install.sh @@ -1,9 +1,17 @@ #!/bin/bash -UBUNTU_CODENAME=bionic 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 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/99languages @@ -44,7 +52,6 @@ apt-get install -y \ python3-numpy \ python3-psycopg2 \ python3-pyinotify \ - python3-simplejson \ python3-lxml \ python3-cssselect \ python3-html5lib \ @@ -53,7 +60,6 @@ apt-get install -y \ oxframe \ ffmpeg \ mkvtoolnix \ - gpac \ imagemagick \ poppler-utils \ ipython3 \ diff --git a/docker/build.sh b/docker/build.sh index dd661ba1..fc33b7c8 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -11,7 +11,7 @@ else proxy= fi -docker build $proxy -t 0x2620/pandora-base base -docker build -t 0x2620/pandora-nginx nginx +docker build $proxy -t code.0x2620.org/0x2620/pandora-base base +docker build -t code.0x2620.org/0x2620/pandora-nginx nginx cd .. -docker build -t 0x2620/pandora . +docker build -t code.0x2620.org/0x2620/pandora . diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 123883f8..4813b00a 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -6,7 +6,7 @@ user=pandora export LANG=en_US.UTF-8 mkdir -p /run/pandora -chown -R ${user}.${user} /run/pandora +chown -R ${user}:${user} /run/pandora update="/usr/bin/sudo -u $user -E -H /srv/pandora/update.py" @@ -32,7 +32,7 @@ if [ "$action" = "pandora" ]; then /srv/pandora/pandora/manage.py init_db $update db echo "Generating static files..." - chown -R ${user}.${user} /srv/pandora/ + chown -R ${user}:${user} /srv/pandora/ $update static touch /srv/pandora/initialized fi @@ -52,7 +52,7 @@ if [ "$action" = "encoding" ]; then -A app worker \ -Q encoding -n ${name} \ --pidfile /run/pandora/encoding.pid \ - --maxtasksperchild 500 \ + --max-tasks-per-child 500 \ -c 1 \ -l INFO fi @@ -66,7 +66,7 @@ if [ "$action" = "tasks" ]; then -A app worker \ -Q default,celery -n ${name} \ --pidfile /run/pandora/tasks.pid \ - --maxtasksperchild 1000 \ + --max-tasks-per-child 1000 \ -l INFO fi if [ "$action" = "cron" ]; then @@ -103,9 +103,9 @@ fi # pan.do/ra setup hooks if [ "$action" = "docker-compose.yml" ]; then cat /srv/pandora_base/docker-compose.yml | \ - sed "s#build: \.#image: 0x2620/pandora:latest#g" | \ + sed "s#build: \.#image: code.0x2620.org/0x2620/pandora:latest#g" | \ sed "s#\./overlay:#.:#g" | \ - sed "s#build: docker/nginx#image: 0x2620/pandora-nginx:latest#g" + sed "s#build: docker/nginx#image: code.0x2620.org/0x2620/pandora-nginx:latest#g" exit fi if [ "$action" = ".env" ]; then @@ -131,5 +131,5 @@ echo " docker run 0x2620/pandora setup | sh" echo echo adjust created files to match your needs and run: echo -echo " docker-compose up" +echo " docker compose up" echo diff --git a/docker/publish.sh b/docker/publish.sh index f4ef0193..2766a474 100644 --- a/docker/publish.sh +++ b/docker/publish.sh @@ -1,5 +1,5 @@ #!/bin/bash -# push new version of pan.do/ra to docker hub +# push new version of pan.do/ra to code.0x2620.org set -e cd /tmp @@ -7,6 +7,6 @@ git clone https://code.0x2620.org/0x2620/pandora cd pandora ./docker/build.sh -docker push 0x2620/pandora-base:latest -docker push 0x2620/pandora-nginx:latest -docker push 0x2620/pandora:latest +docker push code.0x2620.org/0x2620/pandora-base:latest +docker push code.0x2620.org/0x2620/pandora-nginx:latest +docker push code.0x2620.org/0x2620/pandora:latest diff --git a/docker/setup-docker-compose.sh b/docker/setup-docker-compose.sh index 2f48179f..fc25efde 100755 --- a/docker/setup-docker-compose.sh +++ b/docker/setup-docker-compose.sh @@ -1,18 +1,18 @@ #!/bin/sh -docker run 0x2620/pandora docker-compose.yml > docker-compose.yml +docker run --rm code.0x2620.org/0x2620/pandora docker-compose.yml > docker-compose.yml if [ ! -e .env ]; then - docker run 0x2620/pandora .env > .env + docker run --rm code.0x2620.org/0x2620/pandora .env > .env echo .env >> .gitignore fi if [ ! -e config.jsonc ]; then - docker run 0x2620/pandora config.jsonc > config.jsonc + docker run --rm code.0x2620.org/0x2620/pandora config.jsonc > config.jsonc fi cat > README.md << EOF pan.do/ra docker instance this folder was created with - docker run 0x2620/pandora setup | sh + docker run --rm code.0x2620.org/0x2620/pandora setup | sh To start pan.do/ra adjust the files in this folder: @@ -22,11 +22,14 @@ To start pan.do/ra adjust the files in this folder: and to get started run this: - docker-compose up -d + docker compose up -d To update pan.do/ra run: - docker-compose run pandora ctl update + docker compose run --rm pandora ctl update +To run pan.do/ra manage shell: + + docker compose run --rm pandora ctl manage shell EOF touch __init__.py diff --git a/etc/nginx/pandora b/etc/nginx/pandora index 419120aa..1c021368 100644 --- a/etc/nginx/pandora +++ b/etc/nginx/pandora @@ -17,6 +17,7 @@ server { #server_name pandora.YOURDOMAIN.COM; listen 80 default; + listen [::]:80 default; access_log /var/log/nginx/pandora.access.log; error_log /var/log/nginx/pandora.error.log; diff --git a/etc/sudoers.d/pandora b/etc/sudoers.d/pandora new file mode 100644 index 00000000..d05bab77 --- /dev/null +++ b/etc/sudoers.d/pandora @@ -0,0 +1 @@ +pandora ALL=(ALL:ALL) NOPASSWD:/usr/local/bin/pandoractl diff --git a/etc/systemd/system/pandora-cron.service b/etc/systemd/system/pandora-cron.service index 9f1ef157..7c6b0028 100644 --- a/etc/systemd/system/pandora-cron.service +++ b/etc/systemd/system/pandora-cron.service @@ -11,7 +11,7 @@ PIDFile=/run/pandora/cron.pid WorkingDirectory=/srv/pandora/pandora ExecStart=/srv/pandora/bin/celery \ -A app beat \ - -s /run/pandora/celerybeat-schedule \ + --scheduler django_celery_beat.schedulers:DatabaseScheduler \ --pidfile /run/pandora/cron.pid \ -l INFO ExecReload=/bin/kill -HUP $MAINPID diff --git a/etc/systemd/system/pandora-encoding.service b/etc/systemd/system/pandora-encoding.service index b2a48626..ee8725ce 100644 --- a/etc/systemd/system/pandora-encoding.service +++ b/etc/systemd/system/pandora-encoding.service @@ -7,14 +7,16 @@ Type=simple Restart=always User=pandora Group=pandora +EnvironmentFile=/srv/pandora/pandora/encoding.conf PIDFile=/run/pandora/encoding.pid WorkingDirectory=/srv/pandora/pandora ExecStart=/srv/pandora/bin/celery \ -A app worker \ -Q encoding -n pandora-encoding \ --pidfile /run/pandora/encoding.pid \ - --maxtasksperchild 500 \ - -l INFO + -c $CONCURRENCY \ + --max-tasks-per-child $MAX_TASKS_PER_CHILD \ + -l $LOGLEVEL ExecReload=/bin/kill -TERM $MAINPID [Install] diff --git a/etc/systemd/system/pandora-tasks.service b/etc/systemd/system/pandora-tasks.service index 19cf04af..d1d49420 100644 --- a/etc/systemd/system/pandora-tasks.service +++ b/etc/systemd/system/pandora-tasks.service @@ -7,14 +7,16 @@ Type=simple Restart=always User=pandora Group=pandora +EnvironmentFile=/srv/pandora/pandora/tasks.conf PIDFile=/run/pandora/tasks.pid WorkingDirectory=/srv/pandora/pandora ExecStart=/srv/pandora/bin/celery \ -A app worker \ -Q default,celery -n pandora-default \ --pidfile /run/pandora/tasks.pid \ - --maxtasksperchild 1000 \ - -l INFO + -c $CONCURRENCY \ + --max-tasks-per-child $MAX_TASKS_PER_CHILD \ + -l $LOGLEVEL ExecReload=/bin/kill -TERM $MAINPID [Install] diff --git a/pandora/annotation/apps.py b/pandora/annotation/apps.py new file mode 100644 index 00000000..8b0d41a8 --- /dev/null +++ b/pandora/annotation/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AnnotationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'annotation' diff --git a/pandora/annotation/management/commands/import_srt.py b/pandora/annotation/management/commands/import_srt.py index 881aa186..ef309f81 100644 --- a/pandora/annotation/management/commands/import_srt.py +++ b/pandora/annotation/management/commands/import_srt.py @@ -27,6 +27,7 @@ class Command(BaseCommand): parser.add_argument('username', help='username') parser.add_argument('item', help='item') parser.add_argument('layer', help='layer') + parser.add_argument('language', help='language', default="") parser.add_argument('filename', help='filename.srt') def handle(self, *args, **options): @@ -34,6 +35,7 @@ class Command(BaseCommand): public_id = options['item'] layer_id = options['layer'] filename = options['filename'] + language = options.get("language") user = User.objects.get(username=username) item = Item.objects.get(public_id=public_id) @@ -47,6 +49,9 @@ class Command(BaseCommand): for i in range(len(annotations)-1): if annotations[i]['out'] == annotations[i+1]['in']: annotations[i]['out'] = annotations[i]['out'] - 0.001 + if language: + for annotation in annotations: + annotation["value"] = '%s' % (language, annotation["value"]) tasks.add_annotations.delay({ 'item': item.public_id, 'layer': layer_id, diff --git a/pandora/annotation/migrations/0004_alter_annotation_findvalue_alter_annotation_id.py b/pandora/annotation/migrations/0004_alter_annotation_findvalue_alter_annotation_id.py new file mode 100644 index 00000000..b767efee --- /dev/null +++ b/pandora/annotation/migrations/0004_alter_annotation_findvalue_alter_annotation_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('annotation', '0003_auto_20160219_1537'), + ] + + operations = [ + migrations.AlterField( + model_name='annotation', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/annotation/models.py b/pandora/annotation/models.py index e1e9d553..9e31d184 100644 --- a/pandora/annotation/models.py +++ b/pandora/annotation/models.py @@ -163,28 +163,25 @@ class Annotation(models.Model): self.sortvalue = None self.languages = None - with transaction.atomic(): - if not self.clip or self.start != self.clip.start or self.end != self.clip.end: - self.clip, created = Clip.get_or_create(self.item, self.start, self.end) + if not self.clip or self.start != self.clip.start or self.end != self.clip.end: + self.clip, created = Clip.get_or_create(self.item, self.start, self.end) + with transaction.atomic(): if set_public_id: self.set_public_id() super(Annotation, self).save(*args, **kwargs) - if self.clip: - Clip.objects.filter(**{ - 'id': self.clip.id, - self.layer: False - }).update(**{self.layer: True}) - # update clip.findvalue - self.clip.save() + if self.clip: + self.clip.update_findvalue() + setattr(self.clip, self.layer, True) + self.clip.save(update_fields=[self.layer, 'sortvalue', 'findvalue']) - # update matches in bulk if called from load_subtitles - if not delay_matches: - self.update_matches() - self.update_documents() - self.update_translations() + # update matches in bulk if called from load_subtitles + if not delay_matches: + self.update_matches() + self.update_documents() + self.update_translations() def update_matches(self): from place.models import Place @@ -267,7 +264,10 @@ class Annotation(models.Model): from translation.models import Translation layer = self.get_layer() if layer.get('translate'): - Translation.objects.get_or_create(lang=lang, key=self.value, defaults={'type': Translation.CONTENT}) + for lang in settings.CONFIG['languages']: + if lang == settings.CONFIG['language']: + continue + Translation.objects.get_or_create(lang=lang, key=self.value, defaults={'type': Translation.CONTENT}) def delete(self, *args, **kwargs): with transaction.atomic(): diff --git a/pandora/annotation/tasks.py b/pandora/annotation/tasks.py index 8054c6e3..298695c3 100644 --- a/pandora/annotation/tasks.py +++ b/pandora/annotation/tasks.py @@ -5,12 +5,12 @@ from django.contrib.auth import get_user_model from django.db import transaction import ox -from celery.task import task +from app.celery import app from .models import Annotation -@task(ignore_results=False, queue='default') +@app.task(ignore_results=False, queue='default') def add_annotations(data): from item.models import Item from entity.models import Entity @@ -51,7 +51,7 @@ def add_annotations(data): annotation.item.update_facets() return True -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_item(id, force=False): from item.models import Item from clip.models import Clip @@ -72,7 +72,7 @@ def update_item(id, force=False): a.item.save() -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_annotations(layers, value): items = {} diff --git a/pandora/annotation/views.py b/pandora/annotation/views.py index 8bfe7006..7db159df 100644 --- a/pandora/annotation/views.py +++ b/pandora/annotation/views.py @@ -180,10 +180,10 @@ def addAnnotation(request, data): text='invalid data')) item = get_object_or_404_json(Item, public_id=data['item']) - + layer_id = data['layer'] layer = get_by_id(settings.CONFIG['layers'], layer_id) - if layer['canAddAnnotations'].get(request.user.profile.get_level()): + if layer['canAddAnnotations'].get(request.user.profile.get_level()) or item.editable(request.user): if layer['type'] == 'entity': try: value = Entity.get_by_name(ox.decode_html(data['value']), layer['entity']).get_id() @@ -241,8 +241,7 @@ def addAnnotations(request, data): layer_id = data['layer'] layer = get_by_id(settings.CONFIG['layers'], layer_id) - if item.editable(request.user) \ - and layer['canAddAnnotations'].get(request.user.profile.get_level()): + if item.editable(request.user): response = json_response() data['user'] = request.user.username t = add_annotations.delay(data) diff --git a/pandora/app/apps.py b/pandora/app/apps.py new file mode 100644 index 00000000..79293398 --- /dev/null +++ b/pandora/app/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'app' + diff --git a/pandora/app/celery.py b/pandora/app/celery.py index 710d0d0e..e8cf17fe 100644 --- a/pandora/app/celery.py +++ b/pandora/app/celery.py @@ -6,16 +6,8 @@ root_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) root_dir = os.path.dirname(root_dir) os.chdir(root_dir) -# set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') -app = Celery('pandora') - -# Using a string here means the worker doesn't have to serialize -# the configuration object to child processes. -# - namespace='CELERY' means all celery-related configuration keys -# should have a `CELERY_` prefix. +app = Celery('pandora', broker_connection_retry_on_startup=True) app.config_from_object('django.conf:settings', namespace='CELERY') - -# Load task modules from all registered Django app configs. app.autodiscover_tasks() diff --git a/pandora/app/config.py b/pandora/app/config.py index 3e96ac27..672c4a3b 100644 --- a/pandora/app/config.py +++ b/pandora/app/config.py @@ -133,7 +133,13 @@ def load_config(init=False): added = [] for key in sorted(d): if key not in c: - added.append("\"%s\": %s," % (key, json.dumps(d[key]))) + if key not in ( + 'hidden', + 'find', + 'findDocuments', + 'videoPoints', + ): + added.append("\"%s\": %s," % (key, json.dumps(d[key]))) c[key] = d[key] if added: sys.stderr.write("adding default %s:\n\t" % section) @@ -321,7 +327,11 @@ def update_static(): #locale for f in sorted(glob(os.path.join(settings.STATIC_ROOT, 'json/locale.pandora.*.json'))): with open(f) as fd: - locale = json.load(fd) + try: + locale = json.load(fd) + except: + print("failed to parse %s" % f) + raise site_locale = f.replace('locale.pandora', 'locale.' + settings.CONFIG['site']['id']) locale_file = f.replace('locale.pandora', 'locale') print('write', locale_file) @@ -365,13 +375,3 @@ def update_geoip(force=False): def init(): load_config(True) - -def shutdown(): - if settings.RELOADER_RUNNING: - RUN_RELOADER = False - settings.RELOADER_RUNNING = False - if NOTIFIER: - NOTIFIER.stop() - - - diff --git a/pandora/app/migrations/0002_alter_page_id_alter_settings_id.py b/pandora/app/migrations/0002_alter_page_id_alter_settings_id.py new file mode 100644 index 00000000..985d04c8 --- /dev/null +++ b/pandora/app/migrations/0002_alter_page_id_alter_settings_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='page', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='settings', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/app/tasks.py b/pandora/app/tasks.py index 03b26778..51a286e4 100644 --- a/pandora/app/tasks.py +++ b/pandora/app/tasks.py @@ -2,13 +2,16 @@ import datetime -from celery.task import periodic_task +from app.celery import app from celery.schedules import crontab - -@periodic_task(run_every=crontab(hour=6, minute=0), queue='encoding') +@app.task(queue='encoding') def cron(**kwargs): from django.db import transaction from django.contrib.sessions.models import Session Session.objects.filter(expire_date__lt=datetime.datetime.now()).delete() transaction.commit() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(crontab(hour=6, minute=0), cron.s()) diff --git a/pandora/app/views.py b/pandora/app/views.py index 5c76b8ec..2abd1f99 100644 --- a/pandora/app/views.py +++ b/pandora/app/views.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- - -import copy from datetime import datetime +import base64 +import copy from django.shortcuts import render, redirect from django.conf import settings @@ -53,17 +53,18 @@ def embed(request, id): }) def redirect_url(request, url): - if request.META['QUERY_STRING']: - url += "?" + request.META['QUERY_STRING'] - + try: + url = base64.decodebytes(url.encode()).decode() + except: + pass if settings.CONFIG['site'].get('sendReferrer', False): return redirect(url) else: - return HttpResponse(''%json.dumps(url)) + return HttpResponse('' % json.dumps(url)) def opensearch_xml(request): osd = ET.Element('OpenSearchDescription') - osd.attrib['xmlns']="http://a9.com/-/spec/opensearch/1.1/" + osd.attrib['xmlns'] = "http://a9.com/-/spec/opensearch/1.1/" e = ET.SubElement(osd, 'ShortName') e.text = settings.SITENAME e = ET.SubElement(osd, 'Description') @@ -162,7 +163,7 @@ def init(request, data): del config['keys'] if 'HTTP_ACCEPT_LANGUAGE' in request.META: - response['data']['locale'] = request.META['HTTP_ACCEPT_LANGUAGE'].split(';')[0].split('-')[0] + response['data']['locale'] = request.META['HTTP_ACCEPT_LANGUAGE'].split(';')[0].split('-')[0].split(',')[0] if request.META.get('HTTP_X_PREFIX') == 'NO': config['site']['videoprefix'] = '' @@ -245,7 +246,7 @@ def getEmbedDefaults(request, data): i = qs[0].cache response['data']['item'] = i['id'] response['data']['itemDuration'] = i['duration'] - response['data']['itemRatio'] = i['videoRatio'] + response['data']['itemRatio'] = i.get('videoRatio', settings.CONFIG['video']['previewRatio']) qs = List.objects.exclude(status='private').order_by('name') if qs.exists(): i = qs[0].json() diff --git a/pandora/archive/apps.py b/pandora/archive/apps.py new file mode 100644 index 00000000..1679d490 --- /dev/null +++ b/pandora/archive/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ArchiveConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'archive' + diff --git a/pandora/archive/external.py b/pandora/archive/external.py index cb0e7ef1..5f4ac3fd 100644 --- a/pandora/archive/external.py +++ b/pandora/archive/external.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- import json -import subprocess -import shutil -import tempfile +import logging import os +import shutil +import subprocess +import tempfile import ox from django.conf import settings @@ -14,6 +15,9 @@ from item.tasks import load_subtitles from . import models +logger = logging.getLogger('pandora.' + __name__) + + info_keys = [ 'title', 'description', @@ -37,7 +41,7 @@ info_key_map = { } def get_info(url, referer=None): - cmd = ['youtube-dl', '-j', '--all-subs', url] + cmd = ['yt-dlp', '-j', '--all-subs', url] if referer: cmd += ['--referer', referer] p = subprocess.Popen(cmd, @@ -88,6 +92,15 @@ def add_subtitles(item, media, tmp): sub.selected = True sub.save() +def load_formats(url): + cmd = ['yt-dlp', '-q', url, '-j', '-F'] + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + formats = stdout.decode().strip().split('\n')[-1] + return json.loads(formats) + def download(item_id, url, referer=None): item = Item.objects.get(public_id=item_id) info = get_info(url, referer) @@ -99,18 +112,19 @@ def download(item_id, url, referer=None): if isinstance(tmp, bytes): tmp = tmp.decode('utf-8') os.chdir(tmp) - cmd = ['youtube-dl', '-q', media['url']] + cmd = ['yt-dlp', '-q', media['url']] if referer: cmd += ['--referer', referer] elif 'referer' in media: cmd += ['--referer', media['referer']] + cmd += ['-o', '%(title)80s.%(ext)s'] if settings.CONFIG['video'].get('reuseUpload', False): max_resolution = max(settings.CONFIG['video']['resolutions']) format = settings.CONFIG['video']['formats'][0] if format == 'mp4': cmd += [ - '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio', + '-f', 'bestvideo[height<=%s][ext=mp4]+bestaudio[ext=m4a]' % max_resolution, '--merge-output-format', 'mp4' ] elif format == 'webm': @@ -120,6 +134,50 @@ def download(item_id, url, referer=None): stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) stdout, stderr = p.communicate() + if stderr and b'Requested format is not available.' in stderr: + formats = load_formats(url) + has_audio = bool([fmt for fmt in formats['formats'] if fmt['resolution'] == 'audio only']) + has_video = bool([fmt for fmt in formats['formats'] if 'x' in fmt['resolution']]) + + cmd = [ + 'yt-dlp', '-q', url, + '-o', '%(title)80s.%(ext)s' + ] + if referer: + cmd += ['--referer', referer] + elif 'referer' in media: + cmd += ['--referer', media['referer']] + if has_video and not has_audio: + cmd += [ + '-f', 'bestvideo[height<=%s][ext=mp4]' % max_resolution, + ] + elif not has_video and has_audio: + cmd += [ + 'bestaudio[ext=m4a]' + ] + else: + cmd = [] + if cmd: + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + if stderr and b'Requested format is not available.' in stderr: + cmd = [ + 'yt-dlp', '-q', url, + '-o', '%(title)80s.%(ext)s' + ] + if referer: + cmd += ['--referer', referer] + elif 'referer' in media: + cmd += ['--referer', media['referer']] + if cmd: + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + if stdout or stderr: + logger.error("import failed:\n%s\n%s\n%s", " ".join(cmd), stdout.decode(), stderr.decode()) parts = list(os.listdir(tmp)) if parts: part = 1 @@ -147,6 +205,7 @@ def download(item_id, url, referer=None): f.extract_stream() status = True else: + logger.error("failed to import %s file already exists %s", url, oshash) status = 'file exists' if len(parts) == 1: add_subtitles(f.item, media, tmp) diff --git a/pandora/archive/extract.py b/pandora/archive/extract.py index ebfd2ae7..3d6c74fe 100644 --- a/pandora/archive/extract.py +++ b/pandora/archive/extract.py @@ -1,26 +1,30 @@ # -*- coding: utf-8 -*- -import os +from distutils.spawn import find_executable +from glob import glob from os.path import exists import fractions +import logging import math +import os import re import shutil import subprocess import tempfile import time -from distutils.spawn import find_executable -from glob import glob import numpy as np import ox import ox.image from ox.utils import json from django.conf import settings -from PIL import Image +from PIL import Image, ImageOps from .chop import Chop, make_keyframe_index + +logger = logging.getLogger('pandora.' + __name__) + img_extension = 'jpg' MAX_DISTANCE = math.sqrt(3 * pow(255, 2)) @@ -57,14 +61,15 @@ def supported_formats(): stdout = stdout.decode('utf-8') stderr = stderr.decode('utf-8') version = stderr.split('\n')[0].split(' ')[2] + mp4 = 'libx264' in stdout and bool(re.compile('DEA.L. aac').findall(stdout)) return { 'version': version.split('.'), 'ogg': 'libtheora' in stdout and 'libvorbis' in stdout, 'webm': 'libvpx' in stdout and 'libvorbis' in stdout, 'vp8': 'libvpx' in stdout and 'libvorbis' in stdout, 'vp9': 'libvpx-vp9' in stdout and 'libopus' in stdout, - 'mp4': 'libx264' in stdout and bool(re.compile('DEA.L. aac').findall(stdout)), - 'h264': 'libx264' in stdout and bool(re.compile('DEA.L. aac').findall(stdout)), + 'mp4': mp4, + 'h264': mp4, } def stream(video, target, profile, info, audio_track=0, flags={}): @@ -155,7 +160,7 @@ def stream(video, target, profile, info, audio_track=0, flags={}): else: height = 96 - if settings.FFMPEG_SUPPORTS_VP9: + if settings.USE_VP9 and settings.FFMPEG_SUPPORTS_VP9: audio_codec = 'libopus' video_codec = 'libvpx-vp9' @@ -218,7 +223,7 @@ def stream(video, target, profile, info, audio_track=0, flags={}): bitrate = height*width*fps*bpp/1000 video_settings = trim + [ - '-vb', '%dk' % bitrate, + '-b:v', '%dk' % bitrate, '-aspect', aspect, # '-vf', 'yadif', '-max_muxing_queue_size', '512', @@ -246,6 +251,8 @@ def stream(video, target, profile, info, audio_track=0, flags={}): '-level', '4.0', '-pix_fmt', 'yuv420p', ] + if info['video'][0].get("force_framerate"): + video_settings += ['-r:v', str(fps)] video_settings += ['-map', '0:%s,0:0' % info['video'][0]['id']] audio_only = False else: @@ -285,7 +292,7 @@ def stream(video, target, profile, info, audio_track=0, flags={}): ac = min(ac, audiochannels) audio_settings += ['-ac', str(ac)] if audiobitrate: - audio_settings += ['-ab', audiobitrate] + audio_settings += ['-b:a', audiobitrate] if format == 'mp4': audio_settings += ['-c:a', 'aac', '-strict', '-2'] elif audio_codec == 'libopus': @@ -318,14 +325,15 @@ def stream(video, target, profile, info, audio_track=0, flags={}): pass1_post = post[:] pass1_post[-1] = '/dev/null' if format == 'webm': - pass1_post = ['-speed', '4'] + pass1_post + if video_codec != 'libvpx-vp9': + pass1_post = ['-speed', '4'] + pass1_post post = ['-speed', '1'] + post - cmds.append(base + ['-an', '-pass', '1', '-passlogfile', '%s.log' % target] - + video_settings + pass1_post) + cmds.append(base + ['-pass', '1', '-passlogfile', '%s.log' % target] + + video_settings + ['-an'] + pass1_post) cmds.append(base + ['-pass', '2', '-passlogfile', '%s.log' % target] - + audio_settings + video_settings + post) + + video_settings + audio_settings + post) else: - cmds.append(base + audio_settings + video_settings + post) + cmds.append(base + video_settings + audio_settings + post) if settings.FFMPEG_DEBUG: print('\n'.join([' '.join(cmd) for cmd in cmds])) @@ -433,10 +441,15 @@ def frame_direct(video, target, position): r = run_command(cmd) return r == 0 +def open_image_rgb(image_source): + source = Image.open(image_source) + source = ImageOps.exif_transpose(source) + source = source.convert('RGB') + return source def resize_image(image_source, image_output, width=None, size=None): if exists(image_source): - source = Image.open(image_source).convert('RGB') + source = open_image_rgb(image_source) source_width = source.size[0] source_height = source.size[1] if size: @@ -457,7 +470,7 @@ def resize_image(image_source, image_output, width=None, size=None): height = max(height, 1) if width < source_width: - resize_method = Image.ANTIALIAS + resize_method = Image.LANCZOS else: resize_method = Image.BICUBIC output = source.resize((width, height), resize_method) @@ -471,7 +484,7 @@ def timeline(video, prefix, modes=None, size=None): size = [64, 16] if isinstance(video, str): video = [video] - cmd = ['../bin/oxtimelines', + cmd = [os.path.normpath(os.path.join(settings.BASE_DIR, '../bin/oxtimelines')), '-s', ','.join(map(str, reversed(sorted(size)))), '-m', ','.join(modes), '-o', prefix, @@ -603,7 +616,7 @@ def timeline_strip(item, cuts, info, prefix): print(frame, 'cut', c, 'frame', s, frame, 'width', widths[s], box) # FIXME: why does this have to be frame+1? frame_image = Image.open(item.frame((frame+1)/fps)) - frame_image = frame_image.crop(box).resize((widths[s], timeline_height), Image.ANTIALIAS) + frame_image = frame_image.crop(box).resize((widths[s], timeline_height), Image.LANCZOS) for x_ in range(widths[s]): line_image.append(frame_image.crop((x_, 0, x_ + 1, timeline_height))) frame += widths[s] @@ -731,19 +744,24 @@ def remux_stream(src, dst): cmd = [ settings.FFMPEG, '-nostats', '-loglevel', 'error', - '-map_metadata', '-1', '-sn', '-i', src, + '-map_metadata', '-1', '-sn', ] + video + [ ] + audio + [ '-movflags', '+faststart', dst ] + print(cmd) p = subprocess.Popen(cmd, stdin=subprocess.PIPE, - stdout=open('/dev/null', 'w'), - stderr=open('/dev/null', 'w'), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, close_fds=True) - p.wait() - return True, None + stdout, stderr = p.communicate() + if stderr: + logger.error("failed to remux %s %s", cmd, stderr) + return False, stderr + else: + return True, None def ffprobe(path, *args): diff --git a/pandora/archive/migrations/0006_alter_file_extension_alter_file_id_alter_file_info_and_more.py b/pandora/archive/migrations/0006_alter_file_extension_alter_file_id_alter_file_info_and_more.py new file mode 100644 index 00000000..a890a3b9 --- /dev/null +++ b/pandora/archive/migrations/0006_alter_file_extension_alter_file_id_alter_file_info_and_more.py @@ -0,0 +1,100 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0005_auto_20180804_1554'), + ] + + operations = [ + migrations.AlterField( + model_name='file', + name='extension', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='file', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='file', + name='info', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='file', + name='language', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='file', + name='part', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='file', + name='part_title', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='file', + name='path', + field=models.CharField(default='', max_length=2048), + ), + migrations.AlterField( + model_name='file', + name='sort_path', + field=models.CharField(default='', max_length=2048), + ), + migrations.AlterField( + model_name='file', + name='type', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='file', + name='version', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='frame', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='instance', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='stream', + name='error', + field=models.TextField(blank=True, default=''), + ), + migrations.AlterField( + model_name='stream', + name='format', + field=models.CharField(default='webm', max_length=255), + ), + migrations.AlterField( + model_name='stream', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='stream', + name='info', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='volume', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/archive/migrations/0007_stream_archive_str_file_id_69a542_idx.py b/pandora/archive/migrations/0007_stream_archive_str_file_id_69a542_idx.py new file mode 100644 index 00000000..4a8bb7e2 --- /dev/null +++ b/pandora/archive/migrations/0007_stream_archive_str_file_id_69a542_idx.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.3 on 2023-08-18 12:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0006_alter_file_extension_alter_file_id_alter_file_info_and_more'), + ] + + operations = [ + migrations.AddIndex( + model_name='stream', + index=models.Index(fields=['file', 'source', 'available'], name='archive_str_file_id_69a542_idx'), + ), + ] diff --git a/pandora/archive/models.py b/pandora/archive/models.py index 56080815..a5800d3d 100644 --- a/pandora/archive/models.py +++ b/pandora/archive/models.py @@ -151,8 +151,10 @@ class File(models.Model): self.sampleate = 0 self.channels = 0 - if self.framerate: + if self.framerate and self.duration > 0: self.pixels = int(self.width * self.height * float(utils.parse_decimal(self.framerate)) * self.duration) + else: + self.pixels = 0 def get_path_info(self): data = {} @@ -181,6 +183,13 @@ class File(models.Model): for type in ox.movie.EXTENSIONS: if data['extension'] in ox.movie.EXTENSIONS[type]: data['type'] = type + if data['extension'] == 'ogg' and self.info.get('video'): + data['type'] = 'video' + if data['type'] == 'unknown': + if self.info.get('video'): + data['type'] = 'video' + elif self.info.get('audio'): + data['type'] = 'audio' if 'part' in data and isinstance(data['part'], int): data['part'] = str(data['part']) return data @@ -268,7 +277,7 @@ class File(models.Model): if self.type not in ('audio', 'video'): self.duration = None - else: + elif self.id: duration = sum([s.info.get('duration', 0) for s in self.streams.filter(source=None)]) if duration: @@ -276,7 +285,7 @@ class File(models.Model): if self.is_subtitle: self.available = self.data and True or False - else: + elif self.id: self.available = not self.uploading and \ self.streams.filter(source=None, available=True).count() super(File, self).save(*args, **kwargs) @@ -365,8 +374,8 @@ class File(models.Model): self.info.update(stream.info) self.parse_info() self.save() - if stream.info.get('video'): - extract.make_keyframe_index(stream.media.path) + #if stream.info.get('video'): + # extract.make_keyframe_index(stream.media.path) return True, stream.media.size return save_chunk(stream, stream.media, chunk, offset, name, done_cb) return False, 0 @@ -395,7 +404,7 @@ class File(models.Model): config = settings.CONFIG['video'] height = self.info['video'][0]['height'] if self.info.get('video') else None max_resolution = max(config['resolutions']) - if height <= max_resolution and self.extension in ('mov', 'mkv', 'mp4', 'm4v'): + if height and height <= max_resolution and self.extension in ('mov', 'mkv', 'mp4', 'm4v'): vcodec = self.get_codec('video') acodec = self.get_codec('audio') if vcodec in self.MP4_VCODECS and acodec in self.MP4_ACODECS: @@ -406,7 +415,7 @@ class File(models.Model): config = settings.CONFIG['video'] height = self.info['video'][0]['height'] if self.info.get('video') else None max_resolution = max(config['resolutions']) - if height <= max_resolution and config['formats'][0] == self.extension: + if height and height <= max_resolution and config['formats'][0] == self.extension: vcodec = self.get_codec('video') acodec = self.get_codec('audio') if self.extension in ['mp4', 'm4v'] and vcodec in self.MP4_VCODECS and acodec in self.MP4_ACODECS: @@ -725,6 +734,9 @@ class Stream(models.Model): class Meta: unique_together = ("file", "resolution", "format") + indexes = [ + models.Index(fields=['file', 'source', 'available']) + ] file = models.ForeignKey(File, related_name='streams', on_delete=models.CASCADE) resolution = models.IntegerField(default=96) @@ -804,9 +816,15 @@ class Stream(models.Model): shutil.move(self.file.data.path, target) self.file.data.name = '' self.file.save() + self.available = True + self.save() + done = True elif self.file.can_remux(): ok, error = extract.remux_stream(media, target) - done = True + if ok: + self.available = True + self.save() + done = True if not done: ok, error = extract.stream(media, target, self.name(), info, flags=self.flags) @@ -814,7 +832,7 @@ class Stream(models.Model): # get current version from db and update try: self.refresh_from_db() - except archive.models.DoesNotExist: + except Stream.DoesNotExist: pass else: self.update_status(ok, error) diff --git a/pandora/archive/queue.py b/pandora/archive/queue.py index 8a8ff4ce..7cb768b2 100644 --- a/pandora/archive/queue.py +++ b/pandora/archive/queue.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- from datetime import datetime -from time import time - -import celery.task.control -import kombu.five +from time import time, monotonic +from app.celery import app from .models import File @@ -18,7 +16,7 @@ def parse_job(job): 'file': f.oshash } if job['time_start']: - start_time = datetime.fromtimestamp(time() - (kombu.five.monotonic() - job['time_start'])) + start_time = datetime.fromtimestamp(time() - (monotonic() - job['time_start'])) r.update({ 'started': start_time, 'running': (datetime.now() - start_time).total_seconds() @@ -30,7 +28,7 @@ def parse_job(job): def status(): status = [] encoding_jobs = ('archive.tasks.extract_stream', 'archive.tasks.process_stream') - c = celery.task.control.inspect() + c = app.control.inspect() for job in c.active(safe=True).get('celery@pandora-encoding', []): if job['name'] in encoding_jobs: status.append(parse_job(job)) @@ -67,7 +65,7 @@ def fill_queue(): def get_celery_worker_status(): ERROR_KEY = "ERROR" try: - insp = celery.task.control.inspect() + insp = app.control.inspect() d = insp.stats() if not d: d = {ERROR_KEY: 'No running Celery workers were found.'} diff --git a/pandora/archive/tasks.py b/pandora/archive/tasks.py index a2aa3d8e..9e061b38 100644 --- a/pandora/archive/tasks.py +++ b/pandora/archive/tasks.py @@ -2,13 +2,14 @@ from glob import glob -from celery.task import task from django.conf import settings +from django.db import transaction from django.db.models import Q from item.models import Item from item.tasks import update_poster, update_timeline from taskqueue.models import Task +from app.celery import app from . import models from . import extract @@ -68,7 +69,7 @@ def update_or_create_instance(volume, f): instance.file.item.update_wanted() return instance -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_files(user, volume, files): user = models.User.objects.get(username=user) volume, created = models.Volume.objects.get_or_create(user=user, name=volume) @@ -100,7 +101,7 @@ def update_files(user, volume, files): Task.start(i, user) update_timeline.delay(i.public_id) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_info(user, info): user = models.User.objects.get(username=user) files = models.File.objects.filter(oshash__in=list(info)) @@ -114,7 +115,7 @@ def update_info(user, info): Task.start(i, user) update_timeline.delay(i.public_id) -@task(queue="encoding") +@app.task(queue="encoding") def process_stream(fileId): ''' process uploaded stream @@ -140,7 +141,7 @@ def process_stream(fileId): Task.finish(file.item) return True -@task(queue="encoding") +@app.task(queue="encoding") def extract_stream(fileId): ''' extract stream from direct upload @@ -169,7 +170,7 @@ def extract_stream(fileId): models.File.objects.filter(id=fileId).update(encoding=False) Task.finish(file.item) -@task(queue="encoding") +@app.task(queue="encoding") def extract_derivatives(fileId, rebuild=False): file = models.File.objects.get(id=fileId) streams = file.streams.filter(source=None) @@ -177,7 +178,7 @@ def extract_derivatives(fileId, rebuild=False): streams[0].extract_derivatives(rebuild) return True -@task(queue="encoding") +@app.task(queue="encoding") def update_stream(id): s = models.Stream.objects.get(pk=id) if not glob("%s*" % s.timeline_prefix): @@ -199,11 +200,11 @@ def update_stream(id): c.update_calculated_values() c.save() -@task(queue="encoding") +@app.task(queue="encoding") def download_media(item_id, url, referer=None): return external.download(item_id, url, referer) -@task(queue='default') +@app.task(queue='default') def move_media(data, user): from changelog.models import add_changelog from item.models import get_item, Item, ItemSort @@ -248,7 +249,8 @@ def move_media(data, user): if old_item and old_item.files.count() == 0 and i.files.count() == len(data['ids']): for a in old_item.annotations.all().order_by('id'): a.item = i - a.set_public_id() + with transaction.atomic(): + a.set_public_id() Annotation.objects.filter(id=a.id).update(item=i, public_id=a.public_id) old_item.clips.all().update(item=i, sort=i.sort) diff --git a/pandora/archive/views.py b/pandora/archive/views.py index 308e7c10..615a2db8 100644 --- a/pandora/archive/views.py +++ b/pandora/archive/views.py @@ -103,7 +103,7 @@ def update(request, data): file__available=False, file__wanted=True)] - if list(filter(lambda l: l['id'] == 'subtitles', settings.CONFIG['layers'])): + if utils.get_by_key(settings.CONFIG['layers'], 'isSubtitles', True): qs = files.filter( file__is_subtitle=True, file__available=False diff --git a/pandora/changelog/apps.py b/pandora/changelog/apps.py new file mode 100644 index 00000000..554ba51d --- /dev/null +++ b/pandora/changelog/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ChangelogConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'changelog' + diff --git a/pandora/changelog/migrations/0003_alter_changelog_id_alter_changelog_value_and_more.py b/pandora/changelog/migrations/0003_alter_changelog_id_alter_changelog_value_and_more.py new file mode 100644 index 00000000..e8aacfaa --- /dev/null +++ b/pandora/changelog/migrations/0003_alter_changelog_id_alter_changelog_value_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('changelog', '0002_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='changelog', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='changelog', + name='value', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='log', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='log', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/clip/apps.py b/pandora/clip/apps.py new file mode 100644 index 00000000..c957657f --- /dev/null +++ b/pandora/clip/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ClipConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'clip' + diff --git a/pandora/clip/managers.py b/pandora/clip/managers.py index eb7c6ca9..45e492b7 100644 --- a/pandora/clip/managers.py +++ b/pandora/clip/managers.py @@ -17,6 +17,7 @@ keymap = { 'place': 'annotations__places__id', 'text': 'findvalue', 'annotations': 'findvalue', + 'layer': 'annotations__layer', 'user': 'annotations__user__username', } case_insensitive_keys = ('annotations__user__username', ) diff --git a/pandora/clip/migrations/0004_id_bigint.py b/pandora/clip/migrations/0004_id_bigint.py new file mode 100644 index 00000000..b0415b96 --- /dev/null +++ b/pandora/clip/migrations/0004_id_bigint.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clip', '0003_auto_20160219_1805'), + ] + + operations = [ + migrations.AlterField( + model_name='clip', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/clip/models.py b/pandora/clip/models.py index 9736adb1..406f13df 100644 --- a/pandora/clip/models.py +++ b/pandora/clip/models.py @@ -8,6 +8,7 @@ import ox from archive import extract from . import managers +from .utils import add_cuts def get_layers(item, interval=None, user=None): @@ -59,9 +60,7 @@ class MetaClip(object): self.hue = self.saturation = self.lightness = 0 self.volume = 0 - def save(self, *args, **kwargs): - if self.duration != self.end - self.start: - self.update_calculated_values() + def update_findvalue(self): if not self.aspect_ratio and self.item: streams = self.item.streams() if streams: @@ -89,6 +88,11 @@ class MetaClip(object): self.findvalue = '\n'.join(list(filter(None, [a.findvalue for a in anns]))) for l in [k['id'] for k in settings.CONFIG['layers']]: setattr(self, l, l in anns_by_layer and bool(len(anns_by_layer[l]))) + + def save(self, *args, **kwargs): + if self.duration != self.end - self.start: + self.update_calculated_values() + self.update_findvalue() models.Model.save(self, *args, **kwargs) clip_keys = ('id', 'in', 'out', 'position', 'created', 'modified', @@ -111,8 +115,7 @@ class MetaClip(object): del j[key] #needed here to make item find with clips work if 'annotations' in keys: - #annotations = self.annotations.filter(layer__in=settings.CONFIG['clipLayers']) - annotations = self.annotations.all() + annotations = self.annotations.all().exclude(value='') if qs: for q in qs: annotations = annotations.filter(q) @@ -150,12 +153,12 @@ class MetaClip(object): data['annotation'] = qs[0].public_id data['parts'] = self.item.cache['parts'] data['durations'] = self.item.cache['durations'] - for key in ('title', 'director', 'year', 'videoRatio'): + for key in settings.CONFIG['itemTitleKeys'] + ['videoRatio']: value = self.item.cache.get(key) if value: data[key] = value data['duration'] = data['out'] - data['in'] - data['cuts'] = tuple([c for c in self.item.get('cuts', []) if c > self.start and c < self.end]) + add_cuts(data, self.item, self.start, self.end) data['layers'] = self.get_layers(user) data['streams'] = [s.file.oshash for s in self.item.streams()] return data @@ -186,6 +189,7 @@ class MetaClip(object): def __str__(self): return self.public_id + class Meta: unique_together = ("item", "start", "end") diff --git a/pandora/clip/utils.py b/pandora/clip/utils.py new file mode 100644 index 00000000..7d6d03fe --- /dev/null +++ b/pandora/clip/utils.py @@ -0,0 +1,22 @@ + + +def add_cuts(data, item, start, end): + cuts = [] + last = False + outer = [] + first = 0 + for cut in item.get('cuts', []): + if cut > start and cut < end: + if not cuts: + outer.append(first) + cuts.append(cut) + last = True + elif cut <= start: + first = cut + elif cut >= end: + if not len(outer): + outer.append(first) + if len(outer) == 1: + outer.append(cut) + data['cuts'] = tuple(cuts) + data['outerCuts'] = tuple(outer) diff --git a/pandora/config.0xdb.jsonc b/pandora/config.0xdb.jsonc index 8440de9a..61dfd6e2 100644 --- a/pandora/config.0xdb.jsonc +++ b/pandora/config.0xdb.jsonc @@ -1009,7 +1009,7 @@ { "id": "tags", "title": "Tags", - "canAddAnnotations": {"member": true, "staff": true, "admin": true}, + "canAddAnnotations": {"member": true, "friend": true, "staff": true, "admin": true}, "item": "Tag", "autocomplete": true, "overlap": true, @@ -1399,10 +1399,8 @@ corner of the screen "resolutions": List of video resolutions. Supported values are 96, 144, 240, 288, 360, 432, 480, 720 and 1080. - "torrent": If true, video downloads are offered via BitTorrent */ "video": { - "torrent": false, "formats": ["webm", "mp4"], // fixme: this should be named "ratio" or "defaultRatio", // as it also applies to clip lists (on load) diff --git a/pandora/config.indiancinema.jsonc b/pandora/config.indiancinema.jsonc index 786a53d4..11e7eb03 100644 --- a/pandora/config.indiancinema.jsonc +++ b/pandora/config.indiancinema.jsonc @@ -73,13 +73,14 @@ "canSeeAccessed": {"researcher": true, "staff": true, "admin": true}, "canSeeAllTasks": {"staff": true, "admin": true}, "canSeeDebugMenu": {"researcher": true, "staff": true, "admin": true}, - "canSeeExtraItemViews": {"researcher": true, "staff": true, "admin": true}, - "canSeeMedia": {"researcher": true, "staff": true, "admin": true}, "canSeeDocument": {"guest": 1, "member": 1, "researcher": 2, "staff": 3, "admin": 3}, + "canSeeExtraItemViews": {"researcher": true, "staff": true, "admin": true}, "canSeeItem": {"guest": 2, "member": 2, "researcher": 2, "staff": 3, "admin": 3}, + "canSeeMedia": {"researcher": true, "staff": true, "admin": true}, "canSeeSize": {"researcher": true, "staff": true, "admin": true}, "canSeeSoftwareVersion": {"researcher": true, "staff": true, "admin": true}, - "canSendMail": {"staff": true, "admin": true} + "canSendMail": {"staff": true, "admin": true}, + "canShare": {"staff": true, "admin": true} }, /* "clipKeys" are the properties that clips can be sorted by (the values are @@ -312,6 +313,14 @@ "autocomplete": true, "columnWidth": 128 }, + { + "id": "fulltext", + "operator": "+", + "title": "Fulltext", + "type": "text", + "fulltext": true, + "find": true + }, { "id": "created", "operator": "-", @@ -1494,6 +1503,7 @@ "hasEvents": true, "hasPlaces": true, "item": "Keyword", + "autocomplete": true, "overlap": true, "type": "string" }, @@ -1875,10 +1885,8 @@ corner of the screen "resolutions": List of video resolutions. Supported values are 96, 144, 240, 288, 360, 432, 480, 720 and 1080. - "torrent": If true, video downloads are offered via BitTorrent */ "video": { - "torrent": false, "formats": ["webm", "mp4"], "previewRatio": 1.375, "resolutions": [240, 480] diff --git a/pandora/config.padma.jsonc b/pandora/config.padma.jsonc index c78740a9..3bd3b748 100644 --- a/pandora/config.padma.jsonc +++ b/pandora/config.padma.jsonc @@ -71,13 +71,14 @@ "canSeeAccessed": {"staff": true, "admin": true}, "canSeeAllTasks": {"staff": true, "admin": true}, "canSeeDebugMenu": {"staff": true, "admin": true}, - "canSeeExtraItemViews": {"staff": true, "admin": true}, - "canSeeMedia": {"staff": true, "admin": true}, "canSeeDocument": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canSeeExtraItemViews": {"staff": true, "admin": true}, "canSeeItem": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canSeeMedia": {"staff": true, "admin": true}, "canSeeSize": {"staff": true, "admin": true}, "canSeeSoftwareVersion": {"staff": true, "admin": true}, - "canSendMail": {"staff": true, "admin": true} + "canSendMail": {"staff": true, "admin": true}, + "canShare": {"staff": true, "admin": true} }, /* "clipKeys" are the properties that clips can be sorted by (the values are @@ -246,6 +247,28 @@ "filter": true, "find": true }, + { + "id": "source", + "title": "Source", + "type": "string", + "autocomplete": true, + "description": true, + "columnWidth": 180, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "project", + "title": "Project", + "type": "string", + "autocomplete": true, + "description": true, + "columnWidth": 120, + "filter": true, + "find": true, + "sort": true + }, { "id": "id", "operator": "+", @@ -291,6 +314,24 @@ "sort": true, "columnWidth": 256 }, + { + "id": "content", + "operator": "+", + "title": "Content", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, + { + "id": "translation", + "operator": "+", + "title": "Translation", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, { "id": "matches", "operator": "-", @@ -310,6 +351,20 @@ "autocomplete": true, "columnWidth": 128 }, + { + "id": "notes", + "title": "Notes", + "type": "text", + "capability": "canEditMetadata" + }, + { + "id": "fulltext", + "operator": "+", + "title": "Fulltext", + "type": "text", + "fulltext": true, + "find": true + }, { "id": "created", "operator": "-", @@ -545,7 +600,6 @@ "title": "Director", "type": ["string"], "autocomplete": true, - "columnRequired": true, "columnWidth": 180, "sort": true, "sortType": "person" @@ -564,7 +618,6 @@ "title": "Featuring", "type": ["string"], "autocomplete": true, - "columnRequired": true, "columnWidth": 180, "filter": true, "sort": true, @@ -620,7 +673,7 @@ { "id": "annotations", "title": "Annotations", - "type": "string", // fixme: not the best type for this magic key + "type": "text", // fixme: not the best type for this magic key "find": true }, { @@ -658,7 +711,7 @@ }, { "id": "numberofannotations", - "title": "Annotations", + "title": "Number of Annotations", "type": "integer", "columnWidth": 60, "sort": true @@ -794,12 +847,16 @@ "id": "user", "title": "User", "type": "string", + "columnWidth": 90, "capability": "canSeeMedia", + "sort": true, "find": true }, { "id": "groups", "title": "Group", + "columnWidth": 90, + "sort": true, "type": ["string"] }, { @@ -1332,10 +1389,8 @@ corner of the screen "resolutions": List of video resolutions. Supported values are 96, 144, 240, 288, 360, 432, 480, 720 and 1080. - "torrent": If true, video downloads are offered via BitTorrent */ "video": { - "torrent": true, "formats": ["webm", "mp4"], "previewRatio": 1.3333333333, //supported resolutions are diff --git a/pandora/config.pandora.jsonc b/pandora/config.pandora.jsonc index bd76faa2..51aaee24 100644 --- a/pandora/config.pandora.jsonc +++ b/pandora/config.pandora.jsonc @@ -29,7 +29,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "text": Text shown on mouseover */ "cantPlay": { - "icon": "noCopyright", + "icon": "NoCopyright", "link": "", "text": "" }, @@ -67,7 +67,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "canManageEntities": {"member": true, "staff": true, "admin": true}, "canManageHome": {"staff": true, "admin": true}, "canManagePlacesAndEvents": {"member": true, "staff": true, "admin": true}, - "canManageTitlesAndNames": {"member": true, "staff": true, "admin": true}, + "canManageTitlesAndNames": {"member": false, "staff": true, "admin": true}, "canManageTranslations": {"admin": true}, "canManageUsers": {"staff": true, "admin": true}, "canPlayClips": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, @@ -102,8 +102,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. ], /* "clipLayers" is the ordered list of public layers that will appear as the - text of clips (in grid view, below the icon). Excluding a layer from this - list means it will not be included in find annotations. + text of clips (in grid view, below the icon). */ "clipLayers": ["publicnotes", "keywords", "subtitles"], /* @@ -351,11 +350,11 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "type": "enum", "columnWidth": 90, "format": {"type": "ColorLevel", "args": [ - ["Public", "Out of Copyright", "Under Copyright", "Private"] + ["Public", "Restricted", "Private"] ]}, "sort": true, "sortOperator": "+", - "values": ["Public", "Out of Copyright", "Under Copyright", "Private", "Unknown"] + "values": ["Public", "Restricted", "Private", "Unknown"] } ], /* @@ -753,6 +752,13 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "capability": "canSeeMedia", "find": true }, + { + "id": "filename", + "title": "Filename", + "type": ["string"], + "capability": "canSeeMedia", + "find": true + }, { "id": "created", "title": "Date Created", @@ -1159,6 +1165,11 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "findDocuments": {"conditions": [], "operator": "&"}, "followPlayer": true, "help": "", + "hidden": { + "collections": [], + "edits": [], + "lists": [] + }, "icons": "posters", "infoIconSize": 256, "item": "", @@ -1267,13 +1278,11 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. corner of the screen "resolutions": List of video resolutions. Supported values are 96, 144, 240, 288, 360, 432, 480, 720 and 1080. - "torrent": If true, video downloads are offered via BitTorrent */ "video": { "downloadFormat": "webm", "formats": ["webm", "mp4"], "previewRatio": 1.3333333333, - "resolutions": [240, 480], - "torrent": false + "resolutions": [240, 480] } } diff --git a/pandora/document/apps.py b/pandora/document/apps.py new file mode 100644 index 00000000..88a5f0b4 --- /dev/null +++ b/pandora/document/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class DocumentConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'document' + diff --git a/pandora/document/fulltext.py b/pandora/document/fulltext.py index 040658ce..c4c9aaff 100644 --- a/pandora/document/fulltext.py +++ b/pandora/document/fulltext.py @@ -1,14 +1,37 @@ +import logging +import os import subprocess +import tempfile from django.conf import settings -def extract_text(pdf): - cmd = ['pdftotext', pdf, '-'] +logger = logging.getLogger('pandora.' + __name__) + + +def extract_text(pdf, page=None): + if page is not None: + page = str(page) + cmd = ['pdftotext', '-f', page, '-l', page, pdf, '-'] + else: + cmd = ['pdftotext', pdf, '-'] p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() - stdout = stdout.decode() - return stdout.strip() + stdout = stdout.decode().strip() + if not stdout: + if page: + # split page from pdf and ocr + fd, page_pdf = tempfile.mkstemp('.pdf') + cmd = ['pdfseparate', '-f', page, '-l', page, pdf, page_pdf] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + text = ocr_image(page_pdf) + os.unlink(page_pdf) + os.close(fd) + return text + else: + return ocr_image(pdf) + return stdout def ocr_image(path): cmd = ['tesseract', path, '-', 'txt'] @@ -43,9 +66,11 @@ class FulltextMixin: if self.has_fulltext_key(): from elasticsearch.exceptions import NotFoundError try: - res = self.elasticsearch().delete(index=self._ES_INDEX, doc_type='document', id=self.id) + res = self.elasticsearch().delete(index=self._ES_INDEX, id=self.id) except NotFoundError: pass + except: + logger.error('failed to delete fulltext document', exc_info=True) def update_fulltext(self): if self.has_fulltext_key(): @@ -54,7 +79,7 @@ class FulltextMixin: doc = { 'text': text.lower() } - res = self.elasticsearch().index(index=self._ES_INDEX, doc_type='document', id=self.id, body=doc) + res = self.elasticsearch().index(index=self._ES_INDEX, id=self.id, body=doc) @classmethod def find_fulltext(cls, query): @@ -95,3 +120,69 @@ class FulltextMixin: ids += [int(r['_id']) for r in res['hits']['hits']] from_ += len(res['hits']['hits']) return ids + + def highlight_page(self, page, query, size): + import pypdfium2 as pdfium + from PIL import Image + from PIL import ImageDraw + + pdfpath = self.file.path + pagenumber = int(page) - 1 + jpg = tempfile.NamedTemporaryFile(suffix='.jpg') + output = jpg.name + TINT_COLOR = (255, 255, 0) + TRANSPARENCY = .45 + OPACITY = int(255 * TRANSPARENCY) + scale = 150/72 + + pdf = pdfium.PdfDocument(pdfpath) + page = pdf[pagenumber] + + bitmap = page.render(scale=scale, rotation=0) + img = bitmap.to_pil().convert('RGBA') + overlay = Image.new('RGBA', img.size, TINT_COLOR+(0,)) + draw = ImageDraw.Draw(overlay) + + textpage = page.get_textpage() + search = textpage.search(query) + result = search.get_next() + while result: + pos, steps = result + steps += 1 + while steps: + box = textpage.get_charbox(pos) + box = [b*scale for b in box] + tl = (box[0], img.size[1] - box[3]) + br = (box[2], img.size[1] - box[1]) + draw.rectangle((tl, br), fill=TINT_COLOR+(OPACITY,)) + pos += 1 + steps -= 1 + result = search.get_next() + img = Image.alpha_composite(img, overlay) + img = img.convert("RGB") + aspect = img.size[0] / img.size[1] + resize_method = Image.LANCZOS + if img.size[0] >= img.size[1]: + width = size + height = int(size / aspect) + else: + width = int(size / aspect) + height = size + img = img.resize((width, height), resize_method) + img.save(output, quality=72) + return jpg + + +class FulltextPageMixin(FulltextMixin): + _ES_INDEX = "document-page-index" + + def extract_fulltext(self): + if self.document.file: + if self.document.extension == 'pdf': + return extract_text(self.document.file.path, self.page) + elif self.extension in ('png', 'jpg'): + return ocr_image(self.document.file.path) + elif self.extension == 'html': + # FIXME: is there a nice way to split that into pages + return self.data.get('text', '') + return '' diff --git a/pandora/document/managers.py b/pandora/document/managers/__init__.py similarity index 97% rename from pandora/document/managers.py rename to pandora/document/managers/__init__.py index 4210cd62..edcc4789 100644 --- a/pandora/document/managers.py +++ b/pandora/document/managers/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from datetime import datetime import unicodedata from django.db.models import Q, Manager @@ -14,6 +15,7 @@ from documentcollection.models import Collection from item import utils from user.models import Group +from .pages import PageManager keymap = { 'item': 'items__public_id', @@ -61,7 +63,7 @@ def parseCondition(condition, user, item=None, owner=None): def buildCondition(k, op, v, user, exclude=False, owner=None): import entity.models - from . import models + from .. import models # fixme: frontend should never call with list if k == 'list': @@ -297,5 +299,8 @@ class DocumentManager(Manager): q |= Q(groups__in=user.groups.all()) rendered_q |= Q(groups__in=user.groups.all()) qs = qs.filter(q) + max_level = len(settings.CONFIG['documentRightsLevels']) + qs = qs.filter(rightslevel__lte=max_level) return qs + diff --git a/pandora/document/managers/pages.py b/pandora/document/managers/pages.py new file mode 100644 index 00000000..2fbfa4fe --- /dev/null +++ b/pandora/document/managers/pages.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +import unicodedata + +from six import string_types +from django.db.models import Q, Manager +from django.conf import settings + +import ox +from oxdjango.query import QuerySet + +import entity.managers +from oxdjango.managers import get_operator + +from documentcollection.models import Collection +from item import utils +from user.models import Group + + +keymap = { + 'item': 'items__public_id', +} +default_key = 'title' + +def get_key_type(k): + key_type = (utils.get_by_id(settings.CONFIG['documentKeys'], k) or {'type': 'string'}).get('type') + if isinstance(key_type, list): + key_type = key_type[0] + key_type = { + 'title': 'string', + 'person': 'string', + 'text': 'string', + 'year': 'string', + 'length': 'string', + 'layer': 'string', + 'list': 'list', + }.get(key_type, key_type) + return key_type + + +def parseCondition(condition, user, item=None, owner=None): + ''' + ''' + k = condition.get('key', default_key) + k = keymap.get(k, k) + if not k: + k = default_key + if item and k == 'description': + item_conditions = condition.copy() + item_conditions['key'] = 'items__itemproperties__description' + return parseCondition(condition, user) | parseCondition(item_conditions, user) + + v = condition['value'] + op = condition.get('operator') + if not op: + op = '=' + + if op.startswith('!'): + return buildCondition(k, op[1:], v, user, True, owner=owner) + else: + return buildCondition(k, op, v, user, owner=owner) + +def buildCondition(k, op, v, user, exclude=False, owner=None): + import entity.models + from .. import models + + # fixme: frontend should never call with list + if k == 'list': + print('fixme: frontend should never call with list', k, op, v) + k = 'collection' + + key_type = get_key_type(k) + + key_config = (utils.get_by_id(settings.CONFIG['documentKeys'], k) or {'type': 'string'}) + + facet_keys = models.Document.facet_keys + if k == 'document': + k = 'document__id' + if op == '&' and isinstance(v, list): + v = [ox.fromAZ(id_) for id_ in v] + k += get_operator(op) + else: + v = ox.fromAZ(v) + q = Q(**{k: v}) + if exclude: + q = ~Q(document__id__in=models.Document.objects.filter(q)) + return q + elif k == 'rightslevel': + q = Q(document__rightslevel=v) + if exclude: + q = ~Q(document__rightslevel=v) + return q + elif k == 'groups': + if op == '==' and v == '$my': + if not owner: + owner = user + groups = owner.groups.all() + else: + key = 'name' + get_operator(op) + groups = Group.objects.filter(**{key: v}) + if not groups.count(): + return Q(id=0) + q = Q(document__groups__in=groups) + if exclude: + q = ~q + return q + elif k in ('oshash', 'items__public_id'): + q = Q(**{k: v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif isinstance(v, bool): + key = k + elif k == 'entity': + entity_key, entity_v = entity.managers.namePredicate(op, v) + key = 'id__in' + v = entity.models.DocumentProperties.objects.filter(**{ + 'entity__' + entity_key: entity_v + }).values_list('document_id', flat=True) + elif k == 'collection': + q = Q(id=0) + l = v.split(":", 1) + if len(l) >= 2: + lqs = list(Collection.objects.filter(name=l[1], user__username=l[0])) + if len(lqs) == 1 and lqs[0].accessible(user): + l = lqs[0] + if l.query.get('static', False) is False: + data = l.query + q = parseConditions(data.get('conditions', []), + data.get('operator', '&'), + user, owner=l.user) + else: + q = Q(id__in=l.documents.all()) + else: + q = Q(id=0) + return q + elif key_config.get('fulltext'): + qs = models.Page.find_fulltext_ids(v) + q = Q(id__in=qs) + if exclude: + q = ~Q(id__in=qs) + return q + elif key_type == 'boolean': + q = Q(**{'find__key': k, 'find__value': v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif key_type == "string": + in_find = True + if in_find: + value_key = 'find__value' + else: + value_key = k + if isinstance(v, string_types): + v = unicodedata.normalize('NFKD', v).lower() + if k in facet_keys: + in_find = False + facet_value = 'facets__value' + get_operator(op, 'istr') + v = models.Document.objects.filter(**{'facets__key': k, facet_value: v}) + value_key = 'id__in' + else: + value_key = value_key + get_operator(op) + k = str(k) + value_key = str(value_key) + if k == '*': + q = Q(**{'find__value' + get_operator(op): v}) | \ + Q(**{'facets__value' + get_operator(op, 'istr'): v}) + elif in_find: + q = Q(**{'find__key': k, value_key: v}) + else: + q = Q(**{value_key: v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif key_type == 'date': + def parse_date(d): + while len(d) < 3: + d.append(1) + return datetime(*[int(i) for i in d]) + + #using sort here since find only contains strings + v = parse_date(v.split('-')) + vk = 'sort__%s%s' % (k, get_operator(op, 'int')) + vk = str(vk) + q = Q(**{vk: v}) + if exclude: + q = ~q + return q + else: # integer, float, list, time + #use sort table here + if key_type == 'time': + v = int(utils.parse_time(v)) + + vk = 'sort__%s%s' % (k, get_operator(op, 'int')) + vk = str(vk) + q = Q(**{vk: v}) + if exclude: + q = ~q + return q + key = str(key) + q = Q(**{key: v}) + if exclude: + q = ~q + return q + + +def parseConditions(conditions, operator, user, item=None, owner=None): + ''' + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + ''' + conn = [] + for condition in conditions: + if 'conditions' in condition: + q = parseConditions(condition['conditions'], + condition.get('operator', '&'), user, item, owner=owner) + if q: + conn.append(q) + pass + else: + conn.append(parseCondition(condition, user, item, owner=owner)) + if conn: + q = conn[0] + for c in conn[1:]: + if operator == '|': + q = q | c + else: + q = q & c + return q + return None + + +class PageManager(Manager): + + def get_query_set(self): + return QuerySet(self.model) + + def find(self, data, user, item=None): + ''' + query: { + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + } + ''' + + #join query with operator + qs = self.get_query_set() + query = data.get('query', {}) + conditions = parseConditions(query.get('conditions', []), + query.get('operator', '&'), + user, item) + if conditions: + qs = qs.filter(conditions) + qs = qs.distinct() + + #anonymous can only see public items + if not user or user.is_anonymous: + level = 'guest' + allowed_level = settings.CONFIG['capabilities']['canSeeDocument'][level] + qs = qs.filter(document__rightslevel__lte=allowed_level) + rendered_q = Q(rendered=True) + #users can see public items, there own items and items of there groups + else: + level = user.profile.get_level() + allowed_level = settings.CONFIG['capabilities']['canSeeDocument'][level] + q = Q(document__rightslevel__lte=allowed_level) | Q(document__user=user) + rendered_q = Q(rendered=True) | Q(document__user=user) + if user.groups.count(): + q |= Q(document__groups__in=user.groups.all()) + rendered_q |= Q(document__groups__in=user.groups.all()) + qs = qs.filter(q) + + return qs + diff --git a/pandora/document/migrations/0012_auto_20200513_0001.py b/pandora/document/migrations/0012_auto_20200513_0001.py new file mode 100644 index 00000000..2bf7b0ab --- /dev/null +++ b/pandora/document/migrations/0012_auto_20200513_0001.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2020-05-13 00:01 +from __future__ import unicode_literals + +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import document.fulltext +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('document', '0011_jsonfield'), + ] + + operations = [ + migrations.CreateModel( + name='Page', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('page', models.IntegerField(default=1)), + ('data', oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ], + bases=(models.Model, document.fulltext.FulltextPageMixin), + ), + migrations.AddField( + model_name='page', + name='document', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pages_set', to='document.Document'), + ), + ] diff --git a/pandora/document/migrations/0013_id_bigint.py b/pandora/document/migrations/0013_id_bigint.py new file mode 100644 index 00000000..09dd44ed --- /dev/null +++ b/pandora/document/migrations/0013_id_bigint.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('document', '0012_auto_20200513_0001'), + ] + + operations = [ + migrations.AlterField( + model_name='access', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='document', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='document', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='facet', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='find', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='itemproperties', + name='description', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='itemproperties', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='page', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/document/models.py b/pandora/document/models.py index e2501244..68819f39 100644 --- a/pandora/document/models.py +++ b/pandora/document/models.py @@ -6,11 +6,12 @@ import os import re import unicodedata +from django.conf import settings +from django.contrib.auth import get_user_model from django.db import models, transaction from django.db.models import Q, Sum, Max -from django.contrib.auth import get_user_model from django.db.models.signals import pre_delete -from django.conf import settings +from django.utils import datetime_safe from oxdjango.fields import JSONField from PIL import Image @@ -21,7 +22,7 @@ from oxdjango.sortmodel import get_sort_field from person.models import get_name_sort from item.models import Item from annotation.models import Annotation -from archive.extract import resize_image +from archive.extract import resize_image, open_image_rgb from archive.chunk import save_chunk from user.models import Group from user.utils import update_groups @@ -29,7 +30,7 @@ from user.utils import update_groups from . import managers from . import utils from . import tasks -from .fulltext import FulltextMixin +from .fulltext import FulltextMixin, FulltextPageMixin User = get_user_model() @@ -79,7 +80,7 @@ class Document(models.Model, FulltextMixin): current_values = [] for k in settings.CONFIG['documentKeys']: if k.get('sortType') == 'person': - current_values += self.get(k['id'], []) + current_values += self.get_value(k['id'], []) if not isinstance(current_values, list): if not current_values: current_values = [] @@ -327,6 +328,9 @@ class Document(models.Model, FulltextMixin): def editable(self, user, item=None): if not user or user.is_anonymous: return False + max_level = len(settings.CONFIG['rightsLevels']) + if self.rightslevel > max_level: + return False if self.user == user or \ self.groups.filter(id__in=user.groups.all()).count() > 0 or \ user.is_staff or \ @@ -346,6 +350,8 @@ class Document(models.Model, FulltextMixin): groups = data.pop('groups') update_groups(self, groups) for key in data: + if key == "id": + continue k = list(filter(lambda i: i['id'] == key, settings.CONFIG['documentKeys'])) ktype = k and k[0].get('type') or '' if key == 'text' and self.extension == 'html': @@ -546,10 +552,10 @@ class Document(models.Model, FulltextMixin): if len(crop) == 4: path = os.path.join(folder, '%dp%d,%s.jpg' % (1024, page, ','.join(map(str, crop)))) if not os.path.exists(path): - img = Image.open(src).crop(crop) + img = open_image_rgb(src).crop(crop) img.save(path) else: - img = Image.open(path) + img = open_image_rgb(path) src = path if size < max(img.size): path = os.path.join(folder, '%dp%d,%s.jpg' % (size, page, ','.join(map(str, crop)))) @@ -562,10 +568,10 @@ class Document(models.Model, FulltextMixin): if len(crop) == 4: path = os.path.join(folder, '%s.jpg' % ','.join(map(str, crop))) if not os.path.exists(path): - img = Image.open(src).crop(crop) + img = open_image_rgb(src).convert('RGB').crop(crop) img.save(path) else: - img = Image.open(path) + img = open_image_rgb(path) src = path if size < max(img.size): path = os.path.join(folder, '%sp%s.jpg' % (size, ','.join(map(str, crop)))) @@ -574,7 +580,7 @@ class Document(models.Model, FulltextMixin): if os.path.exists(src) and not os.path.exists(path): image_size = max(self.width, self.height) if image_size == -1: - image_size = max(*Image.open(src).size) + image_size = max(*open_image_rgb(src).size) if size > image_size: path = src else: @@ -586,6 +592,11 @@ class Document(models.Model, FulltextMixin): image = os.path.join(os.path.dirname(pdf), '1024p%d.jpg' % page) utils.extract_pdfpage(pdf, image, page) + def create_pages(self): + for page in range(self.pages): + page += 1 + p, c = Page.objects.get_or_create(document=self, page=page) + def get_info(self): if self.extension == 'pdf': self.thumbnail(1024) @@ -595,7 +606,7 @@ class Document(models.Model, FulltextMixin): self.pages = utils.pdfpages(self.file.path) elif self.width == -1: self.pages = -1 - self.width, self.height = Image.open(self.file.path).size + self.width, self.height = open_image_rgb(self.file.path).size def get_ratio(self): if self.extension == 'pdf': @@ -702,6 +713,41 @@ class ItemProperties(models.Model): super(ItemProperties, self).save(*args, **kwargs) +class Page(models.Model, FulltextPageMixin): + + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + document = models.ForeignKey(Document, related_name='pages_set', on_delete=models.CASCADE) + page = models.IntegerField(default=1) + data = JSONField(default=dict, editable=False) + + objects = managers.PageManager() + + def __str__(self): + return u"%s:%s" % (self.document, self.page) + + def json(self, keys=None, user=None): + data = {} + data['document'] = ox.toAZ(self.document.id) + data['page'] = self.page + data['id'] = '{document}/{page}'.format(**data) + document_keys = [] + if keys: + for key in list(data): + if key not in keys: + del data[key] + for key in keys: + if 'fulltext' in key: + data['fulltext'] = self.extract_fulltext() + elif key in ('document', 'page', 'id'): + pass + else: + document_keys.append(key) + if document_keys: + data.update(self.document.json(document_keys, user)) + return data + class Access(models.Model): class Meta: unique_together = ("document", "user") diff --git a/pandora/document/page_views.py b/pandora/document/page_views.py new file mode 100644 index 00000000..d0ae7c0b --- /dev/null +++ b/pandora/document/page_views.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +import os +import re +from glob import glob +import unicodedata + +import ox +from ox.utils import json +from oxdjango.api import actions +from oxdjango.decorators import login_required_json +from oxdjango.http import HttpFileResponse +from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response, HttpErrorJson +from django import forms +from django.db.models import Count, Sum +from django.conf import settings + +from item import utils +from item.models import Item +from itemlist.models import List +from entity.models import Entity +from archive.chunk import process_chunk +from changelog.models import add_changelog + +from . import models +from . import tasks + +def parse_query(data, user): + query = {} + query['range'] = [0, 100] + query['sort'] = [{'key': 'page', 'operator': '+'}, {'key': 'title', 'operator': '+'}] + for key in ('keys', 'group', 'file', 'range', 'position', 'positions', 'sort'): + if key in data: + query[key] = data[key] + query['qs'] = models.Page.objects.find(data, user) + return query + +def _order_query(qs, sort): + prefix = 'document__sort__' + order_by = [] + for e in sort: + operator = e['operator'] + if operator != '-': + operator = '' + key = { + 'index': 'document__items__itemproperties__index', + 'position': 'id', + 'name': 'title', + }.get(e['key'], e['key']) + if key == 'resolution': + order_by.append('%swidth' % operator) + order_by.append('%sheight' % operator) + else: + if '__' not in key and key not in ('created', 'modified', 'page'): + key = "%s%s" % (prefix, key) + order = '%s%s' % (operator, key) + order_by.append(order) + if order_by: + qs = qs.order_by(*order_by, nulls_last=True) + qs = qs.distinct() + return qs + +def _order_by_group(query): + prefix = 'document__sort__' + if 'sort' in query: + op = '-' if query['sort'][0]['operator'] == '-' else '' + if len(query['sort']) == 1 and query['sort'][0]['key'] == 'items': + order_by = op + prefix + 'items' + if query['group'] == "year": + secondary = op + prefix + 'sortvalue' + order_by = (order_by, secondary) + elif query['group'] != "keyword": + order_by = (order_by, prefix + 'sortvalue') + else: + order_by = (order_by, 'value') + else: + order_by = op + prefix + 'sortvalue' + order_by = (order_by, prefix + 'items') + else: + order_by = ('-' + prefix + 'sortvalue', prefix + 'items') + return order_by + +def findPages(request, data): + ''' + Finds documents pages for a given query + takes { + query: object, // query object, see `find` + sort: [object], // list of sort objects, see `find` + range: [int, int], // range of results, per current sort order + keys: [string] // list of keys to return + } + returns { + items: [{ // list of pages + id: string + page: int + }] + } + ''' + query = parse_query(data, request.user) + #order + qs = _order_query(query['qs'], query['sort']) + + response = json_response() + if 'group' in query: + response['data']['items'] = [] + items = 'items' + document_qs = query['qs'] + order_by = _order_by_group(query) + qs = models.Facet.objects.filter(key=query['group']).filter(document__id__in=document_qs) + qs = qs.values('value').annotate(items=Count('id')).order_by(*order_by) + + if 'positions' in query: + response['data']['positions'] = {} + ids = [j['value'] for j in qs] + response['data']['positions'] = utils.get_positions(ids, query['positions']) + elif 'range' in data: + qs = qs[query['range'][0]:query['range'][1]] + response['data']['items'] = [{'name': i['value'], 'items': i[items]} for i in qs] + else: + response['data']['items'] = qs.count() + elif 'keys' in data: + qs = qs[query['range'][0]:query['range'][1]] + + response['data']['items'] = [l.json(data['keys'], request.user) for l in qs] + elif 'position' in data: + #FIXME: actually implement position requests + response['data']['position'] = 0 + elif 'positions' in data: + ids = list(qs.values_list('id', flat=True)) + response['data']['positions'] = utils.get_positions(ids, query['positions'], decode_id=True) + else: + response['data']['items'] = qs.count() + return render_to_json_response(response) +actions.register(findPages) + diff --git a/pandora/document/tasks.py b/pandora/document/tasks.py index 7bede1a9..fcfb5576 100644 --- a/pandora/document/tasks.py +++ b/pandora/document/tasks.py @@ -1,21 +1,26 @@ import ox -from celery.task import task +from app.celery import app -@task(queue="encoding") +@app.task(queue="encoding") def extract_fulltext(id): from . import models d = models.Document.objects.get(id=id) d.update_fulltext() + d.create_pages() + for page in d.pages_set.all(): + page.update_fulltext() -@task(queue='default') +@app.task(queue='default') def bulk_edit(data, username): from django.db import transaction from . import models from item.models import Item user = models.User.objects.get(username=username) item = 'item' in data and Item.objects.get(public_id=data['item']) or None - documents = models.Document.objects.filter(pk__in=map(ox.fromAZ, data['id'])) + ids = data['id'] + del data['id'] + documents = models.Document.objects.filter(pk__in=map(ox.fromAZ, ids)) for document in documents: if document.editable(user, item): with transaction.atomic(): diff --git a/pandora/document/views.py b/pandora/document/views.py index 5fc47466..c2d9f631 100644 --- a/pandora/document/views.py +++ b/pandora/document/views.py @@ -12,8 +12,10 @@ from oxdjango.decorators import login_required_json from oxdjango.http import HttpFileResponse from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response, HttpErrorJson from django import forms -from django.db.models import Count, Sum from django.conf import settings +from django.db.models import Count, Sum +from django.http import HttpResponse +from django.shortcuts import render from item import utils from item.models import Item @@ -24,6 +26,7 @@ from changelog.models import add_changelog from . import models from . import tasks +from . import page_views def get_document_or_404_json(request, id): response = {'status': {'code': 404, @@ -380,8 +383,12 @@ def file(request, id, name=None): def thumbnail(request, id, size=256, page=None): size = int(size) document = get_document_or_404_json(request, id) + if "q" in request.GET and page: + img = document.highlight_page(page, request.GET["q"], size) + return HttpResponse(img, content_type="image/jpeg") return HttpFileResponse(document.thumbnail(size, page=page)) + @login_required_json def upload(request): if 'id' in request.GET: @@ -506,3 +513,37 @@ def autocompleteDocuments(request, data): response['data']['items'] = [i['value'] for i in qs] return render_to_json_response(response) actions.register(autocompleteDocuments) + + +def document(request, fragment): + context = {} + parts = fragment.split('/') + # FIXME: parse collection urls and return the right metadata for those + id = parts[0] + page = None + crop = None + if len(parts) == 2: + rect = parts[1].split(',') + if len(rect) == 1: + page = rect[0] + else: + crop = rect + try: + document = models.Document.objects.filter(id=ox.fromAZ(id)).first() + except: + document = None + if document and document.access(request.user): + context['title'] = document.data['title'] + if document.data.get('description'): + context['description'] = document.data['description'] + link = request.build_absolute_uri(document.get_absolute_url()) + public_id = ox.toAZ(document.id) + preview = '/documents/%s/512p.jpg' % public_id + if page: + preview = '/documents/%s/512p%s.jpg' % (public_id, page) + if crop: + preview = '/documents/%s/512p%s.jpg' % (public_id, ','.join(crop)) + context['preview'] = request.build_absolute_uri(preview) + context['url'] = request.build_absolute_uri('/documents/' + fragment) + context['settings'] = settings + return render(request, "document.html", context) diff --git a/pandora/documentcollection/apps.py b/pandora/documentcollection/apps.py new file mode 100644 index 00000000..10e2984f --- /dev/null +++ b/pandora/documentcollection/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class DocumentcollectionConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'documentcollection' + diff --git a/pandora/documentcollection/migrations/0005_alter_collection_description_alter_collection_id_and_more.py b/pandora/documentcollection/migrations/0005_alter_collection_description_alter_collection_id_and_more.py new file mode 100644 index 00000000..72155308 --- /dev/null +++ b/pandora/documentcollection/migrations/0005_alter_collection_description_alter_collection_id_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import documentcollection.models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('documentcollection', '0004_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='description', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='collection', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='collection', + name='poster_frames', + field=oxdjango.fields.JSONField(default=list, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='collection', + name='query', + field=oxdjango.fields.JSONField(default=documentcollection.models.default_query, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='collection', + name='sort', + field=oxdjango.fields.JSONField(default=documentcollection.models.get_collectionsort, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='collection', + name='status', + field=models.CharField(default='private', max_length=20), + ), + migrations.AlterField( + model_name='collection', + name='type', + field=models.CharField(default='static', max_length=255), + ), + migrations.AlterField( + model_name='collectiondocument', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='position', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/documentcollection/models.py b/pandora/documentcollection/models.py index 8504ae47..be600b54 100644 --- a/pandora/documentcollection/models.py +++ b/pandora/documentcollection/models.py @@ -34,6 +34,9 @@ def get_collectionview(): def get_collectionsort(): return tuple(settings.CONFIG['user']['ui']['collectionSort']) +def default_query(): + return {"static": True} + class Collection(models.Model): class Meta: @@ -46,7 +49,7 @@ class Collection(models.Model): name = models.CharField(max_length=255) status = models.CharField(max_length=20, default='private') _status = ['private', 'public', 'featured'] - query = JSONField(default=lambda: {"static": True}, editable=False) + query = JSONField(default=default_query, editable=False) type = models.CharField(max_length=255, default='static') description = models.TextField(default='') diff --git a/pandora/documentcollection/views.py b/pandora/documentcollection/views.py index 66fa746d..a678bd17 100644 --- a/pandora/documentcollection/views.py +++ b/pandora/documentcollection/views.py @@ -86,6 +86,11 @@ def findCollections(request, data): for x in data.get('query', {}).get('conditions', []) ) + is_personal = request.user.is_authenticated and any( + (x['key'] == 'user' and x['value'] == request.user.username and x['operator'] == '==') + for x in data.get('query', {}).get('conditions', []) + ) + if is_section_request: qs = query['qs'] if not is_featured and not request.user.is_anonymous: @@ -94,6 +99,9 @@ def findCollections(request, data): else: qs = _order_query(query['qs'], query['sort']) + if is_personal and request.user.profile.ui.get('hidden', {}).get('collections'): + qs = qs.exclude(name__in=request.user.profile.ui['hidden']['collections']) + response = json_response() if 'keys' in data: qs = qs[query['range'][0]:query['range'][1]] @@ -238,7 +246,7 @@ def addCollection(request, data): 'type' and 'view'. see: editCollection, findCollections, getCollection, removeCollection, sortCollections ''' - data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data.get('name', 'Untitled')).strip() name = data['name'] if not name: name = "Untitled" diff --git a/pandora/edit/apps.py b/pandora/edit/apps.py new file mode 100644 index 00000000..56f70942 --- /dev/null +++ b/pandora/edit/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class EditConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'edit' + diff --git a/pandora/edit/migrations/0006_id_bigint_jsonfield.py b/pandora/edit/migrations/0006_id_bigint_jsonfield.py new file mode 100644 index 00000000..a2e58e5c --- /dev/null +++ b/pandora/edit/migrations/0006_id_bigint_jsonfield.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import edit.models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('edit', '0005_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='clip', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='edit', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='edit', + name='poster_frames', + field=oxdjango.fields.JSONField(default=list, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='edit', + name='query', + field=oxdjango.fields.JSONField(default=edit.models.default_query, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='position', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/edit/models.py b/pandora/edit/models.py index d71525a0..5008ac45 100644 --- a/pandora/edit/models.py +++ b/pandora/edit/models.py @@ -13,6 +13,7 @@ from django.conf import settings from django.db import models, transaction from django.db.models import Max from django.contrib.auth import get_user_model +from django.core.cache import cache from oxdjango.fields import JSONField @@ -24,6 +25,7 @@ import clip.models from archive import extract from user.utils import update_groups from user.models import Group +from clip.utils import add_cuts from . import managers @@ -33,6 +35,9 @@ User = get_user_model() def get_path(f, x): return f.path(x) def get_icon_path(f, x): return get_path(f, 'icon.jpg') +def default_query(): + return {"static": True} + class Edit(models.Model): class Meta: @@ -51,7 +56,7 @@ class Edit(models.Model): description = models.TextField(default='') rightslevel = models.IntegerField(db_index=True, default=0) - query = JSONField(default=lambda: {"static": True}, editable=False) + query = JSONField(default=default_query, editable=False) type = models.CharField(max_length=255, default='static') icon = models.ImageField(default=None, blank=True, null=True, upload_to=get_icon_path) @@ -93,6 +98,8 @@ class Edit(models.Model): # dont add clip if in/out are invalid if not c.annotation: duration = c.item.sort.duration + if c.start is None or c.end is None: + return False if c.start > c.end \ or round(c.start, 3) >= round(duration, 3) \ or round(c.end, 3) > round(duration, 3): @@ -507,7 +514,7 @@ class Clip(models.Model): if value: data[key] = value data['duration'] = data['out'] - data['in'] - data['cuts'] = tuple([c for c in self.item.get('cuts', []) if c > self.start and c < self.end]) + add_cuts(data, self.item, self.start, self.end) data['layers'] = self.get_layers(user) data['streams'] = [s.file.oshash for s in self.item.streams()] return data diff --git a/pandora/edit/views.py b/pandora/edit/views.py index 09261310..c096b464 100644 --- a/pandora/edit/views.py +++ b/pandora/edit/views.py @@ -3,14 +3,16 @@ import os import re -import ox +from oxdjango.api import actions from oxdjango.decorators import login_required_json +from oxdjango.http import HttpFileResponse from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response +import ox + +from django.conf import settings from django.db import transaction from django.db.models import Max -from oxdjango.http import HttpFileResponse -from oxdjango.api import actions -from django.conf import settings +from django.db.models import Sum from item import utils from changelog.models import add_changelog @@ -190,7 +192,7 @@ def _order_clips(edit, sort): 'in': 'start', 'out': 'end', 'text': 'sortvalue', - 'volume': 'sortvolume', + 'volume': 'volume' if edit.type == 'smart' else 'sortvolume', 'item__sort__item': 'item__sort__public_id', }.get(key, key) order = '%s%s' % (operator, key) @@ -260,7 +262,7 @@ def addEdit(request, data): } see: editEdit, findEdit, getEdit, removeEdit, sortEdits ''' - data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data.get('name', 'Untitled')).strip() name = data['name'] if not name: name = "Untitled" @@ -412,6 +414,11 @@ def findEdits(request, data): is_featured = any(filter(is_featured_condition, data.get('query', {}).get('conditions', []))) + is_personal = request.user.is_authenticated and any( + (x['key'] == 'user' and x['value'] == request.user.username and x['operator'] == '==') + for x in data.get('query', {}).get('conditions', []) + ) + if is_section_request: qs = query['qs'] if not is_featured and not request.user.is_anonymous: @@ -420,6 +427,9 @@ def findEdits(request, data): else: qs = _order_query(query['qs'], query['sort']) + if is_personal and request.user.profile.ui.get('hidden', {}).get('edits'): + qs = qs.exclude(name__in=request.user.profile.ui['hidden']['edits']) + response = json_response() if 'keys' in data: qs = qs[query['range'][0]:query['range'][1]] diff --git a/pandora/encoding.conf.in b/pandora/encoding.conf.in new file mode 100644 index 00000000..af15426c --- /dev/null +++ b/pandora/encoding.conf.in @@ -0,0 +1,3 @@ +LOGLEVEL=info +MAX_TASKS_PER_CHILD=500 +CONCURRENCY=1 diff --git a/pandora/entity/apps.py b/pandora/entity/apps.py new file mode 100644 index 00000000..621c7135 --- /dev/null +++ b/pandora/entity/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EntityConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'entity' diff --git a/pandora/entity/migrations/0007_alter_documentproperties_data_and_more.py b/pandora/entity/migrations/0007_alter_documentproperties_data_and_more.py new file mode 100644 index 00000000..83d9c0bd --- /dev/null +++ b/pandora/entity/migrations/0007_alter_documentproperties_data_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('entity', '0006_auto_20180918_0903'), + ] + + operations = [ + migrations.AlterField( + model_name='documentproperties', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='documentproperties', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='entity', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='entity', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='entity', + name='name_find', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='find', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='link', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/event/apps.py b/pandora/event/apps.py new file mode 100644 index 00000000..ff9d6ab7 --- /dev/null +++ b/pandora/event/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EventConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'event' diff --git a/pandora/event/migrations/0004_alter_event_duration_alter_event_end_alter_event_id_and_more.py b/pandora/event/migrations/0004_alter_event_duration_alter_event_end_alter_event_id_and_more.py new file mode 100644 index 00000000..7d422113 --- /dev/null +++ b/pandora/event/migrations/0004_alter_event_duration_alter_event_end_alter_event_id_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0003_auto_20160304_1644'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='duration', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='event', + name='end', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='event', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='event', + name='name_find', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='event', + name='start', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='event', + name='type', + field=models.CharField(default='', max_length=255), + ), + ] diff --git a/pandora/event/tasks.py b/pandora/event/tasks.py index 234dd5a7..46acdda2 100644 --- a/pandora/event/tasks.py +++ b/pandora/event/tasks.py @@ -1,20 +1,26 @@ # -*- coding: utf-8 -*- -from celery.task import task +from app.celery import app from .models import Event ''' -@periodic_task(run_every=crontab(hour=7, minute=30), queue='encoding') +from celery.schedules import crontab + +@app.task(ignore_results=True, queue='encoding') def update_all_matches(**kwargs): ids = [e['id'] for e in Event.objects.all().values('id')] for i in ids: e = Event.objects.get(pk=i) e.update_matches() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(crontab(hour=7, minute=30), update_all_matches.s()) ''' -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_matches(eventId): event = Event.objects.get(pk=eventId) event.update_matches() diff --git a/pandora/home/apps.py b/pandora/home/apps.py index 90dc7137..8bb98271 100644 --- a/pandora/home/apps.py +++ b/pandora/home/apps.py @@ -2,4 +2,5 @@ from django.apps import AppConfig class HomeConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" name = 'home' diff --git a/pandora/home/migrations/0003_alter_item_data_alter_item_id_alter_item_index.py b/pandora/home/migrations/0003_alter_item_data_alter_item_id_alter_item_index.py new file mode 100644 index 00000000..84328309 --- /dev/null +++ b/pandora/home/migrations/0003_alter_item_data_alter_item_id_alter_item_index.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0002_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='item', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='item', + name='index', + field=models.IntegerField(default=-1), + ), + ] diff --git a/pandora/item/apps.py b/pandora/item/apps.py new file mode 100644 index 00000000..5c6b394d --- /dev/null +++ b/pandora/item/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ItemConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'item' diff --git a/pandora/item/managers.py b/pandora/item/managers.py index 654f1dfe..f7398a0e 100644 --- a/pandora/item/managers.py +++ b/pandora/item/managers.py @@ -33,7 +33,7 @@ def parseCondition(condition, user, owner=None): k = {'id': 'public_id'}.get(k, k) if not k: k = '*' - v = condition['value'] + v = condition.get('value', '') op = condition.get('operator') if not op: op = '=' @@ -62,6 +62,9 @@ def parseCondition(condition, user, owner=None): if k == 'list': key_type = '' + if k in ('width', 'height'): + key_type = 'integer' + if k == 'groups': if op == '==' and v == '$my': if not owner: @@ -86,8 +89,11 @@ def parseCondition(condition, user, owner=None): elif k == 'rendered': return Q(rendered=v) elif k == 'resolution': - q = parseCondition({'key': 'width', 'value': v[0], 'operator': op}, user) \ - & parseCondition({'key': 'height', 'value': v[1], 'operator': op}, user) + if isinstance(v, list) and len(v) == 2: + q = parseCondition({'key': 'width', 'value': v[0], 'operator': op}, user) \ + & parseCondition({'key': 'height', 'value': v[1], 'operator': op}, user) + else: + q = Q(id=0) if exclude: q = ~q return q @@ -318,6 +324,8 @@ class ItemManager(Manager): q |= Q(groups__in=user.groups.all()) rendered_q |= Q(groups__in=user.groups.all()) qs = qs.filter(q) + max_level = len(settings.CONFIG['rightsLevels']) + qs = qs.filter(level__lte=max_level) if settings.CONFIG.get('itemRequiresVideo') and level != 'admin': qs = qs.filter(rendered_q) return qs diff --git a/pandora/item/migrations/0001_initial.py b/pandora/item/migrations/0001_initial.py index ef095583..a92c0e66 100644 --- a/pandora/item/migrations/0001_initial.py +++ b/pandora/item/migrations/0001_initial.py @@ -71,7 +71,7 @@ class Migration(migrations.Migration): ('poster_width', models.IntegerField(default=0)), ('poster_frame', models.FloatField(default=-1)), ('icon', models.ImageField(blank=True, default=None, upload_to=item.models.get_icon_path)), - ('torrent', models.FileField(blank=True, default=None, max_length=1000, upload_to=item.models.get_torrent_path)), + ('torrent', models.FileField(blank=True, default=None, max_length=1000)), ('stream_info', oxdjango.fields.DictField(default={}, editable=False)), ('stream_aspect', models.FloatField(default=1.3333333333333333)), ], diff --git a/pandora/item/migrations/0005_auto_20230710_0852.py b/pandora/item/migrations/0005_auto_20230710_0852.py new file mode 100644 index 00000000..156ef526 --- /dev/null +++ b/pandora/item/migrations/0005_auto_20230710_0852.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.10 on 2023-07-10 08:52 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('item', '0004_json_cache'), + ] + + operations = [ + migrations.RemoveField( + model_name='item', + name='torrent', + ), + ] diff --git a/pandora/item/migrations/0006_id_bigint_jsonfied_and_more.py b/pandora/item/migrations/0006_id_bigint_jsonfied_and_more.py new file mode 100644 index 00000000..dd85858e --- /dev/null +++ b/pandora/item/migrations/0006_id_bigint_jsonfied_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('item', '0005_auto_20230710_0852'), + ] + + operations = [ + migrations.AlterField( + model_name='access', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='annotationsequence', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='description', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='facet', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='item', + name='cache', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='item', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='item', + name='external_data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='item', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='item', + name='stream_info', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='itemfind', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/item/models.py b/pandora/item/models.py index 04ec81dd..be7f3303 100644 --- a/pandora/item/models.py +++ b/pandora/item/models.py @@ -43,7 +43,7 @@ from user.utils import update_groups from user.models import Group import archive.models -logger = logging.getLogger(__name__) +logger = logging.getLogger('pandora.' + __name__) User = get_user_model() @@ -157,9 +157,6 @@ def get_icon_path(f, x): def get_poster_path(f, x): return get_path(f, 'poster.jpg') -def get_torrent_path(f, x): - return get_path(f, 'torrent.torrent') - class Item(models.Model): created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) @@ -185,7 +182,6 @@ class Item(models.Model): icon = models.ImageField(default=None, blank=True, upload_to=get_icon_path) - torrent = models.FileField(default=None, blank=True, max_length=1000, upload_to=get_torrent_path) stream_info = JSONField(default=dict, editable=False) # stream related fields @@ -233,6 +229,9 @@ class Item(models.Model): def editable(self, user): if user.is_anonymous: return False + max_level = len(settings.CONFIG['rightsLevels']) + if self.level > max_level: + return False if user.profile.capability('canEditMetadata') or \ user.is_staff or \ self.user == user or \ @@ -240,7 +239,7 @@ class Item(models.Model): return True return False - def edit(self, data): + def edit(self, data, is_task=False): data = data.copy() # FIXME: how to map the keys to the right place to write them to? if 'id' in data: @@ -257,11 +256,12 @@ class Item(models.Model): description = data.pop(key) if isinstance(description, dict): for value in description: + value = ox.sanitize_html(value) d, created = Description.objects.get_or_create(key=k, value=value) d.description = ox.sanitize_html(description[value]) d.save() else: - value = data.get(k, self.get(k, '')) + value = ox.sanitize_html(data.get(k, self.get(k, ''))) if not description: description = '' d, created = Description.objects.get_or_create(key=k, value=value) @@ -296,7 +296,10 @@ class Item(models.Model): self.data[key] = ox.escape_html(data[key]) p = self.save() if not settings.USE_IMDB and list(filter(lambda k: k in self.poster_keys, data)): - p = tasks.update_poster.delay(self.public_id) + if is_task: + tasks.update_poster(self.public_id) + else: + p = tasks.update_poster.delay(self.public_id) return p def update_external(self): @@ -475,7 +478,8 @@ class Item(models.Model): for a in self.annotations.all().order_by('id'): a.item = other - a.set_public_id() + with transaction.atomic(): + a.set_public_id() Annotation.objects.filter(id=a.id).update(item=other, public_id=a.public_id) try: other_sort = other.sort @@ -519,6 +523,7 @@ class Item(models.Model): cmd, stdout=open('/dev/null', 'w'), stderr=open('/dev/null', 'w'), close_fds=True) p.wait() os.unlink(tmp_output_txt) + os.close(fd) return True else: return None @@ -636,11 +641,11 @@ class Item(models.Model): if self.poster_height: i['posterRatio'] = self.poster_width / self.poster_height - if keys and 'source' in keys: - i['source'] = self.streams().exclude(file__data='').exists() + if keys and 'hasSource' in keys: + i['hasSource'] = self.streams().exclude(file__data='').exists() streams = self.streams() - i['durations'] = [s.duration for s in streams] + i['durations'] = [s[0] for s in streams.values_list('duration')] i['duration'] = sum(i['durations']) i['audioTracks'] = self.audio_tracks() if not i['audioTracks']: @@ -696,10 +701,12 @@ class Item(models.Model): else: values = self.get(key) if values: + values = [ox.sanitize_html(value) for value in values] for d in Description.objects.filter(key=key, value__in=values): i['%sdescription' % key][d.value] = d.description else: - qs = Description.objects.filter(key=key, value=self.get(key, '')) + value = ox.sanitize_html(self.get(key, '')) + qs = Description.objects.filter(key=key, value=value) i['%sdescription' % key] = '' if qs.count() == 0 else qs[0].description if keys: info = {} @@ -1019,10 +1026,14 @@ class Item(models.Model): set_value(s, name, value) elif sort_type == 'person': value = sortNames(self.get(source, [])) + if value is None: + value = '' value = utils.sort_string(value)[:955] set_value(s, name, value) elif sort_type == 'string': value = self.get(source, '') + if value is None: + value = '' if isinstance(value, list): value = ','.join([str(v) for v in value]) value = utils.sort_string(value)[:955] @@ -1198,7 +1209,7 @@ class Item(models.Model): if not r: return False path = video.name - duration = sum(item.cache['durations']) + duration = sum(self.item.cache['durations']) else: path = stream.media.path duration = stream.info['duration'] @@ -1294,90 +1305,6 @@ class Item(models.Model): self.files.filter(selected=True).update(selected=False) self.save() - def get_torrent(self, request): - if self.torrent: - self.torrent.seek(0) - data = ox.torrent.bdecode(self.torrent.read()) - url = request.build_absolute_uri("%s/torrent/" % self.get_absolute_url()) - if url.startswith('https://'): - url = 'http' + url[5:] - data['url-list'] = ['%s%s' % (url, u.split('torrent/')[1]) for u in data['url-list']] - return ox.torrent.bencode(data) - - def make_torrent(self): - if not settings.CONFIG['video'].get('torrent'): - return - streams = self.streams() - if streams.count() == 0: - return - base = self.path('torrent') - base = os.path.abspath(os.path.join(settings.MEDIA_ROOT, base)) - if not isinstance(base, bytes): - base = base.encode('utf-8') - if os.path.exists(base): - shutil.rmtree(base) - ox.makedirs(base) - - filename = utils.safe_filename(ox.decode_html(self.get('title'))) - base = self.path('torrent/%s' % filename) - base = os.path.abspath(os.path.join(settings.MEDIA_ROOT, base)) - size = 0 - duration = 0.0 - if streams.count() == 1: - v = streams[0] - media_path = v.media.path - extension = media_path.split('.')[-1] - url = "%s/torrent/%s.%s" % (self.get_absolute_url(), - quote(filename.encode('utf-8')), - extension) - video = "%s.%s" % (base, extension) - if not isinstance(media_path, bytes): - media_path = media_path.encode('utf-8') - if not isinstance(video, bytes): - video = video.encode('utf-8') - media_path = os.path.relpath(media_path, os.path.dirname(video)) - os.symlink(media_path, video) - size = v.media.size - duration = v.duration - else: - url = "%s/torrent/" % self.get_absolute_url() - part = 1 - ox.makedirs(base) - for v in streams: - media_path = v.media.path - extension = media_path.split('.')[-1] - video = "%s/%s.Part %d.%s" % (base, filename, part, extension) - part += 1 - if not isinstance(media_path, bytes): - media_path = media_path.encode('utf-8') - if not isinstance(video, bytes): - video = video.encode('utf-8') - media_path = os.path.relpath(media_path, os.path.dirname(video)) - os.symlink(media_path, video) - size += v.media.size - duration += v.duration - video = base - - torrent = '%s.torrent' % base - url = "http://%s%s" % (settings.CONFIG['site']['url'], url) - meta = { - 'filesystem_encoding': 'utf-8', - 'target': torrent, - 'url-list': url, - } - if duration: - meta['playtime'] = ox.format_duration(duration*1000)[:-4] - - # slightly bigger torrent file but better for streaming - piece_size_pow2 = 15 # 1 mbps -> 32KB pieces - if size / duration >= 1000000: - piece_size_pow2 = 16 # 2 mbps -> 64KB pieces - meta['piece_size_pow2'] = piece_size_pow2 - - ox.torrent.create_torrent(video, settings.TRACKER_URL, meta) - self.torrent.name = torrent[len(settings.MEDIA_ROOT)+1:] - self.save() - def audio_tracks(self): tracks = [f['language'] for f in self.files.filter(selected=True).filter(Q(is_video=True) | Q(is_audio=True)).values('language') @@ -1385,11 +1312,10 @@ class Item(models.Model): return sorted(set(tracks)) def streams(self, track=None): + files = self.files.filter(selected=True).filter(Q(is_audio=True) | Q(is_video=True)) qs = archive.models.Stream.objects.filter( - source=None, available=True, file__item=self, file__selected=True - ).filter( - Q(file__is_audio=True) | Q(file__is_video=True) - ) + file__in=files, source=None, available=True + ).select_related() if not track: tracks = self.audio_tracks() if len(tracks) > 1: @@ -1428,7 +1354,6 @@ class Item(models.Model): self.select_frame() self.make_poster() self.make_icon() - self.make_torrent() self.rendered = streams.count() > 0 self.save() if self.rendered: @@ -1614,8 +1539,15 @@ class Item(models.Model): cmd += ['-l', timeline] if frame: cmd += ['-f', frame] - p = subprocess.Popen(cmd, close_fds=True) - p.wait() + if settings.ITEM_ICON_DATA: + cmd += '-d', '-' + data = self.json() + data = utils.normalize_dict('NFC', data) + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, close_fds=True) + p.communicate(json.dumps(data, default=to_json).encode('utf-8')) + else: + p = subprocess.Popen(cmd, close_fds=True) + p.wait() # remove cached versions icon = os.path.abspath(os.path.join(settings.MEDIA_ROOT, icon)) for f in glob(icon.replace('.jpg', '*.jpg')): @@ -1627,11 +1559,13 @@ class Item(models.Model): return icon def add_empty_clips(self): + if not settings.EMPTY_CLIPS: + return subtitles = utils.get_by_key(settings.CONFIG['layers'], 'isSubtitles', True) if not subtitles: return # otherwise add empty 5 seconds annotation every minute - duration = sum([s.duration for s in self.streams()]) + duration = sum([s[0] for s in self.streams().values_list('duration')]) layer = subtitles['id'] # FIXME: allow annotations from no user instead? user = User.objects.all().order_by('id')[0] @@ -1880,6 +1814,8 @@ class Description(models.Model): value = models.CharField(max_length=1000, db_index=True) description = models.TextField() + def __str__(self): + return "%s=%s" % (self.key, self.value) class AnnotationSequence(models.Model): item = models.OneToOneField('Item', related_name='_annotation_sequence', on_delete=models.CASCADE) @@ -1895,13 +1831,12 @@ class AnnotationSequence(models.Model): @classmethod def nextid(cls, item): - with transaction.atomic(): - s, created = cls.objects.get_or_create(item=item) - if created: - nextid = s.value - else: - cursor = connection.cursor() - sql = "UPDATE %s SET value = value + 1 WHERE item_id = %s RETURNING value" % (cls._meta.db_table, item.id) - cursor.execute(sql) - nextid = cursor.fetchone()[0] + s, created = cls.objects.get_or_create(item=item) + if created: + nextid = s.value + else: + cursor = connection.cursor() + sql = "UPDATE %s SET value = value + 1 WHERE item_id = %s RETURNING value" % (cls._meta.db_table, item.id) + cursor.execute(sql) + nextid = cursor.fetchone()[0] return "%s/%s" % (item.public_id, ox.toAZ(nextid)) diff --git a/pandora/item/site.py b/pandora/item/site.py index cb186eab..a67871db 100644 --- a/pandora/item/site.py +++ b/pandora/item/site.py @@ -24,10 +24,6 @@ urls = [ re_path(r'^(?P[A-Z0-9].*)/(?P\d+)p(?P\d*)\.(?Pwebm|ogv|mp4)$', views.video), re_path(r'^(?P[A-Z0-9].*)/(?P\d+)p(?P\d*)\.(?P.+)\.(?Pwebm|ogv|mp4)$', views.video), - #torrent - re_path(r'^(?P[A-Z0-9].*)/torrent$', views.torrent), - re_path(r'^(?P[A-Z0-9].*)/torrent/(?P.*?)$', views.torrent), - #export re_path(r'^(?P[A-Z0-9].*)/json$', views.item_json), re_path(r'^(?P[A-Z0-9].*)/xml$', views.item_xml), diff --git a/pandora/item/tasks.py b/pandora/item/tasks.py index a7c0c68a..92b3b08f 100644 --- a/pandora/item/tasks.py +++ b/pandora/item/tasks.py @@ -2,31 +2,35 @@ from datetime import timedelta, datetime from urllib.parse import quote +import xml.etree.ElementTree as ET import gzip import os import random import logging -from celery.task import task, periodic_task +from app.celery import app +from celery.schedules import crontab from django.conf import settings from django.db import connection, transaction from django.db.models import Q -from ox.utils import ET from app.utils import limit_rate from taskqueue.models import Task -logger = logging.getLogger(__name__) +logger = logging.getLogger('pandora.' + __name__) - -@periodic_task(run_every=timedelta(days=1), queue='encoding') +@app.task(queue='encoding') def cronjob(**kwargs): if limit_rate('item.tasks.cronjob', 8 * 60 * 60): update_random_sort() update_random_clip_sort() clear_cache.delay() +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(days=1), cronjob.s()) + def update_random_sort(): from . import models if list(filter(lambda f: f['id'] == 'random', settings.CONFIG['itemKeys'])): @@ -54,7 +58,7 @@ def update_random_clip_sort(): cursor.execute(row) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_clips(public_id): from . import models try: @@ -63,7 +67,7 @@ def update_clips(public_id): return item.clips.all().update(user=item.user.id) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_poster(public_id): from . import models try: @@ -81,7 +85,7 @@ def update_poster(public_id): icon=item.icon.name ) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_file_paths(public_id): from . import models try: @@ -90,7 +94,7 @@ def update_file_paths(public_id): return item.update_file_paths() -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_external(public_id): from . import models try: @@ -99,7 +103,7 @@ def update_external(public_id): return item.update_external() -@task(queue="encoding") +@app.task(queue="encoding") def update_timeline(public_id): from . import models try: @@ -109,7 +113,7 @@ def update_timeline(public_id): item.update_timeline(async_=False) Task.finish(item) -@task(queue="encoding") +@app.task(queue="encoding") def rebuild_timeline(public_id): from . import models i = models.Item.objects.get(public_id=public_id) @@ -117,7 +121,7 @@ def rebuild_timeline(public_id): s.make_timeline() i.update_timeline(async_=False) -@task(queue="encoding") +@app.task(queue="encoding") def load_subtitles(public_id): from . import models try: @@ -130,7 +134,7 @@ def load_subtitles(public_id): item.update_facets() -@task(queue="encoding") +@app.task(queue="encoding") def extract_clip(public_id, in_, out, resolution, format, track=None): from . import models try: @@ -142,7 +146,7 @@ def extract_clip(public_id, in_, out, resolution, format, track=None): return False -@task(queue="encoding") +@app.task(queue="encoding") def clear_cache(days=60): import subprocess path = os.path.join(settings.MEDIA_ROOT, 'media') @@ -156,7 +160,7 @@ def clear_cache(days=60): subprocess.check_output(cmd) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_sitemap(base_url): from . import models sitemap = os.path.abspath(os.path.join(settings.MEDIA_ROOT, 'sitemap.xml.gz')) @@ -356,7 +360,7 @@ def update_sitemap(base_url): f.write(data) -@task(queue='default') +@app.task(queue='default') def bulk_edit(data, username): from django.db import transaction from . import models @@ -367,5 +371,5 @@ def bulk_edit(data, username): if item.editable(user): with transaction.atomic(): item.refresh_from_db() - response = edit_item(user, item, data) + response = edit_item(user, item, data, is_task=True) return {} diff --git a/pandora/item/timelines.py b/pandora/item/timelines.py index f6ff9f72..7e2e12f8 100644 --- a/pandora/item/timelines.py +++ b/pandora/item/timelines.py @@ -71,7 +71,7 @@ def join_tiles(source_paths, durations, target_path): if not w or large_tile_i < large_tile_n - 1: w = 60 data['target_images']['large'] = data['target_images']['large'].resize( - (w, small_tile_h), Image.ANTIALIAS + (w, small_tile_h), Image.LANCZOS ) if data['target_images']['small']: data['target_images']['small'].paste( @@ -90,7 +90,7 @@ def join_tiles(source_paths, durations, target_path): if data['full_tile_widths'][0]: resized = data['target_images']['large'].resize(( data['full_tile_widths'][0], large_tile_h - ), Image.ANTIALIAS) + ), Image.LANCZOS) data['target_images']['full'].paste(resized, (data['full_tile_offset'], 0)) data['full_tile_offset'] += data['full_tile_widths'][0] data['full_tile_widths'] = data['full_tile_widths'][1:] @@ -196,7 +196,7 @@ def join_tiles(source_paths, durations, target_path): #print(image_file) image_file = '%stimeline%s%dp.jpg' % (target_path, full_tile_mode, small_tile_h) data['target_images']['full'].resize( - (full_tile_w, small_tile_h), Image.ANTIALIAS + (full_tile_w, small_tile_h), Image.LANCZOS ).save(image_file) #print(image_file) diff --git a/pandora/item/utils.py b/pandora/item/utils.py index fce7d724..83932967 100644 --- a/pandora/item/utils.py +++ b/pandora/item/utils.py @@ -61,7 +61,7 @@ def sort_title(title): title = sort_string(title) #title - title = re.sub('[\'!¿¡,\.;\-"\:\*\[\]]', '', title) + title = re.sub(r'[\'!¿¡,\.;\-"\:\*\[\]]', '', title) return title.strip() def get_positions(ids, pos, decode_id=False): diff --git a/pandora/item/views.py b/pandora/item/views.py index 62c6b21a..8ad2db2e 100644 --- a/pandora/item/views.py +++ b/pandora/item/views.py @@ -16,12 +16,14 @@ from wsgiref.util import FileWrapper from django.conf import settings from ox.utils import json, ET - -from oxdjango.decorators import login_required_json -from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response -from oxdjango.http import HttpFileResponse import ox +from oxdjango.api import actions +from oxdjango.decorators import login_required_json +from oxdjango.http import HttpFileResponse +from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response +import oxdjango + from . import models from . import utils from . import tasks @@ -32,7 +34,6 @@ from clip.models import Clip from user.models import has_capability from changelog.models import add_changelog -from oxdjango.api import actions def _order_query(qs, sort, prefix='sort__'): @@ -308,7 +309,7 @@ def find(request, data): responsive UI: First leave out `keys` to get totals as fast as possible, then pass `positions` to get the positions of previously selected items, finally make the query with the `keys` you need and an appropriate `range`. - For more examples, see https://wiki.0x2620.org/wiki/pandora/QuerySyntax. + For more examples, see https://code.0x2620.org/0x2620/pandora/wiki/QuerySyntax. see: add, edit, get, lookup, remove, upload ''' if settings.JSON_DEBUG: @@ -533,7 +534,7 @@ def get(request, data): return render_to_json_response(response) actions.register(get) -def edit_item(user, item, data): +def edit_item(user, item, data, is_task=False): data = data.copy() update_clips = False response = json_response(status=200, text='ok') @@ -558,7 +559,7 @@ def edit_item(user, item, data): user_groups = set([g.name for g in user.groups.all()]) other_groups = list(groups - user_groups) data['groups'] = [g for g in data['groups'] if g in user_groups] + other_groups - r = item.edit(data) + r = item.edit(data, is_task=is_task) if r: r.wait() if update_clips: @@ -595,7 +596,7 @@ def add(request, data): if p: p.wait() else: - i.make_poster() + item.make_poster() del data['title'] if data: response = edit_item(request.user, item, data) @@ -948,9 +949,11 @@ def timeline(request, id, size, position=-1, format='jpg', mode=None): if not item.access(request.user): return HttpResponseForbidden() + modes = [t['id'] for t in settings.CONFIG['timelines']] if not mode: mode = 'antialias' - modes = [t['id'] for t in settings.CONFIG['timelines']] + if mode not in modes: + mode = modes[0] if mode not in modes: raise Http404 modes.pop(modes.index(mode)) @@ -1044,27 +1047,6 @@ def download(request, id, resolution=None, format='webm', part=None): response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % quote(filename.encode('utf-8')) return response -def torrent(request, id, filename=None): - item = get_object_or_404(models.Item, public_id=id) - if not item.access(request.user): - return HttpResponseForbidden() - if not item.torrent: - raise Http404 - if not filename or filename.endswith('.torrent'): - response = HttpResponse(item.get_torrent(request), - content_type='application/x-bittorrent') - filename = utils.safe_filename("%s.torrent" % item.get('title')) - response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % quote(filename.encode('utf-8')) - return response - while filename.startswith('/'): - filename = filename[1:] - filename = filename.replace('/../', '/') - filename = item.path('torrent/%s' % filename) - filename = os.path.abspath(os.path.join(settings.MEDIA_ROOT, filename)) - response = HttpFileResponse(filename) - response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % \ - quote(os.path.basename(filename.encode('utf-8'))) - return response def video(request, id, resolution, format, index=None, track=None): resolution = int(resolution) @@ -1286,12 +1268,6 @@ def atom_xml(request): el.text = "1:1" if has_capability(request.user, 'canDownloadVideo'): - if item.torrent: - el = ET.SubElement(entry, "link") - el.attrib['rel'] = 'enclosure' - el.attrib['type'] = 'application/x-bittorrent' - el.attrib['href'] = '%s/torrent/' % page_link - el.attrib['length'] = '%s' % ox.get_torrent_size(item.torrent.path) # FIXME: loop over streams # for s in item.streams().filter(resolution=max(settings.CONFIG['video']['resolutions'])): for s in item.streams().filter(source=None): @@ -1314,12 +1290,15 @@ def atom_xml(request): 'application/atom+xml' ) + def oembed(request): format = request.GET.get('format', 'json') maxwidth = int(request.GET.get('maxwidth', 640)) maxheight = int(request.GET.get('maxheight', 480)) - url = request.GET['url'] + url = request.GET.get('url') + if not url: + raise Http404 parts = urlparse(url).path.split('/') if len(parts) < 2: raise Http404 diff --git a/pandora/itemlist/apps.py b/pandora/itemlist/apps.py new file mode 100644 index 00000000..541419f0 --- /dev/null +++ b/pandora/itemlist/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ItemListConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'itemlist' + diff --git a/pandora/itemlist/migrations/0004_alter_list_description_alter_list_id_and_more.py b/pandora/itemlist/migrations/0004_alter_list_description_alter_list_id_and_more.py new file mode 100644 index 00000000..f4f04ebf --- /dev/null +++ b/pandora/itemlist/migrations/0004_alter_list_description_alter_list_id_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import itemlist.models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('itemlist', '0003_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='list', + name='description', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='list', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='list', + name='poster_frames', + field=oxdjango.fields.JSONField(default=list, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='list', + name='query', + field=oxdjango.fields.JSONField(default=itemlist.models.default_query, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='list', + name='sort', + field=oxdjango.fields.JSONField(default=itemlist.models.get_listsort, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='list', + name='status', + field=models.CharField(default='private', max_length=20), + ), + migrations.AlterField( + model_name='list', + name='type', + field=models.CharField(default='static', max_length=255), + ), + migrations.AlterField( + model_name='listitem', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='position', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/itemlist/models.py b/pandora/itemlist/models.py index ad2cfb14..a7a6f8f4 100644 --- a/pandora/itemlist/models.py +++ b/pandora/itemlist/models.py @@ -26,6 +26,9 @@ def get_icon_path(f, x): return get_path(f, 'icon.jpg') def get_listview(): return settings.CONFIG['user']['ui']['listView'] def get_listsort(): return tuple(settings.CONFIG['user']['ui']['listSort']) +def default_query(): + return {"static": True} + class List(models.Model): class Meta: @@ -38,7 +41,7 @@ class List(models.Model): name = models.CharField(max_length=255) status = models.CharField(max_length=20, default='private') _status = ['private', 'public', 'featured'] - query = JSONField(default=lambda: {"static": True}, editable=False) + query = JSONField(default=default_query, editable=False) type = models.CharField(max_length=255, default='static') description = models.TextField(default='') diff --git a/pandora/itemlist/views.py b/pandora/itemlist/views.py index 04edcc9d..a55f5f04 100644 --- a/pandora/itemlist/views.py +++ b/pandora/itemlist/views.py @@ -84,6 +84,11 @@ def findLists(request, data): for x in data.get('query', {}).get('conditions', []) ) + is_personal = request.user.is_authenticated and any( + (x['key'] == 'user' and x['value'] == request.user.username and x['operator'] == '==') + for x in data.get('query', {}).get('conditions', []) + ) + if is_section_request: qs = query['qs'] if not is_featured and not request.user.is_anonymous: @@ -92,6 +97,9 @@ def findLists(request, data): else: qs = _order_query(query['qs'], query['sort']) + if is_personal and request.user.profile.ui.get('hidden', {}).get('lists'): + qs = qs.exclude(name__in=request.user.profile.ui['hidden']['lists']) + response = json_response() if 'keys' in data: qs = qs[query['range'][0]:query['range'][1]] @@ -234,7 +242,7 @@ def addList(request, data): 'type' and 'view'. see: editList, findLists, getList, removeList, sortLists ''' - data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data.get('name', 'Untitled')).strip() name = data['name'] if not name: name = "Untitled" @@ -412,7 +420,10 @@ def sortLists(request, data): models.Position.objects.filter(section=section, list=l).exclude(id=pos.id).delete() else: for i in ids: - l = get_list_or_404_json(i) + try: + l = get_list_or_404_json(i) + except: + continue pos, created = models.Position.objects.get_or_create(list=l, user=request.user, section=section) if pos.position != position: diff --git a/pandora/log/apps.py b/pandora/log/apps.py new file mode 100644 index 00000000..b7cd1d6a --- /dev/null +++ b/pandora/log/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class LogConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'log' + diff --git a/pandora/log/migrations/0002_alter_log_id_alter_log_url.py b/pandora/log/migrations/0002_alter_log_id_alter_log_url.py new file mode 100644 index 00000000..4fe90645 --- /dev/null +++ b/pandora/log/migrations/0002_alter_log_id_alter_log_url.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('log', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='log', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='log', + name='url', + field=models.CharField(default='', max_length=1000), + ), + ] diff --git a/pandora/log/tasks.py b/pandora/log/tasks.py index 768fd731..bdd9cd1f 100644 --- a/pandora/log/tasks.py +++ b/pandora/log/tasks.py @@ -2,10 +2,15 @@ from datetime import timedelta, datetime -from celery.task import periodic_task +from app.celery import app +from celery.schedules import crontab from . import models -@periodic_task(run_every=timedelta(days=1), queue='encoding') +@app.task(queue='encoding') def cronjob(**kwargs): models.Log.objects.filter(modified__lt=datetime.now()-timedelta(days=30)).delete() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(days=1), cronjob.s()) diff --git a/pandora/log/views.py b/pandora/log/views.py index d8dfa4f4..c0fa7675 100644 --- a/pandora/log/views.py +++ b/pandora/log/views.py @@ -65,7 +65,7 @@ actions.register(removeErrorLogs, cache=False) def parse_query(data, user): query = {} query['range'] = [0, 100] - query['sort'] = [{'key':'name', 'operator':'+'}] + query['sort'] = [{'key': 'modified', 'operator': '-'}] for key in ('keys', 'group', 'list', 'range', 'sort', 'query'): if key in data: query[key] = data[key] diff --git a/pandora/manage.py b/pandora/manage.py index 4800a366..56cc6a7b 100755 --- a/pandora/manage.py +++ b/pandora/manage.py @@ -10,7 +10,8 @@ def activate_venv(base): bin_path = os.path.join(base, 'bin') if bin_path not in old_os_path: os.environ['PATH'] = os.path.join(base, 'bin') + os.pathsep + old_os_path - site_packages = os.path.join(base, 'lib', 'python%s' % sys.version[:3], 'site-packages') + version = '%s.%s' % (sys.version_info.major, sys.version_info.minor) + site_packages = os.path.join(base, 'lib', 'python%s' % version, 'site-packages') prev_sys_path = list(sys.path) import site site.addsitedir(site_packages) diff --git a/pandora/mobile/__init__.py b/pandora/mobile/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/mobile/admin.py b/pandora/mobile/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/pandora/mobile/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/pandora/mobile/apps.py b/pandora/mobile/apps.py new file mode 100644 index 00000000..708babdb --- /dev/null +++ b/pandora/mobile/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MobileConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'mobile' diff --git a/pandora/mobile/migrations/__init__.py b/pandora/mobile/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/mobile/models.py b/pandora/mobile/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/pandora/mobile/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/pandora/mobile/tests.py b/pandora/mobile/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/pandora/mobile/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/pandora/mobile/views.py b/pandora/mobile/views.py new file mode 100644 index 00000000..57a19053 --- /dev/null +++ b/pandora/mobile/views.py @@ -0,0 +1,120 @@ +from django.conf import settings +from django.shortcuts import render + +import ox + + +def index(request, fragment): + from item.models import Item, Annotation + from edit.models import Edit + from document.models import Document + from edit.views import _order_clips + context = {} + parts = fragment.split('/') + if parts[0] in ('document', 'documents'): + type = 'document' + id = parts[1] + page = None + crop = None + if len(parts) == 3: + rect = parts[2].split(',') + if len(rect) == 1: + page = rect[0] + else: + crop = rect + document = Document.objects.filter(id=ox.fromAZ(id)).first() + if document and document.access(request.user): + context['title'] = document.data['title'] + if document.data.get('description'): + context['description'] = document.data['description'] + link = request.build_absolute_uri(document.get_absolute_url()) + # FIXME: get preview image or fragment parse from url + public_id = ox.toAZ(document.id) + if document.extension != 'html': + preview = '/documents/%s/512p.jpg' % public_id + if page: + preview = '/documents/%s/512p%s.jpg' % (public_id, page) + if crop: + preview = '/documents/%s/512p%s.jpg' % (public_id, ','.join(crop)) + context['preview'] = request.build_absolute_uri(preview) + + elif parts[0] == 'edits': + type = 'edit' + id = parts[1] + id = id.split(':') + username = id[0] + name = ":".join(id[1:]) + name = name.replace('_', ' ') + edit = Edit.objects.filter(user__username=username, name=name).first() + if edit and edit.accessible(request.user): + link = request.build_absolute_uri('/m' + edit.get_absolute_url()) + context['title'] = name + context['description'] = edit.description.split('\n\n')[0] + resolution = max(settings.CONFIG['video']['resolutions']) + if len(parts) > 3: + sort = parts[3] + if sort[0] in ('+', '-'): + sort = [{ + 'operator': sort[0], + 'key': sort[1:], + }] + else: + sort = [{ + 'operator': '+', + 'key': sort, + }] + clips = _order_clips(edit, sort) + else: + clips = edit.get_clips(request.user) + + preview = None + + if len(parts) >= 3 and ':' in parts[-1]: + ts = ox.parse_timecode(parts[-1]) + position = 0 + for clip in clips: + c = clip.json() + if ts > position and ts < position + c['duration']: + start = ts - position + c.get('in', 0) + preview = '/%s/%sp%0.03f.jpg' % (clip.item.public_id, resolution, float(start)) + break + position += c['duration'] + if not preview: + clip = clips.first() + if clip: + start = clip.json()['in'] + preview = '/%s/%sp%0.03f.jpg' % (clip.item.public_id, resolution, float(start)) + if preview: + context['preview'] = request.build_absolute_uri(preview) + else: + type = 'item' + id = parts[0] + item = Item.objects.filter(public_id=id).first() + if item and item.access(request.user): + link = request.build_absolute_uri(item.get_absolute_url()) + if len(parts) > 1 and parts[1] in ('editor', 'player'): + parts = [parts[0]] + parts[2:] + if len(parts) > 1: + aid = '%s/%s' % (id, parts[1]) + annotation = Annotation.objects.filter(public_id=aid).first() + if annotation: + inout = [annotation.start, annotation.end] + else: + inout = parts[1] + if '-' in inout: + inout = inout.split('-') + else: + inout = inout.split(',') + print(inout) + if len(inout) == 3: + inout.pop(1) + if len(inout) == 2: + inout = [ox.parse_timecode(p) for p in inout] + context['preview'] = link + '/480p%s.jpg' % inout[0] + else: + context['preview'] = link + '/480p.jpg' + context['title'] = item.get('title') + if context: + context['url'] = request.build_absolute_uri('/m/' + fragment) + context['settings'] = settings + return render(request, "mobile/index.html", context) diff --git a/pandora/news/apps.py b/pandora/news/apps.py new file mode 100644 index 00000000..8ebaec28 --- /dev/null +++ b/pandora/news/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NewsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'news' diff --git a/pandora/news/managers.py b/pandora/news/managers.py index bb1f242d..213d5ee1 100644 --- a/pandora/news/managers.py +++ b/pandora/news/managers.py @@ -11,7 +11,7 @@ keymap = { default_key = 'name' def parseCondition(condition, user): - k = condition.get('key', defauly_key) + k = condition.get('key', default_key) k = keymap.get(k, k) if not k: k = default_key diff --git a/pandora/news/migrations/0002_alter_news_id.py b/pandora/news/migrations/0002_alter_news_id.py new file mode 100644 index 00000000..06cfafd2 --- /dev/null +++ b/pandora/news/migrations/0002_alter_news_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='news', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/oxdjango/fields.py b/pandora/oxdjango/fields.py index daebd27c..6227ffdc 100644 --- a/pandora/oxdjango/fields.py +++ b/pandora/oxdjango/fields.py @@ -5,12 +5,12 @@ import copy from django.db import models from django.utils import datetime_safe -import django.contrib.postgres.fields from django.core.serializers.json import DjangoJSONEncoder from ox.utils import json -class JSONField(django.contrib.postgres.fields.JSONField): + +class JSONField(models.JSONField): def __init__(self, *args, **kwargs): if 'encoder' not in kwargs: diff --git a/pandora/oxdjango/query.py b/pandora/oxdjango/query.py index 46500717..474f894a 100644 --- a/pandora/oxdjango/query.py +++ b/pandora/oxdjango/query.py @@ -37,12 +37,12 @@ class NullsLastQuery(Query): obj.nulls_last = self.nulls_last return obj - def get_compiler(self, using=None, connection=None): + def get_compiler(self, using=None, connection=None, elide_empty=True): if using is None and connection is None: raise ValueError("Need either using or connection") if using: connection = connections[using] - return NullLastSQLCompiler(self, connection, using) + return NullLastSQLCompiler(self, connection, using, elide_empty) class QuerySet(django.db.models.query.QuerySet): diff --git a/pandora/person/apps.py b/pandora/person/apps.py new file mode 100644 index 00000000..100989a3 --- /dev/null +++ b/pandora/person/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class PersonConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'person' + diff --git a/pandora/person/migrations/0003_alter_person_id.py b/pandora/person/migrations/0003_alter_person_id.py new file mode 100644 index 00000000..36e5f691 --- /dev/null +++ b/pandora/person/migrations/0003_alter_person_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0002_auto_20190723_1446'), + ] + + operations = [ + migrations.AlterField( + model_name='person', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/person/tasks.py b/pandora/person/tasks.py index a94ed273..4dcb7bad 100644 --- a/pandora/person/tasks.py +++ b/pandora/person/tasks.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -from celery.task import task from . import models +from app.celery import app -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_itemsort(id): try: p = models.Person.objects.get(pk=id) @@ -13,7 +13,7 @@ def update_itemsort(id): except models.Person.DoesNotExist: pass -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_file_paths(id): from item.models import Item, ItemFind p = models.Person.objects.get(pk=id) diff --git a/pandora/place/apps.py b/pandora/place/apps.py new file mode 100644 index 00000000..6dcfb91a --- /dev/null +++ b/pandora/place/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class PlaceConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'place' + diff --git a/pandora/place/migrations/0003_alter_place_countrycode_alter_place_id_and_more.py b/pandora/place/migrations/0003_alter_place_countrycode_alter_place_id_and_more.py new file mode 100644 index 00000000..66a7c355 --- /dev/null +++ b/pandora/place/migrations/0003_alter_place_countrycode_alter_place_id_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('place', '0002_auto_20160304_1644'), + ] + + operations = [ + migrations.AlterField( + model_name='place', + name='countryCode', + field=models.CharField(db_index=True, default='', max_length=16), + ), + migrations.AlterField( + model_name='place', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='place', + name='name_find', + field=models.TextField(default='', editable=False), + ), + migrations.AlterField( + model_name='place', + name='type', + field=models.CharField(db_index=True, default='', max_length=1000), + ), + ] diff --git a/pandora/place/tasks.py b/pandora/place/tasks.py index 3feb88dd..96926528 100644 --- a/pandora/place/tasks.py +++ b/pandora/place/tasks.py @@ -1,20 +1,26 @@ # -*- coding: utf-8 -*- -from celery.task import task +from app.celery import app from . import models ''' -@periodic_task(run_every=crontab(hour=6, minute=30), queue='encoding') +from celery.schedules import crontab + +@app.task(queue='encoding') def update_all_matches(**kwargs): ids = [p['id'] for p in models.Place.objects.all().values('id')] for i in ids: p = models.Place.objects.get(pk=i) p.update_matches() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(crontab(hour=6, minute=30), update_all_matches.s()) ''' -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_matches(id): place = models.Place.objects.get(pk=id) place.update_matches() diff --git a/pandora/sequence/migrations/0002_alter_sequence_mode.py b/pandora/sequence/migrations/0002_alter_sequence_mode.py new file mode 100644 index 00000000..06ce99db --- /dev/null +++ b/pandora/sequence/migrations/0002_alter_sequence_mode.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sequence', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='sequence', + name='mode', + field=models.IntegerField(choices=[(0, 'shape'), (1, 'color')], default=0), + ), + ] diff --git a/pandora/sequence/migrations/0003_alter_sequence_id.py b/pandora/sequence/migrations/0003_alter_sequence_id.py new file mode 100644 index 00000000..711e7d41 --- /dev/null +++ b/pandora/sequence/migrations/0003_alter_sequence_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-29 11:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sequence', '0002_alter_sequence_mode'), + ] + + operations = [ + migrations.AlterField( + model_name='sequence', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/sequence/tasks.py b/pandora/sequence/tasks.py index b90c0d5a..0747bf8c 100644 --- a/pandora/sequence/tasks.py +++ b/pandora/sequence/tasks.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- from django.db import connection, transaction -from celery.task import task +from app.celery import app import item.models from . import extract -@task(ignore_results=True, queue='encoding') +@app.task(ignore_results=True, queue='encoding') def get_sequences(public_id): from . import models i = item.models.Item.objects.get(public_id=public_id) diff --git a/pandora/settings.py b/pandora/settings.py index b2596015..7268c31c 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -67,7 +67,8 @@ STATICFILES_DIRS = ( STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - #'django.contrib.staticfiles.finders.DefaultStorageFinder', + "sass_processor.finders.CssFinder", + "compressor.finders.CompressorFinder", ) GEOIP_PATH = normpath(join(PROJECT_ROOT, '..', 'data', 'geo')) @@ -123,6 +124,10 @@ INSTALLED_APPS = ( 'django_extensions', 'django_celery_results', + 'django_celery_beat', + 'compressor', + 'sass_processor', + 'app', 'log', 'annotation', @@ -149,6 +154,7 @@ INSTALLED_APPS = ( 'websocket', 'taskqueue', 'home', + 'mobile', ) AUTH_USER_MODEL = 'system.User' @@ -161,13 +167,18 @@ LOGGING = { 'errors': { 'level': 'ERROR', 'class': 'log.utils.ErrorHandler' - } + }, }, 'loggers': { - 'django.request': { + 'django': { 'handlers': ['errors'], 'level': 'ERROR', - 'propagate': True, + 'propagate': False, + }, + 'pandora': { + 'handlers': ['errors'], + 'level': 'ERROR', + 'propagate': False, }, } } @@ -179,10 +190,14 @@ CACHES = { } } +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + AUTH_PROFILE_MODULE = 'user.UserProfile' AUTH_CHECK_USERNAME = True FFMPEG = 'ffmpeg' FFPROBE = 'ffprobe' +USE_VP9 = True FFMPEG_SUPPORTS_VP9 = True FFMPEG_DEBUG = False @@ -204,6 +219,8 @@ CELERY_RESULT_BACKEND = 'django-db' CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TIMEZONE = 'UTC' +CELERY_ENABLE_UTC = True CELERY_BROKER_URL = 'amqp://pandora:box@localhost:5672//pandora' @@ -221,9 +238,6 @@ XACCELREDIRECT = False SITE_CONFIG = join(PROJECT_ROOT, 'config.jsonc') DEFAULT_CONFIG = join(PROJECT_ROOT, 'config.pandora.jsonc') -#used if CONFIG['canDownloadVideo'] is set -TRACKER_URL = "udp://tracker.openbittorrent.com:80" - DATA_SERVICE = '' POSTER_PRECEDENCE = () POSTER_ONLY_PORTRAIT = () @@ -264,6 +278,7 @@ SCRIPT_ROOT = normpath(join(PROJECT_ROOT, '..', 'scripts')) #change script to customize ITEM_POSTER = join(SCRIPT_ROOT, 'poster.py') ITEM_ICON = join(SCRIPT_ROOT, 'item_icon.py') +ITEM_ICON_DATA = False LIST_ICON = join(SCRIPT_ROOT, 'list_icon.py') COLLECTION_ICON = join(SCRIPT_ROOT, 'list_icon.py') @@ -274,9 +289,12 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') DATA_UPLOAD_MAX_MEMORY_SIZE = 32 * 1024 * 1024 +EMPTY_CLIPS = True + #you can ignore things below this line #========================================================================= LOCAL_APPS = [] +LOCAL_URLPATTERNS = [] #load installation specific settings from local_settings.py try: from local_settings import * @@ -303,4 +321,3 @@ except NameError: INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS) - diff --git a/pandora/system/apps.py b/pandora/system/apps.py index 5dc4d64b..b3f493ea 100644 --- a/pandora/system/apps.py +++ b/pandora/system/apps.py @@ -2,4 +2,5 @@ from django.apps import AppConfig class SystemConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" name = 'system' diff --git a/pandora/system/migrations/0004_alter_user_first_name_alter_user_id.py b/pandora/system/migrations/0004_alter_user_first_name_alter_user_id.py new file mode 100644 index 00000000..06f57e29 --- /dev/null +++ b/pandora/system/migrations/0004_alter_user_first_name_alter_user_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0003_field_length'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/system/models.py b/pandora/system/models.py index 2f59135c..cbb4d971 100644 --- a/pandora/system/models.py +++ b/pandora/system/models.py @@ -1,6 +1,6 @@ from django.db import models from django.contrib.auth.models import AbstractUser -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class User(AbstractUser): diff --git a/pandora/taskqueue/apps.py b/pandora/taskqueue/apps.py new file mode 100644 index 00000000..b699dccd --- /dev/null +++ b/pandora/taskqueue/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TaskQueueConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'taskqueue' + diff --git a/pandora/taskqueue/migrations/0002_alter_task_id_alter_task_status.py b/pandora/taskqueue/migrations/0002_alter_task_id_alter_task_status.py new file mode 100644 index 00000000..63627462 --- /dev/null +++ b/pandora/taskqueue/migrations/0002_alter_task_id_alter_task_status.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('taskqueue', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='task', + name='status', + field=models.CharField(default='unknown', max_length=32), + ), + ] diff --git a/pandora/taskqueue/models.py b/pandora/taskqueue/models.py index 214f9a41..1d114a2c 100644 --- a/pandora/taskqueue/models.py +++ b/pandora/taskqueue/models.py @@ -8,10 +8,11 @@ from django.contrib.auth import get_user_model from django.conf import settings from django.db import models from django.db.models import Q -import celery.task.control -import kombu.five import ox +from app.celery import app + + User = get_user_model() def get_tasks(username): @@ -111,7 +112,7 @@ class Task(models.Model): return False def get_job(self): - c = celery.task.control.inspect() + c = app.control.inspect() active = c.active(safe=True) if active: for queue in active: @@ -166,7 +167,7 @@ class Task(models.Model): job = self.get_job() if job: print(job) - r = celery.task.control.revoke(job['id']) + r = app.control.revoke(job['id']) print(r) for f in self.item.files.filter(encoding=True): f.delete() diff --git a/pandora/tasks.conf.in b/pandora/tasks.conf.in new file mode 100644 index 00000000..245a3562 --- /dev/null +++ b/pandora/tasks.conf.in @@ -0,0 +1,3 @@ +LOGLEVEL=info +MAX_TASKS_PER_CHILD=1000 +CONCURRENCY=2 diff --git a/pandora/templates/document.html b/pandora/templates/document.html new file mode 100644 index 00000000..34139cc4 --- /dev/null +++ b/pandora/templates/document.html @@ -0,0 +1,38 @@ + + + + + {{head_title}} + + {% include "baseheader.html" %} + + {%if description %} {%endif%} + + + + + + + {%if description %}{%endif%} + + + + + + diff --git a/pandora/templates/item.html b/pandora/templates/item.html index 6b6e22fd..20a84eb7 100644 --- a/pandora/templates/item.html +++ b/pandora/templates/item.html @@ -22,7 +22,7 @@
-
+
{{title}}
{{description_html|safe}}
{% for c in clips %} diff --git a/pandora/templates/mobile/index.html b/pandora/templates/mobile/index.html new file mode 100644 index 00000000..5cda3d82 --- /dev/null +++ b/pandora/templates/mobile/index.html @@ -0,0 +1,45 @@ +{% load static sass_tags compress %} + + + + + {% if title %} + {{title}} + + + {% endif %} + {%if description %} + + + {%endif%} + {% if preview %} + + + + {% endif %} + {% if url %} + + {% endif %} + + {% compress css file m %} + + + {% endcompress %} + + + +
+ {% compress js file m %} + + + + + + + + + + + {% endcompress %} + + diff --git a/pandora/text/apps.py b/pandora/text/apps.py new file mode 100644 index 00000000..7f181df1 --- /dev/null +++ b/pandora/text/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TextConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'text' + diff --git a/pandora/text/migrations/0002_alter_position_id_alter_text_description_and_more.py b/pandora/text/migrations/0002_alter_position_id_alter_text_description_and_more.py new file mode 100644 index 00000000..d77e3220 --- /dev/null +++ b/pandora/text/migrations/0002_alter_position_id_alter_text_description_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('text', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='position', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='text', + name='description', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='text', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='text', + name='status', + field=models.CharField(default='private', max_length=20), + ), + migrations.AlterField( + model_name='text', + name='text', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='text', + name='type', + field=models.CharField(default='html', max_length=255), + ), + ] diff --git a/pandora/text/views.py b/pandora/text/views.py index 33338675..552db47f 100644 --- a/pandora/text/views.py +++ b/pandora/text/views.py @@ -39,7 +39,7 @@ def addText(request, data): } see: editText, findTexts, getText, removeText, sortTexts ''' - data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data.get('name', 'Untitled')).strip() name = data['name'] if not name: name = "Untitled" diff --git a/pandora/title/apps.py b/pandora/title/apps.py new file mode 100644 index 00000000..d9e33f02 --- /dev/null +++ b/pandora/title/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TitleConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'title' + diff --git a/pandora/title/migrations/0003_alter_title_id.py b/pandora/title/migrations/0003_alter_title_id.py new file mode 100644 index 00000000..dec18542 --- /dev/null +++ b/pandora/title/migrations/0003_alter_title_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('title', '0002_auto_20190723_1446'), + ] + + operations = [ + migrations.AlterField( + model_name='title', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/translation/apps.py b/pandora/translation/apps.py index bb46e697..0e3f39b0 100644 --- a/pandora/translation/apps.py +++ b/pandora/translation/apps.py @@ -2,4 +2,5 @@ from django.apps import AppConfig class TranslationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" name = 'translation' diff --git a/pandora/translation/migrations/0003_alter_translation_id.py b/pandora/translation/migrations/0003_alter_translation_id.py new file mode 100644 index 00000000..4daa151c --- /dev/null +++ b/pandora/translation/migrations/0003_alter_translation_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('translation', '0002_translation_type'), + ] + + operations = [ + migrations.AlterField( + model_name='translation', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/translation/tasks.py b/pandora/translation/tasks.py index 8b0be30b..9e9143d7 100644 --- a/pandora/translation/tasks.py +++ b/pandora/translation/tasks.py @@ -2,17 +2,22 @@ from datetime import timedelta, datetime -from celery.task import task, periodic_task from django.conf import settings from app.utils import limit_rate +from app.celery import app +from celery.schedules import crontab -@periodic_task(run_every=timedelta(days=1), queue='encoding') +@app.task(queue='encoding') def cronjob(**kwargs): if limit_rate('translations.tasks.cronjob', 8 * 60 * 60): load_translations() -@task(ignore_results=True, queue='encoding') +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(days=1), cronjob.s()) + +@app.task(ignore_results=True, queue='encoding') def load_translations(): from .models import load_itemkey_translations, load_translations load_translations() diff --git a/pandora/tv/apps.py b/pandora/tv/apps.py new file mode 100644 index 00000000..66d6f783 --- /dev/null +++ b/pandora/tv/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TvConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'tv' + diff --git a/pandora/tv/migrations/0003_id_bigint.py b/pandora/tv/migrations/0003_id_bigint.py new file mode 100644 index 00000000..08bc4cd1 --- /dev/null +++ b/pandora/tv/migrations/0003_id_bigint.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tv', '0002_auto_20160219_1734'), + ] + + operations = [ + migrations.AlterField( + model_name='channel', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='program', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/tv/models.py b/pandora/tv/models.py index e9778d70..600c06d0 100644 --- a/pandora/tv/models.py +++ b/pandora/tv/models.py @@ -31,23 +31,28 @@ class Channel(models.Model): items = Item.objects.filter(rendered=True, level__lte=cansee, sort__duration__gt=0) if items.count() == 0: return None - program = self.program.order_by('-start') changed = False - while program.count() < 1 or program[0].end < now: - not_played = items.exclude(program__in=self.program.filter(run=self.run)) - not_played_count = not_played.count() - if not_played_count == 0: - self.run += 1 + play_now = program.filter(start__lte=now, end__gt=now).first() + while not play_now or not play_now.next(): + played = self.program.filter(run=self.run) + if played.exists(): + not_played = items.exclude(program__in=self.program.filter(run=self.run)) + not_played_count = not_played.count() + if not_played_count == 0: + self.run += 1 + changed = True + else: changed = True + if changed: not_played = items not_played_count = not_played.count() - if not_played_count > 1: + if not_played_count > 1 and program.exists(): not_played = not_played.exclude(id=program[0].id) not_played_count = not_played.count() item = not_played[randint(0, not_played_count-1)] - if program.count() > 0: - start = program.aggregate(Max('end'))['end__max'] + if program.exists(): + start = program.order_by('-end')[0].end else: start = now p = Program() @@ -58,9 +63,10 @@ class Channel(models.Model): p.channel = self p.save() program = self.program.order_by('-start') + play_now = program.filter(start__lte=now, end__gt=now).first() if changed: self.save() - return program[0] + return play_now def json(self, user): now = datetime.now() @@ -82,6 +88,9 @@ class Program(models.Model): def __str__(self): return "%s %s" % (self.item, self.start) + def next(self): + return self.channel.program.filter(start__gte=self.end).order_by('start').first() + def json(self, user, current=False): item_json = self.item.json() r = { @@ -92,8 +101,6 @@ class Program(models.Model): r['layers'] = self.item.get_layers(user) r['streams'] = [s.file.oshash for s in self.item.streams()] if current: - #requires python2.7 - #r['position'] = (current - self.start).total_seconds() - td = current - self.start - r['position'] = td.seconds + td.days * 24 * 3600 + float(td.microseconds)/10**6 + r['position'] = (current - self.start).total_seconds() + r['next'] = self.next().json(user) return r diff --git a/pandora/tv/tasks.py b/pandora/tv/tasks.py index 20a9df94..35dcf416 100644 --- a/pandora/tv/tasks.py +++ b/pandora/tv/tasks.py @@ -2,17 +2,22 @@ from datetime import datetime, timedelta -from celery.task import periodic_task +from app.celery import app +from celery.schedules import crontab from app.utils import limit_rate from . import models -@periodic_task(run_every=timedelta(days=1), queue='encoding') +@app.task(queue='encoding') def update_program(**kwargs): if limit_rate('tv.tasks.update_program', 8 * 60 * 60): for c in models.Channel.objects.all(): c.update_program() old = datetime.now() - timedelta(days=180) models.Program.objects.filter(created__lt=old).delete() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(days=1), update_program.s()) diff --git a/pandora/urlalias/apps.py b/pandora/urlalias/apps.py new file mode 100644 index 00000000..4a518b48 --- /dev/null +++ b/pandora/urlalias/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class UrlaliasConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'urlalias' + diff --git a/pandora/urlalias/migrations/0002_id_bigint.py b/pandora/urlalias/migrations/0002_id_bigint.py new file mode 100644 index 00000000..0b979d0d --- /dev/null +++ b/pandora/urlalias/migrations/0002_id_bigint.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('urlalias', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='alias', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='idalias', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='layeralias', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='listalias', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/urlalias/views.py b/pandora/urlalias/views.py index 9ed1a36c..541f61d8 100644 --- a/pandora/urlalias/views.py +++ b/pandora/urlalias/views.py @@ -64,7 +64,7 @@ def padma_video(request, url): except: return app.views.index(request) if view: - timecodes = re.compile('(\d{2}:\d{2}:\d{2}\.\d{3})-(\d{2}:\d{2}:\d{2}\.\d{3})').findall(view) + timecodes = re.compile(r'(\d{2}:\d{2}:\d{2}\.\d{3})-(\d{2}:\d{2}:\d{2}\.\d{3})').findall(view) if timecodes: view = ','.join(timecodes[0]) if view: diff --git a/pandora/urls.py b/pandora/urls.py index cd241d66..36af1aa0 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import os +import importlib from django.urls import path, re_path from oxdjango.http import HttpFileResponse @@ -25,6 +26,7 @@ import edit.views import itemlist.views import item.views import item.site +import mobile.views import translation.views import urlalias.views @@ -46,6 +48,7 @@ urlpatterns = [ re_path(r'^collection/(?P.*?)/icon(?P\d*).jpg$', documentcollection.views.icon), re_path(r'^documents/(?P[A-Z0-9]+)/(?P\d*)p(?P[\d,]*).jpg$', document.views.thumbnail), re_path(r'^documents/(?P[A-Z0-9]+)/(?P.*?\.[^\d]{3})$', document.views.file), + re_path(r'^documents/(?P.*?)$', document.views.document), re_path(r'^edit/(?P.*?)/icon(?P\d*).jpg$', edit.views.icon), re_path(r'^list/(?P.*?)/icon(?P\d*).jpg$', itemlist.views.icon), re_path(r'^text/(?P.*?)/icon(?P\d*).jpg$', text.views.icon), @@ -64,6 +67,7 @@ urlpatterns = [ re_path(r'^robots.txt$', app.views.robots_txt), re_path(r'^sitemap.xml$', item.views.sitemap_xml), re_path(r'^sitemap(?P\d+).xml$', item.views.sitemap_part_xml), + re_path(r'm/(?P.*?)$', mobile.views.index), path(r'', item.site.urls), ] #sould this not be enabled by default? nginx should handle those @@ -88,3 +92,17 @@ urlpatterns += [ path(r'', app.views.index), ] +if settings.LOCAL_URLPATTERNS: + patterns = [] + for pattern, fn in settings.LOCAL_URLPATTERNS: + if isinstance(fn, str): + m, f = fn.rsplit('.', 1) + try: + m = importlib.import_module(m) + except ImportError: + logger.error('failed to import urllib module: %s', fn, exc_info=True) + continue + fn = getattr(m, f) + patterns.append(re_path(pattern, fn)) + urlpatterns = patterns + urlpatterns + diff --git a/pandora/user/apps.py b/pandora/user/apps.py new file mode 100644 index 00000000..b21c7aba --- /dev/null +++ b/pandora/user/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'user' + diff --git a/pandora/user/migrations/0005_id_bigint_jsonfield.py b/pandora/user/migrations/0005_id_bigint_jsonfield.py new file mode 100644 index 00000000..b164e387 --- /dev/null +++ b/pandora/user/migrations/0005_id_bigint_jsonfield.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0004_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='sessiondata', + name='info', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='userprofile', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='userprofile', + name='notes', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='userprofile', + name='preferences', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='userprofile', + name='ui', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + ] diff --git a/pandora/user/models.py b/pandora/user/models.py index fba06561..7f0a51a8 100644 --- a/pandora/user/models.py +++ b/pandora/user/models.py @@ -79,7 +79,9 @@ class SessionData(models.Model): self.level = self.user.profile.level self.firstseen = self.user.date_joined if self.user.groups.exists(): - self.groupssort = ''.join([g.name for g in self.user.groups.all()]) + self.groupsort = ''.join( + ox.sorted_strings(self.user.groups.all().values_list('name', flat=True)) + ).lower() else: self.groupssort = None self.numberoflists = self.user.lists.count() diff --git a/pandora/user/tasks.py b/pandora/user/tasks.py index c1da304c..ad3869c8 100644 --- a/pandora/user/tasks.py +++ b/pandora/user/tasks.py @@ -4,17 +4,22 @@ from datetime import timedelta from itertools import zip_longest import json -from celery.task import task, periodic_task +from celery.schedules import crontab +from app.celery import app from app.utils import limit_rate from app.models import Settings from .statistics import Statistics -@periodic_task(run_every=timedelta(hours=1), queue='encoding') +@app.task(queue='encoding') def cronjob(**kwargs): if limit_rate('user.tasks.cronjob', 30 * 60): update_statistics() +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(hours=1), cronjob.s()) + def update_statistics(): from . import models @@ -31,7 +36,7 @@ def update_statistics(): stats.add(u.json()) Settings.set('statistics', stats) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def parse_data(key): from . import models try: @@ -41,7 +46,7 @@ def parse_data(key): session_data.parse_data() session_data.save() -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_numberoflists(username): from . import models user = models.User.objects.get(username=username) @@ -51,7 +56,7 @@ def update_numberoflists(username): numberoflists=user.lists.count() ) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_numberofcollections(username): from . import models user = models.User.objects.get(username=username) diff --git a/pandora/user/urls.py b/pandora/user/urls.py deleted file mode 100644 index 429e1da9..00000000 --- a/pandora/user/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.conf.urls.defaults import * - - -urlpatterns = patterns("user.views", - (r'^preferences', 'preferences'), - (r'^login', 'login'), - (r'^logout', 'logout'), - (r'^register', 'register'), -) - diff --git a/pandora/user/utils.py b/pandora/user/utils.py index 5328a464..db557dfe 100644 --- a/pandora/user/utils.py +++ b/pandora/user/utils.py @@ -24,7 +24,7 @@ def get_location(ip): location = g.city(ip) except: try: - location = g.country(s.ip) + location = g.country(ip) except: location = None if location: diff --git a/pandora/user/views.py b/pandora/user/views.py index aa452cec..a2a678f1 100644 --- a/pandora/user/views.py +++ b/pandora/user/views.py @@ -914,7 +914,7 @@ def addGroup(request, data): created = False n = 1 name = data['name'] - _name = re.sub(' \[\d+\]$', '', name).strip() + _name = re.sub(r' \[\d+\]$', '', name).strip() while not created: g, created = Group.objects.get_or_create(name=name) n += 1 @@ -946,7 +946,7 @@ def editGroup(request, data): name = data['name'] n = 1 name = data['name'] - _name = re.sub(' \[\d+\]$', '', name).strip() + _name = re.sub(r' \[\d+\]$', '', name).strip() while Group.objects.filter(name=name).count(): n += 1 name = '%s [%d]' % (_name, n) diff --git a/pandora/websocket/__init__.py b/pandora/websocket/__init__.py index e012f868..ef74deed 100644 --- a/pandora/websocket/__init__.py +++ b/pandora/websocket/__init__.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -from celery.execute import send_task from django.conf import settings +from app.celery import app key = 'websocket' def trigger_event(event, data): if settings.WEBSOCKET: - send_task('trigger_event', [event, data], exchange=key, routing_key=key) + app.send_task('trigger_event', [event, data], exchange=key, routing_key=key) diff --git a/pandora/websocket/apps.py b/pandora/websocket/apps.py new file mode 100644 index 00000000..63053af1 --- /dev/null +++ b/pandora/websocket/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class WebsocketConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'websocket' + diff --git a/requirements.txt b/requirements.txt index b03274a2..b1ef09f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,22 @@ -Django==3.0.10 -simplejson +Django==4.2.7 chardet -celery<5.0,>4.3 -django-celery-results -django-extensions==2.2.9 +celery==5.3.5 +django-celery-results==2.5.1 +django-celery-beat==2.5.0 +django-extensions==3.2.3 +libsass +django-compressor +django-sass-processor gunicorn==20.0.4 html5lib requests<3.0.0,>=2.24.0 urllib3<2.0.0,>=1.25.2 -tornado<5 -geoip2==4.1.0 -youtube-dl>=2021.4.26 +tornado==6.3.3 +geoip2==4.7.0 +yt-dlp>=2023.11.16 python-memcached -elasticsearch +elasticsearch<8 future +pytz +pypdfium2 +Pillow>=10 diff --git a/scripts/item_icon.pandora.py b/scripts/item_icon.pandora.py index 4f4b1e35..c069bbbb 100755 --- a/scripts/item_icon.pandora.py +++ b/scripts/item_icon.pandora.py @@ -20,11 +20,11 @@ def render_icon(frame, timeline, icon): frame_image = Image.open(frame) if frame else Image.new('RGB', (1024, 768), (0, 0, 0)) frame_image_ratio = frame_image.size[0] / frame_image.size[1] if frame_ratio < frame_image_ratio: - frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.ANTIALIAS) + frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.LANCZOS) left = int((frame_image.size[0] - frame_width) / 2) frame_image = frame_image.crop((left, 0, left + frame_width, frame_height)) else: - frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.ANTIALIAS) + frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.LANCZOS) top = int((frame_image.size[1] - frame_height) / 2) frame_image = frame_image.crop((0, top, frame_width, top + frame_height)) icon_image.paste(frame_image, (0, 0)) @@ -36,7 +36,7 @@ def render_icon(frame, timeline, icon): mask_image = Image.open(os.path.join(static_root, 'iconTimelineOuterMask.png')) icon_image.paste(timeline_image, (timeline_left - 4, timeline_top - 4), mask=mask_image) timeline_image = Image.open(timeline) if timeline else Image.new('RGB', (timeline_width, timeline_height), (0, 0, 0)) - timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.ANTIALIAS) + timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.LANCZOS) mask_image = Image.open(os.path.join(static_root, 'iconTimelineInnerMask.png')) icon_image.paste(timeline_image, (timeline_left, timeline_top), mask=mask_image) # we're using jpegs with border-radius diff --git a/scripts/list_icon.pandora.py b/scripts/list_icon.pandora.py index 8c930b7d..3d0fbbf1 100755 --- a/scripts/list_icon.pandora.py +++ b/scripts/list_icon.pandora.py @@ -24,11 +24,11 @@ def render_list_icon(frames, icon): frame_image_ratio = frame_image.size[0] / frame_image.size[1] frame_width_ = frame_width + (1 if i % 2 == 1 else 0) if frame_ratio < frame_image_ratio: - frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.ANTIALIAS) + frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.LANCZOS) left = int((frame_image.size[0] - frame_width_) / 2) frame_image = frame_image.crop((left, 0, left + frame_width_, frame_height)) else: - frame_image = frame_image.resize((frame_width_, int(frame_width_ / frame_image_ratio)), Image.ANTIALIAS) + frame_image = frame_image.resize((frame_width_, int(frame_width_ / frame_image_ratio)), Image.LANCZOS) top = int((frame_image.size[1] - frame_height) / 2) frame_image = frame_image.crop((0, top, frame_width_, top + frame_height)) icon_image.paste(frame_image, (i % 2 * frame_width + (1 if i % 2 == 2 else 0), diff --git a/scripts/poster.0xdb.py b/scripts/poster.0xdb.py index 9b9f2470..7666702a 100755 --- a/scripts/poster.0xdb.py +++ b/scripts/poster.0xdb.py @@ -64,11 +64,11 @@ def render_poster(data, poster): frame_image = Image.open(frame) frame_image_ratio = frame_image.size[0] / frame_image.size[1] if frame_ratio < frame_image_ratio: - frame_image = frame_image.resize((int(frame_size[1] * frame_image_ratio), frame_size[1]), Image.ANTIALIAS) + frame_image = frame_image.resize((int(frame_size[1] * frame_image_ratio), frame_size[1]), Image.LANCZOS) left = int((frame_image.size[0] - frame_size[0]) / 2) frame_image = frame_image.crop((left, 0, left + frame_size[0], frame_size[1])) else: - frame_image = frame_image.resize((frame_size[0], int(frame_size[0] / frame_image_ratio)), Image.ANTIALIAS) + frame_image = frame_image.resize((frame_size[0], int(frame_size[0] / frame_image_ratio)), Image.LANCZOS) top = int((frame_image.size[1] - frame_size[1]) / 2) frame_image = frame_image.crop((0, top, frame_size[0], top + frame_size[1])) poster_image.paste(frame_image, (0, 0)) @@ -77,7 +77,7 @@ def render_poster(data, poster): # logo logo_image = Image.open(os.path.join(static_root, 'logo.0xdb.png')) - logo_image = logo_image.resize(logo_size, Image.ANTIALIAS) + logo_image = logo_image.resize(logo_size, Image.LANCZOS) for y in range(logo_size[1]): for x in range(logo_size[0]): poster_color = poster_image.getpixel((margin + x, margin + y)) @@ -95,11 +95,11 @@ def render_poster(data, poster): if small_frame_image: small_frame_image_ratio = small_frame_image.size[0] / small_frame_image.size[1] if small_frame_ratio < small_frame_image_ratio: - small_frame_image = small_frame_image.resize((int(small_frame_size[1] * small_frame_image_ratio), small_frame_size[1]), Image.ANTIALIAS) + small_frame_image = small_frame_image.resize((int(small_frame_size[1] * small_frame_image_ratio), small_frame_size[1]), Image.LANCZOS) left = int((small_frame_image.size[0] - small_frame_size[0]) / 2) small_frame_image = small_frame_image.crop((left, 0, left + small_frame_size[0], small_frame_size[1])) else: - small_frame_image = small_frame_image.resize((small_frame_size[0], int(small_frame_size[0] / small_frame_image_ratio)), Image.ANTIALIAS) + small_frame_image = small_frame_image.resize((small_frame_size[0], int(small_frame_size[0] / small_frame_image_ratio)), Image.LANCZOS) top = int((small_frame_image.size[1] - small_frame_size[1]) / 2) small_frame_image = small_frame_image.crop((0, top, small_frame_size[0], top + small_frame_size[1])) poster_image.paste(small_frame_image, (i * small_frame_size[0], frame_size[1])) @@ -189,7 +189,7 @@ def render_poster(data, poster): # timeline if timeline: timeline_image = Image.open(timeline) - timeline_image = timeline_image.resize(timeline_size, Image.ANTIALIAS) + timeline_image = timeline_image.resize(timeline_size, Image.LANCZOS) poster_image.paste(timeline_image, (0, poster_size[1] - timeline_size[1])) else: draw.rectangle(((0, poster_size[1] - timeline_size[1]), poster_size), fill=image_color) diff --git a/scripts/poster.indiancinema.py b/scripts/poster.indiancinema.py index bc37fca5..9146af31 100755 --- a/scripts/poster.indiancinema.py +++ b/scripts/poster.indiancinema.py @@ -160,7 +160,7 @@ def render_poster(data, poster): if frame_ratio < frame_image_ratio: frame_image = frame_image.resize( (int(frame_height * frame_image_ratio), frame_height), - Image.ANTIALIAS + Image.LANCZOS ) left = int((frame_image.size[0] - poster_width) / 2) frame_image = frame_image.crop( @@ -169,7 +169,7 @@ def render_poster(data, poster): else: frame_image = frame_image.resize( (poster_width, int(poster_width / frame_image_ratio)), - Image.ANTIALIAS + Image.LANCZOS ) top = int((frame_image.size[1] - frame_height) / 2) frame_image = frame_image.crop( @@ -187,7 +187,7 @@ def render_poster(data, poster): timeline_image = Image.open(timeline) timeline_image = timeline_image.resize( (poster_width, timeline_height), - Image.ANTIALIAS + Image.LANCZOS ) poster_image.paste(timeline_image, (0, poster_height - timeline_height)) else: diff --git a/scripts/poster.padma.py b/scripts/poster.padma.py index 015aaa9b..bf949801 100755 --- a/scripts/poster.padma.py +++ b/scripts/poster.padma.py @@ -33,7 +33,7 @@ def render_poster(data, poster): timeline_lines = 16 if timeline: timeline_image = Image.open(timeline) - timeline_image = timeline_image.resize((10240, timeline_height), Image.ANTIALIAS) + timeline_image = timeline_image.resize((10240, timeline_height), Image.LANCZOS) for i in range(timeline_lines): line_image = timeline_image.crop((i * poster_width, 0, (i + 1) * poster_width, 64)) poster_image.paste(line_image, (0, i * timeline_height)) diff --git a/scripts/poster.pandora.py b/scripts/poster.pandora.py index 5f196562..eef8cdac 100755 --- a/scripts/poster.pandora.py +++ b/scripts/poster.pandora.py @@ -62,11 +62,11 @@ def render_poster(data, poster): frame_image = Image.open(frame) frame_image_ratio = frame_image.size[0] / frame_image.size[1] if frame_ratio < frame_image_ratio: - frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.ANTIALIAS) + frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.LANCZOS) left = int((frame_image.size[0] - frame_width) / 2) frame_image = frame_image.crop((left, 0, left + frame_width, frame_height)) else: - frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.ANTIALIAS) + frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.LANCZOS) top = int((frame_image.size[1] - frame_height) / 2) frame_image = frame_image.crop((0, top, frame_width, top + frame_height)) poster_image.paste(frame_image, (0, 0)) @@ -76,7 +76,7 @@ def render_poster(data, poster): timeline_height = 64 if timeline: timeline_image = Image.open(timeline) - timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.ANTIALIAS) + timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.LANCZOS) poster_image.paste(timeline_image, (0, frame_height)) # text @@ -115,7 +115,7 @@ def render_poster(data, poster): logo_height = 32 logo_image = Image.open(os.path.join(static_root, '..', '..', 'static', 'png', 'logo.png')) logo_width = int(round(logo_height * logo_image.size[0] / logo_image.size[1])) - logo_image = logo_image.resize((logo_width, logo_height), Image.ANTIALIAS) + logo_image = logo_image.resize((logo_width, logo_height), Image.LANCZOS) logo_left = text_width - text_margin - logo_width logo_top = text_bottom - text_margin - logo_height for y in range(logo_height): diff --git a/static/css/pandora.css b/static/css/pandora.css new file mode 100644 index 00000000..5a99c177 --- /dev/null +++ b/static/css/pandora.css @@ -0,0 +1,6 @@ +.InlineImages img { + float: left; + max-width: 256px; + max-height: 256px; + margin: 0 16px 16px 0; +} diff --git a/static/js/PDFViewer.js b/static/js/PDFViewer.js index 5404a423..d128f26e 100644 --- a/static/js/PDFViewer.js +++ b/static/js/PDFViewer.js @@ -59,7 +59,7 @@ Ox.PDFViewer = function(options, self) { .attr({ frameborder: 0, height: self.options.height + 'px', - src: self.options.pdfjsURL + '?file=' + encodeURIComponent(self.options.url) + '#page=' + self.options.page, + src: self.options.pdfjsURL + '?' + pandora.getVersion() + '&file=' + encodeURIComponent(self.options.url) + '#page=' + self.options.page, width: self.options.width + 'px' }) .onMessage(function(data, event) { diff --git a/static/js/URL.js b/static/js/URL.js index 4a08db11..82480f96 100644 --- a/static/js/URL.js +++ b/static/js/URL.js @@ -399,13 +399,14 @@ pandora.URL = (function() { // Documents views['documents'] = { - list: ['grid', 'list'], - item: ['view', 'info'] + list: ['grid', 'list', 'pages'], + item: ['view', 'info', 'data'] }; sortKeys['documents'] = { list: { list: pandora.site.documentKeys, - grid: pandora.site.documentKeys + grid: pandora.site.documentKeys, + pages: pandora.site.documentKeys }, item: {} }; diff --git a/static/js/apiDialog.js b/static/js/apiDialog.js index 21f6690d..8e7566ce 100644 --- a/static/js/apiDialog.js +++ b/static/js/apiDialog.js @@ -190,9 +190,9 @@ pandora.ui.apiDialog = function() { + '

If you are using the API in JavaScript, you may want to' + ' take a look at ' + ' OxJS, and if you are using Python, there is' - + ' ' + + ' ' + ' python-ox, which is used by' - + ' ' + + ' ' + ' pandora_client to automate certain tasks.

\n' + '

To get started, just open the console and paste the' + ' following snippet. For the first ten items that are' diff --git a/static/js/clipList.js b/static/js/clipList.js index e0c5ae8e..c623de09 100644 --- a/static/js/clipList.js +++ b/static/js/clipList.js @@ -12,6 +12,7 @@ pandora.ui.clipList = function(videoRatio) { fixedRatio: fixedRatio, item: function(data, sort, size) { size = size || 128; // fixme: is this needed? + data.videoRatio = data.videoRatio || pandora.site.video.previewRatio; var ratio, width, height, format, info, sortKey, title, url; if (!ui.item) { @@ -75,7 +76,14 @@ pandora.ui.clipList = function(videoRatio) { var itemsQuery, query; if (!ui.item) { itemsQuery = ui.find; - query = {conditions: [], operator: itemsQuery.operator}; + query = { + conditions: [{ + key: "layer", + operator: "&", + value: pandora.site.clipLayers + }], + operator: itemsQuery.operator + }; // if the item query contains a layer condition, // then this condition is added to the clip query addConditions(query, itemsQuery.conditions); @@ -98,13 +106,20 @@ pandora.ui.clipList = function(videoRatio) { operator: '&' }; query = { - conditions: ui.itemFind === '' ? [] : [{ - key: 'annotations', - value: ui.itemFind, - operator: '=' + conditions: [{ + key: "layer", + operator: "&", + value: pandora.site.clipLayers }], operator: '&' }; + if(ui.itemFind) { + query.conditions.push({ + key: 'annotations', + value: ui.itemFind, + operator: '=' + }) + } findClips(); } diff --git a/static/js/collection.js b/static/js/collection.js index 5b188f7f..1dc50e73 100644 --- a/static/js/collection.js +++ b/static/js/collection.js @@ -124,6 +124,68 @@ pandora.ui.collection = function() { unique: 'id' }) .addClass('OxMedia'); + } else if (view == 'pages') { + that = Ox.InfoList({ + borderRadius: 0, + defaultRatio: 640/1024, + draggable: true, + id: 'list', + item: function(data, sort, size) { + size = 128; + var sortKey = sort[0].key, + infoKey = sortKey == 'title' ? 'extension' : sortKey, + key = Ox.getObjectById(pandora.site.documentKeys, infoKey), + info = pandora.formatDocumentKey(key, data, size); + return { + icon: { + height: Math.round(data.ratio > 1 ? size / data.ratio : size), + id: data.id, + info: info, + title: data.title, + url: pandora.getMediaURL('/documents/' + data.id + '/256p.jpg?' + data.modified), + width: Math.round(data.ratio >= 1 ? size : size * data.ratio) + }, + info: { + css: {marginTop: '2px'}, + element: pandora.ui.documentPages, + id: data.id, + options: { + id: data.id, + pages: data.pages, + query: ui.findDocuments, + ratio: data.ratio, + title: data.title + } + } + }; + }, + items: function(data, callback) { + pandora.api.findDocuments(Ox.extend(data, { + query: ui.findDocuments + }), callback); + return Ox.clone(data, true); + }, + keys: ['pages'].concat(keys), + selected: ui.listSelection, + size: 192, + sort: ui.collectionSort.concat([ + {key: 'extension', operator: '+'}, + {key: 'title', operator: '+'} + ]), + unique: 'id', + width: window.innerWidth + - ui.showSidebar * ui.sidebarSize - 1 + - Ox.UI.SCROLLBAR_SIZE + }) + .addClass('OxMedia') + .bindEvent({ + key_left: function() { + // ... + }, + key_right: function() { + // ... + } + }); } if (['list', 'grid'].indexOf(view) > -1) { @@ -138,7 +200,7 @@ pandora.ui.collection = function() { }); } - if (['list', 'grid'].indexOf(view) > -1) { + if (['list', 'grid', 'pages'].indexOf(view) > -1) { //fixme diff --git a/static/js/deleteDocumentDialog.js b/static/js/deleteDocumentDialog.js index 05d8f55e..f8dabbe4 100644 --- a/static/js/deleteDocumentDialog.js +++ b/static/js/deleteDocumentDialog.js @@ -3,6 +3,13 @@ pandora.ui.deleteDocumentDialog = function(files, callback) { var string = Ox._(files.length == 1 ? 'Document' : 'Documents'), + $content = Ox.Element().html( + files.length == 1 + ? Ox._('Are you sure you want to delete the document "{0}"?', [files[0].title + '.' + files[0].extension]) + : Ox._('Are you sure you want to delete {0} documents?', [files.length]) + ).css({ + overflow: 'hidden', + }), that = pandora.ui.iconDialog({ buttons: [ @@ -28,15 +35,12 @@ pandora.ui.deleteDocumentDialog = function(files, callback) { } }) ], - content: files.length == 1 - ? Ox._('Are you sure you want to delete the document "{0}"?', [files[0].title + '.' + files[0].extension]) - : Ox._('Are you sure you want to delete {0} documents?', [files.length]), + content: $content, keys: {enter: 'delete', escape: 'keep'}, title: files.length == 1 ? Ox._('Delete {0}', [string]) : Ox._('Delete {0} Documents', [files.length]) }); - return that; }; diff --git a/static/js/document.js b/static/js/document.js index 22fee0a0..143b5bf7 100644 --- a/static/js/document.js +++ b/static/js/document.js @@ -52,6 +52,13 @@ pandora.ui.document = function() { $content.replaceWith( $content = pandora.ui.documentInfoView(result.data) ); + } else if (pandora.user.ui.documentView == 'data') { + $content.replaceWith( + $content = Ox.TreeList({ + data: result.data, + width: pandora.$ui.mainPanel.size(1) - Ox.UI.SCROLLBAR_SIZE + }) + ); } else { setContent(); } @@ -95,6 +102,13 @@ pandora.ui.document = function() { {position: $content.getArea().map(Math.round)} ); }, + embed: function(data) { + var id = item.id; + pandora.$ui.embedDocumentDialog = pandora.ui.embedDocumentDialog( + id, + data.page + ).open(); + }, key_escape: function() { // ... }, diff --git a/static/js/documentContentPanel.js b/static/js/documentContentPanel.js index b1bbc5d0..a5b0f32c 100644 --- a/static/js/documentContentPanel.js +++ b/static/js/documentContentPanel.js @@ -37,9 +37,6 @@ pandora.ui.documentContentPanel = function() { orientation: 'vertical' }) .bindEvent({ - resize: function(data) { - Ox.print('split resize'); - }, pandora_document: function(data) { if (data.value && data.previousValue) { that.replaceElement(1, pandora.$ui.document = pandora.ui.document()); diff --git a/static/js/documentDialog.js b/static/js/documentDialog.js index 89bc072b..f993be94 100644 --- a/static/js/documentDialog.js +++ b/static/js/documentDialog.js @@ -222,6 +222,13 @@ pandora.ui.documentDialog = function(options) { {position: $content.getArea().map(Math.round)} ); }, + embed: function(data) { + var id = options.items[options.index].id; + pandora.$ui.embedDocumentDialog = pandora.ui.embedDocumentDialog( + id, + data.page + ).open(); + }, key_escape: function() { pandora.$ui.documentDialog.close(); }, diff --git a/static/js/documentFilter.js b/static/js/documentFilter.js index ab7f39fe..c98ea3a0 100644 --- a/static/js/documentFilter.js +++ b/static/js/documentFilter.js @@ -189,7 +189,9 @@ pandora.ui.documentFilter = function(id) { {id: 'clearFilters', title: Ox._('Clear All Filters'), keyboard: 'shift alt control a'}, {}, {group: 'filter', max: 1, min: 1, items: pandora.site.documentFilters.map(function(filter) { - return Ox.extend({checked: filter.id == id}, filter); + return Ox.extend({checked: filter.id == id}, filter, { + title: Ox._(filter.title) + }); })} ], type: 'image', diff --git a/static/js/documentInfoView.js b/static/js/documentInfoView.js index bb69563e..f526d7a3 100644 --- a/static/js/documentInfoView.js +++ b/static/js/documentInfoView.js @@ -34,7 +34,7 @@ pandora.ui.documentInfoView = function(data, isMixed) { 'extension', 'dimensions', 'size', 'matches', 'created', 'modified', 'accessed', 'random', 'entity' - ], + ].concat(pandora.site.documentKeys.filter(key => { return key.fulltext }).map(key => key.id)), statisticsWidth = 128, $bar = Ox.Bar({size: 16}) @@ -251,16 +251,11 @@ pandora.ui.documentInfoView = function(data, isMixed) { if (canEdit || data.description) { $('

') + .addClass("InlineImages") .append( Ox.EditableContent({ clickLink: pandora.clickLink, editable: canEdit, - format: function(value) { - return value.replace( - /' + value + '' + ? '' + value + '' : value; }).join(', '); } @@ -559,6 +554,7 @@ pandora.ui.documentInfoView = function(data, isMixed) { $('').html(formatKey(key)).appendTo($element); Ox.EditableContent({ clickLink: pandora.clickLink, + editable: canEdit, format: function(value) { return formatValue(key, value); }, diff --git a/static/js/documentInfoView.padma.js b/static/js/documentInfoView.padma.js new file mode 100644 index 00000000..270106b8 --- /dev/null +++ b/static/js/documentInfoView.padma.js @@ -0,0 +1,896 @@ +'use strict'; + +pandora.ui.documentInfoView = function(data, isMixed) { + isMixed = isMixed || {}; + + var ui = pandora.user.ui, + isMultiple = arguments.length == 2, + canEdit = pandora.hasCapability('canEditMetadata') || isMultiple || data.editable, + canRemove = pandora.hasCapability('canRemoveItems'), + css = { + marginTop: '4px', + textAlign: 'justify' + }, + html, + descriptions = [], + iconRatio = data.ratio, + iconSize = isMultiple ? 0 : ui.infoIconSize, + iconWidth = isMultiple ? 0 : iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio), + iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio), + iconLeft = iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0, + margin = 16, + nameKeys = pandora.site.documentKeys.filter(function(key) { + return key.sortType == 'person'; + }).map(function(key) { + return key.id; + }), + listKeys = pandora.site.documentKeys.filter(function(key) { + return Ox.isArray(key.type); + }).map(function(key){ + return key.id; + }), + linkKeys = [ + 'type', 'publisher', 'source', 'project' + ], + displayedKeys = [ // FIXME: can tis be a flag in the config? + 'title', 'notes', 'name', 'description', 'id', + 'user', 'rightslevel', 'timesaccessed', + 'extension', 'dimensions', 'size', 'matches', + 'created', 'modified', 'accessed', + 'random', 'entity', + 'content', 'translation' + ].concat(pandora.site.documentKeys.filter(key => { return key.fulltext }).map(key => key.id)), + statisticsWidth = 128, + + $bar = Ox.Bar({size: 16}) + .bindEvent({ + doubleclick: function(e) { + if ($(e.target).is('.OxBar')) { + $info.animate({scrollTop: 0}, 250); + } + } + }), + + $options = Ox.MenuButton({ + items: [ + { + id: 'delete', + title: Ox._('Delete {0}...', [Ox._('Document')]), + disabled: !canRemove + } + ], + style: 'square', + title: 'set', + tooltip: Ox._('Options'), + type: 'image', + }) + .css({ + float: 'left', + borderColor: 'rgba(0, 0, 0, 0)', + background: 'rgba(0, 0, 0, 0)' + }) + .bindEvent({ + click: function(data_) { + if (data_.id == 'delete') { + pandora.ui.deleteDocumentDialog( + [data], + function() { + Ox.Request.clearCache(); + if (ui.document) { + pandora.UI.set({document: ''}); + } else { + pandora.$ui.list.reloadList() + } + } + ).open(); + } + } + }) + .appendTo($bar), + + $edit = Ox.MenuButton({ + items: [ + { + id: 'insert', + title: Ox._('Insert HTML...'), + disabled: true + } + ], + style: 'square', + title: 'edit', + tooltip: Ox._('Edit'), + type: 'image', + }) + .css({ + float: 'right', + borderColor: 'rgba(0, 0, 0, 0)', + background: 'rgba(0, 0, 0, 0)' + }) + .bindEvent({ + click: function(data) { + // ... + } + }) + .appendTo($bar), + + $info = Ox.Element().css({overflowY: 'auto'}), + + that = Ox.SplitPanel({ + elements: [ + {element: $bar, size: isMultiple ? 0 : 16}, + {element: $info} + ], + orientation: 'vertical' + }), + + $left = Ox.Element() + .css({ + position: 'absolute' + }) + .appendTo($info); + + + if (!isMultiple) { + var $icon = Ox.Element({ + element: '', + }) + .attr({ + src: '/documents/' + data.id + '/512p.jpg?' + data.modified + }) + .css({ + position: 'absolute', + left: margin + iconLeft + 'px', + top: margin + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px' + }) + .bindEvent({ + singleclick: toggleIconSize + }) + .appendTo($left), + + $reflection = $('
') + .addClass('OxReflection') + .css({ + position: 'absolute', + left: margin + 'px', + top: margin + iconHeight + 'px', + width: iconSize + 'px', + height: iconSize / 2 + 'px', + overflow: 'hidden' + }) + .appendTo($left), + + $reflectionIcon = $('') + .attr({ + src: '/documents/' + data.id + '/512p.jpg?' + data.modified + }) + .css({ + position: 'absolute', + left: iconLeft + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + }) + .appendTo($reflection), + + $reflectionGradient = $('
') + .css({ + position: 'absolute', + width: iconSize + 'px', + height: iconSize / 2 + 'px' + }) + .appendTo($reflection); + } + + var $data = $('
') + .addClass('OxTextPage') + .css({ + position: 'absolute', + left: margin + 'px', + top: margin + iconHeight + margin + 'px', + width: iconSize + 'px', + }) + .appendTo($left); + + var $text = Ox.Element() + .addClass('OxTextPage') + .css({ + position: 'absolute', + left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px', + top: margin + 'px', + right: margin + statisticsWidth + margin + 'px', + }) + .appendTo($info), + + $statistics = $('
') + .css({ + position: 'absolute', + width: statisticsWidth + 'px', + top: margin + 'px', + right: margin + 'px' + }) + .appendTo($info), + + $capabilities; + + [$options, $edit].forEach(function($element) { + $element.find('input').css({ + borderWidth: 0, + borderRadius: 0, + padding: '3px' + }); + }); + + listKeys.forEach(function(key) { + if (Ox.isString(data[key])) { + data[key] = [data[key]]; + } + }); + + if (!canEdit) { + pandora.createLinks($info); + } + + // Source & Project -------------------------------------------------------- + if (!isMultiple) { + ['source', 'project'].forEach(function(key) { + displayedKeys.push(key); + if (canEdit || data[key]) { + var $div = $('
') + .addClass('OxSelectable') + .css(css) + .css({margin: 0}) + .appendTo($data); + $('') + .html( + formatKey({ + category: 'categories', + }[key] || key).replace('', ' ') + ) + .appendTo($div); + Ox.EditableContent({ + clickLink: pandora.clickLink, + format: function(value) { + return formatValue(key, value); + }, + placeholder: formatLight(Ox._(isMixed[key] ? 'mixed' : 'unknown')), + editable: canEdit, + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: listKeys.indexOf(key) >= 0 + ? (data[key] || []).join(', ') + : data[key] || '' + }) + .bindEvent({ + submit: function(event) { + editMetadata(key, event.value); + } + }) + .appendTo($div); + /* + if (canEdit || data[key + 'description']) { + $('
') + .addClass("InlineImages") + .append( + descriptions[key] = Ox.EditableContent({ + clickLink: pandora.clickLink, + editable: canEdit, + placeholder: formatLight(Ox._('No {0} Description', [Ox._(Ox.toTitleCase(key))])), + tooltip: canEdit ? pandora.getEditTooltip() : '', + type: 'textarea', + value: data[key + 'description'] || '' + }) + .css(css) + .bindEvent({ + submit: function(event) { + editMetadata(key + 'description', event.value); + } + }) + ).css({ + margin: '12px 0', + }) + .appendTo($div); + } + */ + } + }); + } + + // Title ------------------------------------------------------------------- + + $('
') + .css({ + marginTop: '-2px', + }) + .append( + Ox.EditableContent({ + editable: canEdit, + placeholder: formatLight(Ox._( isMixed.title ? 'Mixed title' : 'Untitled')), + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: data.title || '' + }) + .css({ + marginBottom: '-3px', + fontWeight: 'bold', + fontSize: '13px' + }) + .bindEvent({ + submit: function(event) { + editMetadata('title', event.value); + } + }) + ) + .appendTo($text); + + // Director, Year and Country ---------------------------------------------- + + renderGroup(['author', 'date', 'type']); + renderGroup(['publisher', 'place', 'series', 'edition', 'language']); + + Ox.getObjectById(pandora.site.documentKeys, 'keywords') && renderGroup(['keywords']) + if (isMultiple) { + renderGroup(['source', 'project']); + } + + // Render any remaing keys defined in config + + renderRemainingKeys(); + + + // Description ------------------------------------------------------------- + + if (canEdit || data.description) { + $('
') + .addClass("InlineImages") + .append( + Ox.EditableContent({ + clickLink: pandora.clickLink, + editable: canEdit, + maxHeight: Infinity, + placeholder: formatLight(Ox._('No Description')), + tooltip: canEdit ? pandora.getEditTooltip() : '', + type: 'textarea', + value: data.description || '' + }) + .css(css) + .css({ + marginTop: '12px', + overflow: 'hidden' + }) + .bindEvent({ + submit: function(event) { + editMetadata('description', event.value); + } + }) + ) + .appendTo($text); + } + + ;['content', 'translation'].forEach(key => { + if (canEdit || data[key]) { + var item = Ox.getObjectById(pandora.site.documentKeys, key); + $('
') + .addClass("InlineImages") + .append( + Ox.EditableContent({ + clickLink: pandora.clickLink, + editable: canEdit, + maxHeight: Infinity, + placeholder: formatLight(Ox._('No {0}', [Ox._(Ox.toTitleCase(key))])), + tooltip: canEdit ? pandora.getEditTooltip() : '', + type: 'textarea', + value: data[key] || '' + }) + .css(css) + .css({ + marginTop: '12px', + overflow: 'hidden' + }) + .bindEvent({ + submit: function(event) { + editMetadata(key, event.value); + } + }) + ) + .appendTo($text); + } + }) + + // Referenced -------------------------------------------------------------- + if ( + !isMultiple && ( + data.referenced.items.length + || data.referenced.annotations.length + || data.referenced.documents.length + || data.referenced.entities.length + )) { + + var itemsById = {} + data.referenced.items.forEach(function(item) { + itemsById[item.id] = Ox.extend(item, {annotations: [], referenced: true}); + }); + data.referenced.annotations.forEach(function(annotation) { + var itemId = annotation.id.split('/')[0]; + if (!itemsById[itemId]) { + itemsById[itemId] = { + id: itemId, + title: annotation.title, + annotations: [] + }; + } + itemsById[itemId].annotations = itemsById[itemId].annotations.concat(annotation); + }); + var html = Ox.sortBy(Object.values(itemsById), 'title').map(function(item) { + return (item.referenced ? '' : '') + + item.title //Ox.encodeHTMLEntities(item.title) + + (item.referenced ? '' : '') + + (item.annotations.length + ? ' (' + Ox.sortBy(item.annotations, 'in').map(function(annotation) { + return '' + + Ox.formatDuration(annotation.in) + '' + }).join(', ') + + ')' + : '') + }).join(', '); + html += data.referenced.documents.map(function(document) { + return ', ' + document.title + ''; + }).join(''); + html += data.referenced.entities.map(function(entity) { + return ', ' + entity.name + ''; + }).join(''); + + var $div = $('
') + .css({marginTop: '12px'}) + .html(formatKey('Referenced', 'text') + html) + .appendTo($text); + + pandora.createLinks($div); + + } + + + // Extension, Dimensions, Size --------------------------------------------- + + ['extension', 'dimensions', 'size'].forEach(function(key) { + $('
') + .css({marginBottom: '4px'}) + .append(formatKey(key, 'statistics')) + .append( + Ox.Theme.formatColor(null, 'gradient') + .css({textAlign: 'right'}) + .html(formatValue(key, data[key])) + ) + .appendTo($statistics); + }); + + // Rights Level ------------------------------------------------------------ + + var $rightsLevel = $('
'); + $('
') + .css({marginBottom: '4px'}) + .append(formatKey('Rights Level', 'statistics')) + .append($rightsLevel) + .appendTo($statistics); + renderRightsLevel(); + + // User and Groups --------------------------------------------------------- + if (!isMultiple) { + + ['user', 'groups'].forEach(function(key) { + var $input; + (canEdit || data[key] && data[key].length) && $('
') + .css({marginBottom: '4px'}) + .append(formatKey(key, 'statistics')) + .append( + $('
') + .css({margin: '2px 0 0 -1px'}) // fixme: weird + .append( + $input = Ox.Editable({ + placeholder: key == 'groups' ? formatLight(Ox._('No Groups')) : '', + editable: key == 'user' && canEdit, + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: key == 'user' ? data[key] : data[key].join(', ') + }) + .bindEvent(Ox.extend({ + submit: function(event) { + editMetadata(key, event.value); + } + }, key == 'groups' ? { + doubleclick: canEdit ? function() { + setTimeout(function() { + if (window.getSelection) { + window.getSelection().removeAllRanges(); + } else if (document.selection) { + document.selection.empty(); + } + }); + pandora.$ui.groupsDialog = pandora.ui.groupsDialog({ + id: data.id, + name: data.title, + type: 'document' + }) + .bindEvent({ + groups: function(data) { + $input.options({ + value: data.groups.join(', ') + }); + } + }) + .open(); + } : function() {} + } : {})) + ) + ) + .appendTo($statistics); + }); + + } + + // Created and Modified ---------------------------------------------------- + if (!isMultiple && canEdit) { + ['created', 'modified'].forEach(function(key) { + $('
') + .css({marginBottom: '4px'}) + .append(formatKey(key, 'statistics')) + .append( + $('
').html(Ox.formatDate(data[key], '%B %e, %Y')) + ) + .appendTo($statistics); + }); + } + + // Notes -------------------------------------------------------------------- + + if (canEdit) { + $('
') + .css({marginBottom: '4px'}) + .append( + formatKey('Notes', 'statistics').options({ + tooltip: Ox._('Only {0} can see and edit these comments', [ + Object.keys(pandora.site.capabilities.canEditMetadata).map(function(level, i) { + return ( + i == 0 ? '' + : i < Ox.len(pandora.site.capabilities.canEditMetadata) - 1 ? ', ' + : ' ' + Ox._('and') + ' ' + ) + Ox.toTitleCase(level) + }).join('')]) + }) + ) + .append( + Ox.EditableContent({ + height: 128, + placeholder: formatLight(Ox._(isMixed.notes ? 'Mixed notes' : 'No notes')), + tooltip: pandora.getEditTooltip(), + type: 'textarea', + value: data.notes || '', + width: 128 + }) + .bindEvent({ + submit: function(event) { + editMetadata('notes', event.value); + } + }) + ) + .appendTo($statistics); + } + + function editMetadata(key, value) { + if (value != data[key]) { + var edit = { + id: isMultiple ? ui.collectionSelection : data.id, + }; + if (key == 'title') { + edit[key] = value; + } else if (listKeys.indexOf(key) >= 0) { + edit[key] = value ? value.split(', ') : []; + } else { + edit[key] = value ? value : null; + } + pandora.api.editDocument(edit, function(result) { + if (!isMultiple) { + var src; + data[key] = result.data[key]; + Ox.Request.clearCache(); // fixme: too much? can change filter/list etc + if (result.data.id != data.id) { + pandora.UI.set({document: result.data.id}); + pandora.$ui.browser.value(data.id, 'id', result.data.id); + } + //pandora.updateItemContext(); + //pandora.$ui.browser.value(result.data.id, key, result.data[key]); + pandora.$ui.itemTitle + .options({title: '' + (pandora.getDocumentTitle(result.data)) + ''}); + } + that.triggerEvent('change', Ox.extend({}, key, value)); + }); + } + } + + function formatKey(key, mode) { + var item = Ox.getObjectById(pandora.site.documentKeys, key); + key = Ox._(item ? item.title : key); + mode = mode || 'text'; + return mode == 'text' + ? '' + Ox.toTitleCase(key) + ': ' + : mode == 'description' + ? Ox.toTitleCase(key) + : Ox.Element() + .css({marginBottom: '4px', fontWeight: 'bold'}) + .html(Ox.toTitleCase(key) + .replace(' Per ', ' per ')); + } + + function formatLight(str) { + return '' + str + ''; + } + + function formatLink(value, key) { + return (Ox.isArray(value) ? value : [value]).map(function(value) { + return key + ? '' + value + '' + : value; + }).join(', '); + } + + function formatValue(key, value) { + var ret; + if (key == 'date' && (!value || value.split('-').length < 4)) { + ret = pandora.formatDate(value); + } else if (nameKeys.indexOf(key) > -1) { + ret = formatLink(value.split(', '), key); + } else if (listKeys.indexOf(key) > -1) { + ret = formatLink(value.split(', '), key); + } else if (linkKeys.indexOf(key) > -1) { + ret = formatLink(value, key); + } else { + if (isMixed[key]) { + ret = 'Mixed' + } else { + ret = pandora.formatDocumentKey(Ox.getObjectById(pandora.site.documentKeys, key), data); + } + } + return ret; + } + + function getRightsLevelElement(rightsLevel) { + return Ox.Theme.formatColorLevel( + rightsLevel, + pandora.site.documentRightsLevels.map(function(rightsLevel) { + return rightsLevel.name; + }) + ); + } + + function getValue(key, value) { + return !value ? '' + : Ox.contains(listKeys, key) ? value.join(', ') + : value; + } + + function renderCapabilities(rightsLevel) { + var capabilities = [].concat( + canEdit ? [{name: 'canSeeItem', symbol: 'Find'}] : [], + [ + {name: 'canPlayClips', symbol: 'PlayInToOut'}, + {name: 'canPlayVideo', symbol: 'Play'}, + {name: 'canDownloadVideo', symbol: 'Download'} + ] + ), + userLevels = canEdit ? pandora.site.userLevels : [pandora.user.level]; + $capabilities.empty(); + userLevels.forEach(function(userLevel, i) { + var $element, + $line = $('
') + .css({ + height: '16px', + marginBottom: '4px' + }) + .appendTo($capabilities); + if (canEdit) { + $element = Ox.Theme.formatColorLevel(i, userLevels.map(function(userLevel) { + return Ox.toTitleCase(userLevel); + }), [0, 240]); + Ox.Label({ + textAlign: 'center', + title: Ox.toTitleCase(userLevel), + width: 60 + }) + .addClass('OxColor OxColorGradient') + .css({ + float: 'left', + height: '12px', + paddingTop: '2px', + background: $element.css('background'), + fontSize: '8px', + color: $element.css('color') + }) + .data({OxColor: $element.data('OxColor')}) + .appendTo($line); + } + capabilities.forEach(function(capability) { + var hasCapability = pandora.hasCapability(capability.name, userLevel) >= rightsLevel, + $element = Ox.Theme.formatColorLevel(hasCapability, ['', '']); + Ox.Button({ + tooltip: (canEdit ? Ox.toTitleCase(userLevel) : 'You') + ' ' + + (hasCapability ? 'can' : 'can\'t') + ' ' + + Ox.toSlashes(capability.name) + .split('/').slice(1).join(' ') + .toLowerCase(), + title: capability.symbol, + type: 'image' + }) + .addClass('OxColor OxColorGradient') + .css({background: $element.css('background')}) + .css('margin' + (canEdit ? 'Left' : 'Right'), '4px') + .data({OxColor: $element.data('OxColor')}) + .appendTo($line); + }); + if (!canEdit) { + Ox.Button({ + title: Ox._('Help'), + tooltip: Ox._('About Rights'), + type: 'image' + }) + .css({marginLeft: '52px'}) + .bindEvent({ + click: function() { + pandora.UI.set({page: 'rights'}); + } + }) + .appendTo($line); + } + }); + } + + function renderGroup(keys) { + var $element; + keys.forEach(function(key) { displayedKeys.push(key) }); + if (canEdit || keys.filter(function(key) { + return data[key]; + }).length) { + $element = $('
').addClass('OxSelectable').css(css); + keys.forEach(function(key, i) { + if (canEdit || data[key]) { + if ($element.children().length) { + $('').html('; ').appendTo($element); + } + $('').html(formatKey(key)).appendTo($element); + Ox.EditableContent({ + clickLink: pandora.clickLink, + editable: canEdit, + format: function(value) { + return formatValue(key, value); + }, + placeholder: formatLight(Ox._(isMixed[key] ? 'mixed' : 'unknown')), + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: getValue(key, data[key]) + }) + .bindEvent({ + submit: function(data) { + editMetadata(key, data.value); + } + }) + .appendTo($element); + if (isMixed[key] && Ox.contains(listKeys, key)) { + pandora.ui.addRemoveKeyDialog({ + ids: ui.collectionSelection, + key: key, + section: ui.section + }).appendTo($element) + } + } + }); + $element.appendTo($text); + } + return $element; + } + + function renderRemainingKeys() { + var keys = pandora.site.documentKeys.filter(function(item) { + return item.id != '*' && !Ox.contains(displayedKeys, item.id); + }).map(function(item) { + return item.id; + }); + if (keys.length) { + renderGroup(keys) + } + } + + function renderRightsLevel() { + var $rightsLevelElement = getRightsLevelElement(data.rightslevel), + $rightsLevelSelect; + $rightsLevel.empty(); + if (canEdit) { + $rightsLevelSelect = Ox.Select({ + items: pandora.site.documentRightsLevels.map(function(rightsLevel, i) { + return {id: i, title: rightsLevel.name}; + }), + width: 128, + value: data.rightslevel + }) + .addClass('OxColor OxColorGradient') + .css({ + marginBottom: '4px', + background: $rightsLevelElement.css('background') + }) + .data({OxColor: $rightsLevelElement.data('OxColor')}) + .bindEvent({ + change: function(event) { + var rightsLevel = event.value; + $rightsLevelElement = getRightsLevelElement(rightsLevel); + $rightsLevelSelect + .css({background: $rightsLevelElement.css('background')}) + .data({OxColor: $rightsLevelElement.data('OxColor')}) + //renderCapabilities(rightsLevel); + var edit = { + id: isMultiple ? ui.collectionSelection : data.id, + rightslevel: rightsLevel + }; + pandora.api.editDocument(edit, function(result) { + that.triggerEvent('change', Ox.extend({}, 'rightslevel', rightsLevel)); + }); + } + }) + .appendTo($rightsLevel); + } else { + $rightsLevelElement + .css({ + marginBottom: '4px' + }) + .appendTo($rightsLevel); + } + $capabilities = $('
').appendTo($rightsLevel); + //renderCapabilities(data.rightslevel); + } + + function toggleIconSize() { + iconSize = iconSize == 256 ? 512 : 256; + iconWidth = iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio); + iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio); + iconLeft = iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0, + $icon.animate({ + left: margin + iconLeft + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + }, 250); + $reflection.animate({ + top: margin + iconHeight + 'px', + width: iconSize + 'px', + height: iconSize / 2 + 'px' + }, 250); + $reflectionIcon.animate({ + left: iconLeft + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + }, 250); + $reflectionGradient.animate({ + width: iconSize + 'px', + height: iconSize / 2 + 'px' + }, 250); + $text.animate({ + left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px' + }, 250); + pandora.UI.set({infoIconSize: iconSize}); + } + + that.reload = function() { + /* + var src = '/documents/' + data.id + '/512p.jpg?' + data.modified; + $icon.attr({src: src}); + $reflectionIcon.attr({src: src}); + iconSize = iconSize == 256 ? 512 : 256; + iconRatio = ui.icons == 'posters' + ? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1; + toggleIconSize(); + */ + }; + + that.bindEvent({ + mousedown: function() { + setTimeout(function() { + !Ox.Focus.focusedElementIsInput() && that.gainFocus(); + }); + } + }); + + return that; + +}; diff --git a/static/js/documentPages.js b/static/js/documentPages.js new file mode 100644 index 00000000..6be22490 --- /dev/null +++ b/static/js/documentPages.js @@ -0,0 +1,133 @@ +'use strict'; + +pandora.ui.documentPages = function(options) { + + var self = {}, + that = Ox.Element() + .css({ + height: '192px', + margin: '4px', + display: 'flex' + }) + .bindEvent({ + doubleclick: doubleclick, + singleclick: singleclick + }); + + self.options = Ox.extend({ + id: '', + pages: 1, + query: null, + ratio: 8/5 + }, options); + + self.size = 128; + self.width = self.options.ratio > 1 ? self.size : Math.round(self.size * self.options.ratio); + self.height = self.options.ratio > 1 ? Math.round(self.size / self.options.ratio) : self.size; + + function renderPage(page, query) { + self.pages.push(page) + var url = `/documents/${self.options.id}/${self.size}p${page}.jpg` + if (query) { + url += '?q=' + encodeURIComponent(query) + } + var $item = Ox.IconItem({ + imageHeight: self.height, + imageWidth: self.width, + id: `${self.options.id}/${page}`, + info: '', + title: `Page ${page}`, + url: url + }) + .addClass('OxInfoIcon') + .css({ + }) + .data({ + page: page + }); + $item.find('.OxTarget').addClass('OxSpecialTarget'); + that.append($item); + } + + function renderPages(pages, query) { + self.pages = [] + if (pages) { + pages.forEach(page => { + renderPage(page.page, query) + }) + } else { + if (self.options.pages > 1) { + Ox.range(Ox.min([self.options.pages, 5])).forEach(page => { renderPage(page + 2) }) + } + } + } + var query + if (self.options.query) { + var condition = self.options.query.conditions.filter(condition => { + return condition.key == 'fulltext' + }) + if (condition.length) { + query = { + 'conditions': [ + {'key': 'document', 'operator': '==', 'value': self.options.id}, + {'key': 'fulltext', 'operator': '=', 'value': condition[0].value} + ] + } + } + } + if (query) { + pandora.api.findPages({ + query: query, + range: [0, 100], + keys: ['page'] + }, function(result) { + renderPages(result.data.items, pandora.getFulltextQuery()) + }) + } else { + renderPages() + } + + function doubleclick(data) { + var $item, $target = $(data.target), annotation, item, points, set; + if ($target.parent().parent().is('.OxSpecialTarget')) { + $target = $target.parent().parent(); + } + if ($target.is('.OxSpecialTarget')) { + $item = $target.parent().parent(); + var page = $item.data('page') + pandora.URL.push(`/documents/${self.options.id}/${page}`); + } + } + + function singleclick(data) { + var $item, $target = $(data.target), annotation, item, points, set; + if ($target.parent().parent().is('.OxSpecialTarget')) { + $target = $target.parent().parent(); + } + if ($target.is('.OxSpecialTarget')) { + $item = $target.parent().parent(); + var page = $item.data('page') + if (!pandora.$ui.pageDialog) { + pandora.$ui.pageDialog = pandora.ui.pageDialog({ + document: self.options.id, + page: page, + pages: self.pages, + query: pandora.getFulltextQuery(), + dimensions: [self.width, self.height], + title: self.options.title, + size: self.size + }); + pandora.$ui.pageDialog.open() + } else { + pandora.$ui.pageDialog.update({ + page: page, + pages: self.pages, + }); + } + } + } + + return that; + +}; + diff --git a/static/js/documentPanel.js b/static/js/documentPanel.js index 5dd9445f..f24ffb09 100644 --- a/static/js/documentPanel.js +++ b/static/js/documentPanel.js @@ -18,6 +18,7 @@ pandora.ui.documentPanel = function() { resize: function(data) { if (!pandora.user.ui.document) { pandora.$ui.list && pandora.$ui.list.size(); + pandora.resizeFilters(); } else { pandora.$ui.document && pandora.$ui.document.update(); } diff --git a/static/js/documentToolbar.js b/static/js/documentToolbar.js index ef12a856..a6d42ac2 100644 --- a/static/js/documentToolbar.js +++ b/static/js/documentToolbar.js @@ -184,7 +184,12 @@ pandora.ui.documentToolbar = function() { items: [ {id: 'info', title: Ox._('View Info')}, {id: 'view', title: Ox._('View Document')} - ], + ].concat( + pandora.hasCapability('canSeeExtraItemViews') ? [ + {id: 'data', title: Ox._('View Data')} + ] : [] + + ), value: ui.documentView, width: 128 }) diff --git a/static/js/documentsPanel.js b/static/js/documentsPanel.js index 2337352b..73cbdf22 100644 --- a/static/js/documentsPanel.js +++ b/static/js/documentsPanel.js @@ -64,10 +64,7 @@ pandora.ui.documentSortSelect = function() { pandora.ui.documentViewSelect = function() { var ui = pandora.user.ui, that = Ox.Select({ - items: [ - {id: 'list', title: Ox._('View as List')}, - {id: 'grid', title: Ox._('View as Grid')} - ], + items: pandora.site.collectionViews, value: ui.documentsView, width: 128 }) diff --git a/static/js/downloadVideoDialog.js b/static/js/downloadVideoDialog.js index 9e32cad9..0eb3acac 100644 --- a/static/js/downloadVideoDialog.js +++ b/static/js/downloadVideoDialog.js @@ -128,7 +128,7 @@ pandora.ui.downloadVideoDialog = function(options) { }, function(result) { if (result.data.taskId) { pandora.wait(result.data.taskId, function(result) { - console.log('wait -> ', result) + //console.log('wait -> ', result) if (result.data.result) { url = '/' + options.item + '/' + values.resolution @@ -163,7 +163,7 @@ pandora.ui.downloadVideoDialog = function(options) { } if (url) { that.close(); - document.location.href = url + document.location.href = pandora.getMediaURL(url) } } }) diff --git a/static/js/editDialog.js b/static/js/editDialog.js index 8e646d97..0c35a67a 100644 --- a/static/js/editDialog.js +++ b/static/js/editDialog.js @@ -31,9 +31,6 @@ pandora.ui.editDialog = function() { }) .bindEvent({ click: function() { - if (!ui.updateResults && hasChanged) { - pandora.$ui.list.reloadList() - } that.close(); } }) @@ -48,6 +45,12 @@ pandora.ui.editDialog = function() { ) ]), width: 768 + }).bindEvent({ + close: function() { + if (!ui.updateResults && hasChanged) { + pandora.$ui.list.reloadList() + } + } }), $updateCheckbox = Ox.Checkbox({ diff --git a/static/js/editDocumentsDialog.js b/static/js/editDocumentsDialog.js index 97b329ff..1207c6da 100644 --- a/static/js/editDocumentsDialog.js +++ b/static/js/editDocumentsDialog.js @@ -30,9 +30,6 @@ pandora.ui.editDocumentsDialog = function() { }) .bindEvent({ click: function() { - if (!ui.updateResults && hasChanged) { - pandora.$ui.list.reloadList() - } that.close(); } }) @@ -47,6 +44,12 @@ pandora.ui.editDocumentsDialog = function() { ) ]), width: 768 + }).bindEvent({ + close: function() { + if (!ui.updateResults && hasChanged) { + pandora.$ui.list.reloadList() + } + } }), $updateCheckbox = Ox.Checkbox({ diff --git a/static/js/editPanel.js b/static/js/editPanel.js index 21c98623..850c2eb1 100644 --- a/static/js/editPanel.js +++ b/static/js/editPanel.js @@ -52,6 +52,9 @@ pandora.ui.editPanel = function(isEmbed) { } function getSmallTimelineURL() { + if (!edit.duration) { + return '' + } smallTimelineCanvas = getSmallTimelineCanvas(); smallTimelineContext = smallTimelineCanvas.getContext('2d'); return smallTimelineCanvas.toDataURL(); @@ -73,6 +76,7 @@ pandora.ui.editPanel = function(isEmbed) { if (ui.section != 'edits' || ui.edit != edit.id) { return; } + var editSettings = ui.edits[ui.edit] || pandora.site.editSettings that = pandora.$ui.editPanel = Ox.VideoEditPanel({ annotationsCalendarSize: ui.annotationsCalendarSize, annotationsMapSize: ui.annotationsMapSize, @@ -83,7 +87,7 @@ pandora.ui.editPanel = function(isEmbed) { clips: Ox.clone(edit.clips), clipSize: ui.clipSize + Ox.UI.SCROLLBAR_SIZE, clipTooltip: 'clips ' + Ox.SYMBOLS.shift + 'C', - clipView: ui.edits[ui.edit].view, + clipView: editSettings.view, controlsTooltips: { open: Ox._('Open in {0} View', [Ox._(Ox.getObjectById( pandora.site.itemViews, pandora.user.ui.videoView @@ -104,15 +108,15 @@ pandora.ui.editPanel = function(isEmbed) { pandora.getLargeEditTimelineURL(edit, type, i, callback); }, height: pandora.$ui.appPanel.size(1), - 'in': ui.edits[ui.edit]['in'], + 'in': editSettings['in'], layers: getLayers(edit.clips), loop: ui.videoLoop, muted: ui.videoMuted, - out: ui.edits[ui.edit].out, - position: ui.edits[ui.edit].position, + out: editSettings.out, + position: editSettings.position, resolution: ui.videoResolution, scaleToFill: ui.videoScale == 'fill', - selected: ui.edits[ui.edit].selection, + selected: editSettings.selection, showAnnotationsCalendar: ui.showAnnotationsCalendar, showAnnotationsMap: ui.showAnnotationsMap, showClips: ui.showClips, @@ -120,7 +124,7 @@ pandora.ui.editPanel = function(isEmbed) { showTimeline: ui.showTimeline, showUsers: pandora.site.annotations.showUsers, smallTimelineURL: getSmallTimelineURL(), - sort: ui.edits[ui.edit].sort, + sort: editSettings.sort, sortOptions: ( edit.type == 'static' ? [{id: 'index', title: Ox._('Sort Manually'), operator: '+'}] @@ -411,7 +415,7 @@ pandora.ui.editPanel = function(isEmbed) { that.css('right', right); updateSmallTimelineURL(); - ui.edits[ui.edit].view == 'grid' && enableDragAndDrop(); + editSettings.view == 'grid' && enableDragAndDrop(); if (!Ox.Focus.focusedElementIsInput()) { that.gainFocus(); } @@ -464,12 +468,13 @@ pandora.ui.editPanel = function(isEmbed) { } function renderEmbedEdit() { + var editSettings = ui.edits[ui.edit] || pandora.site.editSettings that = Ox.VideoPlayer({ clickLink: pandora.clickLink, clipRatio: pandora.site.video.previewRatio, clips: Ox.clone(edit.clips), clipTooltip: 'clips ' + Ox.SYMBOLS.shift + 'C', - clipView: ui.edits[ui.edit].view, + clipView: editSettings.view, controlsBottom: [ 'play', 'volume', 'scale', 'timeline', 'position', 'settings' ], @@ -673,6 +678,10 @@ pandora.ui.editPanel = function(isEmbed) { timelineIteration = self.timelineIteration = Ox.uid(); Ox.serialForEach(edit.clips, function(clip) { var callback = Ox.last(arguments); + if (!clip.duration) { + callback() + return; + } pandora[ fps == 1 ? 'getSmallClipTimelineURL' : 'getLargeClipTimelineURL' ](clip.item, clip['in'], clip.out, ui.videoTimeline, function(url) { diff --git a/static/js/editor.js b/static/js/editor.js index 28d3b5ba..90558d63 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -4,6 +4,7 @@ pandora.ui.editor = function(data) { var ui = pandora.user.ui, rightsLevel = data.rightslevel, + canEdit = pandora.hasCapability('canEditMetadata') || data.editable, that = Ox.VideoAnnotationPanel({ annotationsCalendarSize: ui.annotationsCalendarSize, @@ -44,7 +45,7 @@ pandora.ui.editor = function(data) { itemName: pandora.site.itemName, layers: data.annotations.map(function(layer) { return Ox.extend({ - editable: layer.canAddAnnotations[pandora.user.level] + editable: layer.canAddAnnotations[pandora.user.level] || canEdit }, layer, { autocomplete: layer.type == 'entity' ? function(key, value, callback) { @@ -98,10 +99,10 @@ pandora.ui.editor = function(data) { showLayers: Ox.clone(ui.showLayers), showUsers: pandora.site.annotations.showUsers, subtitles: data.subtitles, - subtitlesDefaultTrack: Ox.getLanguageNameByCode(pandora.site.language), + subtitlesDefaultTrack: data.subtitlesDefaultTrack || Ox.getLanguageNameByCode(pandora.site.language), subtitlesLayer: data.subtitlesLayer, subtitlesOffset: ui.videoSubtitlesOffset, - subtitlesTrack: Ox.getLanguageNameByCode(pandora.site.language), + subtitlesTrack: data.subtitlesTrack || Ox.getLanguageNameByCode(pandora.site.language), timeline: ui.videoTimeline, timelines: pandora.site.timelines, video: data.video, diff --git a/static/js/embedPlayer.js b/static/js/embedPlayer.js index 0db6a7b8..504d012e 100644 --- a/static/js/embedPlayer.js +++ b/static/js/embedPlayer.js @@ -42,7 +42,9 @@ pandora.ui.embedPlayer = function() { sizes = getSizes(); options.height = sizes.videoHeight; - video.subtitles = pandora.getSubtitles(video); + if (!video.subtitles) { + video.subtitles = pandora.getSubtitles(video); + } if (options.title) { $title = Ox.Element() @@ -107,7 +109,10 @@ pandora.ui.embedPlayer = function() { scaleToFill: ui.videoScale == 'fill', showIconOnLoad: true, subtitles: video.subtitles, + subtitlesDefaultTrack: video.subtitlesDefaultTrack || Ox.getLanguageNameByCode(pandora.site.language), + subtitlesLayer: video.subtitlesLayer, subtitlesOffset: ui.videoSubtitlesOffset, + subtitlesTrack: video.subtitlesTrack || Ox.getLanguageNameByCode(pandora.site.language), timeline: options.playInToOut ? function(size, i) { return pandora.getMediaURL('/' + options.item + '/timelineantialias' diff --git a/static/js/filter.js b/static/js/filter.js index c52fdcda..c65733ed 100644 --- a/static/js/filter.js +++ b/static/js/filter.js @@ -187,7 +187,9 @@ pandora.ui.filter = function(id) { {id: 'clearFilters', title: Ox._('Clear All Filters'), keyboard: 'shift alt control a'}, {}, {group: 'filter', max: 1, min: 1, items: pandora.site.filters.map(function(filter) { - return Ox.extend({checked: filter.id == id}, filter); + return Ox.extend({checked: filter.id == id}, filter, { + title: Ox._(filter.title) + }); })} ], type: 'image', diff --git a/static/js/folderList.js b/static/js/folderList.js index d9d6bca7..35b917ba 100644 --- a/static/js/folderList.js +++ b/static/js/folderList.js @@ -104,7 +104,11 @@ pandora.ui.folderList = function(id, section) { operator: '+', tooltip: function(data) { return data.type == 'static' - ? (data.editable ? Ox._('Edit {0}', [Ox._(folderItem)]) : '') + ? (data.editable + ? folderItem == "Edit" + ? Ox._('Edit this {0}', [Ox._(folderItem)]) + : Ox._('Edit {0}', [Ox._(folderItem)]) + : '') : data.type == 'smart' ? (data.editable ? Ox._('Edit Query') : Ox._('Show Query')) : data.type.toUpperCase(); @@ -377,7 +381,7 @@ pandora.ui.folderList = function(id, section) { } }, init: function(data) { - if (pandora.site.sectionFolders[section][i]) { + if (pandora.site.sectionFolders[section][i] && pandora.$ui.folder[i] && pandora.$ui.folderList[id]) { pandora.site.sectionFolders[section][i].items = data.items; pandora.$ui.folder[i].$content.css({ height: (data.items || 1) * 16 + 'px' @@ -402,6 +406,11 @@ pandora.ui.folderList = function(id, section) { pandora.ui.listDialog().open(); } }, + key_control_v: function() { + if (pandora.user.ui.section == 'edits' && pandora.$ui.editPanel) { + pandora.$ui.editPanel.triggerEvent("paste") + } + }, move: function(data) { pandora.api['sort' + folderItems]({ section: id, @@ -411,7 +420,7 @@ pandora.ui.folderList = function(id, section) { }); }, paste: function() { - pandora.$ui.list.triggerEvent('paste'); + pandora.$ui.list && pandora.$ui.list.triggerEvent('paste'); }, select: function(data) { var list = data.ids.length ? data.ids[0] : ''; @@ -478,7 +487,9 @@ pandora.ui.folderList = function(id, section) { }, range: [0, 1] }, function(result) { - that.value(item, 'items', result.data.items[0].items); + if(result.data.items && result.data.items.length) { + that.value(item, 'items', result.data.items[0].items); + } callback(); }) }) diff --git a/static/js/folders.js b/static/js/folders.js index 20d4f613..c23feedf 100644 --- a/static/js/folders.js +++ b/static/js/folders.js @@ -60,7 +60,7 @@ pandora.ui.folders = function(section) { {}, { id: 'duplicatelist', title: Ox._('Duplicate Selected {0}', [Ox._(folderItem)]), keyboard: 'control d', disabled: ui.section == 'documents' ? !ui._collection : !ui._list }, { id: 'editlist', title: Ox._('Edit Selected {0}...', [Ox._(folderItem)]), keyboard: 'control e', disabled: !editable }, - { id: 'deletelist', title: Ox._('Delete Selected {0}...', [Ox._(folderItem)]), keyboard: 'delete', disabled: !editable } + { id: 'deletelist', title: Ox._('Delete Selected {0}...', [Ox._(folderItem)]), keyboard: 'delete', disabled: !editable }, ], title: 'edit', tooltip: Ox._('Manage Personal ' + folderItems), @@ -367,7 +367,7 @@ pandora.ui.folders = function(section) { } pandora.$ui.folder[i] = Ox.CollapsePanel({ id: folder.id, - collapsed: !ui.showFolder.items[folder.id], + collapsed: !ui.showFolder[section][folder.id], extras: extras, size: 16, title: Ox._(folder.title) diff --git a/static/js/importMediaDialog.js b/static/js/importMediaDialog.js deleted file mode 100644 index 419aeb4e..00000000 --- a/static/js/importMediaDialog.js +++ /dev/null @@ -1,219 +0,0 @@ -'use strict'; - -pandora.ui.importMediaDialog = function(options) { - - var help = Ox._('You can import videos from external sites, like YouTube or Vimeo.'), - - $content = Ox.Element().css({margin: '16px'}), - - $button = Ox.Button({ - overlap: 'left', - title: Ox._('Preview'), - width: 128 - }) - .css({ - marginLeft: '-20px', - paddingLeft: '20px', - position: 'absolute', - right: '16px', - top: '16px' - }) - .bindEvent({ - click: submitURL - }) - .appendTo($content), - - $input = Ox.Input({ - label: Ox._('URL'), - labelWidth: 64, - width: 384 - }) - .css({ - left: '16px', - position: 'absolute', - top: '16px' - }) - .bindEvent({ - change: submitURL - }) - .appendTo($content), - - $info = Ox.Element() - .css({ - left: '16px', - position: 'absolute', - top: '48px' - }) - .html(help) - .appendTo($content), - - $loading = Ox.LoadingScreen({ - width: 512, - height: 224 - }), - - that = Ox.Dialog({ - buttons: [ - Ox.Button({ - id: 'close', - title: Ox._('Close') - }) - .bindEvent({ - click: function() { - that.close(); - } - }), - Ox.Button({ - disabled: true, - id: 'import', - title: Ox._('Import Video') - }).bindEvent({ - click: importMedia - }) - ], - content: $content, - fixedSize: true, - height: 288, - keys: { - escape: 'close' - }, - removeOnClose: true, - title: Ox._('Import Video'), - width: 544 - }); - - function getMediaUrlInfo(url, callback) { - pandora.api.getMediaUrlInfo({url: url}, function(result) { - // FIXME: support playlists / multiple items - var info = result.data.items[0]; - var infoKeys = [ - 'date', 'description', 'id', 'tags', - 'title', 'uploader', 'url' - ]; - var values = Ox.map(pandora.site.importMetadata, function(value, key) { - var isArray = Ox.isArray( - Ox.getObjectById(pandora.site.itemKeys, key).type - ); - if (isArray && value == '{tags}') { - value = info.tags; - } else { - infoKeys.forEach(function(infoKey) { - var infoValue = info[infoKey] || ''; - if (key == 'year' && infoKey == 'date') { - infoValue = infoValue.substr(0, 4); - } - if (infoKey == 'tags') { - infoValue = infoValue.join(', '); - } - value = value.replace( - new RegExp('\{' + infoKey + '\}', 'g'), infoValue - ); - }); - // For example: director -> uploader - if (isArray) { - value = [value]; - } - } - return value; - }); - callback(values, info) - }); - } - - function addMedia(url, callback) { - getMediaUrlInfo(url, function(values, info) { - pandora.api.add({title: values.title || info.title}, function(result) { - var edit = Ox.extend( - Ox.filter(values, function(value, key) { - return key != 'title'; - }), - {'id': result.data.id} - ); - pandora.api.edit(edit, function(result) { - pandora.api.addMediaUrl({ - url: info.url, - referer: info.referer, - item: edit.id - }, function(result) { - if (result.data.taskId) { - pandora.wait(result.data.taskId, function(result) { - callback(edit.id); - }); - } else { - callback(edit.id); - } - }); - }); - }); - }); - }; - - function getInfo(url, callback) { - pandora.api.getMediaUrlInfo({url: url}, function(result) { - callback(result.data.items); - }); - } - - function importMedia() { - var url = $input.value(); - $input.options({disabled: true}); - $button.options({disabled: true}); - $info.empty().append($loading.start()); - that.disableButton('close'); - that.disableButton('import'); - addMedia(url, function(item) { - if (item) { - that.close(); - Ox.Request.clearCache(); - pandora.URL.push('/' + item + '/media'); - } else { - $input.options({disabled: false}); - $button.options({disabled: false}); - $info.empty().html(Ox._('Import failed. Please try again')); - that.enableButton('close'); - } - }); - } - - function submitURL() { - var value = $input.value(); - if (value) { - $input.options({disabled: true}); - $button.options({disabled: true}); - $info.empty().append($loading.start()); - that.disableButton('close'); - getInfo(value, function(items) { - $input.options({disabled: false}); - $button.options({disabled: false}); - $loading.stop(); - $info.empty(); - if (items.length) { - // FIXME: support playlists / multiple items - var info = items[0]; - info.thumbnail && $info.append($('').css({ - position: 'absolute', - width: '248px' - }).attr('src', info.thumbnail)); - $info.append($('
').addClass('OxText').css({ - height: '192px', - overflow: 'hidden', - position: 'absolute', - left: '264px', - textOverflow: 'ellipsis', - width: '248px' - }).html( - '' + info.title - + '

' + (info.description || '') - )); - that.enableButton('import'); - } else { - $info.html(help); - } - }); - that.enableButton('close'); - } - } - - return that; - -}; diff --git a/static/js/info.js b/static/js/info.js index 38bcef79..305b40b2 100644 --- a/static/js/info.js +++ b/static/js/info.js @@ -142,7 +142,7 @@ pandora.ui.info = function() { }) .bindEvent({ click: function(data) { - if (ui.item && ['timeline', 'player', 'editor'].indexOf(ui.itemView) > -1) { + if (ui.item && ['timeline', 'player', 'editor'].indexOf(ui.itemView) > -1 && pandora.$ui[ui.itemView]) { pandora.$ui[ui.itemView].options({ position: data.position }); diff --git a/static/js/infoView.0xdb.js b/static/js/infoView.0xdb.js index f2324286..79dc25a2 100644 --- a/static/js/infoView.0xdb.js +++ b/static/js/infoView.0xdb.js @@ -895,7 +895,12 @@ pandora.ui.infoView = function(data, isMixed) { } else if (capability.name == 'canPlayVideo') { pandora.UI.set({itemView: ui.videoView}); } else if (capability.name == 'canDownloadVideo') { - document.location.href = '/' + ui.item + '/torrent/'; + pandora.ui.downloadVideoDialog({ + item: ui.item, + rightsLevel: data.rightsLevel, + title: data.title, + video: data.video + }).open(); } } } diff --git a/static/js/infoView.indiancinema.js b/static/js/infoView.indiancinema.js index 277c706d..94a1b281 100644 --- a/static/js/infoView.indiancinema.js +++ b/static/js/infoView.indiancinema.js @@ -362,17 +362,12 @@ pandora.ui.infoView = function(data, isMixed) { clickLink: pandora.clickLink, collapseToEnd: false, editable: canEdit, - format: function(value) { - return value.replace( - /' + value + '' + ) + '=' + pandora.escapeQueryValue(Ox.decodeHTMLEntities(linkValue[idx])) + '">' + value + '' : value; }).join(Ox.contains(specialListKeys, key) ? '; ' : ', '); } @@ -1043,17 +1033,12 @@ pandora.ui.infoView = function(data, isMixed) { Ox.EditableContent({ clickLink: pandora.clickLink, editable: canEdit, - format: function(value) { - return value.replace( - /') + .addClass("InlineImages") .append( Ox.EditableContent({ clickLink: pandora.clickLink, editable: canEdit, - format: function(value) { - return value.replace( - /') + .css({marginBottom: '4px'}) + .append(formatKey(key, 'statistics')) + .append( + $('
') + .css({margin: '2px 0 0 -1px'}) // fixme: weird + .append( + $input = Ox.Editable({ + placeholder: key == 'groups' + ? formatLight(Ox._(isMixed[key] ? 'Mixed Groups' : 'No Groups')) + : isMixed[key] ? formatLight(Ox._('Mixed Users')) : '', + editable: key == 'user' && canEdit, + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: isMixed[key] + ? '' + : key == 'user' ? data[key] : data[key].join(', ') + }) + .bindEvent(Ox.extend({ + submit: function(event) { + editMetadata(key, event.value); + } + }, key == 'groups' ? { + doubleclick: canEdit ? function() { + setTimeout(function() { + if (window.getSelection) { + window.getSelection().removeAllRanges(); + } else if (document.selection) { + document.selection.empty(); + } + }); + pandora.$ui.groupsDialog = pandora.ui.groupsDialog({ + id: data.id, + name: data.title, + type: 'item' + }) + .bindEvent({ + groups: function(data) { + $input.options({ + value: data.groups.join(', ') + }); + } + }) + .open(); + } : function() {} + } : {})) + ) + ) + .appendTo($statistics); + }); + + } + + // Created and Modified ---------------------------------------------------- + if (!isMultiple && canEdit) { + + ['created', 'modified'].forEach(function(key) { + $('
') + .css({marginBottom: '4px'}) + .append(formatKey(key, 'statistics')) + .append( + $('
').html(Ox.formatDate(data[key], '%B %e, %Y')) + ) + .appendTo($statistics); + }); + + } + // Notes -------------------------------------------------------------------- if (canEdit) { @@ -461,14 +531,16 @@ pandora.ui.infoView = function(data, isMixed) { return key ? '' + value + '' + ) + '=' + pandora.escapeQueryValue(Ox.decodeHTMLEntities(linkValue[idx])) + '">' + value + '' : value; }).join(Ox.contains(specialListKeys, key) ? '; ' : ', '); } function formatValue(key, value) { var ret; - if (nameKeys.indexOf(key) > -1) { + if (key == 'date' && (!value || value.split('-').length < 4)) { + ret = pandora.formatDate(value); + } else if (nameKeys.indexOf(key) > -1) { ret = formatLink(value.split(', '), 'name'); } else if ( listKeys.indexOf(key) > -1 && Ox.getObjectById(pandora.site.itemKeys, key).type[0] == 'date' @@ -517,10 +589,11 @@ pandora.ui.infoView = function(data, isMixed) { $('').html(formatKey(key)).appendTo($element); Ox.EditableContent({ clickLink: pandora.clickLink, + editable: canEdit, format: function(value) { return formatValue(key, value); }, - placeholder: formatLight(Ox._( isMixed[key] ? 'mixed' : 'unknown')), + placeholder: formatLight(Ox._(isMixed[key] ? 'mixed' : 'unknown')), tooltip: canEdit ? pandora.getEditTooltip() : '', value: getValue(key, data[key]) }) @@ -541,6 +614,7 @@ pandora.ui.infoView = function(data, isMixed) { }); $element.appendTo($text); } + return $element; } function renderRemainingKeys() { @@ -587,6 +661,10 @@ pandora.ui.infoView = function(data, isMixed) { pandora.UI.set({infoIconSize: iconSize}); } + that.resizeElement = function() { + // overwrite splitpanel resize + }; + that.reload = function() { var src = src = '/' + data.id + '/' + ( ui.icons == 'posters' ? 'poster' : 'icon' diff --git a/static/js/infoView.padma.js b/static/js/infoView.padma.js index e5e894d0..42e7ac53 100644 --- a/static/js/infoView.padma.js +++ b/static/js/infoView.padma.js @@ -31,6 +31,16 @@ pandora.ui.infoView = function(data, isMixed) { return key.id; }), posterKeys = ['title', 'date'], + displayedKeys = [ // FIXME: can tis be a flag in the config? + 'title', 'notes', 'name', 'summary', 'id', + 'hue', 'saturation', 'lightness', 'cutsperminute', 'volume', + 'user', 'rightslevel', 'bitrate', 'timesaccessed', + 'numberoffiles', 'numberofannotations', 'numberofcuts', 'words', 'wordsperminute', + 'annotations', 'groups', 'filename', + 'duration', 'aspectratio', 'pixels', 'size', 'resolution', + 'created', 'modified', 'accessed', + 'random' + ], statisticsWidth = 128, $bar = Ox.Bar({size: 16}) @@ -236,6 +246,7 @@ pandora.ui.infoView = function(data, isMixed) { if (!isMultiple) { ['source', 'project'].forEach(function(key) { + displayedKeys.push(key); if (canEdit || data[key]) { var $div = $('
') .addClass('OxSelectable') @@ -269,16 +280,11 @@ pandora.ui.infoView = function(data, isMixed) { .appendTo($div); if (canEdit || data[key + 'description']) { $('
') + .addClass("InlineImages") .append( descriptions[key] = Ox.EditableContent({ clickLink: pandora.clickLink, editable: canEdit, - format: function(value) { - return value.replace( - /') + .addClass("InlineImages") .css({ marginTop: '12px', marginBottom: '12px' @@ -351,12 +358,6 @@ pandora.ui.infoView = function(data, isMixed) { clickLink: pandora.clickLink, collapseToEnd: false, editable: canEdit, - format: function(value) { - return value.replace( - /') @@ -388,6 +389,9 @@ pandora.ui.infoView = function(data, isMixed) { .css({height: '16px'}) .appendTo($text); + // Render any remaing keys defined in config + + renderRemainingKeys(); // Duration, Aspect Ratio -------------------------------------------------- @@ -446,9 +450,12 @@ pandora.ui.infoView = function(data, isMixed) { pandora.renderRightsLevel(that, $rightsLevel, data, isMixed, isMultiple, canEdit); // User and Groups --------------------------------------------------------- - if (!isMultiple) { + if (!isMultiple || pandora.hasCapability('canEditUsers')) { ['user', 'groups'].forEach(function(key) { + if (key == 'groups' && isMultiple) { + return + }; var $input; (canEdit || data[key] && data[key].length) && $('
') .css({marginBottom: '4px'}) @@ -458,10 +465,14 @@ pandora.ui.infoView = function(data, isMixed) { .css({margin: '2px 0 0 -1px'}) // fixme: weird .append( $input = Ox.Editable({ - placeholder: key == 'groups' ? formatLight(Ox._(isMixed[key] ? 'Mixed Groups' : 'No Groups')) : '', + placeholder: key == 'groups' + ? formatLight(Ox._(isMixed[key] ? 'Mixed Groups' : 'No Groups')) + : isMixed[key] ? formatLight(Ox._('Mixed Users')) : '', editable: key == 'user' && canEdit, tooltip: canEdit ? pandora.getEditTooltip() : '', - value: key == 'user' ? data[key] : isMixed[key] ? '' : data[key].join(', ') + value: isMixed[key] + ? '' + : key == 'user' ? data[key] : data[key].join(', ') }) .bindEvent(Ox.extend({ submit: function(event) { @@ -614,7 +625,9 @@ pandora.ui.infoView = function(data, isMixed) { function formatLink(key, value, linkValue) { return (Ox.isArray(value) ? value : [value]).map(function(value) { return key - ? '' + value + '' + ? '' + value + '' : value; }).join(', '); } @@ -642,6 +655,7 @@ pandora.ui.infoView = function(data, isMixed) { function renderGroup(keys) { var $element; + keys.forEach(function(key) { displayedKeys.push(key) }); if (canEdit || keys.filter(function(key) { return data[key]; }).length) { @@ -656,6 +670,7 @@ pandora.ui.infoView = function(data, isMixed) { $('').html(formatKey(key)).appendTo($element); Ox.EditableContent({ clickLink: pandora.clickLink, + editable: canEdit, format: function(value) { return formatValue(key, value); }, @@ -669,12 +684,32 @@ pandora.ui.infoView = function(data, isMixed) { } }) .appendTo($element); + if (isMixed[key] && Ox.contains(listKeys, key)) { + pandora.ui.addRemoveKeyDialog({ + ids: ui.listSelection, + key: key, + section: ui.section + }).appendTo($element) + } } }); $element.appendTo($text); } + return $element; } + function renderRemainingKeys() { + var keys = pandora.site.itemKeys.filter(function(item) { + return item.id != '*' && item.type != 'layer' && !Ox.contains(displayedKeys, item.id); + }).map(function(item) { + return item.id; + }); + if (keys.length) { + renderGroup(keys) + } + } + + function toggleIconSize() { iconSize = iconSize == 256 ? 512 : 256; iconWidth = iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio); diff --git a/static/js/infoViewUtils.js b/static/js/infoViewUtils.js index 6c415c1b..037f7e2c 100644 --- a/static/js/infoViewUtils.js +++ b/static/js/infoViewUtils.js @@ -137,7 +137,7 @@ pandora.renderRightsLevel = function(that, $rightsLevel, data, isMixed, isMultip .data({OxColor: $element.data('OxColor')}) .appendTo($line); }); - if (!canEdit) { + if (!canEdit && Ox.getObjectById(pandora.site.sitePages, 'rights')) { Ox.Button({ title: Ox._('Help'), tooltip: Ox._('About Rights'), diff --git a/static/js/item.js b/static/js/item.js index 8de8740f..2837fedc 100644 --- a/static/js/item.js +++ b/static/js/item.js @@ -31,6 +31,17 @@ pandora.ui.item = function() { // fixme: layers have value, subtitles has text? isVideoView && Ox.extend(result.data, pandora.getVideoOptions(result.data)); + if (isVideoView && result.data.duration) { + var videoPoints = pandora.user.ui.videoPoints[item], set = {}; + ['in', 'out', 'position'].forEach(point => { + if (videoPoints && videoPoints[point] > result.data.duration) { + set[point] = result.data.duration; + } + }) + if (!Ox.isEmpty(set)) { + pandora.UI.set('videoPoints.' + item, Ox.extend(videoPoints[point], set[point])) + } + } if (!result.data.rendered && [ 'clips', 'timeline', 'player', 'editor', 'map', 'calendar' diff --git a/static/js/list.js b/static/js/list.js index 1ad41d8b..bc680f85 100644 --- a/static/js/list.js +++ b/static/js/list.js @@ -181,6 +181,7 @@ pandora.ui.list = function() { id: 'list', item: function(data, sort, size) { size = 128; + data.videoRatio = data.videoRatio || pandora.site.video.previewRatio; var ratio = ui.icons == 'posters' ? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1, url = pandora.getMediaURL('/' + data.id + '/' + ( diff --git a/static/js/listDialog.js b/static/js/listDialog.js index 35051d32..4b0e9deb 100644 --- a/static/js/listDialog.js +++ b/static/js/listDialog.js @@ -36,6 +36,9 @@ pandora.ui.listDialog = function(section) { mode: 'list', list: listData }) + .css({ + 'overflow-y': 'auto' + }) .bindEvent({ change: function(data) { listData.query = data.query; diff --git a/static/js/mainMenu.js b/static/js/mainMenu.js index 9ff8a94e..2efc217f 100644 --- a/static/js/mainMenu.js +++ b/static/js/mainMenu.js @@ -284,6 +284,67 @@ pandora.ui.mainMenu = function() { } } else if (data.id == 'deletelist') { pandora.ui.deleteListDialog().open(); + } else if (data.id.startsWith('hidden:')) { + var folderItems = { + documents: 'Collections', + edits: 'Edits', + items: 'Lists' + }[ui.section], + folderKey = folderItems.toLowerCase(), + name = data.id.slice(7).replace(/\t/g, '_'), + set = {} + + if (ui.section == "items") { + set.find = { + conditions: [ + {key: 'list', value: pandora.user.username + ":" + name, operator: '=='} + ], + operator: '&' + } + } else if (ui.section == "edits") { + set.edit = pandora.user.username + ":" + name; + } else if (ui.section == "documents") { + set.findDocuments = { + conditions: [ + {key: 'collection', value: pandora.user.username + ":" + name, operator: '=='} + ], + operator: '&' + } + } + set['hidden.' + folderKey] = ui.hidden[folderKey].filter(other => { return other != name }) + pandora.UI.set(set) + Ox.Request.clearCache('find' + folderItems); + pandora.$ui.folderList.personal.reloadList() + } else if (data.id == 'hidelist') { + var folderItems = { + documents: 'Collections', + edits: 'Edits', + items: 'Lists' + }[ui.section], + folderKey = folderItems.toLowerCase(), + listName = ({ + documents: ui._collection, + edits: ui.edit, + items: ui._list + }[ui.section]).split(':').slice(1).join(':'), + set = {}; + if (ui.section == "items") { + set.find = { + conditions: [], + operator: '&' + } + } else if (ui.section == "edits") { + set.edit = "" + } else if (ui.section == "documents") { + set.findDocuments = { + conditions: [], + operator: '&' + }; + } + set['hidden.' + folderKey] = Ox.sort(Ox.unique([listName].concat(pandora.user.ui.hidden[folderKey]))) + pandora.UI.set(set) + Ox.Request.clearCache('find' + folderItems); + pandora.$ui.folderList.personal.reloadList() } else if (data.id == 'print') { window.open(document.location.href + '#print', '_blank'); } else if (data.id == 'tv') { @@ -361,11 +422,23 @@ pandora.ui.mainMenu = function() { } } else if (ui.section == 'documents') { var items = pandora.clipboard.paste('document'); + /* items.length && pandora.doHistory('paste', items, ui._collection, function() { - //fixme: - //pandora.UI.set({listSelection: items}); - //pandora.reloadList(); + pandora.UI.set({listSelection: items}); + pandora.reloadList(); + pandora.UI.set({collectionSelection: items}); + pandora.reloadList(); }); + */ + if (items.length) { + pandora.api.addCollectionItems({ + collection: ui._collection, + items: items + }, function() { + pandora.UI.set({collectionSelection: items}); + pandora.reloadList(); + }); + } } else if (ui.section == 'edits') { var clips = pandora.clipboard.paste('clip'); clips.length && pandora.doHistory('paste', clips, ui.edit, function(result) { @@ -536,6 +609,8 @@ pandora.ui.mainMenu = function() { pandora.$ui.player.options({fullscreen: true}); } else if (data.id == 'embed') { pandora.$ui.embedDialog = pandora.ui.embedDialog().open(); + } else if (data.id == 'share') { + pandora.$ui.shareDialog = pandora.ui.shareDialog().open(); } else if (data.id == 'advancedfind') { pandora.$ui.filterDialog = pandora.ui.filterDialog().open(); } else if (data.id == 'clearquery') { @@ -615,7 +690,11 @@ pandora.ui.mainMenu = function() { that.uncheckItem(previousList == '' ? 'allitems' : 'viewlist' + previousList.replace(/_/g, Ox.char(9))); that.checkItem(list == '' ? 'allitems' : 'viewlist' + list.replace(/_/g, '\t')); } - that[ui._list ? 'enableItem' : 'disableItem']('duplicatelist'); + that[list ? 'enableItem' : 'disableItem']('duplicatelist'); + that[ + list && pandora.$ui.folderList && pandora.$ui.folderList.personal.options('selected').length + ? 'enableItem' : 'disableItem' + ]('hidelist'); that[action]('editlist'); that[action]('deletelist'); that[ui.listSelection.length ? 'enableItem' : 'disableItem']('newlistfromselection'); @@ -634,6 +713,10 @@ pandora.ui.mainMenu = function() { that.checkItem(edit == '' ? 'allitems' : 'viewlist' + edit.replace(/_/g, '\t')); } that[!isGuest && edit ? 'enableItem' : 'disableItem']('duplicatelist'); + that[ + !isGuest && edit && pandora.$ui.folderList && pandora.$ui.folderList.personal.options('selected').length + ? 'enableItem' : 'disableItem' + ]('hidelist'); that[action]('editlist'); that[action]('deletelist'); that[!isGuest && edit ? 'enableItem' : 'disableItem']('newlistfromselection'); @@ -653,7 +736,11 @@ pandora.ui.mainMenu = function() { that.uncheckItem(previousList == '' ? 'allitems' : 'viewlist' + previousList.replace(/_/g, Ox.char(9))); that.checkItem(list == '' ? 'allitems' : 'viewlist' + list.replace(/_/g, '\t')); } - that[ui._list ? 'enableItem' : 'disableItem']('duplicatelist'); + that[list ? 'enableItem' : 'disableItem']('duplicatelist'); + that[ + list && pandora.$ui.folderList && pandora.$ui.folderList.personal.options('selected').length + ? 'enableItem' : 'disableItem' + ]('hidelist'); that[action]('editlist'); that[action]('deletelist'); that[ui.listSelection.length ? 'enableItem' : 'disableItem']('newlistfromselection'); @@ -842,7 +929,7 @@ pandora.ui.mainMenu = function() { if (!pandora.hasDialogOrScreen()) { if (ui._findState.key != 'advanced') { setTimeout(function() { - pandora.$ui.findInput.focusInput(true); + pandora.$ui.findInput && pandora.$ui.findInput.focusInput(true); }, 25); } else { pandora.$ui.filterDialog = pandora.ui.filterDialog().open(); @@ -983,7 +1070,7 @@ pandora.ui.mainMenu = function() { if (ui.document && i < 2) { pandora.UI.set({documentView: ['info', 'view'][i]}); } else if (i < 2) { - pandora.UI.set({collectionView: ['list', 'grid'][i]}); + pandora.UI.set({collectionView: ['list', 'grid', 'pages'][i]}); } } }); @@ -1157,6 +1244,18 @@ pandora.ui.mainMenu = function() { }) }; }), + ui.hidden[itemNamePlural.toLowerCase()].length ? [ + { + id: 'hiddenlists', + title: Ox._('Hidden ' + itemNamePlural), + items: ui.hidden[itemNamePlural.toLowerCase()].map(id => { + return { + id: 'hidden:' + id.replace(/_/g, Ox.char(9)), + title: id + } + }) + } + ] : [], [ {}, { id: 'newlist', title: Ox._('New ' + itemNameSingular), disabled: isGuest, keyboard: 'control n' }, @@ -1168,6 +1267,8 @@ pandora.ui.mainMenu = function() { { id: 'editlist', title: Ox._('Edit Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'control e' }, { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'delete' }, {}, + { id: 'hidelist', title: Ox._('Hide Selected ' + itemNameSingular + '...'), disabled: disableEdit || !pandora.$ui.folderList || !pandora.$ui.folderList.personal.options('selected').length}, + {}, { id: 'print', title: Ox._('Print'), keyboard: 'control p' } ] )}; @@ -1204,6 +1305,18 @@ pandora.ui.mainMenu = function() { }) }; }), + ui.hidden[itemNamePlural.toLowerCase()].length ? [ + { + id: 'hiddenlists', + title: Ox._('Hidden ' + itemNamePlural), + items: ui.hidden[itemNamePlural.toLowerCase()].map(id => { + return { + id: 'hidden:' + id.replace(/_/g, Ox.char(9)), + title: id + } + }) + } + ] : [], [ {}, { id: 'newlist', title: Ox._('New ' + itemNameSingular), disabled: isGuest, keyboard: 'control n' }, @@ -1212,7 +1325,9 @@ pandora.ui.mainMenu = function() { {}, { id: 'duplicatelist', title: Ox._('Duplicate Selected ' + itemNameSingular), disabled: disableEdit, keyboard: 'control d' }, { id: 'editlist', title: Ox._('Edit Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'control e' }, - { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'delete' } + { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'delete' }, + {}, + { id: 'hidelist', title: Ox._('Hide Selected ' + itemNameSingular + '...'), disabled: disableEdit || !pandora.$ui.folderList || !pandora.$ui.folderList.personal.options('selected').length}, ] )}; } @@ -1272,6 +1387,18 @@ pandora.ui.mainMenu = function() { }) }; }), + ui.hidden[itemNamePlural.toLowerCase()].length ? [ + { + id: 'hiddenlists', + title: Ox._('Hidden ' + itemNamePlural), + items: ui.hidden[itemNamePlural.toLowerCase()].map(id => { + return { + id: 'hidden:' + id.replace(/_/g, Ox.char(9)), + title: id + } + }) + } + ] : [], [ {}, { id: 'newlist', title: Ox._('New ' + itemNameSingular), disabled: isGuest, keyboard: 'control n' }, @@ -1285,6 +1412,8 @@ pandora.ui.mainMenu = function() { { id: 'editlist', title: Ox._('Edit Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'control e' }, { id: 'deletelist', title: Ox._('Delete Selected ' + itemNameSingular + '...'), disabled: disableEdit, keyboard: 'delete' }, {}, + { id: 'hidelist', title: Ox._('Hide Selected ' + itemNameSingular + '...'), disabled: disableEdit || !pandora.$ui.folderList || !pandora.$ui.folderList.personal.options('selected').length}, + {}, { id: 'print', title: Ox._('Print'), keyboard: 'control p' }, { id: 'tv', title: Ox._('TV'), keyboard: 'control space' } ] @@ -1486,7 +1615,7 @@ pandora.ui.mainMenu = function() { return [ { id: 'documents', title: Ox._('View Documents'), items: [ { group: 'collectionview', min: 1, max: 1, items: pandora.site.listViews.filter(function(view) { - return Ox.contains(['list', 'grid'], view.id) + return Ox.contains(['list', 'grid', 'pages'], view.id) }).map(function(view) { return Ox.extend({ checked: ui.collectionView == view.id @@ -1563,19 +1692,40 @@ pandora.ui.mainMenu = function() { Ox._('Open {0}', [Ox._(pandora.site.itemName.singular)]), Ox._('Open {0}', [Ox._(pandora.site.itemName.plural)]) ], items: [ - { group: 'itemview', min: 1, max: 1, items: pandora.site.itemViews.filter(function(view) { - return view.id != 'data' && view.id != 'media' || - pandora.hasCapability('canSeeExtraItemViews'); - }).map(function(view) { - return Ox.extend({ - checked: ui.itemView == view.id - }, view, { - keyboard: itemViewKey <= 10 - ? 'shift ' + (itemViewKey++%10) - : void 0, - title: Ox._(view.title) - }); - }) }, + { + group: 'itemview', + min: 1, + max: 1, + items: [].concat( + pandora.site.itemViews.filter(function(view) { + return view.id != 'data' && view.id != 'media' + }).map(function(view) { + return Ox.extend({ + checked: ui.itemView == view.id + }, view, { + keyboard: itemViewKey <= 10 + ? 'shift ' + (itemViewKey++%10) + : void 0, + title: Ox._(view.title) + }); + }), + pandora.hasCapability('canSeeExtraItemViews') ? [{}] : [], + pandora.hasCapability('canSeeExtraItemViews') + ? pandora.site.itemViews.filter(function(view) { + return view.id == 'data' || view.id == 'media' + }).map(function(view) { + return Ox.extend({ + checked: ui.itemView == view.id + }, view, { + keyboard: itemViewKey <= 10 + ? 'shift ' + (itemViewKey++%10) + : void 0, + title: Ox._(view.title) + }); + }) + : [], + ) + }, ] }, { id: 'clips', title: Ox._('Open Clips'), items: [ { group: 'videoview', min: 1, max: 1, items: ['player', 'editor', 'timeline'].map(function(view) { @@ -1680,8 +1830,8 @@ pandora.ui.mainMenu = function() { function getViewMenu() { return { id: 'viewMenu', title: Ox._('View'), items: [ { id: 'section', title: Ox._('Section'), items: [ - { group: 'viewsection', min: 1, max: 1, items: pandora.site.sections.map(function(section) { - section = Ox.extend({}, section) + { group: 'viewsection', min: 1, max: 1, items: Ox.clone(pandora.site.sections, true).map(function(section) { + section.title = Ox._(section.title); section.checked = section.id == ui.section; return section; }) } @@ -1761,7 +1911,8 @@ pandora.ui.mainMenu = function() { }) } ] }, {}, - { id: 'embed', title: Ox._('Embed...') } + { id: 'embed', title: Ox._('Embed...') }, + { id: 'share', title: Ox._('Share...'), disabled: !pandora.canShareView() } ]} } diff --git a/static/js/mainPanel.js b/static/js/mainPanel.js index 7213ae59..adc10947 100644 --- a/static/js/mainPanel.js +++ b/static/js/mainPanel.js @@ -78,7 +78,7 @@ pandora.ui.mainPanel = function() { if (['clips', 'clip'].indexOf(ui.listView) > -1) { pandora.$ui.list.options({find: ui.itemFind}); } - pandora.$ui.list.reloadList(); + pandora.$ui.list && pandora.$ui.list.reloadList(); } // FIXME: why is this being handled _here_? ui._filterState.forEach(function(data, i) { diff --git a/static/js/pageDialog.js b/static/js/pageDialog.js new file mode 100644 index 00000000..3e836486 --- /dev/null +++ b/static/js/pageDialog.js @@ -0,0 +1,167 @@ +'use strict'; + + +pandora.ui.pageDialog = function(options, self) { + self = self || {} + self.options = Ox.extend({ + }, options); + + console.log(options) + + var dialogHeight = Math.round((window.innerHeight - 48) * 0.9) - 24, + dialogWidth = Math.round(window.innerWidth * 0.9) - 48, + isItemView = !pandora.$ui.documentsDialog, + + $content = Ox.Element(), + + that = Ox.Dialog({ + closeButton: true, + content: $content, + focus: false, + height: dialogHeight, + maximizeButton: true, + minHeight: 256, + minWidth: 512, + padding: 0, + removeOnClose: true, + title: '', + width: dialogWidth + }) + .bindEvent({ + close: function() { + delete pandora.$ui.pageDialog; + }, + resize: function(data) { + dialogHeight = data.height; + dialogWidth = data.width; + $content.options({ + height: dialogHeight, + width: dialogWidth + }); + }, + }), + + $infoButton = Ox.Button({ + title: 'info', + tooltip: Ox._('Open PDF'), + type: 'image' + }) + .css({ + position: 'absolute', + right: '24px', + top: '4px' + }) + .bindEvent({ + click: function(data) { + that.close(); + pandora.URL.push(`/documents/${self.options.document}/${self.options.page}`); + } + }), + + $selectButton = Ox.ButtonGroup({ + buttons: [ + {id: 'previous', title: 'left', tooltip: Ox._('Previous')}, + {id: 'next', title: 'right', tooltip: Ox._('Next')} + ], + type: 'image' + }) + .css({ + position: 'absolute', + right: '44px', + top: '4px' + }) + [self.options.pages.length > 1 ? 'show' : 'hide']() + .bindEvent({ + click: function(data) { + var pageIdx = self.options.pages.indexOf(self.options.page) + if (data.id == 'previous') { + pageIdx-- + } else { + pageIdx++ + } + if (pageIdx < 0) { + pageIdx = self.options.pages.length - 1 + } else if (pageIdx >= self.options.pages.length) { + pageIdx = 0 + } + that.update({ + page: self.options.pages[pageIdx] + }) + } + }); + + $(that.find('.OxBar')[0]) + .append($infoButton) + .append($selectButton); + // fixme: why is this needed? + $(that.find('.OxContent')[0]).css({overflow: 'hidden'}); + + setTitle(); + setContent(); + + function setContent() { + var url = `/documents/${self.options.document}/1024p${self.options.page}.jpg` + if (self.options.query) { + url += '?q=' + encodeURIComponent(self.options.query) + } + $content.replaceWith( + $content = ( + Ox.ImageViewer({ + area: [], + height: dialogHeight, + imageHeight: self.options.dimensions[1], + imagePreviewURL: url.replace('/1024p', `/${self.options.size}p`), + imageURL: url, + imageWidth: self.options.dimensions[0], + width: dialogWidth + }) + ) + .bindEvent({ + center: function(data) { + /* + pandora.UI.set( + 'documents.' + item.id, + {position: $content.getArea().map(Math.round)} + ); + */ + }, + key_escape: function() { + pandora.$ui.pageDialog.close(); + }, + page: function(data) { + /* + pandora.UI.set( + 'documents.' + item.id, + {position: data.page} + ); + */ + }, + zoom: function(data) { + /* + pandora.UI.set( + 'documents.' + item.id, + {position: $content.getArea().map(Math.round)} + ); + */ + } + }) + ); + } + + function setTitle() { + that.options({ + title: (self.options.title || "") + " Page " + self.options.page + }); + } + + + that.update = function(options) { + self.options = Ox.extend(self.options, options) + setTitle(); + setContent(); + }; + + return that; + +}; + diff --git a/static/js/pandora.js b/static/js/pandora.js index 8770be61..caeef67a 100644 --- a/static/js/pandora.js +++ b/static/js/pandora.js @@ -253,10 +253,12 @@ appPanel }); } }); + window.pandora.getVersion = getPandoraVersion } function loadPandoraFiles(callback) { var prefix = '/static/'; + Ox.getFile(prefix + 'css/pandora.css?' + getPandoraVersion()) if (enableDebugMode) { Ox.getJSON( prefix + 'json/pandora.json?' + Ox.random(1000), @@ -402,28 +404,30 @@ appPanel ], sectionFolders: { items: [ - {id: 'personal', title: 'Personal Lists'}, - {id: 'favorite', title: 'Favorite Lists', showBrowser: false}, - {id: 'featured', title: 'Featured Lists', showBrowser: false}, - {id: 'volumes', title: 'Local Volumes'} + {id: 'personal', title: Ox._('Personal Lists')}, + {id: 'favorite', title: Ox._('Favorite Lists'), showBrowser: false}, + {id: 'featured', title: Ox._('Featured Lists'), showBrowser: false}, + {id: 'volumes', title: Ox._('Local Volumes')} ], edits: [ - {id: 'personal', title: 'Personal Edits'}, - {id: 'favorite', title: 'Favorite Edits', showBrowser: false}, - {id: 'featured', title: 'Featured Edits', showBrowser: false} + {id: 'personal', title: Ox._('Personal Edits')}, + {id: 'favorite', title: Ox._('Favorite Edits'), showBrowser: false}, + {id: 'featured', title: Ox._('Featured Edits'), showBrowser: false} ], documents: [ - {id: 'personal', title: 'Personal Collections'}, - {id: 'favorite', title: 'Favorite Collections', showBrowser: false}, - {id: 'featured', title: 'Featured Collections', showBrowser: false} + {id: 'personal', title: Ox._('Personal Collections')}, + {id: 'favorite', title: Ox._('Favorite Collections'), showBrowser: false}, + {id: 'featured', title: Ox._('Featured Collections'), showBrowser: false} ] }, sortKeys: pandora.getSortKeys(), - documentSortKeys: pandora.getDocumentSortKeys(), - collectionViews: [ - {id: 'list', title: Ox._('View as List')}, - {id: 'grid', title: Ox._('View as Grid')} - ] + documentSortKeys: pandora.getDocumentSortKeys() + }); + pandora.site.collectionViews = (pandora.site.collectionViews || [ + {id: 'list', title: Ox._('as List')}, + {id: 'grid', title: Ox._('as Grid')} + ]).map(view => { + return {id: view.id, title: Ox._('View {0}', [Ox._(view.title)])}; }); pandora.site.listSettings = {}; Ox.forEach(pandora.site.user.ui, function(val, key) { @@ -493,6 +497,7 @@ appPanel pandora.$ui.embedPanel.options(data); } }); + pandora.localInit && pandora.localInit(); } else if (isPrint) { pandora.$ui.printView = pandora.ui.printView().display(); } else if (isHome) { diff --git a/static/js/player.js b/static/js/player.js index 409f1821..01904e17 100644 --- a/static/js/player.js +++ b/static/js/player.js @@ -1,6 +1,10 @@ 'use strict'; pandora.ui.player = function(data) { + // FIXME: is this the right location to load subtitles? + if (!data.subtitles) { + data.subtitles = pandora.getSubtitles(data); + } var ui = pandora.user.ui, @@ -47,10 +51,11 @@ pandora.ui.player = function(data) { showUsers: pandora.site.annotations.showUsers, showTimeline: ui.showTimeline, smallTimelineURL: pandora.getMediaURL('/' + ui.item + '/timeline16p.jpg?' + data.modified), - subtitlesDefaultTrack: Ox.getLanguageNameByCode(pandora.site.language), + subtitles: data.subtitles || [], + subtitlesDefaultTrack: data.subtitlesDefaultTrack || Ox.getLanguageNameByCode(pandora.site.language), subtitlesLayer: data.subtitlesLayer, subtitlesOffset: ui.videoSubtitlesOffset, - subtitlesTrack: Ox.getLanguageNameByCode(pandora.site.language), + subtitlesTrack: data.subtitlesTrack || Ox.getLanguageNameByCode(pandora.site.language), timeline: ui.videoTimeline, timelineTooltip: Ox._('timeline') + ' ' + Ox.SYMBOLS.shift + 'T', video: data.video, @@ -83,7 +88,13 @@ pandora.ui.player = function(data) { }), 'clip'); }, downloadvideo: function() { - document.location.href = pandora.getDownloadLink(ui.item, data.rightslevel); + pandora.ui.downloadVideoDialog({ + item: ui.item, + rightsLevel: data.rightsLevel, + source: data.source && pandora.hasCapability('canDownloadSource'), + title: data.title, + video: data.video + }).open(); }, find: function(data) { pandora.UI.set({itemFind: data.find}); diff --git a/static/js/sectionButtons.js b/static/js/sectionButtons.js index 79d1a1fa..55d2cf6d 100644 --- a/static/js/sectionButtons.js +++ b/static/js/sectionButtons.js @@ -2,7 +2,10 @@ pandora.ui.sectionButtons = function(section) { var that = Ox.ButtonGroup({ - buttons: pandora.site.sections, + buttons: Ox.clone(pandora.site.sections, true).map(function(section) { + section.title = Ox._(section.title); + return section; + }), id: 'sectionButtons', selectable: true, value: section || pandora.user.ui.section diff --git a/static/js/sectionSelect.js b/static/js/sectionSelect.js index 1abdf825..9becbd65 100644 --- a/static/js/sectionSelect.js +++ b/static/js/sectionSelect.js @@ -4,7 +4,10 @@ pandora.ui.sectionSelect = function(section) { // fixme: duplicated var that = Ox.Select({ id: 'sectionSelect', - items: pandora.site.sections, + items: Ox.clone(pandora.site.sections, true).map(function(section) { + section.title = Ox._(section.title); + return section; + }), value: section || pandora.user.ui.section }).css({ float: 'left', diff --git a/static/js/shareDialog.js b/static/js/shareDialog.js new file mode 100644 index 00000000..cf399f2b --- /dev/null +++ b/static/js/shareDialog.js @@ -0,0 +1,37 @@ +'use strict'; + +pandora.ui.shareDialog = function(/*[url, ]callback*/) { + + if (arguments.length == 1) { + var url, callback = arguments[0]; + } else { + var url = arguments[0], callback = arguments[1]; + } + var url = document.location.href.replace(document.location.hostname, document.location.hostname + '/m'), + $content = Ox.Element().append( + Ox.Input({ + height: 100, + width: 256, + placeholder: 'Share Link', + type: 'textarea', + disabled: true, + value: url + }) + ), + that = pandora.ui.iconDialog({ + buttons: [ + Ox.Button({ + id: 'close', + title: Ox._('Close') + }).bindEvent({ + click: function() { + that.close(); + } + }), + ], + keys: {enter: 'close', escape: 'close'}, + content: $content, + title: "Share current view", + }); + return that; +} diff --git a/static/js/toolbar.js b/static/js/toolbar.js index 464a7fc1..6eb0a845 100644 --- a/static/js/toolbar.js +++ b/static/js/toolbar.js @@ -95,7 +95,7 @@ pandora.ui.toolbar = function() { return ['map', 'calendar'].indexOf(pandora.user.ui.listView) > -1 ? 152 : 316; } that.updateListName = function(listId) { - pandora.$ui.listTitle.options({title: getListName(listId)}); + pandora.$ui.listTitle && pandora.$ui.listTitle.options({title: getListName(listId)}); }; return that; }; diff --git a/static/js/uploadDocumentDialog.js b/static/js/uploadDocumentDialog.js index 4672e0af..5a9193ca 100644 --- a/static/js/uploadDocumentDialog.js +++ b/static/js/uploadDocumentDialog.js @@ -18,7 +18,10 @@ pandora.ui.uploadDocumentDialog = function(options, callback) { $errorDialog, - $content = Ox.Element().css({margin: '16px'}), + $content = Ox.Element().css({ + margin: '16px', + overflow: 'hidden' + }), $text = $('
') .html(Ox._('Uploading {0}', [files[0].name])) diff --git a/static/js/utils.js b/static/js/utils.js index 574e4a87..7bf3fa5e 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -330,6 +330,12 @@ pandora.beforeUnloadWindow = function() { pandora.isUnloading = true; }; + +pandora.canShareView = function() { + return pandora.hasCapability('canShare') +}; + + pandora.changeFolderItemStatus = function(id, status, callback) { var ui = pandora.user.ui, folderItems = pandora.getFolderItems(ui.section), @@ -559,7 +565,7 @@ pandora.uploadDroppedFiles = function(files) { var clips, type = getType(items); if (Ox.isUndefined(callback)) { callback = index; - index = pandora.$ui.editPanel + index = pandora.$ui.editPanel && pandora.$ui.editPanel.getPasteIndex ? pandora.$ui.editPanel.getPasteIndex() : void 0; } @@ -729,11 +735,11 @@ pandora.uploadDroppedFiles = function(files) { pandora.enableBatchEdit = function(section) { var ui = pandora.user.ui; if (section == 'documents') { - return !ui.document && ui.collectionSelection.length > 1 && ui.collectionSelection.some(function(item) { + return !ui.document && ui.collectionSelection.length > 0 && ui.collectionSelection.some(function(item) { return pandora.$ui.list && pandora.$ui.list.value(item, 'editable'); }) } else { - return !ui.item && ui.listSelection.length > 1 && ui.listSelection.some(function(item) { + return !ui.item && ui.listSelection.length > 0 && ui.listSelection.some(function(item) { return pandora.$ui.list && pandora.$ui.list.value(item, 'editable'); }) } @@ -1110,7 +1116,9 @@ pandora.escapeQueryValue = function(value) { if (!Ox.isString(value)) { value = value.toString(); } - return value.replace(/%/, '%25') + return value + .replace(/%/g, '%25') + .replace(/&/g, '%26') .replace(/_/g, '%09') .replace(/\s/g, '_') .replace(/ { + return condition.key == 'fulltext' + }) + if (conditions.length) { + return conditions[0].value + } + } +}; + pandora.getHash = function(state, callback) { // FIXME: remove this var embedKeys = [ @@ -1761,6 +1793,9 @@ pandora.getLargeEditTimelineURL = function(edit, type, i, callback) { if (clipIn >= timelineOut) { return false; // break } + if (!clip.duration) { + return; + } if ( (timelineIn <= clipIn && clipIn <= timelineOut) || (timelineIn <= clipOut && clipOut <= timelineOut) @@ -2460,6 +2495,7 @@ pandora.getVideoOptions = function(data) { }); }); data.videoRatio = data.videoRatio || pandora.site.video.previewRatio; + return options; }; @@ -2634,7 +2670,10 @@ pandora.openLink = function(url) { if (Ox.startsWith(url, 'mailto:')) { window.open(url); } else { - window.open('/url=' + encodeURIComponent(url), '_blank'); + if (!pandora.site.site.sendReferrer) { + url = '/url=' + btoa(url); + } + window.open(url, '_blank'); } }; @@ -2942,7 +2981,7 @@ pandora.resizeFolders = function(section) { : section == 'edits' ? width - 16 : width - 48 ) - 8); - Ox.forEach(pandora.$ui.folderList, function($list, id) { + pandora.$ui.folderList && Ox.forEach(pandora.$ui.folderList, function($list, id) { var pos = Ox.getIndexById(pandora.site.sectionFolders[section], id); pandora.$ui.folder[pos] && pandora.$ui.folder[pos].css({ width: width + 'px' @@ -2983,7 +3022,7 @@ pandora.resizeWindow = function() { pandora.resizeFolders(); if (pandora.user.ui.section == 'items') { if (!pandora.user.ui.item) { - pandora.resizeFilters(pandora.$ui.rightPanel.width()); + pandora.$ui.rightPanel && pandora.resizeFilters(pandora.$ui.rightPanel.width()); if (pandora.user.ui.listView == 'clips') { var clipsItems = pandora.getClipsItems(), previousClipsItems = pandora.getClipsItems( @@ -3045,7 +3084,7 @@ pandora.resizeWindow = function() { } } } else if (pandora.user.ui.section == 'documents') { - pandora.resizeFilters(pandora.$ui.documentPanel.width()); + pandora.$ui.documentPanel && pandora.resizeFilters(pandora.$ui.documentPanel.width()); if (pandora.user.ui.document) { pandora.$ui.document && pandora.$ui.document.update(); } else { @@ -3193,7 +3232,7 @@ pandora.unloadWindow = function() { && pandora.user.ui.item && ['video', 'timeline'].indexOf(pandora.user.ui.itemView) > -1 && pandora.UI.set( - 'videoPosition.' + pandora.user.ui.item, + 'videoPoints.' + pandora.user.ui.item, pandora.$ui[ pandora.user.ui.itemView == 'video' ? 'player' : 'editor' ].options('position') @@ -3215,11 +3254,11 @@ pandora.updateItemContext = function() { pandora.$ui.contentPanel.replaceElement( 0, pandora.$ui.browser = pandora.ui.browser() ); - } else { + } else if (pandora.$ui.browser) { pandora.$ui.browser.reloadList(); } }); - } else { + } else if (pandora.$ui.browser) { pandora.$ui.browser.reloadList(); } }; diff --git a/static/json/locale.pandora.de.json b/static/json/locale.pandora.de.json index a941abc9..f072e70a 100644 --- a/static/json/locale.pandora.de.json +++ b/static/json/locale.pandora.de.json @@ -24,7 +24,7 @@ "Alternative Titles": "Alternative Titel", "Always Show {0} Poster": "Immer {0} Poster anzeigen", "Annotation": "Annotation", - "annotations {}": "", + "annotations {}": "Annotationen {}", "Annotations": "Annotationen", "another": "", "Anti-Alias": "Anti-Alias", @@ -39,9 +39,9 @@ "Are you sure you want to sign out?": "Sind Sie sicher, dass Sie sich abmelden möchten?", "Are you sure you want to update the value": "Sind Sie sicher, dass Sie den Wert ändern möchten?", "Ascending": "Aufsteigend", - "as Clips": "", - "as Grid": "", - "as List": "", + "as Clips": "als Clips", + "as Grid": "als Raster", + "as List": "als Liste", "Aspect Ratio": "Seitenverhältnis", "Back to {0}": "Zurück zu {0}", "Basic": "Einfach", @@ -58,7 +58,7 @@ "Clear Filter": "Filter zurücksetzen", "Clear Form": "Formular zurücksetzen", "Clip": "Clip", - "clips": "", + "clips": "Clips", "Clips": "Clips", "Code": "Code", "Columns": "Spalten", @@ -69,8 +69,8 @@ "Current": "Aktuell", "Current Reference": "Aktuelle Referenz", "Cut{control_x}": "Ausschneiden", - "cuts per minute": "", - "Cuts per Minute": "Schnitte pro Mitnute", + "cuts per minute": "Schnitte pro Minute", + "Cuts per Minute": "Schnitte pro Minute", "Data": "Daten", "Debug": "Debug", "Default View: ...": "Voreingestellte Ansicht", @@ -131,21 +131,21 @@ "Encoding cancelled.": "Enkodierung abgebrochen.", "Encoding failed.": "Enkodierung fehlgeschlagen.", "Encoding is currently running\\nDo you want to leave this page?": "Die Enkodierung ist noch nicht abgeschlossen. Möchten Sie diese Seite dennoch verlassen?", - "Enter": "", - "Enter {0}": "", - "Enter Fullscreen": "", - "Enter Video Fullscreen": "", - "Error Logs": "", - "Exit": "", - "Exit Fullscreen": "", + "Enter": "Eingeben", + "Enter {0}": "{} betreten", + "Enter Fullscreen": "Vollbildmodus aktivieren", + "Enter Video Fullscreen": "Videovollbildmodus aktivieren", + "Error Logs": "Fehlerprotokolle", + "Exit": "Beenden", + "Exit Fullscreen": "Vollbild beenden", "Export E-Mail Addresses": "", "Extension": "", - "Favorite ": "", - "Favorite": "", - "Favorite {0}": "", - "Favorite Edits": "", - "Favorite Lists": "", - "Favorite Texts": "", + "Favorite ": "Favorit ", + "Favorite": "Favorit", + "Favorite {0}": "Favorit {}", + "Favorite Edits": "Favoritenedits", + "Favorite Lists": "Favoritenlisten", + "Favorite Texts": "Lieblingstexte", "Featured": "", "Featured {0}": "", "Featured {0} are selected public {0}, picked by the {1} staff.": "", @@ -179,7 +179,7 @@ "Go to Position": "", "Grid View": "", "Groups": "", - "

API Documentation

\n

{0} uses a JSON API to communicate between the browser and the server. This API is 100% public, which means that there is virtually no limit to what you can do with the site, or build on top of it — from writing simple scripts to read or write specific information to including data from {0} (not just videos, but also metadata, annotations, lists, or a custom search interface) in your own website.

\n

If you are using the API in JavaScript, you may want to take a look at OxJS, and if you are using Python, there is python-ox, which is used by pandora_client to automate certain tasks.

\n

To get started, just open the console and paste the following snippet. For the first ten items that are both shorter than one hour and whose title does not start with \"X\" (sorted by duration, then title, both in ascending order), it will return their duration,undefined": "", + "

API Documentation

\n

{0} uses a JSON API to communicate between the browser and the server. This API is 100% public, which means that there is virtually no limit to what you can do with the site, or build on top of it — from writing simple scripts to read or write specific information to including data from {0} (not just videos, but also metadata, annotations, lists, or a custom search interface) in your own website.

\n

If you are using the API in JavaScript, you may want to take a look at OxJS, and if you are using Python, there is python-ox, which is used by pandora_client to automate certain tasks.

\n

To get started, just open the console and paste the following snippet. For the first ten items that are both shorter than one hour and whose title does not start with \"X\" (sorted by duration, then title, both in ascending order), it will return their duration,undefined": "", "

pan.do/ra

open media archive

{0} is based on pan.do/ra, a free, open source platform for media archives.

pan.do/ra includes OxJS, a new JavaScript library for web applications.

To learn more about pan.do/ra and OxJS, please visit pan.do/ra and oxjs.org.

{0} is running pan.do/ra revision {1}.": "", "Help...": "", "Help": "", @@ -215,8 +215,9 @@ "Invalid ": "", "Invalid e-mail address": "", "Invert Selection": "", - "IP Address": "", - "Items": "", + "IP Address": "IP Adresse", + "Item": "Objekt", + "Items": "Objekte", "just switch to the editor.": "", "Keep {0}": "", "Keep Document": "", diff --git a/static/json/locale.pandora.tr.json b/static/json/locale.pandora.tr.json new file mode 100644 index 00000000..46295769 --- /dev/null +++ b/static/json/locale.pandora.tr.json @@ -0,0 +1,704 @@ +{ + "...": "...", + "{0} annotations imported.": "{0} açıklama alındı", + "{0} browser": "{0} tarayıcı", + "{0} can download the video": "{0} videoyu indirebilir", + "{0} can play clips": "{0} klipleri oynatabilir", + "{0} can play the full video": "{0} videonun tamamını oynatabilir", + "{0} can see the item": "{0} öğe görülebilir", + "{0} can't download the video": "{0} videoyu indiremez", + "{0} can't play the full video": "{0} videoyu oynatamaz", + "{0} can't see the item": "{0} öğe görünemez", + "{0} Edits": "Editler", + "{0} Lists": "Listeler", + "{0} not found": "bulunamadı", + "{0} or click to hide": "veya gizleyi tıkla", + "{0} subscribers": "aboneler", + "{0} Texts": "Metinler", + "{0} users selected": "kullanıcılar seçildi", + "{0} View": "Görüntüle", + "About {0}": "Hakkında {0}", + "About Rights": "Haklar Hakkında", + "About": "Hakkında", + "Accounts": "Hesaplar", + "Account": "Hesap", + "Add {0} to {1} {2}": "Ekle {0}dan {1}e {2}", + "Add Documents to {0}": "Belge Ekle {0}", + "Add Documents to {0}...": "Belgeyi Ekle {0}...", + "Add to": "Ekle", + "Add Volume...": "Sayı Ekle...", + "Admin": "Admin", + "Advanced Find...": "Gelişmiş Ara...", + "Advanced Find": "Gelişmiş Ara", + "Advanced Sort...": "Gelişmiş Sıralama...", + "Advanced": "Gelişmiş ", + "All {0}": "Bütün {0}", + "All users": "Tüm kullanıcılar", + "Alternatively, you can contact us via {0}": "Veya bizimle iletişime geçeçbilirsiniz {0}", + "Alternative Titles": "Alternatif Başlıklar", + "Alternative Title": "Alternatif Başlık", + "Always Show {0} Poster": "Her Zaman {0} Poster Göster", + "Annotations": "Açıklamalar", + "Annotation": "Açıklama", + "another": "diğeri", + "Anti-Alias": "Anti-Alias", + "API Documentation...": "API Belgeleme...", + "API Documentation": "API Belgeleme", + "Appearance": "Görünüm", + "Application Error": "Uygulama Hatası", + "Archives...": "Arşivler...", + "Are you sure you want to delete the {0} \"{1}\"?": "{0} \"{1}\"? Silmek istediğinden emin misin", + "Are you sure you want to delete the file \"{0}\"?": "{0}\"?\" Silmek istediğinden emin misin", + "Are you sure you want to make the list \"{0}\" private and lose its {1}?": "\"{0}\" Listeyi gizli yapmak istediğinden ve listenin {1}ini kaybedeceğinden emin misin", + "Are you sure you want to reset all UI settings?": "Tüm UI ayarları yeniden ayarla", + "Are you sure you want to sign out?": "Oturumu kapatmak istediğinden emin misin", + "Are you sure you want to update the value": "içeriği değiştirmek istediğinden emin misin", + "Ascending": "artan değere göre sırala", + "as Clips": "Klipler", + "as Grid": "Izgara Düzeni", + "as List": "Liste", + "Aspect Ratio": "Görüntü Boyutu", + "Back to {0}": "Geri dön {0}", + "Basic": "Temel", + "Bitrate": "Bit hızı", + "Browsers": "Tarayıcılar", + "Browser Versions": "Tarayıcı Versiyonları", + "Browser": "Tarayıcı", + "Browse": "Gözat", + "Calendars": "Takvimler", + "Calendar": "Takvimler", + "calendar": "takvim", + "Cancel Upload": "Yüklemeyi iptal et", + "Clear All Filters": "Tüm filtreleri sil", + "Clear Cache": "Önbelleği Sil", + "Clear Clipboard": "Panoyu Sil", + "Clear Filters": "Filtreleri Sil", + "Clear Filter": "Filtreyi Sil", + "Clear Form": "Formu Sil", + "Clear History": "Geçmişi Sil", + "Click to clear or doubleclick to reset query": "Sil'i tıkla veya sorguyu sıfırlamak için çift tıkla", + "Click to open preferences or doubleclick to sign out": "Referanslar için tıkle veya çıkmak için çift tıkla", + "Click to reload {0}": "Yeniden yüklemek için tıkla {0}", + "Click to sign up or doubleclick to sign in": "Çıkış için tıkla veya giriş için çift tıkla", + "Clips": "Klipler", + "clips": "klipler", + "Clip": "klip", + "Code": "Kod", + "Columns": "Sütun", + "Contact": "İletişim", + "Copy {0}": "Kopyala {0}", + "Copy and Add to Clipboard": "Kopyala ve Panoya Ekle", + "Copy": "Kopyala", + "Country": "Ülke", + "Created": "Oluşturuldu", + "Current Reference": "Güncel Referans", + "Current": "Güncel", + "Cut {0}": "Kes {0}", + "Cut and Add to Clipboard": "Kes ve Panoya Ekle", + "Cut{control_x}": "Kes {control_x}", + "cuts per minute": "dakika başına keser", + "Cuts per Minute": "Dakika başına Keser", + "Data": "Veri", + "Debug": "Debug", + "Default View: ...": "Default Görüntü...", + "Default": "Varsayılan", + "Define Place or Event...": "Yer ve Olay Tanımla...", + "Delete {0}...": "Sil {0}...", + "Delete {0}": "Sil {0}", + "Delete {0} {1}": "Sil {0} {1}", + "Delete Document...": "Belgeyi Sil...", + "Delete Document": "Belgeyi Sil", + "Delete Selected Edit...": "Seçilen Kesmeyi Sil...", + "Delete Selected List...": "Seçili Listeyi Sil...", + "Delete Selected Text...": "Seçili Metni Sil...", + "Delete": "Sil", + "Descending": "Azalan", + "Description": "Tanım", + "description": "tanım", + "Dimensions": "Boyutlar", + "Director": "Yönetmen", + "Disable Cache": "Önbelleği Devre Dışı Bırak", + "Disable Debug Mode": "Hataları Ayıklama Modunu Devre Dışı Bırak", + "Disabled": "Devre Dışı", + "Disable Event Logging": "Olay Günlüğünü Devre Dışı Bırak", + "Disable": "Devre Dışı", + "Documents{2}": "Belgeler{2}", + "Documents": "Belgeler", + "documents": "belgeler", + "Document": "Belge", + "Don't Loop": "Loop Yapma", + "Don't Reset": "Sıfırlama", + "Don't send me a receipt": "Alındı onayı gönderme", + "Don't Update": "Güncelleme", + "Dont use this file": "Bu belgeyi kullanma", + "Doubleclick to edit icon": "Simgeyi düzenlemek için çift tıkla", + "Doubleclick to edit text": "Belgeyi düzenlemek için çift tıkla", + "Doubleclick to edit title": "Başlığı düzenlemek için çift tıkla", + "Doubleclick to edit": "Editlemek için çift tıkla", + "Doubleclick to insert text": "Metin girmek için çift tıkla", + "down": "aşağı", + "Duplicate Selected Edit": "Seçili Düzenlemeyi Yedekle", + "Duplicate Selected List": "Seçili Listeyi Yedekle", + "Edit {0}...": "Düzenle {0}...", + "Edit Default View": "Default Görüntüyü Düzenle", + "Edit Document...": "Belgeyi Düzenle...", + "Edit Icon": "Simgeyi Düzenle", + "Editor View": "Editör Görünümü", + "Editor": "Editör", + "editor": "editör", + "Edit Path": "Yolu düzenle", + "Edit Query...": "Sorguyu Düzenle...", + "Edit Query": "Sorguyu Düzenle", + "Edit Selected Edit...": "Seçili Düzenlemeyi Düzenle...", + "Edit Selected List...": "Seçili Listeyi Düzenle...", + "Edit Selected Text...": "Seçili Metni Düzenle...", + "Edit Sort Name": "Ada Göre Sıralamayı Düzenle", + "edits": "editler", + "Edits": "Editler", + "Edit Title": "Başlığı Editle", + "Edit ": "Editle", + "Edit...": "Editle...", + "E-Mail Address already exists": "E-posta adresi zaten var", + "E-Mail Addresses": "E-posta Adresi", + "e-mail address": "e-posta adresi", + "E-Mail Address": "E-posta Adresi", + "E-Mail": "E-posta", + "Embed Document...": "Belgeyi Yerleştir...", + "Embed Document": "Belgeyi Yerleştir", + "Embeds": "Yerleştirmeler", + "Embed Video": "Videoyu Yerleştir", + "Embed...": "Yerleştir...", + "Embed": "Yerleştir", + "Enable Debug Mode": "Hata Ayıklama Modunu Etkinleştir", + "Enabled": "Etkinleştirildi", + "Enable Event Logging": "Olay Günlüğünü Etkinleştir", + "Enable": "Etkinleştir", + "Encoding cancelled.": "Şifreleme iptal", + "Encoding failed.": "Şifreleme başarısız", + "Encoding is currently running\\Do you want to leave this page?": "Şifreleme aktif\\Sayfadan ayrılmak mı istiyorsun?", + "encoding...": "şifreleme...", + "Enter {0}": "{0} Gir", + "Enter Fullscreen": "Tam ekran yap", + "Enter Video Fullscreen": "Videoyu tam ekran yap", + "Enter": "Gir", + "Error Logs": "Hata Kaydı", + "Exit Fullscreen": "Tam ekran yap", + "Exit": "Çıkış", + "Export E-Mail Addresses...": "E-posta Adreslerini Dışa Aktar...", + "Extension": "Uzantı", + "Favorite {0}": "Sık Kullanılan {0}", + "Favorite Edits": "Sık Kullanılan Editler", + "Favorite Lists": "Sık Kullanılan Listeler", + "Favorite Texts": "Sık Kullanılan Metinler", + "Favorite": "Sık Kullanılan", + "favorite": "sık kullanılan", + "Featured {0} are selected public {0}, picked by the {1} staff.": "Öne çıkan {0}, {1} personeli tarafından seçilen halka açık {0}'dır.", + "Featured {0}": "Öne çıkan", + "Featured Edits": "Öne çıkan kesmeler", + "Featured Lists and Texts": "Öne Çıkan Listeler ve Metinler", + "Featured Lists": "Öne Çıkan Listeler", + "Featured Texts": "Öne Çıkan Metinler", + "Featured": "Öne Çıkan", + "featured": "öne çıkan", + "Featuring": "öne çıkar", + "File contains {0} annotation": "Dosya {0} ek açıklama içeriyor", + "Filename": "Dosya adı", + "Filters": "Filtreler", + "filters": "filtreler", + "Find: {0}": "Ara: {0}", + "Find: Advanced...": "Bul: Gelişmiş...", + "Find: Advanced": "Bul: Gelişmiş", + "Find: All {0}": "Bul: Tümü {0}", + "Find Clips": "Klipleri Bul", + "Find: E-Mail Address": "Bul: E-posta Adresi", + "Find: Filename": "Bul: Dosya adı", + "Find in Texts": "Metinlerde Bul", + "Find Similar Clips...": "Benzer Klipleri Bul...", + "Find: Text": "Bul: Metin", + "Find: This List": "Bul: Liste", + "Find: URL": "Bul: URL", + "Find: Username": "Bul: Kullanıcı Adı", + "Find: User": "Bul: Kullanıcı", + "First Seen & Last Seen": "İlk Görüntüleme & Son Görüntüleme", + "First Seen": "İlk Görüntülenme", + "Frames": "Çerçeveler", + "Frequently Asked Questions": "Sık Sorulan Sorular", + "from Edit": "Düzenlemelerden", + "from List": "Listelerden", + "From": "-den/-dan", + "General": "Genel", + "Go to Position": "Pozisyona Git", + "Grid View": "Izgara Görünümü", + "Grid": "Izgara", + "groups": "gruplar", + "Groups": "Gruplar", + "Guest": "Ziyaretçi", + "Help...": "Destek...", + "Help": "Destek", + "Hide {0} Browser": "{0} Tarayıcıyı Gizle", + "Hide Annotations": "Açıklamaları Gizle", + "Hide Clips": "Klipleri Gizle", + "Hide Document": "Belgeyi Gizle", + "Hide Filters": "Filtreleri Gizle", + "Hide Info": "Infoyu Gizle", + "Hide Sidebar": "Kenarçubuğunu Gizle", + "Hide Timeline": "Zaman Çizelgesini Gizle", + "HTML": "HTML", + "Hue": "Renk tonu", + "Icons": "Simgeler", + "Icon": "Simge", + "ID": "Kimlik", + "Ignore Selected Files": "Seçilen Dosyaları Yoksay", + "Import {0}...": "{0} İçeri Aktar...", + "Import Annotations": "Açıklamaları İçeri Aktar", + "Importing {0} annotations...": "{0} açıklamaları içeri aktar...", + "Importing annotations failed.": "Açıklamaları içeri aktarma başarısız oldu.", + "Import": "İçeri Aktar", + "Include Guests": "Ziyaretçileri Dahil Et", + "Include Robots": "Robotları Dahil Et", + "Include": "Dahil Et", + "Incorrect code": "Hatalı Kod", + "Incorrect password": "Hatalı şifre", + "Index": "Indeks", + "Info View": "Info Görünümü", + "Info": "Bilgi", + "info": "Bilgi", + "In Point": "Giriş", + "Insert Embed...": "Embed ekle...", + "Insert Embed": "Embed ekle", + "Insert HTML...": "HTML ekle...", + "Instances": "örnek", + "Invalid e-mail address": "Geçersiz e-posta adresi", + "Invalid ": "Geçersiz", + "Invert Selection": "Seçimi Ters Çevir", + "In": "Giriş", + "IP Address": "IP Adresi", + "Items": "Öğeler", + "Item": "Öğe", + "Join Selected Clips at Cuts": "Seçili Kliplere Kesmelerde Birleştir", + "just switch to the editor.": "editöre geçin.", + "Keep {0}": "Tut {0}", + "Keep Document": "Belgeyi Tut", + "Keep List Public": "Genel Listeyi Tut", + "Keyframes": "Keyframe", + "Keywords": "Anahtar kelimeler", + "Keyword": "Anahtar kelime", + "Languages": "Diller", + "Language": "Dil", + "Last 30 Days": "Son 30 Gün", + "Last Accessed": "Son Erişim", + "Last Modified": "Son Değişiklik", + "Last Seen": "Son Görülme", + "Level": "Seviye", + "License": "Lisans", + "Lightness": "Işık", + "Links": "Linkler", + "List {0}": "Liste {0}", + "Lists": "Listeler", + "lists": "listeler", + "Loading...": "Yükleme...", + "Load Layout...": "Düzeni Yükle...", + "Local Volumes": "Yerel Sayılar", + "Locations": "Konumlar", + "Location": "Konum", + "Loop": "Loop", + "Mail": "Posta", + "Make List Private": "Listeyi Kişisel Yap", + "Make Private": "Kişisel Yap", + "Make Public": "Genel Yap", + "Make Selected Clips Editable": "Seçilen Klipleri Düznelenebilir Yap", + "Manage Documents...": "Belgelere Eriş...", + "Manage Documents": "Belgelere Eriş", + "Manage Events...": "Olaylara Eriş...", + "Manage Events": "Olaylara Eriş", + "Manage Favorite {0}": "Favori {0} Eriş", + "Manage Featured {0}": "Sık Kullanılan {0} Eriş", + "Manage Names...": "Adlar Eriş...", + "Manage Names": "Adlara Eriş", + "Manage Personal Edits": "Kişisel Editlere Eriş", + "Manage Personal Lists": "Kişisel Listelere Eriş", + "Manage Personal Texts": "Kişisel Metinlere Eriş", + "Manage Places...": "Yerler Eriş...", + "Manage Places": "Yerlere Eriş", + "Manage Titles...": "Başlıklar Eriş...", + "Manage Titles": "Başlıklara Eriş", + "Manage Users...": "Kullanıcı Eriş...", + "Manage Users": "Kullanıcılara Eriş", + "Manage Volumes": "Sayılara Eriş", + "Maps": "Haritalar", + "Map": "Harita", + "map": "harita", + "Media": "Medya", + "Member": "Üye", + "Message Sent": "Mesaj Gönderildi", + "Message": "Mesaj", + "Modified": "Değiştirilen", + "Mount Volume": "Depoyu Bağla", + "Move Files": "Dosyaları Taşı", + "Move selected files to another {0}": "Seçilen dosyaları bir başka {0} taşı", + "movies": "filmler", + "movie": "film", + "Moving files...": "Taşınan dosyalar...", + "names": "isimler", + "Names": "İsimler", + "name": "isim", + "Navigation": "Yönlendirme", + "New Edit from Selection": "Seçilenlerden Yeni Edit", + "New Edit": "Yeni Edit", + "New List from Selection": "Seçilenlerden Yeni Liste", + "New List": "Yeni Liste", + "New Password": "Yeni Şifre", + "New PDF": "Yeni PDF", + "New PDF": "Yeni PDF", + "Newsletter": "Haber bülteni", + "New Smart Edit from Results": "Sonuçlardan Yeni Smart Edit", + "New Smart Edit": "Yeni Smart Edit", + "New Smart List from Results": "Sonuçlardan Yeni Smart Edit", + "New Smart List": "Yeni Smart Liste", + "News": "Haberler", + "New Text": "Yeni Metin", + "Next Clip": "Yeni Klip", + "Next Reference": "Sonraki Referans", + "No {0} {1} found": "{0} {1} bulunamadı", + "No {0} {1}": "{0} {1} Yok", + "No {0} Lists": "{0} Liste Yok", + "No description": "Açıklama yok", + "No Documents": "Doküman yok", + "No document selected": "Seçili doküman yok", + "No Embeds": "Embded yok", + "No local volumes": "Yerel depolama hacmi yok", + "No recipients": "Alıcı yok", + "Notes": "Notlar", + "No text": "Metin yok", + "Note": "Not", + "Not Now": "Şimdi değil", + "not signed in": "giriş yapılmadı", + "No user selected": "Hiç kullanıcı seçilmedi", + "No Video": "Video yok", + "Number of Cuts": "Kesme Sayısı", + "Number of Documents": "Doküman Sayısı", + "Number of Files": "Dosya Sayısı", + "Number of Words": "Kelime Sayısı", + "on Calendar": "Takvimde", + "Only show my documents": "Sadece dokümanları göster", + "on Map": "Haritada", + "Open {0}": "Aç {0}", + "Open Clips": "Klipleri Aç", + "Open Document": "Dokümanı Aç", + "Open in {0} View": "{0}Görünümünde Aç", + "Open in {0} View": "{0} Görünümünde Aç", + "Open Selected Clip": "Seçilen Klibi Aç", + "Order {0}": "Sırala {0}", + "Order {0} Filter": "Sırala {0} Filtre", + "Order Clips": "Klipleri Sırala", + "Order Filters": "Filtreleri Sırala", + "Other...": "Diğer...", + "Out Point": "Çıkış Noktası", + "Out": "Çıkış", + "pan.do/ra. \\u2620 2007-{0} 0x2620. All Open Source.": "pan.do/ra. \\u2620 2007-{0} 0x2620. Tamamı Açık Kaynaklı.", + "Part Title": "Başlık", + "Part": "Bölüm", + "Password": "Şifre", + "Paste": "Yapıştır", + "Path": "Yol", + "People": "İnsanlar", + "Personal {0}": "Kişisel", + "Personal Edits": "Kişisel Editler", + "Personal Lists": "Kişisel Listeler", + "Personal Texts": "Kişisel Metinler", + "personal": "kişisel", + "Personal": "Kişisel", + "Pixels": "Pikseller", + "Places": "Mekanlar", + "Platform & Browser Versions": "Platform & Tarayıcı Versiyonu", + "Platforms & Browsers": "Platform & Tarayıcı", + "Platforms": "Platformlar", + "Platform Versions": "Platform Versiyonları", + "Player": "Oynatıcı", + "player": "oynatıcı", + "Please enter a message": "Lütfen bir mesaj girin", + "Please enter a valid e-mail address": "Lütfen geçerli bir posta adresi girin", + "Please enter ": "Giriş", + "Please select layer and .srt file": "Lütfen katmanı ve .srt dosyasını seçin", + "Please select the video file you want to upload.": "Lütfen yüklemek istediğiniz video dosyasını seçin.", + "please sign up or sign in.": "lütfen kaydolun veya oturum açın.", + "Position": "Pozisyon", + "Posters": "Posterler", + "Preferences...": "Tercihler...", + "Preferences": "Tercihler", + "Previous Clip": "Önceki Klip", + "Previous Reference": "Önceki Referans", + "Print": "Yazdır", + "Private Notes": "Özel Notlar", + "Private": "Özel", + "Processing video on server": "Sunucuda video işleniyor", + "Protocol": "Protokol", + "Public Lists": "Genel Listeler", + "Public": "Genel", + "public": "genel", + "Query": "Sıra", + "Random": "Rasgele", + "Redo": "Yeniden yap", + "Reload Application": "Aplikasyonu Yeniden Yükle", + "Remove {0} from {1}": "{0}ı {1}den kaldır", + "Remove from {0} {1}": "{0} {1} den kaldır", + "Remove from": "-dan kaldır", + "Remove Selected Volume...": "Seçileni Kaldır...", + "Replace {0}...": "Yerini değiştir {0}...", + "Reset Filters": "Filtreleri sıfırla", + "Reset Layout": "Layoutı sıfırla", + "Reset Password": "Şifreyi sıfırla", + "Reset Password...": "Şifre... sıfırla", + "Reset UI Settings...": "UI ayarlarını sıfırla...", + "Reset UI Settings": "UI ayarlarını sıfırla", + "Reset": "Sıfırla", + "Restricted": "Sınırlandırılmış", + "Rights Level": "Kullanım Hakları", + "Run Script on Load...": "Yüklendiğinde Komut Dosyasını Çalıştır", + "Run Script on Load": "Yüklendiğinde Komut Dosyasını Çalıştır", + "Saturation": "Renk Doygunluğu", + "Save Layout...": "Layout'u Kaydet", + "Saving Changes...": "Değişiklikleri Kaydediyor...", + "Scan Selected Volume...": "Seçilen Dosyayı Tara", + "Scan Volume": "Dosyayı Tara", + "Screen Size": "Ekran Ölçüsü", + "Section": "Seç", + "Select All {0}": "Tümünü Seç", + "Select All Updates": "Tüm Güncellemeleri Seç", + "Select All": "Tümünü Seç", + "Selected: ": "Seçilen", + "Select Layer": "Layer'ı Seç", + "Select None": "Hiçbirini Seçme", + "Select No Updates": "Yeniden Yüklenenleri Seçme", + "Select SRT...": "SRT... Seç", + "Select Video": "Videoyu Seç", + "Send a receipt to {0}": "{0} adresine bir onay gönderin", + "Sending Message...": "Mesaj Gönderiliyor...", + "Sending": "Gönderiliyor", + "Send Message": "Mesajı Gönder", + "Send": "Gönder", + "Server {0}": "Sunucu", + "Shift+doubleclick to edit": "Düzenlemek için Shift+çift tıklayın", + "Show {0} Browser": "Tarayıcı göster", + "Show Annotations": "Açıklamaları Göster", + "Show Filters": "Filtreleri Göster", + "Show Info": "Bilgiyi göster", + "Show Large Timeline": "Büyük Zaman Çizelgesini Göster", + "Show Layers": "Katmanları Göster", + "Show Query": "Sırayı Göster", + "Show Reflections": "Yansımaları Göster", + "Show Sidebar": "kenar çubuğunu göster", + "Show Timeline": "Zaman Çizelgesini Göster", + "Show": "Göster", + "sidebar": "kenar çubuğu", + "Sign In...": "Gir...", + "Sign In": "Oturum Aç", + "Sign Out...": "Çık...", + "Sign Out": "Oturumdan Çık", + "Sign Up...": "Giriş Yap...", + "Sign Up": "Üye Ol", + "sign up": "üye ol", + "Similar Clips": "Benzer Klipler", + "Similar Colors": "Benzer Renkler", + "Similar Shapes": "Benzer Oranlar", + "Site": "Site", + "Size": "Ölçü", + "Slit-Scan": "Slit tara", + "Smart List - {0}": "Smart Liste - {0}", + "Software": "Yazılım", + "Sorry, a server {0}": "Üzgünüz, bir sunucu {0}", + "Sorry, {0} currently doesn't have a {1} view.": "Üzgünüz, {0} şu anda {1} görünümüne sahip değil.", + "Sorry, {0} currently doesn't have an {1} view.": "Üzgünüz, {0} şu anda bir {1} görünümüne sahip değil.", + "Sorry, {0}": "Üzgünüz", + "Sorry, you have made an unauthorized request.": "Üzgünüz, yetkisiz bir istekte bulundunuz.", + "Sorry, your browser is currently not supported.": "Üzgünüz, tarayıcınız şu anda desteklenmiyor.", + "Sort {0} by": "{0}'a göre sırala", + "Sort {0} Filter by": "Sırala {0} Filtreye göre", + "Sort by {0}": "{0}'a göre sırala", + "Sort by Clip {0}": "Klip {0}'a göre sırala", + "Sort Clips by": "Klipleri Sırala", + "Sort clips": "Klipleri sırala", + "Sort Filters": "Filtreleri Sırala", + "Sort Manually": "Elle Sırala", + "Sort Name": "Sıralama Adı", + "Sort Title": "Sıralama Başlığı", + "Sort": "Sırala", + "Source ({0})": "Kaynak {0}", + "Split Selected Clips at Cuts": "Seçilen Klipleri Kesimlerde Böl", + "Staff": "Personel", + "Statistics...": "İstatistikler...", + "Statistics": "İstatistik", + "Status": "Durum", + "Stay Signed In": "Çevrimiçi kal", + "Student": "Öğrenci", + "Subject": "Konu", + "Subscribed": "Katılım sağlandı", + "Subscribers only": "Yalnızca katılımcılar", + "Subscribers": "katılımcılar", + "subscriber": "katılımcı", + "Subtitle": "Altyazı", + "Summary": "Özet", + "Supported file types are GIF, JPG, PNG and PDF.": "Desteklenen dosya türleri GIF, JPG, PNG ve PDF'dir.", + "Switch to {0} View": "{0} Görüntüsüne Geç", + "Switch to Editor": "Editöre Geç", + "Switch to this {0} after moving files": "Dosyaları oynattıktan sonra {0}'a geç", + "System": "Sistem", + "system": "sistem", + "Terms of Service": "Hizmet Kullanım Şartları", + "Texts": "Metinler", + "texts": "metinler", + "Text": "Metin", + "text": "metin", + "Thanks for your message!

We will get back to you as soon as possible.": "Mesajınız için teşekkürler!

Size en yakın zamanda geri döneceğiz. ", + "The file {0} already exists as {1}": "Bu dosya {0} zaten {1} olarak var", + "The file {0} already exists": "Bu dosya {0} zaten var", + "Theme": "Tema", + "This view cannot
be embedded.": "Bu görünüm
be gömülemez ", + "This view is not
implemented.": "Bu görünüm
be uygulanmadı", + "Timelines": "Zaman Çizelgeleri", + "timeline": "zaman çizelgesi", + "Times Accessed": "Erişim Sayısı", + "Times Seen": "Görüntülenme Sayısı", + "titles": "başlıklar", + "title": "başlık", + "To add or edit {0}, ": "{0}ı ekle veya düzenle", + "To browse and subscribe to shared {0} from other users, please sign up or sign in.": "Diğer kullanıcılardan gelen {0} paylaşımlarına göz atmak ve abone olmak için lütfen kaydolun veya oturum açın.", + "To create and share your own {0} please sign up or sign in.": "Kendi {0}'ınızı oluşturmak ve paylaşmak için lütfen kaydolun veya oturum açın.", + "To create and share your own list of {0} please sign up or sign in.": "Kendi {0} listenizi oluşturmak ve paylaşmak için lütfen kaydolun veya oturum açın.", + "To embed this clip, use the following HTML:
": "Bu klibi yerleştirmek için aşağıdaki HTML'i kullanın:
", + "To embed this file, use the following HTML:
": "Bu dosyayı yerleştirmek için aşağıdaki HTML'yi kullanın:
", + "To import {0} from a local disk, please sign up or sign in.": "{0}'ı yerel bir diskten içe aktarmak için lütfen kaydolun veya oturum açın.", + "Top 30 Days": "İlk 30 Gün", + "To reset your password, please enter either your username or your e-mail address.": "Şifrenizi sıfırlamak için lütfen kullanıcı adınızı veya e-posta adresinizi girin.", + "To sign in to your account, please choose a new password, and enter the code that we have just e-mailed to you.": "Hesabınızda oturum açmak için lütfen yeni bir şifre seçin ve size e-posta ile gönderdiğimiz kodu girin.", + "To sign in to your account, please enter your username and password.": "Hesabınızda oturum açmak için lütfen kullanıcı adınızı ve şifrenizi girin", + "To sign up for an account, please choose a username and password, and enter your e-mail address.": "Bir hesaba kaydolmak için lütfen bir kullanıcı adı ve şifre seçin ve e-posta adresinizi girin", + "To sign up for an account, please choose a username.": "Bir hesaba kaydolmak için lütfen bir kullanıcı adı seçin", + "Total: ": "Tamamı", + "to the list \"{0}\"": "listeye \"{0}\"", + "To update the metadata for this {0}, please enter its IMDb ID.": "Metadatayı güncellemek için {0}, lütfen IMDb kimliğini girin", + "To": "-e/-a", + "TV": "TV", + "Undo": "Geri al", + "Unknown Director": "Bilinmeyen Yönetmen", + "Unknown": "Bilinmeyen", + "unknown": "bilinmeyen", + "Unmount Volume": "Sayıyı kaldır", + "Unsubscribed": "Abonelikten çık", + "Update IMDb ID...": "IMDb ID... yeniden yükle", + "Update IMDb ID": "IMDb ID'yi yeniden yükle", + "Update Metadata...": "Metadatayı yeniden yükle...", + "Update Metadata": "Metdatayı Yeniden Yükle", + "Update Results in the Background": "Sonuçları Arka Planda Güncelle", + "Update": "Yeniden yükle", + "Upload {0}...": "{0} yükle", + "Upload cancelled.": "Yükleme iptal edildi.", + "Upload Document...": "Belge Yükle...", + "Upload Document": "Belge Yükle", + "Upload failed.": "Yükleme hatası.", + "Uploading {0}": "Yükleniyor {0}", + "uploading...": "yükleniyor...", + "Upload PDF": "PDF Yükle", + "Upload Video...": "Video... Yükle", + "Upload Video": "Video Yükle", + "Upload": "Yükle", + "up": "yukarı", + "URL": "URL", + "User: {0}": "Kullanıcı {0}", + "User Agent": "Aracı Kullanıcı", + "Username": "Kullanıcı Adı", + "Users": "Kullanıcılar", + "Use this file": "Bu dosyayı kullan", + "Version": "Versiyon", + "videos": "videolar", + "Videos": "Videolar", + "Video": "Video", + "View {0}": "Görünüm {0}", + "View Clips as Grid": "Izgara Görünümü", + "View Clips as List": "Liste Görünümü", + "View Data": "Data Görünümü", + "View Error Logs...": "Hatalı Girişleri Görüntüle...", + "View List": "Liste Görünümü", + "View Media": "Media Görünümü", + "View on IMDb": "IMDb'de Görüntüle", + "Views": "Görünümler", + "Visit {0}": "{0} Ziyaret Et", + "Watch on {0}": "{0} da İzle", + "Waveform": "Dalgalı", + "Welcome, {0}!

Your account has been created.": "Hoş geldiniz, {0}!

Hesabınız oluşturuldu.", + "Welcome to {0}": "{0} ya Hoşgeldin", + "Window Size": "Pencere Ölçüsü", + "with Clips": "Kliplerle", + "with Timelines": "Zaman Çizelgesi ile", + "Words per Minute": "Dakika Başına Kelimeler", + "Year": "Yıl", + "You can only {0} {1}
to your own lists": "Listelerine sadece {0} {1}
yapabilirsin", + "You can only remove {0}
from your own lists.": "Kendi listelerinizden yalnızca {0}
'ı kaldırabilirsiniz.", + "You can only upload a video to an existing {0}.": "Yalnızca {0} a video yükleyebilirsin", + "You can't {0} {1}
to smart lists": "Smart listelerde {0} {1}
yapamazsın", + "You can't remove {0}
from smart lists.": "Smart listelerden {0}
kaldıramazsın", + "Your E-Mail Address": "E-posta Adresiniz", + "Your message could not be sent. Please try again.": "Mesajınız gönderilemedi. Tekrar deneyin.", + "Your message has been sent.": "Mesajınız gönderildi.", + "Your Name": "Adınız", + "Your video will be transcoded before upload.": "Videonuz yüklenmeden önce yeniden kodlanacak", + "Your video will be uploaded directly.": "Videonuz yüklenecek", + "your": "sizin", + "Manage Home Screen": "Ana Ekranı Yönet", + "New Active Item": "Yeni Aktif Öğe", + "Delete Selected Item": "Seçili Öğeyi Sil", + "Manage Items": "Öğeleri Yönet", + "Active Items": "Aktif Öğeler", + "No items": "Öğe Yok", + "New Inactive Item": "Yeni İnaktif Öğe", + "Inactive Items": "İnaktif Öğeler", + "Active": "Aktif", + "Inactive": "İnaktif", + "Custom": "Özel", + "Collection": "Koleksiyon", + "Collections": "Koleksiyonlar", + "Personal Collections": "Kişisel Koleksiyonlar", + "Favorite Collections": "Favoriler", + "Featured Collections": "Öne Çıkanlar", + "Image URL": "İmge URL", + "Link URL": "Link URL", + "Embed a Poster and Basic Metadata": "Poster ve Temel Meta Verileri Yerleştir", + "Embed a Clip or a Full Video": "Klip veya Tam Video Göm", + "Embed a Timeline": "Zaman Çizelgesi Göm", + "Embed a List Icon with Description": "Açıklamalı Bir Liste Simgesi Göm", + "Embed Movies as a Grid": "Filmleri Izgara Olarak Göm", + "Embed a Map View": "Harita Görünümü Göm", + "Embed a Calendar View": "Takvim Görünümü Yerleştir", + "Embed a Document": "Belge Göm", + "Embed an Edited Video": "Kurgulanmış Video Göm", + "Show Advanced Options": "Gelişmiş Seçenekleri Göster", + "Embed in Text": "Metne Göm", + "Embed in External Site": "Harici Siteye Göm", + "Link Text": "Bağlantı Metni", + "Frame Size": "Kare Boyutu", + "Edit{noun}": "Düzenle", + "Icon and Description": "Simge ve Açıklama", + "Categories": "Kategoriler", + "Sort by": "Sırala:", + "Show Timeline": "Zaman Çizelgesini Göster", + "Match Video Ratio": "Video Oranını Eşleştir", + "Groups and Collectives": "Gruplar ve Kolektifler", + "Subtitles": "Altyazılar", + "Smart List": "Akıllı Liste", + "Save as {0}": "{0} olarak kaydet", + "Upload Video Files": "Video Dosyalarını Yükle", + "Import Video Files": "Video Dosyalarını İçe Aktar", + "File": "Dosya", + "Select Video Files": "Video Dosyaları Seç", + "YouTube, Vimeo, etc.": "YouTuve, Vimeo, vs.", + "No Summary": "Açıklama yok", + "No Groups": "", + "Mixed Notes": "", + "Mixed Titles": "", + "Publisher": "Yayıncı", + "Author": "Yazar", + "Year": "Yıl", + "as List": "Liste", + "as Grid": "Izgara Düzeni" + +} \ No newline at end of file diff --git a/static/mobile/css/reset.css b/static/mobile/css/reset.css new file mode 100644 index 00000000..b84ca0d8 --- /dev/null +++ b/static/mobile/css/reset.css @@ -0,0 +1,46 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +u, i, center, +ol, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; + +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/static/mobile/css/style.css b/static/mobile/css/style.css new file mode 100644 index 00000000..2dd8e28c --- /dev/null +++ b/static/mobile/css/style.css @@ -0,0 +1,203 @@ +:root { + --bg: rgb(16, 16, 16); + --fg: #CCCCCC; + --title: rgb(240, 240, 240); +} + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: "Noto Sans", "Lucida Grande", "Segoe UI", "DejaVu Sans", "Lucida Sans Unicode", Helvetica, Arial, sans-serif; + line-height: normal; +} + +*, *::after, *::before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +*:focus { + outline: none; +} + +a { + color: var(--fg) +} +iframe { + max-width: 100%; +} + +.player { + max-width: 100%; +} +video, .poster { + border-bottom: 1px solid yellow; +} +.content { + display: flex; + flex-direction: column; + min-height: 100vh; + max-width: 1000px; + margin: auto; + padding-top: 16px; +} +.item-title { + color: var(--title); + text-decoration: underline; + padding-bottom: 0; + font-size: 16px; + text-wrap: balance; +} +.title { + padding-bottom: 0; + margin-bottom: 4px; + font-size: 18px; + font-weight: bold; + border-bottom: 1px solid pink; + text-wrap: balance; +} +.byline { + padding: 0; + padding-bottom: 16px; +} +.player { + text-align: center; + padding-top: 0px; + padding-bottom: 0px; +} +@media(max-width:768px) { + .item-title, + .title, + .byline { + padding-left: 4px; + padding-right: 4px; + } + .player { + position: sticky; + top: 0px; + } +} +.player video { + z-index: 0; +} +.value { + padding: 4px; + padding-top: 16px; + padding-left: 0; + padding-right: 0; + flex: 1; +} +@media(max-width:768px) { + .value { + padding-left: 4px; + padding-right: 4px; + } +} +.more { + padding-top: 16px; + padding-bottom: 16px; + text-align: center; + font-size: 14px; +} +.more a { + color: rgb(144, 144, 144); +} +.more nav { + margin-top: 24px; + margin-bottom: 24px; +} +.layer.active { + padding-top: 8px; +} +.layer.active:first-child { + padding-top: 0px; +} +.layer h3 { + font-weight: bold; + padding: 4px; + padding-left: 0; + margin: 0; + //display: none; +} +.layer.active h3 { + display: block; +} + +.annotation { + padding: 4px; + border-bottom: 1px solid blueviolet; + display: none; +} +.annotation.active { + display: block; +} +.annotation.active.place, +.annotation.active.string { + display: inline-block; + border: 1px solid blueviolet; + margin-bottom: 4px; +} +.annotation .user { + font-style: italic; +} + +@media(max-width:768px) { + .annotation a img { + width: 100%; + } +} + +.layer h3 { + cursor: pointer; +} +.layer .icon svg { + width: 12px; + height: 12px; +} +.layer.collapsed .annotation.active { + display: none; +} +.rotate { + transform: rotate(90deg) translateY(-100%); + transform-origin:bottom left; +} + +.document.text { + line-height: 1.5; + letter-spacing: 0.1px; + word-wrap: break-word; + hyphens: auto; +} +.document.text p { + padding-bottom: 1em; +} +.document.text img { + max-width: 100vw; + margin-left: -4px; + margin-right: -4px; +} +figure { + text-align: center; +} +figure img { + max-width: 100vw; + margin-left: -4px; + margin-right: -4px; +} +@media(max-width:768px) { +.document.text { + padding-left: 4px; + padding-right: 4px; +} +} + +ol li { + margin-left: 1em; +} + +.blocked svg { + width: 64px; + height: 64px; +} diff --git a/static/mobile/js/VideoElement.js b/static/mobile/js/VideoElement.js new file mode 100644 index 00000000..8238d95d --- /dev/null +++ b/static/mobile/js/VideoElement.js @@ -0,0 +1,730 @@ +'use strict'; + +/*@ +VideoElement VideoElement Object + options Options object + autoplay autoplay + items array of objects with src,in,out,duration + loop loop playback + playbackRate playback rate + position start position + self Shared private variable + ([options[, self]]) -> VideoElement Object + loadedmetadata loadedmetadata + itemchange itemchange + seeked seeked + seeking seeking + sizechange sizechange + ended ended +@*/ + +(function() { + var queue = [], + queueSize = 100, + restrictedElements = [], + requiresUserGesture = mediaPlaybackRequiresUserGesture(), + unblock = []; + + +window.VideoElement = function(options) { + + var self = {}, + that = document.createElement("div"); + + self.options = { + autoplay: false, + items: [], + loop: false, + muted: false, + playbackRate: 1, + position: 0, + volume: 1 + } + Object.assign(self.options, options); + debug(self.options) + + that.style.position = "relative" + that.style.width = "100%" + that.style.height = "100%" + that.style.maxHeight = "100vh" + that.style.margin = 'auto' + if (self.options.aspectratio) { + that.style.aspectRatio = self.options.aspectratio + } else { + that.style.height = '128px' + } + that.triggerEvent = function(event, data) { + if (event != 'timeupdate') { + debug('Video', 'triggerEvent', event, data); + } + event = new Event(event) + event.data = data + that.dispatchEvent(event) + } + + + /* + .update({ + items: function() { + self.loadedMetadata = false; + loadItems(function() { + self.loadedMetadata = true; + var update = true; + if (self.currentItem >= self.numberOfItems) { + self.currentItem = 0; + } + if (!self.numberOfItems) { + self.video.src = ''; + that.triggerEvent('durationchange', { + duration: that.duration() + }); + } else { + if (self.currentItemId != self.items[self.currentItem].id) { + // check if current item is in new items + self.items.some(function(item, i) { + if (item.id == self.currentItemId) { + self.currentItem = i; + loadNextVideo(); + update = false; + return true; + } + }); + if (update) { + self.currentItem = 0; + self.currentItemId = self.items[self.currentItem].id; + } + } + if (!update) { + that.triggerEvent('seeked'); + that.triggerEvent('durationchange', { + duration: that.duration() + }); + } else { + setCurrentVideo(function() { + that.triggerEvent('seeked'); + that.triggerEvent('durationchange', { + duration: that.duration() + }); + }); + } + } + }); + }, + playbackRate: function() { + self.video.playbackRate = self.options.playbackRate; + } + }) + .css({width: '100%', height: '100%'}); + */ + + debug('Video', 'VIDEO ELEMENT OPTIONS', self.options); + + self.currentItem = -1; + self.currentTime = 0; + self.currentVideo = 0; + self.items = []; + self.loadedMetadata = false; + that.paused = self.paused = true; + self.seeking = false; + self.loading = true; + self.buffering = true; + self.videos = [getVideo(), getVideo()]; + self.video = self.videos[self.currentVideo]; + self.video.classList.add("active") + self.volume = self.options.volume; + self.muted = self.options.muted; + self.brightness = document.createElement('div') + self.brightness.style.top = '0' + self.brightness.style.left = '0' + self.brightness.style.width = '100%' + self.brightness.style.height = '100%' + self.brightness.style.background = 'rgb(0, 0, 0)' + self.brightness.style.opacity = '0' + self.brightness.style.position = "absolute" + that.append(self.brightness) + + self.timeupdate = setInterval(function() { + if (!self.paused + && !self.loading + && self.loadedMetadata + && self.items[self.currentItem] + && self.items[self.currentItem].out + && self.video.currentTime >= self.items[self.currentItem].out) { + setCurrentItem(self.currentItem + 1); + } + }, 30); + + // mobile browsers only allow playing media elements after user interaction + if (restrictedElements.length > 0) { + unblock.push(setSource) + setTimeout(function() { + that.triggerEvent('requiresusergesture'); + }) + } else { + setSource(); + } + + function getCurrentTime() { + var item = self.items[self.currentItem]; + return self.seeking || self.loading + ? self.currentTime + : item ? item.position + self.video.currentTime - item['in'] : 0; + } + + function getset(key, value) { + var ret; + if (isUndefined(value)) { + ret = self.video[key]; + } else { + self.video[key] = value; + ret = that; + } + return ret; + } + + function getVideo() { + var video = getVideoElement() + video.style.display = "none" + video.style.width = "100%" + video.style.height = "100%" + video.style.margin = "auto" + video.style.background = '#000' + if (self.options.aspectratio) { + video.style.aspectRatio = self.options.aspectratio + } else { + video.style.height = '128px' + } + video.style.top = 0 + video.style.left = 0 + video.style.position = "absolute" + video.preload = "metadata" + video.addEventListener("ended", event => { + if (self.video == video) { + setCurrentItem(self.currentItem + 1); + } + }) + video.addEventListener("loadedmetadata", event => { + //console.log("!!", video.src, "loaded", 'current?', video == self.video) + }) + video.addEventListener("progress", event => { + // stop buffering if buffered to end point + var item = self.items[self.currentItem], + nextItem = mod(self.currentItem + 1, self.numberOfItems), + next = self.items[nextItem], + nextVideo = self.videos[mod(self.currentVideo + 1, self.videos.length)]; + if (self.video == video && (video.preload != 'none' || self.buffering)) { + if (clipCached(video, item)) { + video.preload = 'none'; + self.buffering = false; + if (nextItem != self.currentItem) { + nextVideo.preload = 'auto'; + } + } else { + if (nextItem != self.currentItem && nextVideo.preload != 'none' && nextVideo.src) { + nextVideo.preload = 'none'; + } + } + } else if (nextVideo == video && video.preload != 'none' && nextVideo.src) { + if (clipCached(video, next)) { + video.preload = 'none'; + } + } + + function clipCached(video, item) { + var cached = false + for (var i=0; i= item.out) { + cached = true + } + } + return cached + } + }) + video.addEventListener("volumechange", event => { + if (self.video == video) { + that.triggerEvent('volumechange') + } + }) + video.addEventListener("play", event => { + /* + if (self.video == video) { + that.triggerEvent('play') + } + */ + }) + video.addEventListener("pause", event => { + /* + if (self.video == video) { + that.triggerEvent('pause') + } + */ + }) + video.addEventListener("timeupdate", event => { + if (self.video == video) { + /* + var box = self.video.getBoundingClientRect() + if (box.width && box.height) { + that.style.width = box.width + 'px' + that.style.height = box.height + 'px' + } + */ + that.triggerEvent('timeupdate', { + currentTime: getCurrentTime() + }) + } + }) + video.addEventListener("seeking", event => { + if (self.video == video) { + that.triggerEvent('seeking') + } + }) + video.addEventListener("stop", event => { + if (self.video == video) { + //self.video.pause(); + that.triggerEvent('ended'); + } + }) + that.append(video) + return video + } + + function getVideoElement() { + var video; + if (requiresUserGesture) { + if (queue.length) { + video = queue.pop(); + } else { + video = document.createElement('video'); + restrictedElements.push(video); + } + } else { + video = document.createElement('video'); + } + video.playsinline = true + video.setAttribute('playsinline', 'playsinline') + video.setAttribute('webkit-playsinline', 'webkit-playsinline') + video.WebKitPlaysInline = true + return video + }; + + function getVolume() { + var volume = 1; + if (self.items[self.currentItem] && isNumber(self.items[self.currentItem].volume)) { + volume = self.items[self.currentItem].volume; + } + return self.volume * volume; + } + + + function isReady(video, callback) { + if (video.seeking && !self.paused && !self.seeking) { + that.triggerEvent('seeking'); + debug('Video', 'isReady', 'seeking'); + video.addEventListener('seeked', function(event) { + debug('Video', 'isReady', 'seeked'); + that.triggerEvent('seeked'); + callback(video); + }, {once: true}) + } else if (video.readyState) { + callback(video); + } else { + that.triggerEvent('seeking'); + video.addEventListener('loadedmetadata', function(event) { + callback(video); + }, {once: true}); + video.addEventListener('seeked', event => { + that.triggerEvent('seeked'); + }, {once: true}) + } + } + + function loadItems(callback) { + debug('loadItems') + var currentTime = 0, + items = self.options.items.map(function(item) { + return isObject(item) ? {...item} : {src: item}; + }); + + self.items = items; + self.numberOfItems = self.items.length; + items.forEach(item => { + item['in'] = item['in'] || 0; + item.position = currentTime; + if (item.out) { + item.duration = item.out - item['in']; + } + if (item.duration) { + if (!item.out) { + item.out = item.duration; + } + currentTime += item.duration; + item.id = getId(item); + } else { + getVideoInfo(item.src, function(info) { + item.duration = info.duration; + if (!item.out) { + item.out = item.duration; + } + currentTime += item.duration; + item.id = getId(item); + }); + } + }) + debug('loadItems done') + callback && callback(); + + function getId(item) { + return item.id || item.src + '/' + item['in'] + '-' + item.out; + } + } + + function loadNextVideo() { + if (self.numberOfItems <= 1) { + return; + } + var item = self.items[self.currentItem], + nextItem = mod(self.currentItem + 1, self.numberOfItems), + next = self.items[nextItem], + nextVideo = self.videos[mod(self.currentVideo + 1, self.videos.length)]; + nextVideo.addEventListener('loadedmetadata', function() { + if (self.video != nextVideo) { + nextVideo.currentTime = next['in'] || 0; + } + }, {once: true}); + nextVideo.src = next.src; + nextVideo.preload = 'metadata'; + } + + function setCurrentItem(item) { + debug('Video', 'sCI', item, self.numberOfItems); + var interval; + if (item >= self.numberOfItems || item < 0) { + if (self.options.loop) { + item = mod(item, self.numberOfItems); + } else { + self.seeking = false; + self.ended = true; + that.paused = self.paused = true; + self.video && self.video.pause(); + that.triggerEvent('ended'); + return; + } + } + self.video && self.video.pause(); + self.currentItem = item; + self.currentItemId = self.items[self.currentItem].id; + setCurrentVideo(function() { + if (!self.loadedMetadata) { + self.loadedMetadata = true; + that.triggerEvent('loadedmetadata'); + } + debug('Video', 'sCI', 'trigger itemchange', + self.items[self.currentItem]['in'], self.video.currentTime, self.video.seeking); + that.triggerEvent('sizechange'); + that.triggerEvent('itemchange', { + item: self.currentItem + }); + }); + } + + function setCurrentVideo(callback) { + var css = {}, + muted = self.muted, + item = self.items[self.currentItem], + next; + debug('Video', 'sCV', JSON.stringify(item)); + + ['left', 'top', 'width', 'height'].forEach(function(key) { + css[key] = self.videos[self.currentVideo].style[key]; + }); + self.currentTime = item.position; + self.loading = true; + if (self.video) { + self.videos[self.currentVideo].style.display = "none" + self.videos[self.currentVideo].classList.remove("active") + self.video.pause(); + } + self.currentVideo = mod(self.currentVideo + 1, self.videos.length); + self.video = self.videos[self.currentVideo]; + self.video.classList.add("active") + self.video.muted = true; // avoid sound glitch during load + if (!self.video.attributes.src || self.video.attributes.src.value != item.src) { + self.loadedMetadata && debug('Video', 'caching next item failed, reset src'); + self.video.src = item.src; + } + self.video.preload = 'metadata'; + self.video.volume = getVolume(); + self.video.playbackRate = self.options.playbackRate; + Object.keys(css).forEach(key => { + self.video.style[key] = css[key] + }) + self.buffering = true; + debug('Video', 'sCV', self.video.src, item['in'], + self.video.currentTime, self.video.seeking); + isReady(self.video, function(video) { + var in_ = item['in'] || 0; + + function ready() { + debug('Video', 'sCV', 'ready'); + self.seeking = false; + self.loading = false; + self.video.muted = muted; + !self.paused && self.video.play(); + self.video.style.display = 'block' + callback && callback(); + loadNextVideo(); + } + if (video.currentTime == in_) { + debug('Video', 'sCV', 'already at position', item.id, in_, video.currentTime); + ready(); + } else { + self.video.addEventListener("seeked", event => { + debug('Video', 'sCV', 'seeked callback'); + ready(); + }, {once: true}) + if (!self.seeking) { + debug('Video', 'sCV set in', video.src, in_, video.currentTime, video.seeking); + self.seeking = true; + video.currentTime = in_; + if (self.paused) { + var promise = self.video.play(); + if (promise !== undefined) { + promise.then(function() { + self.video.pause(); + self.video.muted = muted; + }).catch(function() { + self.video.pause(); + self.video.muted = muted; + }); + } else { + self.video.pause(); + self.video.muted = muted; + } + } + } + } + }); + } + + function setCurrentItemTime(currentTime) { + debug('Video', 'sCIT', currentTime, self.video.currentTime, + 'delta', currentTime - self.video.currentTime); + isReady(self.video, function(video) { + if (self.video == video) { + if(self.video.seeking) { + self.video.addEventListener("seeked", event => { + that.triggerEvent('seeked'); + self.seeking = false; + }, {once: true}) + } else if (self.seeking) { + that.triggerEvent('seeked'); + self.seeking = false; + } + video.currentTime = currentTime; + } + }); + } + + function setCurrentTime(time) { + debug('Video', 'sCT', time); + var currentTime, currentItem; + self.items.forEach(function(item, i) { + if (time >= item.position + && time < item.position + item.duration) { + currentItem = i; + currentTime = time - item.position + item['in']; + return false; + } + }); + if (self.items.length) { + // Set to end of items if time > duration + if (isUndefined(currentItem) && isUndefined(currentTime)) { + currentItem = self.items.length - 1; + currentTime = self.items[currentItem].duration + self.items[currentItem]['in']; + } + debug('Video', 'sCT', time, '=>', currentItem, currentTime); + if (currentItem != self.currentItem) { + setCurrentItem(currentItem); + } + self.seeking = true; + self.currentTime = time; + that.triggerEvent('seeking'); + setCurrentItemTime(currentTime); + } else { + self.currentTime = 0; + } + } + + function setSource() { + self.loadedMetadata = false; + loadItems(function() { + setCurrentTime(self.options.position); + self.options.autoplay && setTimeout(function() { + that.play(); + }); + }); + } + + + /*@ + brightness get/set brightness + @*/ + that.brightness = function() { + var ret; + if (arguments.length == 0) { + ret = 1 - parseFloat(self.brightness.style.opacity); + } else { + self.brightness.style.opacity = 1 - arguments[0] + ret = that; + } + return ret; + }; + + /*@ + buffered buffered + @*/ + that.buffered = function() { + return self.video.buffered; + }; + + /*@ + currentTime get/set currentTime + @*/ + that.currentTime = function() { + var ret; + if (arguments.length == 0) { + ret = getCurrentTime(); + } else { + self.ended = false; + setCurrentTime(arguments[0]); + ret = that; + } + return ret; + }; + + /*@ + duration duration + @*/ + that.duration = function() { + return self.items ? self.items.reduce((duration, item) => { + return duration + item.duration; + }, 0) : NaN; + }; + + /*@ + muted get/set muted + @*/ + that.muted = function(value) { + if (!isUndefined(value)) { + self.muted = value; + } + return getset('muted', value); + }; + + /*@ + pause pause + @*/ + that.pause = function() { + that.paused = self.paused = true; + self.video.pause(); + that.paused && that.triggerEvent('pause') + }; + + /*@ + play play + @*/ + that.play = function() { + if (self.ended) { + that.currentTime(0); + } + isReady(self.video, function(video) { + self.ended = false; + that.paused = self.paused = false; + self.seeking = false; + video.play(); + that.triggerEvent('play') + }); + }; + + that.removeElement = function() { + self.currentTime = getCurrentTime(); + self.loading = true; + clearInterval(self.timeupdate); + //Chrome does not properly release resources, reset manually + //http://code.google.com/p/chromium/issues/detail?id=31014 + self.videos.forEach(function(video) { + video.src = '' + }); + return Ox.Element.prototype.removeElement.apply(that, arguments); + }; + + /*@ + videoHeight get videoHeight + @*/ + that.videoHeight = function() { + return self.video.videoHeight; + }; + + /*@ + videoWidth get videoWidth + @*/ + that.videoWidth = function() { + return self.video.videoWidth; + }; + + /*@ + volume get/set volume + @*/ + that.volume = function(value) { + if (isUndefined(value)) { + value = self.volume + } else { + self.volume = value; + self.video.volume = getVolume(); + } + return value; + }; + + return that; + +}; + +// mobile browsers only allow playing media elements after user interaction + + function mediaPlaybackRequiresUserGesture() { + // test if play() is ignored when not called from an input event handler + var video = document.createElement('video'); + video.muted = true + video.play(); + return video.paused; + } + + + async function removeBehaviorsRestrictions() { + debug('Video', 'remove restrictions on video', self.video, restrictedElements.length, queue.length); + if (restrictedElements.length > 0) { + var rElements = restrictedElements; + restrictedElements = []; + rElements.forEach(async function(video) { + await video.load(); + }); + setTimeout(function() { + var u = unblock; + unblock = []; + u.forEach(function(callback) { callback(); }); + }, 1000); + } + while (queue.length < queueSize) { + var video = document.createElement('video'); + video.load(); + queue.push(video); + } + } + + if (requiresUserGesture) { + window.addEventListener('keydown', removeBehaviorsRestrictions); + window.addEventListener('mousedown', removeBehaviorsRestrictions); + window.addEventListener('touchstart', removeBehaviorsRestrictions); + } +})(); diff --git a/static/mobile/js/VideoPlayer.js b/static/mobile/js/VideoPlayer.js new file mode 100644 index 00000000..af219c54 --- /dev/null +++ b/static/mobile/js/VideoPlayer.js @@ -0,0 +1,413 @@ +(function() { + +window.VideoPlayer = function(options) { + + var self = {}, that; + self.options = { + autoplay: false, + controls: true, + items: [], + loop: false, + muted: false, + playbackRate: 1, + position: 0, + volume: 1 + } + Object.assign(self.options, options); + that = VideoElement(options); + + self.controls = document.createElement('div') + self.controls.classList.add('mx-controls') + if (options.poster) { + self.controls.classList.add('poster') + } + //self.controls.style.display = "none" + if (self.options.controls) { + var ratio = `aspect-ratio: ${self.options.aspectratio};` + if (!self.options.aspectratio) { + ratio = 'height: 128px;' + } + self.controls.innerHTML = ` + +
+
${icon.play}
+
+
+
+ ${icon.mute} +
+
+
+
+ +
+
+
+
+
+
+ ${isIOS || !self.options.aspectratio ? "" : icon.enterFullscreen} +
+
+ ` + var toggleVideo = event => { + event.preventDefault() + event.stopPropagation() + if (that.paused) { + self.controls.classList.remove('poster') + that.play() + } else { + that.pause() + } + } + async function toggleFullscreen(event) { + if (isIOS) { + return + } + event.preventDefault() + event.stopPropagation() + if (!document.fullscreenElement) { + that.classList.add('fullscreen') + if (that.webkitRequestFullscreen) { + await that.webkitRequestFullscreen() + } else { + await that.requestFullscreen() + } + console.log('entered fullscreen') + var failed = false + if (!screen.orientation.type.startsWith("landscape")) { + await screen.orientation.lock("landscape").catch(err => { + console.log('no luck with lock', err) + /* + document.querySelector('.error').innerHTML = '' + err + that.classList.remove('fullscreen') + document.exitFullscreen(); + screen.orientation.unlock() + failed = true + */ + }) + } + if (that.paused && !failed) { + self.controls.classList.remove('poster') + that.play() + } + } else { + that.classList.remove('fullscreen') + document.exitFullscreen(); + screen.orientation.unlock() + } + } + var toggleSound = event => { + event.preventDefault() + event.stopPropagation() + if (that.muted()) { + that.muted(false) + } else { + that.muted(true) + } + } + var showControls + var toggleControls = event => { + if (self.controls.style.opacity == '0') { + event.preventDefault() + event.stopPropagation() + self.controls.style.opacity = '1' + showControls = setTimeout(() => { + self.controls.style.opacity = that.paused ? '1' : '0' + showControls = null + }, 3000) + } else { + self.controls.style.opacity = '0' + } + } + self.controls.addEventListener("mousemove", event => { + if (showControls) { + clearTimeout(showControls) + } + self.controls.style.opacity = '1' + showControls = setTimeout(() => { + self.controls.style.opacity = that.paused ? '1' : '0' + showControls = null + }, 3000) + }) + self.controls.addEventListener("mouseleave", event => { + if (showControls) { + clearTimeout(showControls) + } + self.controls.style.opacity = that.paused ? '1' : '0' + showControls = null + }) + self.controls.addEventListener("touchstart", toggleControls) + self.controls.querySelector('.toggle').addEventListener("click", toggleVideo) + self.controls.querySelector('.volume').addEventListener("click", toggleSound) + self.controls.querySelector('.fullscreen-btn').addEventListener("click", toggleFullscreen) + document.addEventListener('fullscreenchange', event => { + if (!document.fullscreenElement) { + screen.orientation.unlock() + that.classList.remove('fullscreen') + that.querySelector('.fullscreen-btn').innerHTML = icon.enterFullscreen + } else { + self.controls.querySelector('.fullscreen-btn').innerHTML = icon.exitFullscreen + } + }) + that.append(self.controls) + } + + function getVideoWidth() { + if (document.fullscreenElement) { + return '' + } + var av = that.querySelector('video.active') + return av ? av.getBoundingClientRect().width + 'px' : '100%' + } + + var playOnLoad = false + var unblock = document.createElement("div") + + that.addEventListener("requiresusergesture", event => { + unblock.style.position = "absolute" + unblock.style.width = '100%' + unblock.style.height = '100%' + unblock.style.backgroundImage = `url(${self.options.poster})` + unblock.style.zIndex = '1000' + unblock.style.backgroundPosition = "top left" + unblock.style.backgroundRepeat = "no-repeat" + unblock.style.backgroundSize = "cover" + unblock.style.display = 'flex' + unblock.classList.add('mx-controls') + unblock.classList.add('poster') + unblock.innerHTML = ` +
+
${icon.play}
+
+
+ ` + self.controls.style.opacity = '0' + unblock.addEventListener("click", event => { + event.preventDefault() + event.stopPropagation() + playOnLoad = true + unblock.querySelector('.toggle').innerHTML = ` +
${icon.loading}
+ ` + }, {once: true}) + that.append(unblock) + }) + var loading = true + that.brightness(0) + that.addEventListener("loadedmetadata", event => { + // + }) + that.addEventListener("seeked", event => { + if (loading) { + that.brightness(1) + loading = false + } + if (playOnLoad) { + playOnLoad = false + var toggle = self.controls.querySelector('.toggle') + toggle.title = 'Pause' + toggle.querySelector('div').innerHTML = icon.pause + self.controls.style.opacity = '0' + unblock.remove() + self.controls.classList.remove('poster') + that.play() + } + }) + + var time = that.querySelector('.controls .time div'), + progress = that.querySelector('.controls .position .progress') + that.querySelector('.controls .position').addEventListener("click", event => { + var bar = event.target + while (bar && !bar.classList.contains('bar')) { + bar = bar.parentElement + } + if (bar && bar.classList.contains('bar')) { + event.preventDefault() + event.stopPropagation() + var rect = bar.getBoundingClientRect() + var x = event.clientX - rect.x + var percent = x / rect.width + var position = percent * self.options.duration + if (self.options.position) { + position += self.options.position + } + progress.style.width = (100 * percent) + '%' + that.currentTime(position) + } + }) + that.addEventListener("timeupdate", event => { + var currentTime = that.currentTime(), + duration = self.options.duration + if (self.options.position) { + currentTime -= self.options.position + } + progress.style.width = (100 * currentTime / duration) + '%' + duration = formatDuration(duration) + currentTime = formatDuration(currentTime) + while (duration && duration.startsWith('00:')) { + duration = duration.slice(3) + } + currentTime = currentTime.slice(currentTime.length - duration.length) + time.innerText = `${currentTime} / ${duration}` + + }) + + that.addEventListener("play", event => { + var toggle = self.controls.querySelector('.toggle') + toggle.title = 'Pause' + toggle.querySelector('div').innerHTML = icon.pause + self.controls.style.opacity = '0' + }) + that.addEventListener("pause", event => { + var toggle = self.controls.querySelector('.toggle') + toggle.title = 'Play' + toggle.querySelector('div').innerHTML = icon.play + self.controls.style.opacity = '1' + }) + that.addEventListener("ended", event => { + var toggle = self.controls.querySelector('.toggle') + toggle.title = 'Play' + toggle.querySelector('div').innerHTML = icon.play + self.controls.style.opacity = '1' + }) + that.addEventListener("seeking", event => { + //console.log("seeking") + + }) + that.addEventListener("seeked", event => { + //console.log("seeked") + }) + that.addEventListener("volumechange", event => { + var volume = self.controls.querySelector('.volume') + if (that.muted()) { + volume.innerHTML = icon.unmute + volume.title = "Unmute" + } else { + volume.innerHTML = icon.mute + volume.title = "Mute" + } + }) + window.addEventListener('resize', event => { + // + }) + return that +}; + +})(); diff --git a/static/mobile/js/api.js b/static/mobile/js/api.js new file mode 100644 index 00000000..5e989597 --- /dev/null +++ b/static/mobile/js/api.js @@ -0,0 +1,25 @@ +var pandora = { + format: getFormat(), + hostname: document.location.hostname || 'pad.ma' +} + +var pandoraURL = document.location.hostname ? "" : `https://${pandora.hostname}` +var cache = cache || {} + +async function pandoraAPI(action, data) { + var url = pandoraURL + '/api/' + var key = JSON.stringify([action, data]) + if (!cache[key]) { + var response = await fetch(url, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + action: action, + data: data + }) + }) + cache[key] = await response.json() + } + return cache[key] +} + diff --git a/static/mobile/js/documents.js b/static/mobile/js/documents.js new file mode 100644 index 00000000..2d58b0ff --- /dev/null +++ b/static/mobile/js/documents.js @@ -0,0 +1,112 @@ + +async function loadDocument(id, args) { + var data = window.data = {} + var parts = id.split('/') + data.id = parts.shift() + data.site = pandora.hostname + + if (parts.length == 2) { + data.page = parts.shift() + } + + if (parts.length == 1) { + var rect = parts[0].split(',') + if (rect.length == 1) { + data.page = parts[0] + } else { + data.crop = rect + } + } else if (parts.length == 2) { + + } + + var response = await pandoraAPI('getDocument', { + id: data.id, + keys: [ + "id", + "title", + "extension", + "text", + ] + }) + if (response.status.code != 200) { + return { + site: data.site, + error: response.status + } + } + data.document = response['data'] + data.title = data.document.name + data.link = `${pandora.proto}://${data.site}/documents/${data.document.id}` + return data +} + +async function renderDocument(data) { + if (data.error) { + return renderError(data) + } + div = document.createElement('div') + div.className = "content" + if (!data.document) { + div.innerHTML = `
document not found
` + } else if (data.document.extension == "html") { + div.innerHTML = ` +

${data.document.title}

+
+ ${data.document.text} +
+ + ` + div.querySelectorAll('.text a').forEach(a => { + a.addEventListener("click", clickLink) + + }) + } else if (data.document.extension == "pdf" && data.page && data.crop) { + var img = `${pandora.proto}://${data.site}/documents/${data.document.id}/1024p${data.page},${data.crop.join(',')}.jpg` + data.link = getLink(`documents/${data.document.id}/${data.page}`) + div.innerHTML = ` +

${data.document.title}

+
+ +
+ + ` + } else if (data.document.extension == "pdf") { + var page = data.page || 1, + file = encodeURIComponent(`/documents/${data.document.id}/${safeDocumentName(data.document.title)}.pdf`) + div.innerHTML = ` +

${data.document.title}

+
+ +
+ + ` + } else if (data.document.extension == "jpg" || data.document.extension == "png") { + var img = `${pandora.proto}://${data.site}/documents/${data.document.id}/${safeDocumentName(data.document.title)}.${data.document.extension}` + var open_text = `Open on ${data.site}` + if (data.crop) { + img = `${pandora.proto}://${data.site}/documents/${data.document.id}/1024p${data.crop.join(',')}.jpg` + data.link = getLink(`documents/${data.document.id}`) + open_text = `Open image` + } + div.innerHTML = ` +

${data.document.title}

+
+ +
+ + ` + + } else { + div.innerHTML = `unsupported document type` + } + document.querySelector(".content").replaceWith(div) +} diff --git a/static/mobile/js/edits.js b/static/mobile/js/edits.js new file mode 100644 index 00000000..be9101ee --- /dev/null +++ b/static/mobile/js/edits.js @@ -0,0 +1,266 @@ + +const getSortValue = function(value) { + var sortValue = value; + function trim(value) { + return value.replace(/^\W+(?=\w)/, ''); + } + if ( + isEmpty(value) + || isNull(value) + || isUndefined(value) + ) { + sortValue = null; + } else if (isString(value)) { + // make lowercase and remove leading non-word characters + sortValue = trim(value.toLowerCase()); + // move leading articles to the end + // and remove leading non-word characters + ['a', 'an', 'the'].forEach(function(article) { + if (new RegExp('^' + article + ' ').test(sortValue)) { + sortValue = trim(sortValue.slice(article.length + 1)) + + ', ' + sortValue.slice(0, article.length); + return false; // break + } + }); + // remove thousand separators and pad numbers + sortValue = sortValue.replace(/(\d),(?=(\d{3}))/g, '$1') + .replace(/\d+/g, function(match) { + return match.padStart(64, '0') + }); + } + return sortValue; +}; + +const sortByKey = function(array, by) { + return array.sort(function(a, b) { + var aValue, bValue, index = 0, key, ret = 0; + while (ret == 0 && index < by.length) { + key = by[index].key; + aValue = getSortValue(a[key]) + bValue = getSortValue(b[key]) + if ((aValue === null) != (bValue === null)) { + ret = aValue === null ? 1 : -1; + } else if (aValue < bValue) { + ret = by[index].operator == '+' ? -1 : 1; + } else if (aValue > bValue) { + ret = by[index].operator == '+' ? 1 : -1; + } else { + index++; + } + } + return ret; + }); +}; + +async function sortClips(edit, sort) { + var key = sort.key, index; + if (key == 'position') { + key = 'in'; + } + if ([ + 'id', 'index', 'in', 'out', 'duration', + 'title', 'director', 'year', 'videoRatio' + ].indexOf(key) > -1) { + sortBy(sort); + index = 0; + edit.clips.forEach(function(clip) { + clip.sort = index++; + if (sort.operator == '-') { + clip.sort = -clip.sort; + } + }); + } else { + var response = await pandoraAPI('sortClips', { + edit: edit.id, + sort: [sort] + }) + edit.clips.forEach(function(clip) { + clip.sort = response.data.clips.indexOf(clip.id); + if (sort.operator == '-') { + clip.sort = -clip.sort; + } + }); + sortBy({ + key: 'sort', + operator: '+' + }); + } + function sortBy(key) { + edit.clips = sortByKey(edit.clips, [key]); + } +} + +function getClip(edit, position) { + const response = {} + let pos = 0 + edit.clips.forEach(function(clip) { + if (clip.position < position && clip.position + clip.duration > position) { + response.item = clip.item + response.position = position - clip.position + if (clip['in']) { + response.position += clip['in'] + } + } + }); + return response +} + +async function loadEdit(id, args) { + var data = window.data = {} + data.id = id + data.site = pandora.hostname + + var response = await pandoraAPI('getEdit', { + id: data.id, + keys: [ + ] + }) + if (response.status.code != 200) { + return { + site: data.site, + error: response.status + } + } + data.edit = response['data'] + if (data.edit.status !== 'public') { + return { + site: data.site, + error: { + code: 403, + text: 'permission denied' + } + } + } + data.layers = {} + data.videos = [] + + if (args.sort) { + await sortClips(data.edit, args.sort) + } + + data.edit.duration = 0; + data.edit.clips.forEach(function(clip) { + clip.position = data.edit.duration; + data.edit.duration += clip.duration; + }); + + data.edit.clips.forEach(clip => { + var start = clip['in'] || 0, end = clip.out, position = 0; + clip.durations.forEach((duration, idx) => { + if (!duration) { + return + } + if (position + duration <= start || position > end) { + // pass + } else { + var video = {} + var oshash = clip.streams[idx] + video.src = getVideoURL(clip.item, pandora.resolution, idx+1, '', oshash) + /* + if (clip['in'] && clip.out) { + video.src += `#t=${clip['in']},${clip.out}` + } + */ + if (isNumber(clip.volume)) { + video.volume = clip.volume; + } + if ( + position <= start + && position + duration > start + ) { + video['in'] = start - position; + } + if (position + duration >= end) { + video.out = end - position; + } + if (video['in'] && video.out) { + video.duration = video.out - video['in'] + } else if (video.out) { + video.duration = video.out; + } else if (!isUndefined(video['in'])) { + video.duration = duration - video['in']; + video.out = duration; + } else { + video.duration = duration; + video['in'] = 0; + video.out = video.duration; + } + data.videos.push(video) + } + position += duration + }) + Object.keys(clip.layers).forEach(layer => { + clip.layers[layer].forEach(annotation => { + if (args.users && !args.users.includes(annotation.user)) { + return + } + if (args.layers && !args.layers.includes(layer)) { + return + } + var a = {...annotation} + a['id'] = clip['id'] + '/' + a['id']; + a['in'] = Math.max( + clip['position'], + a['in'] - clip['in'] + clip['position'] + ); + a.out = Math.min( + clip['position'] + clip['duration'], + a.out - clip['in'] + clip['position'] + ); + data.layers[layer] = data.layers[layer] || [] + data.layers[layer].push(a) + }) + }) + }) + if (data.layers[pandora.subtitleLayer]) { + var previous; + data.layers[pandora.subtitleLayer].forEach(annotation => { + if (previous) { + previous.out = annotation['in'] + } + previous = annotation + }) + } + var value = [] + pandora.layerKeys.forEach(layer => { + if (!data.layers[layer]) { + return + } + var html = [] + var layerData = getObjectById(pandora.site.layers, layer) + html.push(`

+ ${icon.down} + ${layerData.title} +

`) + data.layers[layer].forEach(annotation => { + html.push(` +
+ ${annotation.value} +
+ `) + }) + var layerClass = "" + if (layerData.isSubtitles) { + layerClass = " is-subtitles" + } + value.push('
' + html.join('\n') + '
') + }) + data.value = value.join('\n') + + data.title = data.edit.name + data.byline = data.edit.description + data.link = `${pandora.proto}://${data.site}/edits/${data.edit.id}` + let poster = data.edit.posterFrames[0] + if (args.parts[2] && args.parts[2].indexOf(':') > -1) { + poster = getClip(data.edit, parseDuration(args.parts[2])) + } + if (poster && poster.item) { + data.poster = `${pandora.proto}://${data.site}/${poster.item}/${pandora.resolution}p${poster.position.toFixed(3)}.jpg` + } else { + data.poster = data.videos[0].src.split('/48')[0] + `/${pandora.resolution}p${data.videos[0].in.toFixed(3)}.jpg` + } + data.aspectratio = data.edit.clips[0].videoRatio + data.duration = data.edit.duration + return data + +} diff --git a/static/mobile/js/icons.js b/static/mobile/js/icons.js new file mode 100644 index 00000000..50116108 --- /dev/null +++ b/static/mobile/js/icons.js @@ -0,0 +1,200 @@ +var icon = {} +icon.enterFullscreen = ` + + + + + + + + +` +icon.exitFullscreen = ` + + + + + + + + +` + +icon.mute = ` + + + + + + + +` + +icon.unmute = ` + + + + +` + +icon.play = ` + + + +` +icon.pause = ` + + + + + +` +icon.loading = ` + + + + + + +` + +icon.loading_w = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + +icon.right = ` + + + +` +icon.left = ` + + + +` +icon.down = ` + + + +` + +icon.publishComment = ` + + + + + + +` diff --git a/static/mobile/js/item.js b/static/mobile/js/item.js new file mode 100644 index 00000000..a9942536 --- /dev/null +++ b/static/mobile/js/item.js @@ -0,0 +1,178 @@ + +async function loadData(id, args) { + var data = window.data = {} + data.id = id + data.site = pandora.hostname + + var response = await pandoraAPI('get', { + id: data.id.split('/')[0], + keys: [ + "id", + "title", + "director", + "year", + "date", + "source", + "summary", + "streams", + "duration", + "durations", + "layers", + "rightslevel", + "videoRatio" + ] + }) + + if (response.status.code != 200) { + return { + site: data.site, + error: response.status + } + } + data.item = response['data'] + if (data.item.rightslevel > pandora.site.capabilities.canPlayClips['guest']) { + return { + site: data.site, + error: { + code: 403, + text: 'permission denied' + } + } + } + if (id.split('/').length == 1 || id.split('/')[1] == 'info') { + data.view = 'info' + data.title = data.item.title + if (data.item.source) { + data.byline = data.item.source + } else { + data.byline = data.item.director ? data.item.director.join(', ') : '' + } + if (data.item.year) { + data.byline += ' (' + data.item.year + ')' + } else if (data.item.date) { + data.byline += ' (' + data.item.date.split('-')[0] + ')' + } + data.link = `${pandora.proto}://${data.site}/${data.item.id}/info` + let poster = pandora.site.user.ui.icons == 'posters' ? 'poster' : 'icon' + data.icon = `${pandora.proto}://${data.site}/${data.item.id}/${poster}.jpg` + return data + } + + if (id.includes('-') || id.includes(',')) { + var inout = id.split('/')[1].split(id.includes('-') ? '-' : ',').map(parseDuration) + data.out = inout.pop() + data['in'] = inout.pop() + } else if (args.full) { + data.out = data.item.duration + data['in'] = 0 + } else { + var annotation = await pandoraAPI('getAnnotation', { + id: data.id, + }) + if (annotation.status.code != 200) { + return { + site: data.site, + error: annotation.status + } + } + data.annotation = annotation['data'] + data['in'] = data.annotation['in'] + data.out = data.annotation['out'] + } + + data.layers = {} + + pandora.layerKeys.forEach(layer => { + data.item.layers[layer].forEach(annot => { + if (data.annotation) { + if (annot.id == data.annotation.id) { + data.layers[layer] = data.layers[layer] || [] + data["layers"][layer].push(annot) + } + } else if (annot['out'] > data['in'] && annot['in'] < data['out']) { + if (args.users && !args.users.includes(annot.user)) { + return + } + if (args.layers && !args.layers.includes(layer)) { + return + } + data.layers[layer] = data.layers[layer] || [] + //annot['in'] = Math.max([annot['in'], data['in']]) + //annot['out'] = Math.min([annot['out'], data['out']]) + data["layers"][layer].push(annot) + } + }) + }) + data.videos = [] + data.item.durations.forEach((duration, idx) => { + var oshash = data.item.streams[idx] + var url = getVideoURL(data.item.id, pandora.resolution, idx+1, '', oshash) + data.videos.push({ + src: url, + duration: duration + }) + }) + if (data.layers[pandora.subtitleLayer]) { + var previous; + data.layers[pandora.subtitleLayer].forEach(annotation => { + if (previous) { + previous.out = annotation['in'] + } + previous = annotation + }) + } + var value = [] + Object.keys(data.layers).forEach(layer => { + var html = [] + var layerData = getObjectById(pandora.site.layers, layer) + html.push(`

+ ${icon.down} + ${layerData.title} +

`) + data.layers[layer].forEach(annotation => { + if (pandora.url) { + annotation.value = annotation.value.replace( + /src="\//g, `src="${pandora.url.origin}/` + ).replace( + /href="\//g, `href="${pandora.url.origin}/` + ) + } + let content = annotation.value + if (!layerData.isSubtitles && layerData.type == "text" && args.show && args.show.includes("user")) { + content += `\n
— ${annotation.user}
` + } + html.push(` +
+ ${content} +
+ `) + }) + var layerClass = "" + if (layerData.isSubtitles) { + layerClass = " is-subtitles" + } + value.push('
' + html.join('\n') + '
') + }) + data.value = value.join('\n') + + data.title = data.item.title + if (data.item.source) { + data.byline = data.item.source + } else { + data.byline = data.item.director ? data.item.director.join(', ') : '' + } + if (data.item.year) { + data.byline += ' (' + data.item.year + ')' + } else if (data.item.date) { + data.byline += ' (' + data.item.date.split('-')[0] + ')' + } + data.link = `${pandora.proto}://${data.site}/${data.item.id}/${data["in"]},${data.out}` + data.poster = `${pandora.proto}://${data.site}/${data.item.id}/${pandora.resolution}p${data["in"]}.jpg` + data.aspectratio = data.item.videoRatio + if (data['in'] == data['out']) { + data['out'] += 0.04 + } + data.duration = data.out - data['in'] + return data +} + diff --git a/static/mobile/js/main.js b/static/mobile/js/main.js new file mode 100644 index 00000000..c84b5260 --- /dev/null +++ b/static/mobile/js/main.js @@ -0,0 +1,132 @@ + + +function parseURL() { + var url = pandora.url ? pandora.url : document.location, + fragment = url.hash.slice(1) + if (!fragment && url.pathname.startsWith('/m/')) { + var prefix = url.protocol + '//' + url.hostname + '/m/' + fragment = url.href.slice(prefix.length) + } + var args = fragment.split('?') + var id = args.shift() + if (args) { + args = args.join('?').split('&').map(arg => { + var kv = arg.split('=') + k = kv.shift() + v = kv.join('=') + if (['users', 'layers', 'show'].includes(k)) { + v = v.split(',') + } + return [k, v] + }).filter(kv => { + return kv[0].length + }) + if (args) { + args = Object.fromEntries(args); + } else { + args = {} + } + } else { + args = {} + } + var type = "item" + if (id.startsWith('document')) { + id = id.split('/') + id.shift() + id = id.join('/') + type = "document" + } else if (id.startsWith('edits/')) { + var parts = id.split('/') + parts.shift() + id = parts.shift().replace(/_/g, ' ').replace(/%09/g, '_') + type = "edit" + if (parts.length >= 2) { + args.sort = parts[1] + if (args.sort[0] == '-') { + args.sort = { + key: args.sort.slice(1), + operator: '-' + } + } else if (args.sort[0] == '+') { + args.sort = { + key: args.sort.slice(1), + operator: '+' + } + } else { + args.sort = { + key: args.sort, + operator: '+' + } + } + } + args.parts = parts + } else { + if (id.endsWith('/player') || id.endsWith('/editor')) { + args.full = true + } + id = id.replace('/editor/', '/').replace('/player/', '/') + type = "item" + } + return [type, id, args] +} + +function render() { + var type, id, args; + [type, id, args] = parseURL() + document.querySelector(".content").innerHTML = loadingScreen + if (type == "document") { + loadDocument(id, args).then(renderDocument) + } else if (type == "edit") { + loadEdit(id, args).then(renderItem) + } else { + loadData(id, args).then(renderItem) + } + +} +var loadingScreen = ` + +
${icon.loading}
+` + +document.querySelector(".content").innerHTML = loadingScreen +pandoraAPI("init").then(response => { + pandora = { + ...pandora, + ...response.data + } + pandora.proto = pandora.site.site.https ? 'https' : 'http' + pandora.resolution = Math.max.apply(null, pandora.site.video.resolutions) + if (pandora.site.site.videoprefix.startsWith('//')) { + pandora.site.site.videoprefix = pandora.proto + ':' + pandora.site.site.videoprefix + } + var layerKeys = [] + var subtitleLayer = pandora.site.layers.filter(layer => {return layer.isSubtitles})[0] + if (subtitleLayer) { + layerKeys.push(subtitleLayer.id) + } + pandora.subtitleLayer = subtitleLayer.id + pandora.site.layers.map(layer => { + return layer.id + }).filter(layer => { + return !subtitleLayer || layer != subtitleLayer.id + }).forEach(layer => { + layerKeys.push(layer) + }) + pandora.layerKeys = layerKeys + id = document.location.hash.slice(1) + window.addEventListener("hashchange", event => { + render() + }) + window.addEventListener("popstate", event => { + console.log("popsatte") + render() + }) + window.addEventListener('resize', event => { + }) + render() +}) diff --git a/static/mobile/js/render.js b/static/mobile/js/render.js new file mode 100644 index 00000000..307c5d2f --- /dev/null +++ b/static/mobile/js/render.js @@ -0,0 +1,154 @@ + +function renderItemInfo(data) { + div = document.createElement('div') + div.className = "content" + div.innerHTML = ` +

${item.title}

+

${data.title}

+ +
+ +
+ + ` + if (!item.title) { + div.querySelector('item-title').remove() + } + document.querySelector(".content").replaceWith(div) +} + +function renderItem(data) { + window.item = window.item || {} + if (data.error) { + return renderError(data) + } + if (data.view == "info") { + return renderItemInfo(data) + } + div = document.createElement('div') + div.className = "content" + div.innerHTML = ` +

${item.title}

+

${data.title}

+ +
+
+
+
+ ${data.value} +
+
+ + ` + if (!item.title) { + div.querySelector('.item-title').remove() + } + + var comments = div.querySelector('.comments') + if (window.renderComments) { + renderComments(comments, data) + } else { + comments.remove() + } + + div.querySelectorAll('.layer a').forEach(a => { + a.addEventListener("click", clickLink) + }) + + div.querySelectorAll('.layer').forEach(layer => { + layer.querySelector('h3').addEventListener("click", event => { + var img = layer.querySelector('h3 .icon') + if (layer.classList.contains("collapsed")) { + layer.classList.remove("collapsed") + img.innerHTML = icon.down + } else { + layer.classList.add("collapsed") + img.innerHTML = icon.right + } + }) + }) + + var video = window.video = VideoPlayer({ + items: data.videos, + poster: data.poster, + position: data["in"] || 0, + duration: data.duration, + aspectratio: data.aspectratio + }) + div.querySelector('.video').replaceWith(video) + video.classList.add('video') + + video.addEventListener("loadedmetadata", event => { + // + }) + video.addEventListener("timeupdate", event => { + var currentTime = video.currentTime() + if (currentTime >= data['out']) { + if (!video.paused) { + video.pause() + } + video.currentTime(data['in']) + } + div.querySelectorAll('.annotation').forEach(annot => { + var now = currentTime + var start = parseFloat(annot.dataset.in) + var end = parseFloat(annot.dataset.out) + if (now >= start && now <= end) { + annot.classList.add("active") + annot.parentElement.classList.add('active') + } else { + annot.classList.remove("active") + if (!annot.parentElement.querySelector('.active')) { + annot.parentElement.classList.remove('active') + } + } + }) + + }) + if (item.next || item.previous) { + var nav = document.createElement('nav') + nav.classList.add('items') + if (item.previous) { + var a = document.createElement('a') + a.href = item.previous + a.innerText = '<< previous' + nav.appendChild(a) + } + if (item.previous && item.next) { + var e = document.createElement('span') + e.innerText = ' | ' + nav.appendChild(e) + } + if (item.next) { + var a = document.createElement('a') + a.href = item.next + a.innerText = 'next >>' + nav.appendChild(a) + } + div.appendChild(nav) + } + document.querySelector(".content").replaceWith(div) +} + +function renderError(data) { + var link = '/' + document.location.hash.slice(1) + div = document.createElement('div') + div.className = "content" + div.innerHTML = ` + +
+ Page not found
+ Open on ${data.site} +
+ ` + document.querySelector(".content").replaceWith(div) +} diff --git a/static/mobile/js/utils.js b/static/mobile/js/utils.js new file mode 100644 index 00000000..cbaec6a6 --- /dev/null +++ b/static/mobile/js/utils.js @@ -0,0 +1,163 @@ + +const parseDuration = function(string) { + return string.split(':').reverse().slice(0, 4).reduce(function(p, c, i) { + return p + (parseFloat(c) || 0) * (i == 3 ? 86400 : Math.pow(60, i)); + }, 0); +}; + +const formatDuration = function(seconds) { + var parts = [ + parseInt(seconds / 86400), + parseInt(seconds % 86400 / 3600), + parseInt(seconds % 3600 / 60), + s = parseInt(seconds % 60) + ] + return parts.map(p => { return p.toString().padStart(2, '0')}).join(':') +} + +const typeOf = function(value) { + return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); +}; +const isUndefined = function(value) { + return typeOf(value) == 'undefined'; +} +const isNumber = function(value) { + return typeOf(value) == 'number'; +}; +const isObject = function(value) { + return typeOf(value) == 'object'; +}; +const isNull = function(value) { + return typeOf(value) == 'null'; +}; +const isString = function(value) { + return typeOf(value) == 'string'; +}; +const isEmpty = function(value) { + var type = typeOf(value) + if (['arguments', 'array', 'nodelist', 'string'].includes(value)) { + return value.length == 0 + } + if (['object', 'storage'].includes(type)) { + return Object.keys(value).length; + } + return false +}; +const mod = function(number, by) { + return (number % by + by) % by; +}; + +const getObjectById = function(array, id) { + return array.filter(obj => { return obj.id == id})[0] +} + +const debug = function() { + if (localStorage.debug) { + console.log.apply(null, arguments) + } +}; + +const canPlayMP4 = function() { + var video = document.createElement('video'); + if (video.canPlayType && video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace('no', '')) { + return true + } + return false +}; + +const canPlayWebm = function() { + var video = document.createElement('video'); + if (video.canPlayType && video.canPlayType('video/webm; codecs="vp8, vorbis"').replace('no', '')) { + return true + } + return false +}; + +const getFormat = function() { + //var format = canPlayWebm() ? "webm" : "mp4" + var format = canPlayMP4() ? "mp4" : "webm" + return format +} + +const safeDocumentName = function(name) { + ['\\?', '#', '%', '/'].forEach(function(c) { + var r = new RegExp(c, 'g') + name = name.replace(r, '_'); + }) + return name; +}; + +const getVideoInfo = function() { + console.log("FIXME implement getvideoInfo") +} + +const isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); + + +const getLink = function(fragment) { + if (document.location.hash.length > 2) { + return '#' + fragment + } else { + return '/m/' + fragment + } +} + +const clickLink = function(event) { + var a = event.target + while (a && a.tagName != 'A') { + a = a.parentElement + } + if (!a) { + return + } + var href = a.attributes.href.value + var prefix = document.location.protocol + '//' + document.location.hostname + if (href.startsWith(prefix)) { + href = href.slice(prefix.length) + } + if (href.startsWith('/')) { + event.preventDefault() + event.stopPropagation() + var link = href.split('#embed')[0] + if (document.location.hash.length > 2) { + if (link.startsWith('/m')) { + link = link.slice(2) + } + document.location.hash = '#' + link.slice(1) + } else { + if (!link.startsWith('/m')) { + link = '/m' + link + } + history.pushState({}, '', link); + render() + } + } +} + +const getUid = (function() { + var uid = 0; + return function() { + return ++uid; + }; +}()); + + +const getVideoURLName = function(id, resolution, part, track, streamId) { + return id + '/' + resolution + 'p' + part + (track ? '.' + track : '') + + '.' + pandora.format + (streamId ? '?' + streamId : ''); +}; + +const getVideoURL = function(id, resolution, part, track, streamId) { + var uid = getUid(), + prefix = pandora.site.site.videoprefix + .replace('{id}', id) + .replace('{part}', part) + .replace('{resolution}', resolution) + .replace('{uid}', uid) + .replace('{uid42}', uid % 42); + if (!prefix) { + prefix = pandoraURL + } + return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId); +}; + diff --git a/static/pdf.js/compatibility.js b/static/pdf.js/compatibility.js deleted file mode 100644 index 1119a274..00000000 --- a/static/pdf.js/compatibility.js +++ /dev/null @@ -1,593 +0,0 @@ -/* Copyright 2012 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* globals VBArray, PDFJS */ - -'use strict'; - -// Initializing PDFJS global object here, it case if we need to change/disable -// some PDF.js features, e.g. range requests -if (typeof PDFJS === 'undefined') { - (typeof window !== 'undefined' ? window : this).PDFJS = {}; -} - -// Checking if the typed arrays are supported -// Support: iOS<6.0 (subarray), IE<10, Android<4.0 -(function checkTypedArrayCompatibility() { - if (typeof Uint8Array !== 'undefined') { - // Support: iOS<6.0 - if (typeof Uint8Array.prototype.subarray === 'undefined') { - Uint8Array.prototype.subarray = function subarray(start, end) { - return new Uint8Array(this.slice(start, end)); - }; - Float32Array.prototype.subarray = function subarray(start, end) { - return new Float32Array(this.slice(start, end)); - }; - } - - // Support: Android<4.1 - if (typeof Float64Array === 'undefined') { - window.Float64Array = Float32Array; - } - return; - } - - function subarray(start, end) { - return new TypedArray(this.slice(start, end)); - } - - function setArrayOffset(array, offset) { - if (arguments.length < 2) { - offset = 0; - } - for (var i = 0, n = array.length; i < n; ++i, ++offset) { - this[offset] = array[i] & 0xFF; - } - } - - function TypedArray(arg1) { - var result, i, n; - if (typeof arg1 === 'number') { - result = []; - for (i = 0; i < arg1; ++i) { - result[i] = 0; - } - } else if ('slice' in arg1) { - result = arg1.slice(0); - } else { - result = []; - for (i = 0, n = arg1.length; i < n; ++i) { - result[i] = arg1[i]; - } - } - - result.subarray = subarray; - result.buffer = result; - result.byteLength = result.length; - result.set = setArrayOffset; - - if (typeof arg1 === 'object' && arg1.buffer) { - result.buffer = arg1.buffer; - } - return result; - } - - window.Uint8Array = TypedArray; - window.Int8Array = TypedArray; - - // we don't need support for set, byteLength for 32-bit array - // so we can use the TypedArray as well - window.Uint32Array = TypedArray; - window.Int32Array = TypedArray; - window.Uint16Array = TypedArray; - window.Float32Array = TypedArray; - window.Float64Array = TypedArray; -})(); - -// URL = URL || webkitURL -// Support: Safari<7, Android 4.2+ -(function normalizeURLObject() { - if (!window.URL) { - window.URL = window.webkitURL; - } -})(); - -// Object.defineProperty()? -// Support: Android<4.0, Safari<5.1 -(function checkObjectDefinePropertyCompatibility() { - if (typeof Object.defineProperty !== 'undefined') { - var definePropertyPossible = true; - try { - // some browsers (e.g. safari) cannot use defineProperty() on DOM objects - // and thus the native version is not sufficient - Object.defineProperty(new Image(), 'id', { value: 'test' }); - // ... another test for android gb browser for non-DOM objects - var Test = function Test() {}; - Test.prototype = { get id() { } }; - Object.defineProperty(new Test(), 'id', - { value: '', configurable: true, enumerable: true, writable: false }); - } catch (e) { - definePropertyPossible = false; - } - if (definePropertyPossible) { - return; - } - } - - Object.defineProperty = function objectDefineProperty(obj, name, def) { - delete obj[name]; - if ('get' in def) { - obj.__defineGetter__(name, def['get']); - } - if ('set' in def) { - obj.__defineSetter__(name, def['set']); - } - if ('value' in def) { - obj.__defineSetter__(name, function objectDefinePropertySetter(value) { - this.__defineGetter__(name, function objectDefinePropertyGetter() { - return value; - }); - return value; - }); - obj[name] = def.value; - } - }; -})(); - - -// No XMLHttpRequest#response? -// Support: IE<11, Android <4.0 -(function checkXMLHttpRequestResponseCompatibility() { - var xhrPrototype = XMLHttpRequest.prototype; - var xhr = new XMLHttpRequest(); - if (!('overrideMimeType' in xhr)) { - // IE10 might have response, but not overrideMimeType - // Support: IE10 - Object.defineProperty(xhrPrototype, 'overrideMimeType', { - value: function xmlHttpRequestOverrideMimeType(mimeType) {} - }); - } - if ('responseType' in xhr) { - return; - } - - // The worker will be using XHR, so we can save time and disable worker. - PDFJS.disableWorker = true; - - Object.defineProperty(xhrPrototype, 'responseType', { - get: function xmlHttpRequestGetResponseType() { - return this._responseType || 'text'; - }, - set: function xmlHttpRequestSetResponseType(value) { - if (value === 'text' || value === 'arraybuffer') { - this._responseType = value; - if (value === 'arraybuffer' && - typeof this.overrideMimeType === 'function') { - this.overrideMimeType('text/plain; charset=x-user-defined'); - } - } - } - }); - - // Support: IE9 - if (typeof VBArray !== 'undefined') { - Object.defineProperty(xhrPrototype, 'response', { - get: function xmlHttpRequestResponseGet() { - if (this.responseType === 'arraybuffer') { - return new Uint8Array(new VBArray(this.responseBody).toArray()); - } else { - return this.responseText; - } - } - }); - return; - } - - Object.defineProperty(xhrPrototype, 'response', { - get: function xmlHttpRequestResponseGet() { - if (this.responseType !== 'arraybuffer') { - return this.responseText; - } - var text = this.responseText; - var i, n = text.length; - var result = new Uint8Array(n); - for (i = 0; i < n; ++i) { - result[i] = text.charCodeAt(i) & 0xFF; - } - return result.buffer; - } - }); -})(); - -// window.btoa (base64 encode function) ? -// Support: IE<10 -(function checkWindowBtoaCompatibility() { - if ('btoa' in window) { - return; - } - - var digits = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - - window.btoa = function windowBtoa(chars) { - var buffer = ''; - var i, n; - for (i = 0, n = chars.length; i < n; i += 3) { - var b1 = chars.charCodeAt(i) & 0xFF; - var b2 = chars.charCodeAt(i + 1) & 0xFF; - var b3 = chars.charCodeAt(i + 2) & 0xFF; - var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4); - var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64; - var d4 = i + 2 < n ? (b3 & 0x3F) : 64; - buffer += (digits.charAt(d1) + digits.charAt(d2) + - digits.charAt(d3) + digits.charAt(d4)); - } - return buffer; - }; -})(); - -// window.atob (base64 encode function)? -// Support: IE<10 -(function checkWindowAtobCompatibility() { - if ('atob' in window) { - return; - } - - // https://github.com/davidchambers/Base64.js - var digits = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - window.atob = function (input) { - input = input.replace(/=+$/, ''); - if (input.length % 4 === 1) { - throw new Error('bad atob input'); - } - for ( - // initialize result and counters - var bc = 0, bs, buffer, idx = 0, output = ''; - // get next character - buffer = input.charAt(idx++); - // character found in table? - // initialize bit storage and add its ascii value - ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, - // and if not first of each 4 characters, - // convert the first 8 bits to one ascii character - bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 - ) { - // try to find character in table (0-63, not found => -1) - buffer = digits.indexOf(buffer); - } - return output; - }; -})(); - -// Function.prototype.bind? -// Support: Android<4.0, iOS<6.0 -(function checkFunctionPrototypeBindCompatibility() { - if (typeof Function.prototype.bind !== 'undefined') { - return; - } - - Function.prototype.bind = function functionPrototypeBind(obj) { - var fn = this, headArgs = Array.prototype.slice.call(arguments, 1); - var bound = function functionPrototypeBindBound() { - var args = headArgs.concat(Array.prototype.slice.call(arguments)); - return fn.apply(obj, args); - }; - return bound; - }; -})(); - -// HTMLElement dataset property -// Support: IE<11, Safari<5.1, Android<4.0 -(function checkDatasetProperty() { - var div = document.createElement('div'); - if ('dataset' in div) { - return; // dataset property exists - } - - Object.defineProperty(HTMLElement.prototype, 'dataset', { - get: function() { - if (this._dataset) { - return this._dataset; - } - - var dataset = {}; - for (var j = 0, jj = this.attributes.length; j < jj; j++) { - var attribute = this.attributes[j]; - if (attribute.name.substring(0, 5) !== 'data-') { - continue; - } - var key = attribute.name.substring(5).replace(/\-([a-z])/g, - function(all, ch) { - return ch.toUpperCase(); - }); - dataset[key] = attribute.value; - } - - Object.defineProperty(this, '_dataset', { - value: dataset, - writable: false, - enumerable: false - }); - return dataset; - }, - enumerable: true - }); -})(); - -// HTMLElement classList property -// Support: IE<10, Android<4.0, iOS<5.0 -(function checkClassListProperty() { - var div = document.createElement('div'); - if ('classList' in div) { - return; // classList property exists - } - - function changeList(element, itemName, add, remove) { - var s = element.className || ''; - var list = s.split(/\s+/g); - if (list[0] === '') { - list.shift(); - } - var index = list.indexOf(itemName); - if (index < 0 && add) { - list.push(itemName); - } - if (index >= 0 && remove) { - list.splice(index, 1); - } - element.className = list.join(' '); - return (index >= 0); - } - - var classListPrototype = { - add: function(name) { - changeList(this.element, name, true, false); - }, - contains: function(name) { - return changeList(this.element, name, false, false); - }, - remove: function(name) { - changeList(this.element, name, false, true); - }, - toggle: function(name) { - changeList(this.element, name, true, true); - } - }; - - Object.defineProperty(HTMLElement.prototype, 'classList', { - get: function() { - if (this._classList) { - return this._classList; - } - - var classList = Object.create(classListPrototype, { - element: { - value: this, - writable: false, - enumerable: true - } - }); - Object.defineProperty(this, '_classList', { - value: classList, - writable: false, - enumerable: false - }); - return classList; - }, - enumerable: true - }); -})(); - -// Check console compatibility -// In older IE versions the console object is not available -// unless console is open. -// Support: IE<10 -(function checkConsoleCompatibility() { - if (!('console' in window)) { - window.console = { - log: function() {}, - error: function() {}, - warn: function() {} - }; - } else if (!('bind' in console.log)) { - // native functions in IE9 might not have bind - console.log = (function(fn) { - return function(msg) { return fn(msg); }; - })(console.log); - console.error = (function(fn) { - return function(msg) { return fn(msg); }; - })(console.error); - console.warn = (function(fn) { - return function(msg) { return fn(msg); }; - })(console.warn); - } -})(); - -// Check onclick compatibility in Opera -// Support: Opera<15 -(function checkOnClickCompatibility() { - // workaround for reported Opera bug DSK-354448: - // onclick fires on disabled buttons with opaque content - function ignoreIfTargetDisabled(event) { - if (isDisabled(event.target)) { - event.stopPropagation(); - } - } - function isDisabled(node) { - return node.disabled || (node.parentNode && isDisabled(node.parentNode)); - } - if (navigator.userAgent.indexOf('Opera') !== -1) { - // use browser detection since we cannot feature-check this bug - document.addEventListener('click', ignoreIfTargetDisabled, true); - } -})(); - -// Checks if possible to use URL.createObjectURL() -// Support: IE -(function checkOnBlobSupport() { - // sometimes IE loosing the data created with createObjectURL(), see #3977 - if (navigator.userAgent.indexOf('Trident') >= 0) { - PDFJS.disableCreateObjectURL = true; - } -})(); - -// Checks if navigator.language is supported -(function checkNavigatorLanguage() { - if ('language' in navigator) { - return; - } - PDFJS.locale = navigator.userLanguage || 'en-US'; -})(); - -(function checkRangeRequests() { - // Safari has issues with cached range requests see: - // https://github.com/mozilla/pdf.js/issues/3260 - // Last tested with version 6.0.4. - // Support: Safari 6.0+ - var isSafari = Object.prototype.toString.call( - window.HTMLElement).indexOf('Constructor') > 0; - - // Older versions of Android (pre 3.0) has issues with range requests, see: - // https://github.com/mozilla/pdf.js/issues/3381. - // Make sure that we only match webkit-based Android browsers, - // since Firefox/Fennec works as expected. - // Support: Android<3.0 - var regex = /Android\s[0-2][^\d]/; - var isOldAndroid = regex.test(navigator.userAgent); - - // Range requests are broken in Chrome 39 and 40, https://crbug.com/442318 - var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent); - - if (isSafari || isOldAndroid || isChromeWithRangeBug) { - PDFJS.disableRange = true; - PDFJS.disableStream = true; - } -})(); - -// Check if the browser supports manipulation of the history. -// Support: IE<10, Android<4.2 -(function checkHistoryManipulation() { - // Android 2.x has so buggy pushState support that it was removed in - // Android 3.0 and restored as late as in Android 4.2. - // Support: Android 2.x - if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) { - PDFJS.disableHistory = true; - } -})(); - -// Support: IE<11, Chrome<21, Android<4.4, Safari<6 -(function checkSetPresenceInImageData() { - // IE < 11 will use window.CanvasPixelArray which lacks set function. - if (window.CanvasPixelArray) { - if (typeof window.CanvasPixelArray.prototype.set !== 'function') { - window.CanvasPixelArray.prototype.set = function(arr) { - for (var i = 0, ii = this.length; i < ii; i++) { - this[i] = arr[i]; - } - }; - } - } else { - // Old Chrome and Android use an inaccessible CanvasPixelArray prototype. - // Because we cannot feature detect it, we rely on user agent parsing. - var polyfill = false, versionMatch; - if (navigator.userAgent.indexOf('Chrom') >= 0) { - versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); - // Chrome < 21 lacks the set function. - polyfill = versionMatch && parseInt(versionMatch[2]) < 21; - } else if (navigator.userAgent.indexOf('Android') >= 0) { - // Android < 4.4 lacks the set function. - // Android >= 4.4 will contain Chrome in the user agent, - // thus pass the Chrome check above and not reach this block. - polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent); - } else if (navigator.userAgent.indexOf('Safari') >= 0) { - versionMatch = navigator.userAgent. - match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//); - // Safari < 6 lacks the set function. - polyfill = versionMatch && parseInt(versionMatch[1]) < 6; - } - - if (polyfill) { - var contextPrototype = window.CanvasRenderingContext2D.prototype; - var createImageData = contextPrototype.createImageData; - contextPrototype.createImageData = function(w, h) { - var imageData = createImageData.call(this, w, h); - imageData.data.set = function(arr) { - for (var i = 0, ii = this.length; i < ii; i++) { - this[i] = arr[i]; - } - }; - return imageData; - }; - // this closure will be kept referenced, so clear its vars - contextPrototype = null; - } - } -})(); - -// Support: IE<10, Android<4.0, iOS -(function checkRequestAnimationFrame() { - function fakeRequestAnimationFrame(callback) { - window.setTimeout(callback, 20); - } - - var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); - if (isIOS) { - // requestAnimationFrame on iOS is broken, replacing with fake one. - window.requestAnimationFrame = fakeRequestAnimationFrame; - return; - } - if ('requestAnimationFrame' in window) { - return; - } - window.requestAnimationFrame = - window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || - fakeRequestAnimationFrame; -})(); - -(function checkCanvasSizeLimitation() { - var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); - var isAndroid = /Android/g.test(navigator.userAgent); - if (isIOS || isAndroid) { - // 5MP - PDFJS.maxCanvasPixels = 5242880; - } -})(); - -// Disable fullscreen support for certain problematic configurations. -// Support: IE11+ (when embedded). -(function checkFullscreenSupport() { - var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 && - window.parent !== window); - if (isEmbeddedIE) { - PDFJS.disableFullscreen = true; - } -})(); - -// Provides document.currentScript support -// Support: IE, Chrome<29. -(function checkCurrentScript() { - if ('currentScript' in document) { - return; - } - Object.defineProperty(document, 'currentScript', { - get: function () { - var scripts = document.getElementsByTagName('script'); - return scripts[scripts.length - 1]; - }, - enumerable: true, - configurable: true - }); -})(); diff --git a/static/pdf.js/custom/toolbarButton-crop.png b/static/pdf.js/custom/toolbarButton-crop.png new file mode 100644 index 00000000..00c56d52 Binary files /dev/null and b/static/pdf.js/custom/toolbarButton-crop.png differ diff --git a/static/pdf.js/custom/toolbarButton-crop.svg b/static/pdf.js/custom/toolbarButton-crop.svg new file mode 100644 index 00000000..618195a1 --- /dev/null +++ b/static/pdf.js/custom/toolbarButton-crop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/pdf.js/custom/toolbarButton-crop@10.png b/static/pdf.js/custom/toolbarButton-crop@10.png new file mode 100644 index 00000000..7cc05e5d Binary files /dev/null and b/static/pdf.js/custom/toolbarButton-crop@10.png differ diff --git a/static/pdf.js/custom/toolbarButton-crop@2x.png b/static/pdf.js/custom/toolbarButton-crop@2x.png new file mode 100644 index 00000000..950d2758 Binary files /dev/null and b/static/pdf.js/custom/toolbarButton-crop@2x.png differ diff --git a/static/pdf.js/debugger.css b/static/pdf.js/debugger.css new file mode 100644 index 00000000..b9d9f819 --- /dev/null +++ b/static/pdf.js/debugger.css @@ -0,0 +1,111 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:root { + --panel-width: 300px; +} + +#PDFBug, +#PDFBug :is(input, button, select) { + font: message-box; +} +#PDFBug { + background-color: rgb(255 255 255); + border: 1px solid rgb(102 102 102); + position: fixed; + top: 32px; + right: 0; + bottom: 0; + font-size: 10px; + padding: 0; + width: var(--panel-width); +} +#PDFBug .controls { + background: rgb(238 238 238); + border-bottom: 1px solid rgb(102 102 102); + padding: 3px; +} +#PDFBug .panels { + inset: 27px 0 0; + overflow: auto; + position: absolute; +} +#PDFBug .panels > div { + padding: 5px; +} +#PDFBug button.active { + font-weight: bold; +} +.debuggerShowText, +.debuggerHideText:hover { + background-color: rgb(255 255 0 / 0.25); +} +#PDFBug .stats { + font-family: courier; + font-size: 10px; + white-space: pre; +} +#PDFBug .stats .title { + font-weight: bold; +} +#PDFBug table { + font-size: 10px; + white-space: pre; +} +#PDFBug table.showText { + border-collapse: collapse; + text-align: center; +} +#PDFBug table.showText, +#PDFBug table.showText :is(tr, td) { + border: 1px solid black; + padding: 1px; +} +#PDFBug table.showText td.advance { + color: grey; +} + +#viewer.textLayer-visible .textLayer { + opacity: 1; +} + +#viewer.textLayer-visible .canvasWrapper { + background-color: rgb(128 255 128); +} + +#viewer.textLayer-visible .canvasWrapper canvas { + mix-blend-mode: screen; +} + +#viewer.textLayer-visible .textLayer span { + background-color: rgb(255 255 0 / 0.1); + color: rgb(0 0 0); + border: solid 1px rgb(255 0 0 / 0.5); + box-sizing: border-box; +} + +#viewer.textLayer-visible .textLayer span[aria-owns] { + background-color: rgb(255 0 0 / 0.3); +} + +#viewer.textLayer-hover .textLayer span:hover { + background-color: rgb(255 255 255); + color: rgb(0 0 0); +} + +#viewer.textLayer-shadow .textLayer span { + background-color: rgb(255 255 255 / 0.6); + color: rgb(0 0 0); +} diff --git a/static/pdf.js/debugger.js b/static/pdf.js/debugger.js deleted file mode 100644 index 19d29163..00000000 --- a/static/pdf.js/debugger.js +++ /dev/null @@ -1,618 +0,0 @@ -/* Copyright 2012 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* globals PDFJS */ - -'use strict'; - -var FontInspector = (function FontInspectorClosure() { - var fonts; - var active = false; - var fontAttribute = 'data-font-name'; - function removeSelection() { - var divs = document.querySelectorAll('div[' + fontAttribute + ']'); - for (var i = 0, ii = divs.length; i < ii; ++i) { - var div = divs[i]; - div.className = ''; - } - } - function resetSelection() { - var divs = document.querySelectorAll('div[' + fontAttribute + ']'); - for (var i = 0, ii = divs.length; i < ii; ++i) { - var div = divs[i]; - div.className = 'debuggerHideText'; - } - } - function selectFont(fontName, show) { - var divs = document.querySelectorAll('div[' + fontAttribute + '=' + - fontName + ']'); - for (var i = 0, ii = divs.length; i < ii; ++i) { - var div = divs[i]; - div.className = show ? 'debuggerShowText' : 'debuggerHideText'; - } - } - function textLayerClick(e) { - if (!e.target.dataset.fontName || - e.target.tagName.toUpperCase() !== 'DIV') { - return; - } - var fontName = e.target.dataset.fontName; - var selects = document.getElementsByTagName('input'); - for (var i = 0; i < selects.length; ++i) { - var select = selects[i]; - if (select.dataset.fontName !== fontName) { - continue; - } - select.checked = !select.checked; - selectFont(fontName, select.checked); - select.scrollIntoView(); - } - } - return { - // Properties/functions needed by PDFBug. - id: 'FontInspector', - name: 'Font Inspector', - panel: null, - manager: null, - init: function init() { - var panel = this.panel; - panel.setAttribute('style', 'padding: 5px;'); - var tmp = document.createElement('button'); - tmp.addEventListener('click', resetSelection); - tmp.textContent = 'Refresh'; - panel.appendChild(tmp); - - fonts = document.createElement('div'); - panel.appendChild(fonts); - }, - cleanup: function cleanup() { - fonts.textContent = ''; - }, - enabled: false, - get active() { - return active; - }, - set active(value) { - active = value; - if (active) { - document.body.addEventListener('click', textLayerClick, true); - resetSelection(); - } else { - document.body.removeEventListener('click', textLayerClick, true); - removeSelection(); - } - }, - // FontInspector specific functions. - fontAdded: function fontAdded(fontObj, url) { - function properties(obj, list) { - var moreInfo = document.createElement('table'); - for (var i = 0; i < list.length; i++) { - var tr = document.createElement('tr'); - var td1 = document.createElement('td'); - td1.textContent = list[i]; - tr.appendChild(td1); - var td2 = document.createElement('td'); - td2.textContent = obj[list[i]].toString(); - tr.appendChild(td2); - moreInfo.appendChild(tr); - } - return moreInfo; - } - var moreInfo = properties(fontObj, ['name', 'type']); - var fontName = fontObj.loadedName; - var font = document.createElement('div'); - var name = document.createElement('span'); - name.textContent = fontName; - var download = document.createElement('a'); - if (url) { - url = /url\(['"]?([^\)"']+)/.exec(url); - download.href = url[1]; - } else if (fontObj.data) { - url = URL.createObjectURL(new Blob([fontObj.data], { - type: fontObj.mimeType - })); - download.href = url; - } - download.textContent = 'Download'; - var logIt = document.createElement('a'); - logIt.href = ''; - logIt.textContent = 'Log'; - logIt.addEventListener('click', function(event) { - event.preventDefault(); - console.log(fontObj); - }); - var select = document.createElement('input'); - select.setAttribute('type', 'checkbox'); - select.dataset.fontName = fontName; - select.addEventListener('click', (function(select, fontName) { - return (function() { - selectFont(fontName, select.checked); - }); - })(select, fontName)); - font.appendChild(select); - font.appendChild(name); - font.appendChild(document.createTextNode(' ')); - font.appendChild(download); - font.appendChild(document.createTextNode(' ')); - font.appendChild(logIt); - font.appendChild(moreInfo); - fonts.appendChild(font); - // Somewhat of a hack, should probably add a hook for when the text layer - // is done rendering. - setTimeout(function() { - if (this.active) { - resetSelection(); - } - }.bind(this), 2000); - } - }; -})(); - -// Manages all the page steppers. -var StepperManager = (function StepperManagerClosure() { - var steppers = []; - var stepperDiv = null; - var stepperControls = null; - var stepperChooser = null; - var breakPoints = Object.create(null); - return { - // Properties/functions needed by PDFBug. - id: 'Stepper', - name: 'Stepper', - panel: null, - manager: null, - init: function init() { - var self = this; - this.panel.setAttribute('style', 'padding: 5px;'); - stepperControls = document.createElement('div'); - stepperChooser = document.createElement('select'); - stepperChooser.addEventListener('change', function(event) { - self.selectStepper(this.value); - }); - stepperControls.appendChild(stepperChooser); - stepperDiv = document.createElement('div'); - this.panel.appendChild(stepperControls); - this.panel.appendChild(stepperDiv); - if (sessionStorage.getItem('pdfjsBreakPoints')) { - breakPoints = JSON.parse(sessionStorage.getItem('pdfjsBreakPoints')); - } - }, - cleanup: function cleanup() { - stepperChooser.textContent = ''; - stepperDiv.textContent = ''; - steppers = []; - }, - enabled: false, - active: false, - // Stepper specific functions. - create: function create(pageIndex) { - var debug = document.createElement('div'); - debug.id = 'stepper' + pageIndex; - debug.setAttribute('hidden', true); - debug.className = 'stepper'; - stepperDiv.appendChild(debug); - var b = document.createElement('option'); - b.textContent = 'Page ' + (pageIndex + 1); - b.value = pageIndex; - stepperChooser.appendChild(b); - var initBreakPoints = breakPoints[pageIndex] || []; - var stepper = new Stepper(debug, pageIndex, initBreakPoints); - steppers.push(stepper); - if (steppers.length === 1) { - this.selectStepper(pageIndex, false); - } - return stepper; - }, - selectStepper: function selectStepper(pageIndex, selectPanel) { - var i; - pageIndex = pageIndex | 0; - if (selectPanel) { - this.manager.selectPanel(this); - } - for (i = 0; i < steppers.length; ++i) { - var stepper = steppers[i]; - if (stepper.pageIndex === pageIndex) { - stepper.panel.removeAttribute('hidden'); - } else { - stepper.panel.setAttribute('hidden', true); - } - } - var options = stepperChooser.options; - for (i = 0; i < options.length; ++i) { - var option = options[i]; - option.selected = (option.value | 0) === pageIndex; - } - }, - saveBreakPoints: function saveBreakPoints(pageIndex, bps) { - breakPoints[pageIndex] = bps; - sessionStorage.setItem('pdfjsBreakPoints', JSON.stringify(breakPoints)); - } - }; -})(); - -// The stepper for each page's IRQueue. -var Stepper = (function StepperClosure() { - // Shorter way to create element and optionally set textContent. - function c(tag, textContent) { - var d = document.createElement(tag); - if (textContent) { - d.textContent = textContent; - } - return d; - } - - var opMap = null; - - function simplifyArgs(args) { - if (typeof args === 'string') { - var MAX_STRING_LENGTH = 75; - return args.length <= MAX_STRING_LENGTH ? args : - args.substr(0, MAX_STRING_LENGTH) + '...'; - } - if (typeof args !== 'object' || args === null) { - return args; - } - if ('length' in args) { // array - var simpleArgs = [], i, ii; - var MAX_ITEMS = 10; - for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) { - simpleArgs.push(simplifyArgs(args[i])); - } - if (i < args.length) { - simpleArgs.push('...'); - } - return simpleArgs; - } - var simpleObj = {}; - for (var key in args) { - simpleObj[key] = simplifyArgs(args[key]); - } - return simpleObj; - } - - function Stepper(panel, pageIndex, initialBreakPoints) { - this.panel = panel; - this.breakPoint = 0; - this.nextBreakPoint = null; - this.pageIndex = pageIndex; - this.breakPoints = initialBreakPoints; - this.currentIdx = -1; - this.operatorListIdx = 0; - } - Stepper.prototype = { - init: function init() { - var panel = this.panel; - var content = c('div', 'c=continue, s=step'); - var table = c('table'); - content.appendChild(table); - table.cellSpacing = 0; - var headerRow = c('tr'); - table.appendChild(headerRow); - headerRow.appendChild(c('th', 'Break')); - headerRow.appendChild(c('th', 'Idx')); - headerRow.appendChild(c('th', 'fn')); - headerRow.appendChild(c('th', 'args')); - panel.appendChild(content); - this.table = table; - if (!opMap) { - opMap = Object.create(null); - for (var key in PDFJS.OPS) { - opMap[PDFJS.OPS[key]] = key; - } - } - }, - updateOperatorList: function updateOperatorList(operatorList) { - var self = this; - - function cboxOnClick() { - var x = +this.dataset.idx; - if (this.checked) { - self.breakPoints.push(x); - } else { - self.breakPoints.splice(self.breakPoints.indexOf(x), 1); - } - StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints); - } - - var MAX_OPERATORS_COUNT = 15000; - if (this.operatorListIdx > MAX_OPERATORS_COUNT) { - return; - } - - var chunk = document.createDocumentFragment(); - var operatorsToDisplay = Math.min(MAX_OPERATORS_COUNT, - operatorList.fnArray.length); - for (var i = this.operatorListIdx; i < operatorsToDisplay; i++) { - var line = c('tr'); - line.className = 'line'; - line.dataset.idx = i; - chunk.appendChild(line); - var checked = this.breakPoints.indexOf(i) !== -1; - var args = operatorList.argsArray[i] || []; - - var breakCell = c('td'); - var cbox = c('input'); - cbox.type = 'checkbox'; - cbox.className = 'points'; - cbox.checked = checked; - cbox.dataset.idx = i; - cbox.onclick = cboxOnClick; - - breakCell.appendChild(cbox); - line.appendChild(breakCell); - line.appendChild(c('td', i.toString())); - var fn = opMap[operatorList.fnArray[i]]; - var decArgs = args; - if (fn === 'showText') { - var glyphs = args[0]; - var newArgs = []; - var str = []; - for (var j = 0; j < glyphs.length; j++) { - var glyph = glyphs[j]; - if (typeof glyph === 'object' && glyph !== null) { - str.push(glyph.fontChar); - } else { - if (str.length > 0) { - newArgs.push(str.join('')); - str = []; - } - newArgs.push(glyph); // null or number - } - } - if (str.length > 0) { - newArgs.push(str.join('')); - } - decArgs = [newArgs]; - } - line.appendChild(c('td', fn)); - line.appendChild(c('td', JSON.stringify(simplifyArgs(decArgs)))); - } - if (operatorsToDisplay < operatorList.fnArray.length) { - line = c('tr'); - var lastCell = c('td', '...'); - lastCell.colspan = 4; - chunk.appendChild(lastCell); - } - this.operatorListIdx = operatorList.fnArray.length; - this.table.appendChild(chunk); - }, - getNextBreakPoint: function getNextBreakPoint() { - this.breakPoints.sort(function(a, b) { return a - b; }); - for (var i = 0; i < this.breakPoints.length; i++) { - if (this.breakPoints[i] > this.currentIdx) { - return this.breakPoints[i]; - } - } - return null; - }, - breakIt: function breakIt(idx, callback) { - StepperManager.selectStepper(this.pageIndex, true); - var self = this; - var dom = document; - self.currentIdx = idx; - var listener = function(e) { - switch (e.keyCode) { - case 83: // step - dom.removeEventListener('keydown', listener, false); - self.nextBreakPoint = self.currentIdx + 1; - self.goTo(-1); - callback(); - break; - case 67: // continue - dom.removeEventListener('keydown', listener, false); - var breakPoint = self.getNextBreakPoint(); - self.nextBreakPoint = breakPoint; - self.goTo(-1); - callback(); - break; - } - }; - dom.addEventListener('keydown', listener, false); - self.goTo(idx); - }, - goTo: function goTo(idx) { - var allRows = this.panel.getElementsByClassName('line'); - for (var x = 0, xx = allRows.length; x < xx; ++x) { - var row = allRows[x]; - if ((row.dataset.idx | 0) === idx) { - row.style.backgroundColor = 'rgb(251,250,207)'; - row.scrollIntoView(); - } else { - row.style.backgroundColor = null; - } - } - } - }; - return Stepper; -})(); - -var Stats = (function Stats() { - var stats = []; - function clear(node) { - while (node.hasChildNodes()) { - node.removeChild(node.lastChild); - } - } - function getStatIndex(pageNumber) { - for (var i = 0, ii = stats.length; i < ii; ++i) { - if (stats[i].pageNumber === pageNumber) { - return i; - } - } - return false; - } - return { - // Properties/functions needed by PDFBug. - id: 'Stats', - name: 'Stats', - panel: null, - manager: null, - init: function init() { - this.panel.setAttribute('style', 'padding: 5px;'); - PDFJS.enableStats = true; - }, - enabled: false, - active: false, - // Stats specific functions. - add: function(pageNumber, stat) { - if (!stat) { - return; - } - var statsIndex = getStatIndex(pageNumber); - if (statsIndex !== false) { - var b = stats[statsIndex]; - this.panel.removeChild(b.div); - stats.splice(statsIndex, 1); - } - var wrapper = document.createElement('div'); - wrapper.className = 'stats'; - var title = document.createElement('div'); - title.className = 'title'; - title.textContent = 'Page: ' + pageNumber; - var statsDiv = document.createElement('div'); - statsDiv.textContent = stat.toString(); - wrapper.appendChild(title); - wrapper.appendChild(statsDiv); - stats.push({ pageNumber: pageNumber, div: wrapper }); - stats.sort(function(a, b) { return a.pageNumber - b.pageNumber; }); - clear(this.panel); - for (var i = 0, ii = stats.length; i < ii; ++i) { - this.panel.appendChild(stats[i].div); - } - }, - cleanup: function () { - stats = []; - clear(this.panel); - } - }; -})(); - -// Manages all the debugging tools. -var PDFBug = (function PDFBugClosure() { - var panelWidth = 300; - var buttons = []; - var activePanel = null; - - return { - tools: [ - FontInspector, - StepperManager, - Stats - ], - enable: function(ids) { - var all = false, tools = this.tools; - if (ids.length === 1 && ids[0] === 'all') { - all = true; - } - for (var i = 0; i < tools.length; ++i) { - var tool = tools[i]; - if (all || ids.indexOf(tool.id) !== -1) { - tool.enabled = true; - } - } - if (!all) { - // Sort the tools by the order they are enabled. - tools.sort(function(a, b) { - var indexA = ids.indexOf(a.id); - indexA = indexA < 0 ? tools.length : indexA; - var indexB = ids.indexOf(b.id); - indexB = indexB < 0 ? tools.length : indexB; - return indexA - indexB; - }); - } - }, - init: function init() { - /* - * Basic Layout: - * PDFBug - * Controls - * Panels - * Panel - * Panel - * ... - */ - var ui = document.createElement('div'); - ui.id = 'PDFBug'; - - var controls = document.createElement('div'); - controls.setAttribute('class', 'controls'); - ui.appendChild(controls); - - var panels = document.createElement('div'); - panels.setAttribute('class', 'panels'); - ui.appendChild(panels); - - var container = document.getElementById('viewerContainer'); - container.appendChild(ui); - container.style.right = panelWidth + 'px'; - - // Initialize all the debugging tools. - var tools = this.tools; - var self = this; - for (var i = 0; i < tools.length; ++i) { - var tool = tools[i]; - var panel = document.createElement('div'); - var panelButton = document.createElement('button'); - panelButton.textContent = tool.name; - panelButton.addEventListener('click', (function(selected) { - return function(event) { - event.preventDefault(); - self.selectPanel(selected); - }; - })(i)); - controls.appendChild(panelButton); - panels.appendChild(panel); - tool.panel = panel; - tool.manager = this; - if (tool.enabled) { - tool.init(); - } else { - panel.textContent = tool.name + ' is disabled. To enable add ' + - ' "' + tool.id + '" to the pdfBug parameter ' + - 'and refresh (seperate multiple by commas).'; - } - buttons.push(panelButton); - } - this.selectPanel(0); - }, - cleanup: function cleanup() { - for (var i = 0, ii = this.tools.length; i < ii; i++) { - if (this.tools[i].enabled) { - this.tools[i].cleanup(); - } - } - }, - selectPanel: function selectPanel(index) { - if (typeof index !== 'number') { - index = this.tools.indexOf(index); - } - if (index === activePanel) { - return; - } - activePanel = index; - var tools = this.tools; - for (var j = 0; j < tools.length; ++j) { - if (j === index) { - buttons[j].setAttribute('class', 'active'); - tools[j].active = true; - tools[j].panel.removeAttribute('hidden'); - } else { - buttons[j].setAttribute('class', ''); - tools[j].active = false; - tools[j].panel.setAttribute('hidden', 'true'); - } - } - } - }; -})(); diff --git a/static/pdf.js/debugger.mjs b/static/pdf.js/debugger.mjs new file mode 100644 index 00000000..59c1871b --- /dev/null +++ b/static/pdf.js/debugger.mjs @@ -0,0 +1,623 @@ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { OPS } = globalThis.pdfjsLib || (await import("pdfjs-lib")); + +const opMap = Object.create(null); +for (const key in OPS) { + opMap[OPS[key]] = key; +} + +const FontInspector = (function FontInspectorClosure() { + let fonts; + let active = false; + const fontAttribute = "data-font-name"; + function removeSelection() { + const divs = document.querySelectorAll(`span[${fontAttribute}]`); + for (const div of divs) { + div.className = ""; + } + } + function resetSelection() { + const divs = document.querySelectorAll(`span[${fontAttribute}]`); + for (const div of divs) { + div.className = "debuggerHideText"; + } + } + function selectFont(fontName, show) { + const divs = document.querySelectorAll( + `span[${fontAttribute}=${fontName}]` + ); + for (const div of divs) { + div.className = show ? "debuggerShowText" : "debuggerHideText"; + } + } + function textLayerClick(e) { + if ( + !e.target.dataset.fontName || + e.target.tagName.toUpperCase() !== "SPAN" + ) { + return; + } + const fontName = e.target.dataset.fontName; + const selects = document.getElementsByTagName("input"); + for (const select of selects) { + if (select.dataset.fontName !== fontName) { + continue; + } + select.checked = !select.checked; + selectFont(fontName, select.checked); + select.scrollIntoView(); + } + } + return { + // Properties/functions needed by PDFBug. + id: "FontInspector", + name: "Font Inspector", + panel: null, + manager: null, + init() { + const panel = this.panel; + const tmp = document.createElement("button"); + tmp.addEventListener("click", resetSelection); + tmp.textContent = "Refresh"; + panel.append(tmp); + + fonts = document.createElement("div"); + panel.append(fonts); + }, + cleanup() { + fonts.textContent = ""; + }, + enabled: false, + get active() { + return active; + }, + set active(value) { + active = value; + if (active) { + document.body.addEventListener("click", textLayerClick, true); + resetSelection(); + } else { + document.body.removeEventListener("click", textLayerClick, true); + removeSelection(); + } + }, + // FontInspector specific functions. + fontAdded(fontObj, url) { + function properties(obj, list) { + const moreInfo = document.createElement("table"); + for (const entry of list) { + const tr = document.createElement("tr"); + const td1 = document.createElement("td"); + td1.textContent = entry; + tr.append(td1); + const td2 = document.createElement("td"); + td2.textContent = obj[entry].toString(); + tr.append(td2); + moreInfo.append(tr); + } + return moreInfo; + } + + const moreInfo = fontObj.css + ? properties(fontObj, ["baseFontName"]) + : properties(fontObj, ["name", "type"]); + + const fontName = fontObj.loadedName; + const font = document.createElement("div"); + const name = document.createElement("span"); + name.textContent = fontName; + let download; + if (!fontObj.css) { + download = document.createElement("a"); + if (url) { + url = /url\(['"]?([^)"']+)/.exec(url); + download.href = url[1]; + } else if (fontObj.data) { + download.href = URL.createObjectURL( + new Blob([fontObj.data], { type: fontObj.mimetype }) + ); + } + download.textContent = "Download"; + } + + const logIt = document.createElement("a"); + logIt.href = ""; + logIt.textContent = "Log"; + logIt.addEventListener("click", function (event) { + event.preventDefault(); + console.log(fontObj); + }); + const select = document.createElement("input"); + select.setAttribute("type", "checkbox"); + select.dataset.fontName = fontName; + select.addEventListener("click", function () { + selectFont(fontName, select.checked); + }); + if (download) { + font.append(select, name, " ", download, " ", logIt, moreInfo); + } else { + font.append(select, name, " ", logIt, moreInfo); + } + fonts.append(font); + // Somewhat of a hack, should probably add a hook for when the text layer + // is done rendering. + setTimeout(() => { + if (this.active) { + resetSelection(); + } + }, 2000); + }, + }; +})(); + +// Manages all the page steppers. +const StepperManager = (function StepperManagerClosure() { + let steppers = []; + let stepperDiv = null; + let stepperControls = null; + let stepperChooser = null; + let breakPoints = Object.create(null); + return { + // Properties/functions needed by PDFBug. + id: "Stepper", + name: "Stepper", + panel: null, + manager: null, + init() { + const self = this; + stepperControls = document.createElement("div"); + stepperChooser = document.createElement("select"); + stepperChooser.addEventListener("change", function (event) { + self.selectStepper(this.value); + }); + stepperControls.append(stepperChooser); + stepperDiv = document.createElement("div"); + this.panel.append(stepperControls, stepperDiv); + if (sessionStorage.getItem("pdfjsBreakPoints")) { + breakPoints = JSON.parse(sessionStorage.getItem("pdfjsBreakPoints")); + } + }, + cleanup() { + stepperChooser.textContent = ""; + stepperDiv.textContent = ""; + steppers = []; + }, + enabled: false, + active: false, + // Stepper specific functions. + create(pageIndex) { + const debug = document.createElement("div"); + debug.id = "stepper" + pageIndex; + debug.hidden = true; + debug.className = "stepper"; + stepperDiv.append(debug); + const b = document.createElement("option"); + b.textContent = "Page " + (pageIndex + 1); + b.value = pageIndex; + stepperChooser.append(b); + const initBreakPoints = breakPoints[pageIndex] || []; + const stepper = new Stepper(debug, pageIndex, initBreakPoints); + steppers.push(stepper); + if (steppers.length === 1) { + this.selectStepper(pageIndex, false); + } + return stepper; + }, + selectStepper(pageIndex, selectPanel) { + pageIndex |= 0; + if (selectPanel) { + this.manager.selectPanel(this); + } + for (const stepper of steppers) { + stepper.panel.hidden = stepper.pageIndex !== pageIndex; + } + for (const option of stepperChooser.options) { + option.selected = (option.value | 0) === pageIndex; + } + }, + saveBreakPoints(pageIndex, bps) { + breakPoints[pageIndex] = bps; + sessionStorage.setItem("pdfjsBreakPoints", JSON.stringify(breakPoints)); + }, + }; +})(); + +// The stepper for each page's operatorList. +class Stepper { + // Shorter way to create element and optionally set textContent. + #c(tag, textContent) { + const d = document.createElement(tag); + if (textContent) { + d.textContent = textContent; + } + return d; + } + + #simplifyArgs(args) { + if (typeof args === "string") { + const MAX_STRING_LENGTH = 75; + return args.length <= MAX_STRING_LENGTH + ? args + : args.substring(0, MAX_STRING_LENGTH) + "..."; + } + if (typeof args !== "object" || args === null) { + return args; + } + if ("length" in args) { + // array + const MAX_ITEMS = 10, + simpleArgs = []; + let i, ii; + for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) { + simpleArgs.push(this.#simplifyArgs(args[i])); + } + if (i < args.length) { + simpleArgs.push("..."); + } + return simpleArgs; + } + const simpleObj = {}; + for (const key in args) { + simpleObj[key] = this.#simplifyArgs(args[key]); + } + return simpleObj; + } + + constructor(panel, pageIndex, initialBreakPoints) { + this.panel = panel; + this.breakPoint = 0; + this.nextBreakPoint = null; + this.pageIndex = pageIndex; + this.breakPoints = initialBreakPoints; + this.currentIdx = -1; + this.operatorListIdx = 0; + this.indentLevel = 0; + } + + init(operatorList) { + const panel = this.panel; + const content = this.#c("div", "c=continue, s=step"); + const table = this.#c("table"); + content.append(table); + table.cellSpacing = 0; + const headerRow = this.#c("tr"); + table.append(headerRow); + headerRow.append( + this.#c("th", "Break"), + this.#c("th", "Idx"), + this.#c("th", "fn"), + this.#c("th", "args") + ); + panel.append(content); + this.table = table; + this.updateOperatorList(operatorList); + } + + updateOperatorList(operatorList) { + const self = this; + + function cboxOnClick() { + const x = +this.dataset.idx; + if (this.checked) { + self.breakPoints.push(x); + } else { + self.breakPoints.splice(self.breakPoints.indexOf(x), 1); + } + StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints); + } + + const MAX_OPERATORS_COUNT = 15000; + if (this.operatorListIdx > MAX_OPERATORS_COUNT) { + return; + } + + const chunk = document.createDocumentFragment(); + const operatorsToDisplay = Math.min( + MAX_OPERATORS_COUNT, + operatorList.fnArray.length + ); + for (let i = this.operatorListIdx; i < operatorsToDisplay; i++) { + const line = this.#c("tr"); + line.className = "line"; + line.dataset.idx = i; + chunk.append(line); + const checked = this.breakPoints.includes(i); + const args = operatorList.argsArray[i] || []; + + const breakCell = this.#c("td"); + const cbox = this.#c("input"); + cbox.type = "checkbox"; + cbox.className = "points"; + cbox.checked = checked; + cbox.dataset.idx = i; + cbox.onclick = cboxOnClick; + + breakCell.append(cbox); + line.append(breakCell, this.#c("td", i.toString())); + const fn = opMap[operatorList.fnArray[i]]; + let decArgs = args; + if (fn === "showText") { + const glyphs = args[0]; + const charCodeRow = this.#c("tr"); + const fontCharRow = this.#c("tr"); + const unicodeRow = this.#c("tr"); + for (const glyph of glyphs) { + if (typeof glyph === "object" && glyph !== null) { + charCodeRow.append(this.#c("td", glyph.originalCharCode)); + fontCharRow.append(this.#c("td", glyph.fontChar)); + unicodeRow.append(this.#c("td", glyph.unicode)); + } else { + // null or number + const advanceEl = this.#c("td", glyph); + advanceEl.classList.add("advance"); + charCodeRow.append(advanceEl); + fontCharRow.append(this.#c("td")); + unicodeRow.append(this.#c("td")); + } + } + decArgs = this.#c("td"); + const table = this.#c("table"); + table.classList.add("showText"); + decArgs.append(table); + table.append(charCodeRow, fontCharRow, unicodeRow); + } else if (fn === "restore" && this.indentLevel > 0) { + this.indentLevel--; + } + line.append(this.#c("td", " ".repeat(this.indentLevel * 2) + fn)); + if (fn === "save") { + this.indentLevel++; + } + + if (decArgs instanceof HTMLElement) { + line.append(decArgs); + } else { + line.append(this.#c("td", JSON.stringify(this.#simplifyArgs(decArgs)))); + } + } + if (operatorsToDisplay < operatorList.fnArray.length) { + const lastCell = this.#c("td", "..."); + lastCell.colspan = 4; + chunk.append(lastCell); + } + this.operatorListIdx = operatorList.fnArray.length; + this.table.append(chunk); + } + + getNextBreakPoint() { + this.breakPoints.sort(function (a, b) { + return a - b; + }); + for (const breakPoint of this.breakPoints) { + if (breakPoint > this.currentIdx) { + return breakPoint; + } + } + return null; + } + + breakIt(idx, callback) { + StepperManager.selectStepper(this.pageIndex, true); + this.currentIdx = idx; + + const listener = evt => { + switch (evt.keyCode) { + case 83: // step + document.removeEventListener("keydown", listener); + this.nextBreakPoint = this.currentIdx + 1; + this.goTo(-1); + callback(); + break; + case 67: // continue + document.removeEventListener("keydown", listener); + this.nextBreakPoint = this.getNextBreakPoint(); + this.goTo(-1); + callback(); + break; + } + }; + document.addEventListener("keydown", listener); + this.goTo(idx); + } + + goTo(idx) { + const allRows = this.panel.getElementsByClassName("line"); + for (const row of allRows) { + if ((row.dataset.idx | 0) === idx) { + row.style.backgroundColor = "rgb(251,250,207)"; + row.scrollIntoView(); + } else { + row.style.backgroundColor = null; + } + } + } +} + +const Stats = (function Stats() { + let stats = []; + function clear(node) { + node.textContent = ""; // Remove any `node` contents from the DOM. + } + function getStatIndex(pageNumber) { + for (const [i, stat] of stats.entries()) { + if (stat.pageNumber === pageNumber) { + return i; + } + } + return false; + } + return { + // Properties/functions needed by PDFBug. + id: "Stats", + name: "Stats", + panel: null, + manager: null, + init() {}, + enabled: false, + active: false, + // Stats specific functions. + add(pageNumber, stat) { + if (!stat) { + return; + } + const statsIndex = getStatIndex(pageNumber); + if (statsIndex !== false) { + stats[statsIndex].div.remove(); + stats.splice(statsIndex, 1); + } + const wrapper = document.createElement("div"); + wrapper.className = "stats"; + const title = document.createElement("div"); + title.className = "title"; + title.textContent = "Page: " + pageNumber; + const statsDiv = document.createElement("div"); + statsDiv.textContent = stat.toString(); + wrapper.append(title, statsDiv); + stats.push({ pageNumber, div: wrapper }); + stats.sort(function (a, b) { + return a.pageNumber - b.pageNumber; + }); + clear(this.panel); + for (const entry of stats) { + this.panel.append(entry.div); + } + }, + cleanup() { + stats = []; + clear(this.panel); + }, + }; +})(); + +// Manages all the debugging tools. +class PDFBug { + static #buttons = []; + + static #activePanel = null; + + static tools = [FontInspector, StepperManager, Stats]; + + static enable(ids) { + const all = ids.length === 1 && ids[0] === "all"; + const tools = this.tools; + for (const tool of tools) { + if (all || ids.includes(tool.id)) { + tool.enabled = true; + } + } + if (!all) { + // Sort the tools by the order they are enabled. + tools.sort(function (a, b) { + let indexA = ids.indexOf(a.id); + indexA = indexA < 0 ? tools.length : indexA; + let indexB = ids.indexOf(b.id); + indexB = indexB < 0 ? tools.length : indexB; + return indexA - indexB; + }); + } + } + + static init(container, ids) { + this.loadCSS(); + this.enable(ids); + /* + * Basic Layout: + * PDFBug + * Controls + * Panels + * Panel + * Panel + * ... + */ + const ui = document.createElement("div"); + ui.id = "PDFBug"; + + const controls = document.createElement("div"); + controls.setAttribute("class", "controls"); + ui.append(controls); + + const panels = document.createElement("div"); + panels.setAttribute("class", "panels"); + ui.append(panels); + + container.append(ui); + container.style.right = "var(--panel-width)"; + + // Initialize all the debugging tools. + for (const tool of this.tools) { + const panel = document.createElement("div"); + const panelButton = document.createElement("button"); + panelButton.textContent = tool.name; + panelButton.addEventListener("click", event => { + event.preventDefault(); + this.selectPanel(tool); + }); + controls.append(panelButton); + panels.append(panel); + tool.panel = panel; + tool.manager = this; + if (tool.enabled) { + tool.init(); + } else { + panel.textContent = + `${tool.name} is disabled. To enable add "${tool.id}" to ` + + "the pdfBug parameter and refresh (separate multiple by commas)."; + } + this.#buttons.push(panelButton); + } + this.selectPanel(0); + } + + static loadCSS() { + const { url } = import.meta; + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = url.replace(/\.mjs$/, ".css"); + + document.head.append(link); + } + + static cleanup() { + for (const tool of this.tools) { + if (tool.enabled) { + tool.cleanup(); + } + } + } + + static selectPanel(index) { + if (typeof index !== "number") { + index = this.tools.indexOf(index); + } + if (index === this.#activePanel) { + return; + } + this.#activePanel = index; + for (const [j, tool] of this.tools.entries()) { + const isActive = j === index; + this.#buttons[j].classList.toggle("active", isActive); + tool.active = isActive; + tool.panel.hidden = !isActive; + } + } +} + +globalThis.FontInspector = FontInspector; +globalThis.StepperManager = StepperManager; +globalThis.Stats = Stats; + +export { PDFBug }; diff --git a/static/pdf.js/embeds.js b/static/pdf.js/embeds.js index d6e52d06..c773feeb 100644 --- a/static/pdf.js/embeds.js +++ b/static/pdf.js/embeds.js @@ -3,25 +3,27 @@ Ox.load({ loadCSS: false } }, function() { - var currentPage = PDFView.page; - window.addEventListener('pagechange', function (evt) { - var page = evt.pageNumber; - if (page && page != currentPage) { - currentPage = page; - Ox.$parent.postMessage('page', { - page: Math.round(page) - }); - } - }); + var currentPage = PDFViewerApplication.page; + PDFViewerApplication.initializedPromise.then(function() { + PDFViewerApplication.pdfViewer.eventBus.on("pagechanging", function(event) { + var page = event.pageNumber; + if (page && page != currentPage) { + currentPage = page; + Ox.$parent.postMessage('page', { + page: page + }); + } + }) + }) Ox.$parent.bindMessage({ page: function(data) { - if (data.page != PDFView.page) { - PDFView.page = data.page; + if (data.page != PDFViewerApplication.page) { + PDFViewerApplication.page = data.page; } }, pdf: function(data) { - if (PDFView.url != data.pdf) { - PDFView.open(data.pdf); + if (PDFViewerApplication.url != data.pdf) { + PDFViewerApplication.open(data.pdf); } } }); diff --git a/static/pdf.js/images/altText_add.svg b/static/pdf.js/images/altText_add.svg new file mode 100644 index 00000000..3451b536 --- /dev/null +++ b/static/pdf.js/images/altText_add.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/altText_done.svg b/static/pdf.js/images/altText_done.svg new file mode 100644 index 00000000..f54924eb --- /dev/null +++ b/static/pdf.js/images/altText_done.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/annotation-paperclip.svg b/static/pdf.js/images/annotation-paperclip.svg new file mode 100644 index 00000000..2bed2250 --- /dev/null +++ b/static/pdf.js/images/annotation-paperclip.svg @@ -0,0 +1,6 @@ + + + + diff --git a/static/pdf.js/images/annotation-pushpin.svg b/static/pdf.js/images/annotation-pushpin.svg new file mode 100644 index 00000000..6e0896cf --- /dev/null +++ b/static/pdf.js/images/annotation-pushpin.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/static/pdf.js/images/cursor-editorFreeHighlight.svg b/static/pdf.js/images/cursor-editorFreeHighlight.svg new file mode 100644 index 00000000..513f6bdf --- /dev/null +++ b/static/pdf.js/images/cursor-editorFreeHighlight.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/pdf.js/images/cursor-editorFreeText.svg b/static/pdf.js/images/cursor-editorFreeText.svg new file mode 100644 index 00000000..de2838ef --- /dev/null +++ b/static/pdf.js/images/cursor-editorFreeText.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/cursor-editorInk.svg b/static/pdf.js/images/cursor-editorInk.svg new file mode 100644 index 00000000..1dadb5c0 --- /dev/null +++ b/static/pdf.js/images/cursor-editorInk.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/pdf.js/images/cursor-editorTextHighlight.svg b/static/pdf.js/images/cursor-editorTextHighlight.svg new file mode 100644 index 00000000..800340cb --- /dev/null +++ b/static/pdf.js/images/cursor-editorTextHighlight.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/pdf.js/images/editor-toolbar-delete.svg b/static/pdf.js/images/editor-toolbar-delete.svg new file mode 100644 index 00000000..f84520d8 --- /dev/null +++ b/static/pdf.js/images/editor-toolbar-delete.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/static/pdf.js/images/findbarButton-next-rtl.png b/static/pdf.js/images/findbarButton-next-rtl.png deleted file mode 100644 index bef02743..00000000 Binary files a/static/pdf.js/images/findbarButton-next-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-next-rtl@2x.png b/static/pdf.js/images/findbarButton-next-rtl@2x.png deleted file mode 100644 index 1da6dc94..00000000 Binary files a/static/pdf.js/images/findbarButton-next-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-next.png b/static/pdf.js/images/findbarButton-next.png deleted file mode 100644 index de1d0fc9..00000000 Binary files a/static/pdf.js/images/findbarButton-next.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-next.svg b/static/pdf.js/images/findbarButton-next.svg new file mode 100644 index 00000000..8cb39bec --- /dev/null +++ b/static/pdf.js/images/findbarButton-next.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/findbarButton-next@2x.png b/static/pdf.js/images/findbarButton-next@2x.png deleted file mode 100644 index 0250307c..00000000 Binary files a/static/pdf.js/images/findbarButton-next@2x.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-previous-rtl.png b/static/pdf.js/images/findbarButton-previous-rtl.png deleted file mode 100644 index de1d0fc9..00000000 Binary files a/static/pdf.js/images/findbarButton-previous-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-previous-rtl@2x.png b/static/pdf.js/images/findbarButton-previous-rtl@2x.png deleted file mode 100644 index 0250307c..00000000 Binary files a/static/pdf.js/images/findbarButton-previous-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-previous.png b/static/pdf.js/images/findbarButton-previous.png deleted file mode 100644 index bef02743..00000000 Binary files a/static/pdf.js/images/findbarButton-previous.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-previous.svg b/static/pdf.js/images/findbarButton-previous.svg new file mode 100644 index 00000000..b610879d --- /dev/null +++ b/static/pdf.js/images/findbarButton-previous.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/findbarButton-previous@2x.png b/static/pdf.js/images/findbarButton-previous@2x.png deleted file mode 100644 index 1da6dc94..00000000 Binary files a/static/pdf.js/images/findbarButton-previous@2x.png and /dev/null differ diff --git a/static/pdf.js/images/grab.cur b/static/pdf.js/images/grab.cur deleted file mode 100644 index db7ad5ae..00000000 Binary files a/static/pdf.js/images/grab.cur and /dev/null differ diff --git a/static/pdf.js/images/grabbing.cur b/static/pdf.js/images/grabbing.cur deleted file mode 100644 index e0dfd04e..00000000 Binary files a/static/pdf.js/images/grabbing.cur and /dev/null differ diff --git a/static/pdf.js/images/gv-toolbarButton-download.svg b/static/pdf.js/images/gv-toolbarButton-download.svg new file mode 100644 index 00000000..d56cf3ce --- /dev/null +++ b/static/pdf.js/images/gv-toolbarButton-download.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/loading-small.png b/static/pdf.js/images/loading-small.png deleted file mode 100644 index 8831a805..00000000 Binary files a/static/pdf.js/images/loading-small.png and /dev/null differ diff --git a/static/pdf.js/images/loading-small@2x.png b/static/pdf.js/images/loading-small@2x.png deleted file mode 100644 index b25b4452..00000000 Binary files a/static/pdf.js/images/loading-small@2x.png and /dev/null differ diff --git a/static/pdf.js/images/loading.svg b/static/pdf.js/images/loading.svg new file mode 100644 index 00000000..0a15ff68 --- /dev/null +++ b/static/pdf.js/images/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/pdf.js/images/secondaryToolbarButton-documentProperties.png b/static/pdf.js/images/secondaryToolbarButton-documentProperties.png deleted file mode 100644 index 40925e25..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-documentProperties.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-documentProperties.svg b/static/pdf.js/images/secondaryToolbarButton-documentProperties.svg new file mode 100644 index 00000000..dd3917b9 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-documentProperties.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-documentProperties@2x.png b/static/pdf.js/images/secondaryToolbarButton-documentProperties@2x.png deleted file mode 100644 index adb240ea..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-documentProperties@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-firstPage.png b/static/pdf.js/images/secondaryToolbarButton-firstPage.png deleted file mode 100644 index e68846aa..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-firstPage.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-firstPage.svg b/static/pdf.js/images/secondaryToolbarButton-firstPage.svg new file mode 100644 index 00000000..f5c917f1 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-firstPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-firstPage@2x.png b/static/pdf.js/images/secondaryToolbarButton-firstPage@2x.png deleted file mode 100644 index 3ad8af51..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-firstPage@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-handTool.png b/static/pdf.js/images/secondaryToolbarButton-handTool.png deleted file mode 100644 index cb85a841..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-handTool.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-handTool.svg b/static/pdf.js/images/secondaryToolbarButton-handTool.svg new file mode 100644 index 00000000..b7073b59 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-handTool.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-handTool@2x.png b/static/pdf.js/images/secondaryToolbarButton-handTool@2x.png deleted file mode 100644 index 5c13f77f..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-handTool@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-lastPage.png b/static/pdf.js/images/secondaryToolbarButton-lastPage.png deleted file mode 100644 index be763e0c..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-lastPage.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-lastPage.svg b/static/pdf.js/images/secondaryToolbarButton-lastPage.svg new file mode 100644 index 00000000..c04f6507 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-lastPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-lastPage@2x.png b/static/pdf.js/images/secondaryToolbarButton-lastPage@2x.png deleted file mode 100644 index 8570984f..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-lastPage@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCcw.png b/static/pdf.js/images/secondaryToolbarButton-rotateCcw.png deleted file mode 100644 index 675d6da2..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-rotateCcw.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCcw.svg b/static/pdf.js/images/secondaryToolbarButton-rotateCcw.svg new file mode 100644 index 00000000..da73a1b1 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-rotateCcw.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCcw@2x.png b/static/pdf.js/images/secondaryToolbarButton-rotateCcw@2x.png deleted file mode 100644 index b9e74312..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-rotateCcw@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCw.png b/static/pdf.js/images/secondaryToolbarButton-rotateCw.png deleted file mode 100644 index e1c75988..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-rotateCw.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCw.svg b/static/pdf.js/images/secondaryToolbarButton-rotateCw.svg new file mode 100644 index 00000000..c41ce736 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-rotateCw.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCw@2x.png b/static/pdf.js/images/secondaryToolbarButton-rotateCw@2x.png deleted file mode 100644 index cb257b41..00000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-rotateCw@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-scrollHorizontal.svg b/static/pdf.js/images/secondaryToolbarButton-scrollHorizontal.svg new file mode 100644 index 00000000..fb440b94 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-scrollHorizontal.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-scrollPage.svg b/static/pdf.js/images/secondaryToolbarButton-scrollPage.svg new file mode 100644 index 00000000..64a9f500 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-scrollPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-scrollVertical.svg b/static/pdf.js/images/secondaryToolbarButton-scrollVertical.svg new file mode 100644 index 00000000..dc7e8052 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-scrollVertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-scrollWrapped.svg b/static/pdf.js/images/secondaryToolbarButton-scrollWrapped.svg new file mode 100644 index 00000000..75fe26bc --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-scrollWrapped.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-selectTool.svg b/static/pdf.js/images/secondaryToolbarButton-selectTool.svg new file mode 100644 index 00000000..94d51410 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-selectTool.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-spreadEven.svg b/static/pdf.js/images/secondaryToolbarButton-spreadEven.svg new file mode 100644 index 00000000..ce201e33 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-spreadEven.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-spreadNone.svg b/static/pdf.js/images/secondaryToolbarButton-spreadNone.svg new file mode 100644 index 00000000..e8d487fa --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-spreadNone.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-spreadOdd.svg b/static/pdf.js/images/secondaryToolbarButton-spreadOdd.svg new file mode 100644 index 00000000..9211a427 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-spreadOdd.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/shadow.png b/static/pdf.js/images/shadow.png deleted file mode 100644 index 31d3bdb1..00000000 Binary files a/static/pdf.js/images/shadow.png and /dev/null differ diff --git a/static/pdf.js/images/texture.png b/static/pdf.js/images/texture.png deleted file mode 100644 index eb5ccb5e..00000000 Binary files a/static/pdf.js/images/texture.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-bookmark.png b/static/pdf.js/images/toolbarButton-bookmark.png deleted file mode 100644 index a187be6c..00000000 Binary files a/static/pdf.js/images/toolbarButton-bookmark.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-bookmark.svg b/static/pdf.js/images/toolbarButton-bookmark.svg new file mode 100644 index 00000000..c4c37c90 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-bookmark@2x.png b/static/pdf.js/images/toolbarButton-bookmark@2x.png deleted file mode 100644 index 4efbaa67..00000000 Binary files a/static/pdf.js/images/toolbarButton-bookmark@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-currentOutlineItem.svg b/static/pdf.js/images/toolbarButton-currentOutlineItem.svg new file mode 100644 index 00000000..01e67623 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-currentOutlineItem.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-download.png b/static/pdf.js/images/toolbarButton-download.png deleted file mode 100644 index eaab35f0..00000000 Binary files a/static/pdf.js/images/toolbarButton-download.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-download.svg b/static/pdf.js/images/toolbarButton-download.svg new file mode 100644 index 00000000..e2e850ad --- /dev/null +++ b/static/pdf.js/images/toolbarButton-download.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/pdf.js/images/toolbarButton-download@2x.png b/static/pdf.js/images/toolbarButton-download@2x.png deleted file mode 100644 index 896face4..00000000 Binary files a/static/pdf.js/images/toolbarButton-download@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-editorFreeText.svg b/static/pdf.js/images/toolbarButton-editorFreeText.svg new file mode 100644 index 00000000..13a67bd9 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-editorFreeText.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/pdf.js/images/toolbarButton-editorHighlight.svg b/static/pdf.js/images/toolbarButton-editorHighlight.svg new file mode 100644 index 00000000..b3cd7fda --- /dev/null +++ b/static/pdf.js/images/toolbarButton-editorHighlight.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/static/pdf.js/images/toolbarButton-editorInk.svg b/static/pdf.js/images/toolbarButton-editorInk.svg new file mode 100644 index 00000000..b579eec7 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-editorInk.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/pdf.js/images/toolbarButton-editorStamp.svg b/static/pdf.js/images/toolbarButton-editorStamp.svg new file mode 100644 index 00000000..a1fef492 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-editorStamp.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/static/pdf.js/images/toolbarButton-fullscreen.png b/static/pdf.js/images/toolbarButton-fullscreen.png deleted file mode 100644 index fa730955..00000000 Binary files a/static/pdf.js/images/toolbarButton-fullscreen.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-menuArrow.svg b/static/pdf.js/images/toolbarButton-menuArrow.svg new file mode 100644 index 00000000..82ffeaab --- /dev/null +++ b/static/pdf.js/images/toolbarButton-menuArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-menuArrows.png b/static/pdf.js/images/toolbarButton-menuArrows.png deleted file mode 100644 index 306eb43b..00000000 Binary files a/static/pdf.js/images/toolbarButton-menuArrows.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-menuArrows@2x.png b/static/pdf.js/images/toolbarButton-menuArrows@2x.png deleted file mode 100644 index f7570bc0..00000000 Binary files a/static/pdf.js/images/toolbarButton-menuArrows@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-openFile.png b/static/pdf.js/images/toolbarButton-openFile.png deleted file mode 100644 index b5cf1bd0..00000000 Binary files a/static/pdf.js/images/toolbarButton-openFile.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-openFile.svg b/static/pdf.js/images/toolbarButton-openFile.svg new file mode 100644 index 00000000..e773781d --- /dev/null +++ b/static/pdf.js/images/toolbarButton-openFile.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-openFile@2x.png b/static/pdf.js/images/toolbarButton-openFile@2x.png deleted file mode 100644 index 91ab7659..00000000 Binary files a/static/pdf.js/images/toolbarButton-openFile@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageDown-rtl.png b/static/pdf.js/images/toolbarButton-pageDown-rtl.png deleted file mode 100644 index 1957f79a..00000000 Binary files a/static/pdf.js/images/toolbarButton-pageDown-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageDown-rtl@2x.png b/static/pdf.js/images/toolbarButton-pageDown-rtl@2x.png deleted file mode 100644 index 16ebcb8e..00000000 Binary files a/static/pdf.js/images/toolbarButton-pageDown-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageDown.png b/static/pdf.js/images/toolbarButton-pageDown.png deleted file mode 100644 index 8219ecf8..00000000 Binary files a/static/pdf.js/images/toolbarButton-pageDown.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageDown.svg b/static/pdf.js/images/toolbarButton-pageDown.svg new file mode 100644 index 00000000..1fc12e73 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-pageDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-pageDown@2x.png b/static/pdf.js/images/toolbarButton-pageDown@2x.png deleted file mode 100644 index 758c01d8..00000000 Binary files a/static/pdf.js/images/toolbarButton-pageDown@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageUp-rtl.png b/static/pdf.js/images/toolbarButton-pageUp-rtl.png deleted file mode 100644 index 98e7ce48..00000000 Binary files a/static/pdf.js/images/toolbarButton-pageUp-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageUp-rtl@2x.png b/static/pdf.js/images/toolbarButton-pageUp-rtl@2x.png deleted file mode 100644 index a01b0238..00000000 Binary files a/static/pdf.js/images/toolbarButton-pageUp-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageUp.png b/static/pdf.js/images/toolbarButton-pageUp.png deleted file mode 100644 index fb9daa33..00000000 Binary files a/static/pdf.js/images/toolbarButton-pageUp.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageUp.svg b/static/pdf.js/images/toolbarButton-pageUp.svg new file mode 100644 index 00000000..0936b9a5 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-pageUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-pageUp@2x.png b/static/pdf.js/images/toolbarButton-pageUp@2x.png deleted file mode 100644 index a5cfd755..00000000 Binary files a/static/pdf.js/images/toolbarButton-pageUp@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-presentationMode.png b/static/pdf.js/images/toolbarButton-presentationMode.png deleted file mode 100644 index 3ac21244..00000000 Binary files a/static/pdf.js/images/toolbarButton-presentationMode.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-presentationMode.svg b/static/pdf.js/images/toolbarButton-presentationMode.svg new file mode 100644 index 00000000..901d5672 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-presentationMode.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-presentationMode@2x.png b/static/pdf.js/images/toolbarButton-presentationMode@2x.png deleted file mode 100644 index cada9e79..00000000 Binary files a/static/pdf.js/images/toolbarButton-presentationMode@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-print.png b/static/pdf.js/images/toolbarButton-print.png deleted file mode 100644 index 51275e54..00000000 Binary files a/static/pdf.js/images/toolbarButton-print.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-print.svg b/static/pdf.js/images/toolbarButton-print.svg new file mode 100644 index 00000000..97a39047 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-print.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-print@2x.png b/static/pdf.js/images/toolbarButton-print@2x.png deleted file mode 100644 index 53d18daf..00000000 Binary files a/static/pdf.js/images/toolbarButton-print@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-search.png b/static/pdf.js/images/toolbarButton-search.png deleted file mode 100644 index f9b75579..00000000 Binary files a/static/pdf.js/images/toolbarButton-search.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-search.svg b/static/pdf.js/images/toolbarButton-search.svg new file mode 100644 index 00000000..0cc7ae21 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-search.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-search@2x.png b/static/pdf.js/images/toolbarButton-search@2x.png deleted file mode 100644 index 456b1332..00000000 Binary files a/static/pdf.js/images/toolbarButton-search@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl.png b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl.png deleted file mode 100644 index 84370952..00000000 Binary files a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png deleted file mode 100644 index 9d9bfa4f..00000000 Binary files a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.png b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.png deleted file mode 100644 index 1f90f83d..00000000 Binary files a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.svg b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.svg new file mode 100644 index 00000000..cace8637 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle@2x.png b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle@2x.png deleted file mode 100644 index b066fe5c..00000000 Binary files a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle-rtl.png b/static/pdf.js/images/toolbarButton-sidebarToggle-rtl.png deleted file mode 100644 index 6f85ec06..00000000 Binary files a/static/pdf.js/images/toolbarButton-sidebarToggle-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle-rtl@2x.png b/static/pdf.js/images/toolbarButton-sidebarToggle-rtl@2x.png deleted file mode 100644 index 291e0067..00000000 Binary files a/static/pdf.js/images/toolbarButton-sidebarToggle-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle.png b/static/pdf.js/images/toolbarButton-sidebarToggle.png deleted file mode 100644 index 025dc904..00000000 Binary files a/static/pdf.js/images/toolbarButton-sidebarToggle.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle.svg b/static/pdf.js/images/toolbarButton-sidebarToggle.svg new file mode 100644 index 00000000..1d8d0e4b --- /dev/null +++ b/static/pdf.js/images/toolbarButton-sidebarToggle.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle@2x.png b/static/pdf.js/images/toolbarButton-sidebarToggle@2x.png deleted file mode 100644 index 7f834df9..00000000 Binary files a/static/pdf.js/images/toolbarButton-sidebarToggle@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewAttachments.png b/static/pdf.js/images/toolbarButton-viewAttachments.png deleted file mode 100644 index fcd0b268..00000000 Binary files a/static/pdf.js/images/toolbarButton-viewAttachments.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewAttachments.svg b/static/pdf.js/images/toolbarButton-viewAttachments.svg new file mode 100644 index 00000000..ab73f6e6 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-viewAttachments.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-viewAttachments@2x.png b/static/pdf.js/images/toolbarButton-viewAttachments@2x.png deleted file mode 100644 index b979e523..00000000 Binary files a/static/pdf.js/images/toolbarButton-viewAttachments@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewLayers.svg b/static/pdf.js/images/toolbarButton-viewLayers.svg new file mode 100644 index 00000000..1d726682 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-viewLayers.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-viewOutline-rtl.png b/static/pdf.js/images/toolbarButton-viewOutline-rtl.png deleted file mode 100644 index aaa94302..00000000 Binary files a/static/pdf.js/images/toolbarButton-viewOutline-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewOutline-rtl@2x.png b/static/pdf.js/images/toolbarButton-viewOutline-rtl@2x.png deleted file mode 100644 index 3410f70d..00000000 Binary files a/static/pdf.js/images/toolbarButton-viewOutline-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewOutline.png b/static/pdf.js/images/toolbarButton-viewOutline.png deleted file mode 100644 index 976365a5..00000000 Binary files a/static/pdf.js/images/toolbarButton-viewOutline.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewOutline.svg b/static/pdf.js/images/toolbarButton-viewOutline.svg new file mode 100644 index 00000000..7ed1bd97 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-viewOutline.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-viewOutline@2x.png b/static/pdf.js/images/toolbarButton-viewOutline@2x.png deleted file mode 100644 index b6a197fd..00000000 Binary files a/static/pdf.js/images/toolbarButton-viewOutline@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewThumbnail.png b/static/pdf.js/images/toolbarButton-viewThumbnail.png deleted file mode 100644 index 584ba558..00000000 Binary files a/static/pdf.js/images/toolbarButton-viewThumbnail.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewThumbnail.svg b/static/pdf.js/images/toolbarButton-viewThumbnail.svg new file mode 100644 index 00000000..040d1232 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-viewThumbnail.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-viewThumbnail@2x.png b/static/pdf.js/images/toolbarButton-viewThumbnail@2x.png deleted file mode 100644 index fb7db938..00000000 Binary files a/static/pdf.js/images/toolbarButton-viewThumbnail@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-zoomIn.png b/static/pdf.js/images/toolbarButton-zoomIn.png deleted file mode 100644 index 513d081b..00000000 Binary files a/static/pdf.js/images/toolbarButton-zoomIn.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-zoomIn.svg b/static/pdf.js/images/toolbarButton-zoomIn.svg new file mode 100644 index 00000000..30ec51a2 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-zoomIn.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-zoomIn@2x.png b/static/pdf.js/images/toolbarButton-zoomIn@2x.png deleted file mode 100644 index d5d49d5f..00000000 Binary files a/static/pdf.js/images/toolbarButton-zoomIn@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-zoomOut.png b/static/pdf.js/images/toolbarButton-zoomOut.png deleted file mode 100644 index 156c26b9..00000000 Binary files a/static/pdf.js/images/toolbarButton-zoomOut.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-zoomOut.svg b/static/pdf.js/images/toolbarButton-zoomOut.svg new file mode 100644 index 00000000..f273b599 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-zoomOut.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-zoomOut@2x.png b/static/pdf.js/images/toolbarButton-zoomOut@2x.png deleted file mode 100644 index 959e1919..00000000 Binary files a/static/pdf.js/images/toolbarButton-zoomOut@2x.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-collapsed-rtl.png b/static/pdf.js/images/treeitem-collapsed-rtl.png deleted file mode 100644 index 1c8b9f70..00000000 Binary files a/static/pdf.js/images/treeitem-collapsed-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-collapsed-rtl@2x.png b/static/pdf.js/images/treeitem-collapsed-rtl@2x.png deleted file mode 100644 index 84279368..00000000 Binary files a/static/pdf.js/images/treeitem-collapsed-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-collapsed.png b/static/pdf.js/images/treeitem-collapsed.png deleted file mode 100644 index 06d4d376..00000000 Binary files a/static/pdf.js/images/treeitem-collapsed.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-collapsed.svg b/static/pdf.js/images/treeitem-collapsed.svg new file mode 100644 index 00000000..831cddfc --- /dev/null +++ b/static/pdf.js/images/treeitem-collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/pdf.js/images/treeitem-collapsed@2x.png b/static/pdf.js/images/treeitem-collapsed@2x.png deleted file mode 100644 index eec1e58c..00000000 Binary files a/static/pdf.js/images/treeitem-collapsed@2x.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-expanded.png b/static/pdf.js/images/treeitem-expanded.png deleted file mode 100644 index c8d55735..00000000 Binary files a/static/pdf.js/images/treeitem-expanded.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-expanded.svg b/static/pdf.js/images/treeitem-expanded.svg new file mode 100644 index 00000000..2d45f0c8 --- /dev/null +++ b/static/pdf.js/images/treeitem-expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/pdf.js/images/treeitem-expanded@2x.png b/static/pdf.js/images/treeitem-expanded@2x.png deleted file mode 100644 index 3b3b6103..00000000 Binary files a/static/pdf.js/images/treeitem-expanded@2x.png and /dev/null differ diff --git a/static/pdf.js/index.html b/static/pdf.js/index.html index 5b103831..aef63700 100644 --- a/static/pdf.js/index.html +++ b/static/pdf.js/index.html @@ -20,50 +20,55 @@ Adobe CMap resources are covered by their own copyright but the same license: See https://github.com/adobe-type-tools/cmap-resources --> - + - - - - - - - - - - + PDF.js viewer - - - + + + + + + - - - - - + - +
-
- - - +
+
+ + + + +
+
+ +
+
+
+ + +
@@ -73,80 +78,195 @@ See https://github.com/adobe-type-tools/cmap-resources
+
+
- + +
+ + + + +
+
+ + + + +
+ +
+ + +
+
+ + + + + + + + +
@@ -155,81 +275,86 @@ See https://github.com/adobe-type-tools/cmap-resources
-
- -
-
-
- - + + +
- + + + +
+ +
+ + - - +
- - - Current View - - -
- -
-
-
-
- -
- -
- - - +
+
+ +
+
+ + +
@@ -241,187 +366,147 @@ See https://github.com/adobe-type-tools/cmap-resources
- - - - - - -
- -
-