mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-03-22 12:33:11 -04:00
Compare commits
15 Commits
feat-lti-i
...
frontend-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20682a543a | ||
|
|
c8b47a7922 | ||
|
|
499196b0f6 | ||
|
|
374ae4de6e | ||
|
|
7a5fca6fd8 | ||
|
|
e9af15582f | ||
|
|
1b8e8aae6a | ||
|
|
df4b0422d5 | ||
|
|
0434f24691 | ||
|
|
c2043fafa1 | ||
|
|
9f9dd699b2 | ||
|
|
e2bc9399b9 | ||
|
|
45d94069b9 | ||
|
|
b7427869b6 | ||
|
|
11449c2187 |
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [7.6.0](https://github.com/mediacms-io/mediacms/compare/v7.5.0...v7.6.0) (2026-02-07)
|
||||
|
||||
### Features
|
||||
|
||||
* Create SECURITY.md ([#1485](https://github.com/mediacms-io/mediacms/issues/1485)) ([11449c2](https://github.com/mediacms-io/mediacms/commit/11449c2187d0f450b86915d88f92595a1825e4cf))
|
||||
|
||||
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)
|
||||
|
||||
### Features
|
||||
|
||||
23
HISTORY.md
Normal file
23
HISTORY.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# History
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### Features
|
||||
- Updates Python/Django requirements and Dockerfile to use latest 3.11 Python - https://github.com/mediacms-io/mediacms/pull/826/files. This update requires some manual steps, for existing (not new) installations. Check the update section under the [Admin docs](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#2-server-installation), either for single server or for Docker Compose installations
|
||||
- Upgrade postgres on Docker Compose - https://github.com/mediacms-io/mediacms/pull/749
|
||||
|
||||
### Fixes
|
||||
- video player options for HLS - https://github.com/mediacms-io/mediacms/pull/832
|
||||
- AVI videos not correctly recognised as videos - https://github.com/mediacms-io/mediacms/pull/833
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Fixes
|
||||
- Increase uwsgi buffer-size parameter. This prevents an error by uwsgi with large headers - [#5b60](https://github.com/mediacms-io/mediacms/commit/5b601698a41ad97f08c1830e14b1c18f73ab8315)
|
||||
- Fix issues with comments. These were not reported on the tracker but it is certain that they would not show comments on media files (non videos but also videos). Unfortunately this reverts work done with Timestamps on comments + Mentions on comments, more on PR [#802](https://github.com/mediacms-io/mediacms/pull/802)
|
||||
|
||||
### Features
|
||||
- Allow tags to contains other characters too, not only English alphabet ones [#801](https://github.com/mediacms-io/mediacms/pull/801)
|
||||
- Add simple cookie consent code [#799](https://github.com/mediacms-io/mediacms/pull/799)
|
||||
- Allow password reset & email verify pages on global login required [#790](https://github.com/mediacms-io/mediacms/pull/790)
|
||||
- Add api_url field to search api [#692](https://github.com/mediacms-io/mediacms/pull/692)
|
||||
@@ -108,7 +108,7 @@ There are two ways to run MediaCMS, through Docker Compose and through installin
|
||||
|
||||
## Technology
|
||||
|
||||
This software uses the following list of awesome technologies: Python, Django, Django Rest Framework, Celery, PostgreSQL, Redis, Nginx, Gunicorn, React, Fine Uploader, video.js, FFMPEG, Bento4
|
||||
This software uses the following list of awesome technologies: Python, Django, Django Rest Framework, Celery, PostgreSQL, Redis, Nginx, uWSGI, React, Fine Uploader, video.js, FFMPEG, Bento4
|
||||
|
||||
|
||||
## Who is using it
|
||||
|
||||
54
SECURITY.md
Normal file
54
SECURITY.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Security Policy
|
||||
|
||||
Thank you for helping improve the security of MediaCMS.
|
||||
We take security vulnerabilities seriously and appreciate responsible disclosure.
|
||||
|
||||
---
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in MediaCMS, **please do not open a public GitHub issue**.
|
||||
|
||||
Instead, report it using one of the following methods:
|
||||
|
||||
- **GitHub Security Advisories (preferred)**
|
||||
Use the "Report a vulnerability" feature in this repository.
|
||||
|
||||
- **Contact Form**
|
||||
Submit details via the official contact page:
|
||||
https://mediacms.io/contact/
|
||||
|
||||
Please include as much of the following information as possible:
|
||||
- Affected version(s)
|
||||
- Detailed description of the issue
|
||||
- Steps to reproduce (PoC if available)
|
||||
- Impact assessment (e.g. RCE, XSS, privilege escalation)
|
||||
- Any potential mitigations you are aware of
|
||||
|
||||
---
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are provided for the **latest stable release** of MediaCMS.
|
||||
Older versions may not receive security patches.
|
||||
|
||||
---
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
- We aim to acknowledge reports within **7 days**
|
||||
- We aim to provide a fix or mitigation within **90 days**, depending on severity
|
||||
- Please allow us time to investigate before any public disclosure
|
||||
|
||||
We follow responsible disclosure practices and will coordinate disclosure timelines when appropriate.
|
||||
|
||||
---
|
||||
|
||||
## Recognition
|
||||
|
||||
At this time, MediaCMS does not operate a formal bug bounty program.
|
||||
However, we are happy to acknowledge valid security reports in release notes or advisories (with your permission).
|
||||
|
||||
---
|
||||
|
||||
Thank you for helping keep MediaCMS secure.
|
||||
@@ -24,7 +24,6 @@ INSTALLED_APPS = [
|
||||
"actions.apps.ActionsConfig",
|
||||
"rbac.apps.RbacConfig",
|
||||
"identity_providers.apps.IdentityProvidersConfig",
|
||||
"lti.apps.LtiConfig",
|
||||
"debug_toolbar",
|
||||
"mptt",
|
||||
"crispy_forms",
|
||||
|
||||
@@ -300,7 +300,6 @@ INSTALLED_APPS = [
|
||||
"actions.apps.ActionsConfig",
|
||||
"rbac.apps.RbacConfig",
|
||||
"identity_providers.apps.IdentityProvidersConfig",
|
||||
"lti.apps.LtiConfig",
|
||||
"debug_toolbar",
|
||||
"mptt",
|
||||
"crispy_forms",
|
||||
@@ -556,7 +555,6 @@ DJANGO_ADMIN_URL = "admin/"
|
||||
USE_SAML = False
|
||||
USE_RBAC = False
|
||||
USE_IDENTITY_PROVIDERS = False
|
||||
USE_LTI = False # Enable LTI 1.3 integration
|
||||
JAZZMIN_UI_TWEAKS = {"theme": "flatly"}
|
||||
|
||||
USE_ROUNDED_CORNERS = True
|
||||
@@ -652,18 +650,3 @@ if USERS_NEEDS_TO_BE_APPROVED:
|
||||
)
|
||||
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||
MIDDLEWARE.insert(auth_index + 1, "cms.middleware.ApprovalMiddleware")
|
||||
|
||||
|
||||
# LTI 1.3 Integration Settings
|
||||
if USE_LTI:
|
||||
# Session timeout for LTI launches (seconds)
|
||||
LTI_SESSION_TIMEOUT = 3600 # 1 hour
|
||||
|
||||
# Cookie settings required for iframe embedding from LMS
|
||||
# IMPORTANT: Requires HTTPS to be enabled
|
||||
SESSION_COOKIE_SAMESITE = 'None'
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SAMESITE = 'None'
|
||||
CSRF_COOKIE_SECURE = True
|
||||
# SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||
# Consider using cached_db for reliability if sessions are lost between many LTI launches
|
||||
|
||||
@@ -25,7 +25,6 @@ urlpatterns = [
|
||||
re_path(r"^", include("files.urls")),
|
||||
re_path(r"^", include("users.urls")),
|
||||
re_path(r"^accounts/", include("allauth.urls")),
|
||||
re_path(r"^lti/", include("lti.urls")),
|
||||
re_path(r"^api-auth/", include("rest_framework.urls")),
|
||||
path(settings.DJANGO_ADMIN_URL, admin.site.urls),
|
||||
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||
|
||||
@@ -1 +1 @@
|
||||
VERSION = "7.ki"
|
||||
VERSION = "7.9"
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
# Use existing X-Forwarded-Proto from reverse proxy if present, otherwise use $scheme
|
||||
map $http_x_forwarded_proto $forwarded_proto {
|
||||
default $http_x_forwarded_proto;
|
||||
'' $scheme;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 ;
|
||||
|
||||
@@ -34,10 +28,7 @@ server {
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
|
||||
proxy_pass http://127.0.0.1:9000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
||||
include /etc/nginx/sites-enabled/uwsgi_params;
|
||||
uwsgi_pass 127.0.0.1:9000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ fi
|
||||
|
||||
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-available/default
|
||||
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-enabled/default
|
||||
cp deploy/docker/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
|
||||
cp deploy/docker/nginx.conf /etc/nginx/
|
||||
|
||||
#### Supervisord Configurations #####
|
||||
@@ -44,12 +45,12 @@ cp deploy/docker/nginx.conf /etc/nginx/
|
||||
cp deploy/docker/supervisord/supervisord-debian.conf /etc/supervisor/conf.d/supervisord-debian.conf
|
||||
|
||||
if [ X"$ENABLE_UWSGI" = X"yes" ] ; then
|
||||
echo "Enabling gunicorn app server"
|
||||
cp deploy/docker/supervisord/supervisord-gunicorn.conf /etc/supervisor/conf.d/supervisord-gunicorn.conf
|
||||
echo "Enabling uwsgi app server"
|
||||
cp deploy/docker/supervisord/supervisord-uwsgi.conf /etc/supervisor/conf.d/supervisord-uwsgi.conf
|
||||
fi
|
||||
|
||||
if [ X"$ENABLE_NGINX" = X"yes" ] ; then
|
||||
echo "Enabling nginx as gunicorn app proxy and media server"
|
||||
echo "Enabling nginx as uwsgi app proxy and media server"
|
||||
cp deploy/docker/supervisord/supervisord-nginx.conf /etc/supervisor/conf.d/supervisord-nginx.conf
|
||||
fi
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ else
|
||||
echo "There is no script $PRE_START_PATH"
|
||||
fi
|
||||
|
||||
# Start Supervisor, with Nginx and Gunicorn
|
||||
# Start Supervisor, with Nginx and uWSGI
|
||||
echo "Starting server using supervisord..."
|
||||
|
||||
exec /usr/bin/supervisord
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[program:gunicorn]
|
||||
command=/home/mediacms.io/bin/gunicorn cms.wsgi:application --workers=2 --threads=2 --worker-class=gthread --bind=127.0.0.1:9000 --user=www-data --group=www-data --timeout=120 --keep-alive=5 --max-requests=1000 --max-requests-jitter=50 --access-logfile=- --error-logfile=- --log-level=info --chdir=/home/mediacms.io/mediacms
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=100
|
||||
startinorder=true
|
||||
startsecs=0
|
||||
9
deploy/docker/supervisord/supervisord-uwsgi.conf
Normal file
9
deploy/docker/supervisord/supervisord-uwsgi.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
[program:uwsgi]
|
||||
command=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/docker/uwsgi.ini
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=100
|
||||
startinorder=true
|
||||
startsecs=0
|
||||
24
deploy/docker/uwsgi.ini
Normal file
24
deploy/docker/uwsgi.ini
Normal file
@@ -0,0 +1,24 @@
|
||||
[uwsgi]
|
||||
|
||||
chdir = /home/mediacms.io/mediacms/
|
||||
virtualenv = /home/mediacms.io
|
||||
module = cms.wsgi
|
||||
|
||||
uid=www-data
|
||||
gid=www-data
|
||||
|
||||
processes = 2
|
||||
threads = 2
|
||||
|
||||
master = true
|
||||
|
||||
socket = 127.0.0.1:9000
|
||||
|
||||
workers = 2
|
||||
|
||||
vacuum = true
|
||||
|
||||
hook-master-start = unix_signal:15 gracefully_kill_them_all
|
||||
need-app = true
|
||||
die-on-term = true
|
||||
buffer-size=32768
|
||||
16
deploy/docker/uwsgi_params
Normal file
16
deploy/docker/uwsgi_params
Normal file
@@ -0,0 +1,16 @@
|
||||
uwsgi_param QUERY_STRING $query_string;
|
||||
uwsgi_param REQUEST_METHOD $request_method;
|
||||
uwsgi_param CONTENT_TYPE $content_type;
|
||||
uwsgi_param CONTENT_LENGTH $content_length;
|
||||
|
||||
uwsgi_param REQUEST_URI $request_uri;
|
||||
uwsgi_param PATH_INFO $document_uri;
|
||||
uwsgi_param DOCUMENT_ROOT $document_root;
|
||||
uwsgi_param SERVER_PROTOCOL $server_protocol;
|
||||
uwsgi_param REQUEST_SCHEME $scheme;
|
||||
uwsgi_param HTTPS $https if_not_empty;
|
||||
|
||||
uwsgi_param REMOTE_ADDR $remote_addr;
|
||||
uwsgi_param REMOTE_PORT $remote_port;
|
||||
uwsgi_param SERVER_PORT $server_port;
|
||||
uwsgi_param SERVER_NAME $server_name;
|
||||
22
deploy/local_install/celery_beat.service
Normal file
22
deploy/local_install/celery_beat.service
Normal file
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=MediaCMS celery beat
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
WorkingDirectory=/home/mediacms.io/mediacms
|
||||
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
|
||||
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/beat%n.pid"
|
||||
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/beat%N.log"
|
||||
Environment=CELERYD_LOG_LEVEL="INFO"
|
||||
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms beat --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
|
||||
ExecStop=/bin/kill -s TERM $MAINPID
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
29
deploy/local_install/celery_long.service
Normal file
29
deploy/local_install/celery_long.service
Normal file
@@ -0,0 +1,29 @@
|
||||
[Unit]
|
||||
Description=MediaCMS celery long queue
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=www-data
|
||||
Group=www-data
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
WorkingDirectory=/home/mediacms.io/mediacms
|
||||
Environment=CELERYD_NODES="long1"
|
||||
Environment=CELERY_QUEUE="long_tasks"
|
||||
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
|
||||
Environment=CELERYD_MULTI="multi"
|
||||
Environment=CELERYD_OPTS="-Ofair --prefetch-multiplier=1"
|
||||
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
|
||||
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
|
||||
Environment=CELERYD_LOG_LEVEL="INFO"
|
||||
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
|
||||
|
||||
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
|
||||
|
||||
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
39
deploy/local_install/celery_short.service
Normal file
39
deploy/local_install/celery_short.service
Normal file
@@ -0,0 +1,39 @@
|
||||
[Unit]
|
||||
Description=MediaCMS celery short queue
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=www-data
|
||||
Group=www-data
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
WorkingDirectory=/home/mediacms.io/mediacms
|
||||
Environment=CELERYD_NODES="short1 short2"
|
||||
Environment=CELERY_QUEUE="short_tasks"
|
||||
# Absolute or relative path to the 'celery' command:
|
||||
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
|
||||
# App instance to use
|
||||
# comment out this line if you don't use an app
|
||||
# or fully qualified:
|
||||
#CELERY_APP="proj.tasks:app"
|
||||
# How to call manage.py
|
||||
Environment=CELERYD_MULTI="multi"
|
||||
# Extra command-line arguments to the worker
|
||||
Environment=CELERYD_OPTS="--soft-time-limit=300 -c10"
|
||||
# - %n will be replaced with the first part of the nodename.
|
||||
# - %I will be replaced with the current child process index
|
||||
# and is important when using the prefork pool to avoid race conditions.
|
||||
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
|
||||
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
|
||||
Environment=CELERYD_LOG_LEVEL="INFO"
|
||||
|
||||
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
|
||||
|
||||
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
|
||||
|
||||
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
13
deploy/local_install/dhparams.pem
Normal file
13
deploy/local_install/dhparams.pem
Normal file
@@ -0,0 +1,13 @@
|
||||
-----BEGIN DH PARAMETERS-----
|
||||
MIICCAKCAgEAo3MMiEY/fNbu+usIM0cDi6x8G3JBApv0Lswta4kiyedWT1WN51iQ
|
||||
9zhOFpmcu6517f/fR9MUdyhVKHxxSqWQTcmTEFtz4P3VLTS/W1N5VbKE2VEMLpIi
|
||||
wr350aGvV1Er0ujcp5n4O4h0I1tn4/fNyDe7+pHCdwM+hxe8hJ3T0/tKtad4fnIs
|
||||
WHDjl4f7m7KuFfheiK7Efb8MsT64HDDAYXn+INjtDZrbE5XPw20BqyWkrf07FcPx
|
||||
8o9GW50Ox7/FYq7jVMI/skEu0BRc8u6uUD9+UOuWUQpdeHeFcvLOgW53Z03XwWuX
|
||||
RXosUKzBPuGtUDAaKD/HsGW6xmGr2W9yRmu27jKpfYLUb/eWbbnRJwCw04LdzPqv
|
||||
jmtq02Gioo3lf5H5wYV9IYF6M8+q/slpbttsAcKERimD1273FBRt5VhSugkXWKjr
|
||||
XDhoXu6vZgj8Opei38qPa8pI1RUFoXHFlCe6WpZQmU8efL8gAMrJr9jUIY8eea1n
|
||||
u20t5B9ueb9JMjrNafcq6QkKhZLi6fRDDTUyeDvc0dN9R/3Yts97SXfdi1/lX7HS
|
||||
Ht4zXd5hEkvjo8GcnjsfZpAC39QfHWkDaeUGEqsl3jXjVMfkvoVY51OuokPWZzrJ
|
||||
M5+wyXNpfGbH67dPk7iHgN7VJvgX0SYscDPTtms50Vk7RwEzLeGuSHMCAQI=
|
||||
-----END DH PARAMETERS-----
|
||||
84
deploy/local_install/mediacms.io
Normal file
84
deploy/local_install/mediacms.io
Normal file
@@ -0,0 +1,84 @@
|
||||
server {
|
||||
listen 80 ;
|
||||
server_name localhost;
|
||||
|
||||
gzip on;
|
||||
access_log /var/log/nginx/mediacms.io.access.log;
|
||||
|
||||
error_log /var/log/nginx/mediacms.io.error.log warn;
|
||||
|
||||
# # redirect to https if logged in
|
||||
# if ($http_cookie ~* "sessionid") {
|
||||
# rewrite ^/(.*)$ https://localhost/$1 permanent;
|
||||
# }
|
||||
|
||||
# # redirect basic forms to https
|
||||
# location ~ (login|login_form|register|mail_password_form)$ {
|
||||
# rewrite ^/(.*)$ https://localhost/$1 permanent;
|
||||
# }
|
||||
|
||||
location /static {
|
||||
alias /home/mediacms.io/mediacms/static ;
|
||||
}
|
||||
|
||||
location /media/original {
|
||||
alias /home/mediacms.io/mediacms/media_files/original;
|
||||
}
|
||||
|
||||
location /media {
|
||||
alias /home/mediacms.io/mediacms/media_files ;
|
||||
}
|
||||
|
||||
location / {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
|
||||
include /etc/nginx/sites-enabled/uwsgi_params;
|
||||
uwsgi_pass 127.0.0.1:9000;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate_key /etc/letsencrypt/live/localhost/privkey.pem;
|
||||
ssl_certificate /etc/letsencrypt/live/localhost/fullchain.pem;
|
||||
ssl_dhparam /etc/nginx/dhparams/dhparams.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_ecdh_curve secp521r1:secp384r1;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
gzip on;
|
||||
access_log /var/log/nginx/mediacms.io.access.log;
|
||||
|
||||
error_log /var/log/nginx/mediacms.io.error.log warn;
|
||||
|
||||
location /static {
|
||||
alias /home/mediacms.io/mediacms/static ;
|
||||
}
|
||||
|
||||
location /media/original {
|
||||
alias /home/mediacms.io/mediacms/media_files/original;
|
||||
#auth_basic "auth protected area";
|
||||
#auth_basic_user_file /home/mediacms.io/mediacms/deploy/local_install/.htpasswd;
|
||||
}
|
||||
|
||||
location /media {
|
||||
alias /home/mediacms.io/mediacms/media_files ;
|
||||
}
|
||||
|
||||
location / {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
|
||||
include /etc/nginx/sites-enabled/uwsgi_params;
|
||||
uwsgi_pass 127.0.0.1:9000;
|
||||
}
|
||||
}
|
||||
58
deploy/local_install/mediacms.io_fullchain.pem
Normal file
58
deploy/local_install/mediacms.io_fullchain.pem
Normal file
@@ -0,0 +1,58 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFTjCCBDagAwIBAgISBNOUeDlerH9MkKmHLvZJeMYgMA0GCSqGSIb3DQEBCwUA
|
||||
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
|
||||
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDAzMTAxNzUxNDFaFw0y
|
||||
MDA2MDgxNzUxNDFaMBYxFDASBgNVBAMTC21lZGlhY21zLmlvMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAps5Jn18nW2tq/LYFDgQ1YZGLlpF/B2AAPvvH
|
||||
3yuD+AcT4skKdZouVL/a5pXrptuYL5lthO9dlcja2tuO2ltYrb7Dp01dAIFaJE8O
|
||||
DKd+Sv5wr8VWQZykqzMiMBgviml7TBvUHQjvCJg8UwmnN0XSUILCttd6u4qOzS7d
|
||||
lKMMsKpYzLhElBT0rzhhsWulDiy6aAZbMV95bfR74nIWsBJacy6jx3jvxAuvCtkB
|
||||
OVdOoVL6BPjDE3SNEk53bAZGIb5A9ri0O5jh/zBFT6tQSjUhAUTkmv9oZP547RnV
|
||||
fDj+rdvCVk/fE+Jno36mcT183Qd/Ty3fWuqFoM5g/luhnfvWEwIDAQABo4ICYDCC
|
||||
AlwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTd5EZBt74zu5XxT1uXQs6oM8qOuDAf
|
||||
BgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEw
|
||||
LgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcw
|
||||
LwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcv
|
||||
MBYGA1UdEQQPMA2CC21lZGlhY21zLmlvMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcG
|
||||
CysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5
|
||||
cHQub3JnMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHYAXqdz+d9WwOe1Nkh90Eng
|
||||
MnqRmgyEoRIShBh1loFxRVgAAAFwxcnL+AAABAMARzBFAiAb3yeBuW3j9MxcRc0T
|
||||
icUBvEa/rH7Fv2eB0oQlnZ1exQIhAPf+CtTXmzxoeT/BBiivj4AmGDsq4xWhe/U6
|
||||
BytYrKLeAHYAB7dcG+V9aP/xsMYdIxXHuuZXfFeUt2ruvGE6GmnTohwAAAFwxcnM
|
||||
HAAABAMARzBFAiAuP5gKyyaT0LVXxwjYD9zhezvxf4Icx0P9pk75c5ao+AIhAK0+
|
||||
fSJv+WTXciMT6gA1sk/tuCHuDFAuexSA/6TcRXcVMA0GCSqGSIb3DQEBCwUAA4IB
|
||||
AQCPCYBU4Q/ro2MUkjDPKGmeqdxQycS4R9WvKTG/nmoahKNg30bnLaDPUcpyMU2k
|
||||
sPDemdZ7uTGLZ3ZrlIva8DbrnJmrTPf9BMwaM6j+ZV/QhxvKZVIWkLkZrwiVI57X
|
||||
Ba+rs5IEB4oWJ0EBaeIrzeKG5zLMkRcIdE4Hlhuwu3zGG56c+wmAPuvpIDlYoO6o
|
||||
W22xRdxoTIHBvkzwonpVYUaRcaIw+48xnllxh1dHO+X69DT45wlF4tKveOUi+L50
|
||||
4GWJ8Vjv7Fot/WNHEM4Mnmw0jHj9TPkIZKnPNRMdHmJ5CF/FJFDiptOeuzbfohG+
|
||||
mdvuInb8JDc0XBE99Gf/S4/y
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
|
||||
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
|
||||
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
|
||||
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
|
||||
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
|
||||
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
|
||||
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
|
||||
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
|
||||
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
|
||||
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
|
||||
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
|
||||
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
|
||||
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
|
||||
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
|
||||
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
|
||||
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
|
||||
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
|
||||
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
|
||||
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
|
||||
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
|
||||
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
|
||||
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
|
||||
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
|
||||
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
|
||||
-----END CERTIFICATE-----
|
||||
28
deploy/local_install/mediacms.io_privkey.pem
Normal file
28
deploy/local_install/mediacms.io_privkey.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmzkmfXydba2r8
|
||||
tgUOBDVhkYuWkX8HYAA++8ffK4P4BxPiyQp1mi5Uv9rmleum25gvmW2E712VyNra
|
||||
247aW1itvsOnTV0AgVokTw4Mp35K/nCvxVZBnKSrMyIwGC+KaXtMG9QdCO8ImDxT
|
||||
Cac3RdJQgsK213q7io7NLt2UowywqljMuESUFPSvOGGxa6UOLLpoBlsxX3lt9Hvi
|
||||
chawElpzLqPHeO/EC68K2QE5V06hUvoE+MMTdI0STndsBkYhvkD2uLQ7mOH/MEVP
|
||||
q1BKNSEBROSa/2hk/njtGdV8OP6t28JWT98T4mejfqZxPXzdB39PLd9a6oWgzmD+
|
||||
W6Gd+9YTAgMBAAECggEADnEJuryYQbf5GUwBAAepP3tEZJLQNqk/HDTcRxwTXuPt
|
||||
+tKBD1F79WZu40vTjSyx7l0QOFQo/BDZsd0Ubx89fD1p3xA5nxOT5FTb2IifzIpe
|
||||
4zjokOGo+BGDQjq10vvy6tH1+VWOrGXRwzawvX5UCRhpFz9sptQGLQmDsZy0Oo9B
|
||||
LtavYVUqsbyqRWlzaclHgbythegIACWkqcalOzOtx+l6TGBRjej+c7URcwYBfr7t
|
||||
XTAzbP+vnpaJovZyZT1eekr0OLzMpnjx4HvRvzL+NxauRpn6KfabsTfZlk8nrs4I
|
||||
UdSjeukj1Iz8rGQilHdN/4dVJ3KzrlHVkVTBSjmMUQKBgQDaVXZnhAScfdiKeZbO
|
||||
rdUAWcnwfkDghtRuAmzHaRM/FhFBEoVhdSbBuu+OUyBnIw/Ra4o2ePuEBcKIUiQO
|
||||
w2tnE1CY5PPAcjw+OCSpvzy5xxjaqaRbm9BJp3FTeEYGLXERnchPpHg/NpexuF22
|
||||
QOJ+FrysPyNMxuQp47ZwO9WT3QKBgQDDlSGjq/eeWxemwf7ZqMVlRyqsdJsgnCew
|
||||
DkC62IGiYCBDfeEmndN+vcA/uzJHYV4iXiqS3aYJCWGaZFMhdIhIn5MgULvO1j5G
|
||||
u/MxuzaaNPz22FlNCWTLBw4T1HOOvyTL+nLtZDKJ/BHxgHCmur1kiGvvZWrcCthD
|
||||
afLEmseqrwKBgBuLZKCymxJTHhp6NHhmndSpfzyD8RNibzJhw+90ZiUzV4HqIEGn
|
||||
Ufhm6Qn/mrroRXqaIpm0saZ6Q4yHMF1cchRS73wahlXlE4yV8KopojOd1pjfhgi4
|
||||
o5JnOXjaV5s36GfcjATgLvtqm8CkDc6MaQaXP75LSNzKysYuIDoQkmVRAoGAAghF
|
||||
rja2Pv4BU+lGJarcSj4gEmSvy/nza5/qSka/qhlHnIvtUAJp1TJRkhf24MkBOmgy
|
||||
Fw6YkBV53ynVt05HsEGAPOC54t9VDFUdpNGmMpoEWuhKnUNQuc9b9RbLEJup3TjA
|
||||
Avl8kPR+lzzXbtQX7biBLp6mKp0uPB0YubRGCN8CgYA0JMxK0x38Q2x3AQVhOmZh
|
||||
YubtIa0JqVJhvpweOCFnkq3ebBpLsWYwiLTn86vuD0jupe5M3sxtefjkJmAKd8xY
|
||||
aBU7QWhjh1fX4mzmggnbjcrIFbkIHsxwMeg567U/4AGxOOUsv9QUn37mqycqRKEn
|
||||
YfUyYNLM6F3MmQAOs2kaHw==
|
||||
-----END PRIVATE KEY-----
|
||||
13
deploy/local_install/mediacms.service
Normal file
13
deploy/local_install/mediacms.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=MediaCMS uwsgi
|
||||
|
||||
[Service]
|
||||
ExecStart=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/local_install/uwsgi.ini
|
||||
ExecStop=/usr/bin/killall -9 uwsgi
|
||||
RestartSec=3
|
||||
#ExecRestart=killall -9 uwsgi; sleep 5; /home/sss/bin/uwsgi --ini /home/sss/wordgames/uwsgi.ini
|
||||
Restart=always
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
7
deploy/local_install/mediacms_logrorate
Normal file
7
deploy/local_install/mediacms_logrorate
Normal file
@@ -0,0 +1,7 @@
|
||||
/home/mediacms.io/mediacms/logs/*.log {
|
||||
weekly
|
||||
missingok
|
||||
rotate 7
|
||||
compress
|
||||
notifempty
|
||||
}
|
||||
38
deploy/local_install/nginx.conf
Normal file
38
deploy/local_install/nginx.conf
Normal file
@@ -0,0 +1,38 @@
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 10240;
|
||||
}
|
||||
|
||||
worker_rlimit_nofile 20000; #each connection needs a filehandle (or 2 if you are proxying)
|
||||
http {
|
||||
proxy_connect_timeout 75;
|
||||
proxy_read_timeout 12000;
|
||||
client_max_body_size 5800M;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 10;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
log_format compression '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
|
||||
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
|
||||
34
deploy/local_install/selinux-mediacms.te
Normal file
34
deploy/local_install/selinux-mediacms.te
Normal file
@@ -0,0 +1,34 @@
|
||||
module selinux-mediacms 1.0;
|
||||
|
||||
require {
|
||||
type init_t;
|
||||
type var_t;
|
||||
type redis_port_t;
|
||||
type postgresql_port_t;
|
||||
type httpd_t;
|
||||
type httpd_sys_content_t;
|
||||
type httpd_sys_rw_content_t;
|
||||
class file { append create execute execute_no_trans getattr ioctl lock open read rename setattr unlink write };
|
||||
class dir { add_name remove_name rmdir };
|
||||
class tcp_socket name_connect;
|
||||
class lnk_file read;
|
||||
}
|
||||
|
||||
#============= httpd_t ==============
|
||||
|
||||
allow httpd_t var_t:file { getattr open read };
|
||||
|
||||
#============= init_t ==============
|
||||
allow init_t postgresql_port_t:tcp_socket name_connect;
|
||||
|
||||
allow init_t redis_port_t:tcp_socket name_connect;
|
||||
|
||||
allow init_t httpd_sys_content_t:dir rmdir;
|
||||
|
||||
allow init_t httpd_sys_content_t:file { append create execute execute_no_trans ioctl lock open read rename setattr unlink write };
|
||||
|
||||
allow init_t httpd_sys_content_t:lnk_file read;
|
||||
|
||||
allow init_t httpd_sys_rw_content_t:dir { add_name remove_name rmdir };
|
||||
|
||||
allow init_t httpd_sys_rw_content_t:file { create ioctl lock open read setattr unlink write };
|
||||
27
deploy/local_install/uwsgi.ini
Normal file
27
deploy/local_install/uwsgi.ini
Normal file
@@ -0,0 +1,27 @@
|
||||
[uwsgi]
|
||||
|
||||
chdir = /home/mediacms.io/mediacms/
|
||||
virtualenv = /home/mediacms.io
|
||||
module = cms.wsgi
|
||||
|
||||
uid=www-data
|
||||
gid=www-data
|
||||
|
||||
processes = 2
|
||||
threads = 2
|
||||
|
||||
master = true
|
||||
|
||||
socket = 127.0.0.1:9000
|
||||
#socket = /home/mediacms.io/mediacms/deploy/uwsgi.sock
|
||||
|
||||
|
||||
workers = 2
|
||||
|
||||
|
||||
vacuum = true
|
||||
|
||||
logto = /home/mediacms.io/mediacms/logs/errorlog.txt
|
||||
|
||||
disable-logging = true
|
||||
buffer-size=32768
|
||||
16
deploy/local_install/uwsgi_params
Normal file
16
deploy/local_install/uwsgi_params
Normal file
@@ -0,0 +1,16 @@
|
||||
uwsgi_param QUERY_STRING $query_string;
|
||||
uwsgi_param REQUEST_METHOD $request_method;
|
||||
uwsgi_param CONTENT_TYPE $content_type;
|
||||
uwsgi_param CONTENT_LENGTH $content_length;
|
||||
|
||||
uwsgi_param REQUEST_URI $request_uri;
|
||||
uwsgi_param PATH_INFO $document_uri;
|
||||
uwsgi_param DOCUMENT_ROOT $document_root;
|
||||
uwsgi_param SERVER_PROTOCOL $server_protocol;
|
||||
uwsgi_param REQUEST_SCHEME $scheme;
|
||||
uwsgi_param HTTPS $https if_not_empty;
|
||||
|
||||
uwsgi_param REMOTE_ADDR $remote_addr;
|
||||
uwsgi_param REMOTE_PORT $remote_port;
|
||||
uwsgi_param SERVER_PORT $server_port;
|
||||
uwsgi_param SERVER_NAME $server_name;
|
||||
@@ -23,7 +23,7 @@ and will start all services required for MediaCMS, as Celery/Redis for asynchron
|
||||
For Django, the changes from the image produced by docker-compose.yaml are these:
|
||||
|
||||
* Django runs in debug mode, with `python manage.py runserver`
|
||||
* gunicorn and nginx are not run
|
||||
* uwsgi and nginx are not run
|
||||
* Django runs in Debug mode, with Debug Toolbar
|
||||
* Static files (js/css) are loaded from static/ folder
|
||||
* corsheaders is installed and configured to allow all origins
|
||||
|
||||
@@ -65,7 +65,6 @@ class CategoryAdminForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
# LTI fields will be shown as read-only when USE_LTI is enabled
|
||||
fields = '__all__'
|
||||
|
||||
def clean(self):
|
||||
@@ -136,7 +135,7 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ["title", "user", "add_date", "media_count"]
|
||||
list_filter = []
|
||||
ordering = ("-add_date",)
|
||||
readonly_fields = ("user", "media_count", "lti_platform", "lti_context_id")
|
||||
readonly_fields = ("user", "media_count")
|
||||
change_form_template = 'admin/files/category/change_form.html'
|
||||
|
||||
def get_list_filter(self, request):
|
||||
@@ -146,8 +145,6 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||
list_filter.insert(0, "is_rbac_category")
|
||||
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
|
||||
list_filter.insert(-1, "identity_provider")
|
||||
if getattr(settings, 'USE_LTI', False):
|
||||
list_filter.append("is_lms_course")
|
||||
|
||||
return list_filter
|
||||
|
||||
@@ -157,8 +154,6 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display.insert(-1, "is_rbac_category")
|
||||
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
|
||||
list_display.insert(-1, "identity_provider")
|
||||
if getattr(settings, 'USE_LTI', False):
|
||||
list_display.insert(-1, "is_lms_course")
|
||||
|
||||
return list_display
|
||||
|
||||
@@ -172,14 +167,6 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||
),
|
||||
]
|
||||
|
||||
additional_fieldsets = []
|
||||
|
||||
if getattr(settings, 'USE_LTI', False):
|
||||
lti_fieldset = [
|
||||
('LTI Integration', {'fields': ['lti_platform', 'lti_context_id'], 'classes': ['tab'], 'description': 'LTI/LMS integration settings (automatically managed by LTI provisioning)'}),
|
||||
]
|
||||
additional_fieldsets.extend(lti_fieldset)
|
||||
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_fieldset = [
|
||||
('RBAC Settings', {'fields': ['is_rbac_category'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}),
|
||||
@@ -190,9 +177,9 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||
('RBAC Settings', {'fields': ['is_rbac_category', 'identity_provider'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}),
|
||||
('Group Access', {'fields': ['rbac_groups'], 'description': 'Select the Groups that have access to category'}),
|
||||
]
|
||||
additional_fieldsets.extend(rbac_fieldset)
|
||||
|
||||
return basic_fieldset + additional_fieldsets
|
||||
return basic_fieldset + rbac_fieldset
|
||||
else:
|
||||
return basic_fieldset
|
||||
|
||||
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
|
||||
@@ -64,10 +64,4 @@ def stuff(request):
|
||||
if request.user.is_superuser:
|
||||
ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL
|
||||
|
||||
if getattr(settings, 'USE_LTI', False):
|
||||
lti_session = request.session.get('lti_session')
|
||||
|
||||
if lti_session and request.user.is_authenticated:
|
||||
ret['lti_session'] = lti_session
|
||||
|
||||
return ret
|
||||
|
||||
@@ -127,7 +127,6 @@ class MediaPublishForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
self.request = kwargs.pop('request', None)
|
||||
super(MediaPublishForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.has_custom_permissions = self.instance.permissions.exists() if self.instance.pk else False
|
||||
@@ -170,16 +169,6 @@ class MediaPublishForm(forms.ModelForm):
|
||||
|
||||
self.fields['category'].queryset = Category.objects.filter(id__in=combined_category_ids).order_by('title')
|
||||
|
||||
# Filter for LMS courses only when in embed mode
|
||||
if self.request and 'category' in self.fields:
|
||||
is_embed_mode = self._check_embed_mode()
|
||||
if is_embed_mode:
|
||||
current_queryset = self.fields['category'].queryset
|
||||
self.fields['category'].queryset = current_queryset.filter(is_lms_course=True)
|
||||
self.fields['category'].label = 'Course'
|
||||
self.fields['category'].help_text = 'Media can be shared with one or more courses'
|
||||
self.fields['category'].widget.is_lms_mode = True
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = True
|
||||
self.helper.form_class = 'post-form'
|
||||
@@ -197,22 +186,6 @@ class MediaPublishForm(forms.ModelForm):
|
||||
|
||||
self.helper.layout.append(FormActions(Submit('submit', 'Publish Media', css_class='primaryAction')))
|
||||
|
||||
def _check_embed_mode(self):
|
||||
"""Check if the current request is in embed mode"""
|
||||
if not self.request:
|
||||
return False
|
||||
|
||||
# Check query parameter
|
||||
mode = self.request.GET.get('mode', '')
|
||||
if mode == 'lms_embed_mode':
|
||||
return True
|
||||
|
||||
# Check session storage
|
||||
if self.request.session.get('lms_embed_mode') == 'true':
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
state = cleaned_data.get("state")
|
||||
|
||||
@@ -965,13 +965,3 @@ def get_alphanumeric_only(string):
|
||||
"""
|
||||
string = "".join([char for char in string if char.isalnum()])
|
||||
return string.lower()
|
||||
|
||||
|
||||
def get_alphanumeric_and_spaces(string):
|
||||
"""Returns a query that contains only alphanumeric characters and spaces
|
||||
This include characters other than the English alphabet too
|
||||
"""
|
||||
string = "".join([char for char in string if char.isalnum() or char.isspace()])
|
||||
# Replace multiple spaces with single space and strip
|
||||
string = " ".join(string.split())
|
||||
return string
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-12-29 16:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('files', '0014_alter_subtitle_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='is_lms_course',
|
||||
field=models.BooleanField(db_index=True, default=False, help_text='Whether this category represents an LMS course'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='lti_context_id',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='LTI context ID from platform', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-12-29 16:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('files', '0015_category_is_lms_course_category_lti_context_id'),
|
||||
('lti', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='lti_platform',
|
||||
field=models.ForeignKey(
|
||||
blank=True, help_text='LTI Platform if this is an LTI course', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='categories', to='lti.ltiplatform'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -47,13 +47,6 @@ class Category(models.Model):
|
||||
verbose_name='IDP Config Name',
|
||||
)
|
||||
|
||||
# LTI/LMS integration fields
|
||||
is_lms_course = models.BooleanField(default=False, db_index=True, help_text='Whether this category represents an LMS course')
|
||||
|
||||
lti_platform = models.ForeignKey('lti.LTIPlatform', blank=True, null=True, on_delete=models.SET_NULL, related_name='categories', help_text='LTI Platform if this is an LTI course')
|
||||
|
||||
lti_context_id = models.CharField(max_length=255, blank=True, db_index=True, help_text='LTI context ID from platform')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@@ -144,7 +137,7 @@ class Tag(models.Model):
|
||||
return True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.title = helpers.get_alphanumeric_and_spaces(self.title)
|
||||
self.title = helpers.get_alphanumeric_only(self.title)
|
||||
self.title = self.title[:100]
|
||||
super(Tag, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -352,11 +352,20 @@ class Media(models.Model):
|
||||
# first get anything interesting out of the media
|
||||
# that needs to be search able
|
||||
|
||||
a_tags = ""
|
||||
a_tags = b_tags = ""
|
||||
if self.id:
|
||||
a_tags = " ".join([tag.title for tag in self.tags.all()])
|
||||
b_tags = " ".join([tag.title.replace("-", " ") for tag in self.tags.all()])
|
||||
|
||||
items = [self.friendly_token, self.title, self.user.username, self.user.email, self.user.name, self.description, a_tags]
|
||||
items = [
|
||||
self.title,
|
||||
self.user.username,
|
||||
self.user.email,
|
||||
self.user.name,
|
||||
self.description,
|
||||
a_tags,
|
||||
b_tags,
|
||||
]
|
||||
|
||||
for subtitle in self.subtitles.all():
|
||||
items.append(subtitle.subtitle_text)
|
||||
@@ -736,7 +745,7 @@ class Media(models.Model):
|
||||
|
||||
ret = []
|
||||
for cat in self.category.all():
|
||||
ret.append({"title": cat.title, "url": cat.get_absolute_url(), "is_lms_course": cat.is_lms_course})
|
||||
ret.append({"title": cat.title, "url": cat.get_absolute_url()})
|
||||
return ret
|
||||
|
||||
@property
|
||||
|
||||
@@ -226,7 +226,6 @@ class CategorySerializer(serializers.ModelSerializer):
|
||||
"media_count",
|
||||
"user",
|
||||
"thumbnail_url",
|
||||
"is_lms_course",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ urlpatterns = [
|
||||
views.trim_video,
|
||||
),
|
||||
re_path(r"^api/v1/categories$", views.CategoryList.as_view()),
|
||||
re_path(r"^api/v1/categories/contributor$", views.CategoryListContributor.as_view()),
|
||||
re_path(r"^api/v1/tags$", views.TagList.as_view()),
|
||||
re_path(r"^api/v1/comments$", views.CommentList.as_view()),
|
||||
re_path(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Import all views for backward compatibility
|
||||
|
||||
from .auth import custom_login_view, saml_metadata # noqa: F401
|
||||
from .categories import CategoryList, CategoryListContributor, TagList # noqa: F401
|
||||
from .categories import CategoryList, TagList # noqa: F401
|
||||
from .comments import CommentDetail, CommentList # noqa: F401
|
||||
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
|
||||
from .media import MediaActions # noqa: F401
|
||||
|
||||
@@ -43,49 +43,6 @@ class CategoryList(APIView):
|
||||
return Response(ret)
|
||||
|
||||
|
||||
class CategoryListContributor(APIView):
|
||||
"""List categories where user has contributor access"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
name='lms_courses_only',
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
in_=openapi.IN_QUERY,
|
||||
description='Filter to show only LMS courses (categories with is_lms_course=True)',
|
||||
),
|
||||
],
|
||||
tags=['Categories'],
|
||||
operation_summary='Lists Categories for Contributors',
|
||||
operation_description='Lists all categories where the user has contributor access',
|
||||
responses={
|
||||
200: openapi.Response('response description', CategorySerializer),
|
||||
},
|
||||
)
|
||||
def get(self, request, format=None):
|
||||
if not request.user.is_authenticated:
|
||||
return Response([])
|
||||
|
||||
categories = Category.objects.none()
|
||||
|
||||
# Filter for LMS courses only if requested
|
||||
lms_courses_only = request.GET.get('lms_courses_only', '').lower() in ['true', '1', 'yes']
|
||||
if lms_courses_only:
|
||||
categories = categories.filter(is_lms_course=True)
|
||||
else:
|
||||
categories = Category.objects.filter(is_rbac_category=False).prefetch_related("user")
|
||||
|
||||
# Get RBAC categories where user has contributor access
|
||||
if getattr(settings, 'USE_RBAC', False):
|
||||
rbac_categories = request.user.get_rbac_categories_as_contributor()
|
||||
categories = categories.union(rbac_categories)
|
||||
|
||||
categories = categories.order_by("title")
|
||||
|
||||
serializer = CategorySerializer(categories, many=True, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class TagList(APIView):
|
||||
"""List tags"""
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ class MediaList(APIView):
|
||||
rbac_categories = request.user.get_rbac_categories_as_member()
|
||||
conditions |= Q(category__in=rbac_categories)
|
||||
|
||||
media = base_queryset.filter(conditions).exclude(user=request.user).distinct()
|
||||
media = base_queryset.filter(conditions).distinct()
|
||||
elif author_param:
|
||||
user_queryset = User.objects.all()
|
||||
user = get_object_or_404(user_queryset, username=author_param)
|
||||
@@ -574,31 +574,12 @@ class MediaBulkUserActions(APIView):
|
||||
|
||||
elif action == "add_to_category":
|
||||
category_uids = request.data.get('category_uids', [])
|
||||
lti_context_id = request.data.get('lti_context_id')
|
||||
|
||||
if not category_uids and not lti_context_id:
|
||||
return Response({"detail": "category_uids or lti_context_id is required for add_to_category action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
categories = Category.objects.none()
|
||||
|
||||
# Prioritize category_uids
|
||||
if category_uids:
|
||||
categories = Category.objects.filter(uid__in=category_uids)
|
||||
elif lti_context_id:
|
||||
# Filter categories by lti_context_id and ensure they ARE RBAC categories
|
||||
potential_categories = Category.objects.filter(lti_context_id=lti_context_id, is_rbac_category=True)
|
||||
|
||||
# Check user access (must have contributor access)
|
||||
valid_category_ids = []
|
||||
for cat in potential_categories:
|
||||
if request.user.has_contributor_access_to_category(cat):
|
||||
valid_category_ids.append(cat.id)
|
||||
|
||||
if valid_category_ids:
|
||||
categories = Category.objects.filter(id__in=valid_category_ids)
|
||||
if not category_uids:
|
||||
return Response({"detail": "category_uids is required for add_to_category action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
categories = Category.objects.filter(uid__in=category_uids)
|
||||
if not categories:
|
||||
return Response({"detail": "No matching categories found or access denied"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
added_count = 0
|
||||
for category in categories:
|
||||
@@ -716,9 +697,12 @@ class MediaDetail(APIView):
|
||||
return media
|
||||
|
||||
serializer = SingleMediaSerializer(media, context={"request": request})
|
||||
related_media = show_related_media(media, request=request, limit=100)
|
||||
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
|
||||
related_media = related_media_serializer.data
|
||||
if media.state == "private":
|
||||
related_media = []
|
||||
else:
|
||||
related_media = show_related_media(media, request=request, limit=100)
|
||||
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
|
||||
related_media = related_media_serializer.data
|
||||
ret = serializer.data
|
||||
|
||||
# update rattings info with user specific ratings
|
||||
|
||||
@@ -24,7 +24,7 @@ from ..forms import (
|
||||
WhisperSubtitlesForm,
|
||||
)
|
||||
from ..frontend_translations import translate_string
|
||||
from ..helpers import get_alphanumeric_and_spaces
|
||||
from ..helpers import get_alphanumeric_only
|
||||
from ..methods import (
|
||||
can_transcribe_video,
|
||||
create_video_trim_request,
|
||||
@@ -310,8 +310,8 @@ def edit_media(request):
|
||||
media.tags.remove(tag)
|
||||
if form.cleaned_data.get("new_tags"):
|
||||
for tag in form.cleaned_data.get("new_tags").split(","):
|
||||
tag = get_alphanumeric_and_spaces(tag)
|
||||
tag = tag[:100]
|
||||
tag = get_alphanumeric_only(tag)
|
||||
tag = tag[:99]
|
||||
if tag:
|
||||
try:
|
||||
tag = Tag.objects.get(title=tag)
|
||||
@@ -350,13 +350,13 @@ def publish_media(request):
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
|
||||
if request.method == "POST":
|
||||
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media, request=request)
|
||||
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media)
|
||||
if form.is_valid():
|
||||
media = form.save()
|
||||
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
|
||||
return HttpResponseRedirect(media.get_absolute_url())
|
||||
else:
|
||||
form = MediaPublishForm(request.user, instance=media, request=request)
|
||||
form = MediaPublishForm(request.user, instance=media)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
||||
@@ -3,8 +3,6 @@ import json
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from .models import Category
|
||||
|
||||
|
||||
class CategoryModalWidget(forms.SelectMultiple):
|
||||
"""Two-panel category selector with modal"""
|
||||
@@ -14,42 +12,28 @@ class CategoryModalWidget(forms.SelectMultiple):
|
||||
js = ('js/category_modal.js',)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
is_lms_mode = getattr(self, 'is_lms_mode', False)
|
||||
|
||||
# Get all categories as JSON
|
||||
categories = []
|
||||
for opt_value, opt_label in self.choices:
|
||||
if opt_value: # Skip empty choice
|
||||
# Extract the actual ID value from ModelChoiceIteratorValue if needed
|
||||
category_id = opt_value.value if hasattr(opt_value, 'value') else opt_value
|
||||
|
||||
# Get is_lms_course info from the Category object
|
||||
try:
|
||||
cat_obj = Category.objects.get(id=category_id)
|
||||
categories.append({'id': str(category_id), 'title': str(opt_label), 'is_lms_course': cat_obj.is_lms_course})
|
||||
except Category.DoesNotExist:
|
||||
categories.append({'id': str(category_id), 'title': str(opt_label), 'is_lms_course': False})
|
||||
categories.append({'id': str(opt_value), 'title': str(opt_label)})
|
||||
|
||||
all_categories_json = json.dumps(categories)
|
||||
selected_ids_json = json.dumps([str(v) for v in (value or [])])
|
||||
lms_mode_json = json.dumps(is_lms_mode)
|
||||
|
||||
search_placeholder = "Search courses..." if is_lms_mode else "Search categories..."
|
||||
selected_header = "Selected Courses" if is_lms_mode else "Selected Categories"
|
||||
|
||||
html = f'''<div class="category-widget" data-name="{name}">
|
||||
<div class="category-content">
|
||||
<div class="category-panel">
|
||||
<input type="text" class="category-search" placeholder="{search_placeholder}">
|
||||
<input type="text" class="category-search" placeholder="Search categories...">
|
||||
<div class="category-list scrollable" data-panel="left"></div>
|
||||
</div>
|
||||
<div class="category-panel">
|
||||
<h3>{selected_header}</h3>
|
||||
<h3>Selected Categories</h3>
|
||||
<div class="category-list scrollable" data-panel="right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden-inputs"></div>
|
||||
<script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json},"lms_mode":{lms_mode_json}}}</script>
|
||||
<script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json}}}</script>
|
||||
</div>'''
|
||||
|
||||
return mark_safe(html)
|
||||
|
||||
5
frontend/.vscode/settings.json
vendored
5
frontend/.vscode/settings.json
vendored
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
"editor.formatOnSave": true,
|
||||
"prettier.configPath": "../.prettierrc"
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@ module.exports = {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
},
|
||||
collectCoverageFrom: ['src/**'],
|
||||
collectCoverageFrom: ['src/**', '!src/static/lib/**'],
|
||||
};
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"@babel/core": "^7.26.9",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@types/flux": "^3.1.15",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './BulkActionCategoryModal.scss';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
|
||||
|
||||
interface Category {
|
||||
title: string;
|
||||
@@ -25,7 +24,6 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
|
||||
onError,
|
||||
csrfToken,
|
||||
}) => {
|
||||
const isLmsMode = inEmbeddedApp();
|
||||
const [existingCategories, setExistingCategories] = useState<Category[]>([]);
|
||||
const [allCategories, setAllCategories] = useState<Category[]>([]);
|
||||
const [categoriesToAdd, setCategoriesToAdd] = useState<Category[]>([]);
|
||||
@@ -68,27 +66,20 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
|
||||
const existingData = await existingResponse.json();
|
||||
const existing = existingData.results || [];
|
||||
|
||||
// Fetch all categories (or LMS courses only in embed mode)
|
||||
const categoriesUrl = isLmsMode
|
||||
? '/api/v1/categories/contributor?lms_courses_only=true'
|
||||
: '/api/v1/categories';
|
||||
const allResponse = await fetch(categoriesUrl);
|
||||
// Fetch all categories
|
||||
const allResponse = await fetch('/api/v1/categories');
|
||||
if (!allResponse.ok) {
|
||||
throw new Error(isLmsMode ? translateString('Failed to fetch courses') : translateString('Failed to fetch all categories'));
|
||||
throw new Error(translateString('Failed to fetch all categories'));
|
||||
}
|
||||
|
||||
const allData = await allResponse.json();
|
||||
const all = allData.results || allData;
|
||||
|
||||
// In LMS mode, filter existing to only show LMS course categories
|
||||
const allUids = new Set(all.map((c: Category) => c.uid));
|
||||
const filteredExisting = isLmsMode ? existing.filter((c: Category) => allUids.has(c.uid)) : existing;
|
||||
|
||||
setExistingCategories(filteredExisting);
|
||||
setExistingCategories(existing);
|
||||
setAllCategories(all);
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
onError(isLmsMode ? translateString('Failed to load courses') : translateString('Failed to load categories'));
|
||||
onError(translateString('Failed to load categories'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -135,7 +126,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
|
||||
});
|
||||
|
||||
if (!addResponse.ok) {
|
||||
throw new Error(isLmsMode ? translateString('Failed to add courses') : translateString('Failed to add categories'));
|
||||
throw new Error(translateString('Failed to add categories'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,15 +147,15 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
|
||||
});
|
||||
|
||||
if (!removeResponse.ok) {
|
||||
throw new Error(isLmsMode ? translateString('Failed to remove courses') : translateString('Failed to remove categories'));
|
||||
throw new Error(translateString('Failed to remove categories'));
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess(isLmsMode ? translateString('Successfully updated courses') : translateString('Successfully updated categories'));
|
||||
onSuccess(translateString('Successfully updated categories'));
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
console.error('Error processing categories:', error);
|
||||
onError(isLmsMode ? translateString('Failed to update courses. Please try again.') : translateString('Failed to update categories. Please try again.'));
|
||||
onError(translateString('Failed to update categories. Please try again.'));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
@@ -193,7 +184,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
|
||||
<div className="category-modal-overlay">
|
||||
<div className="category-modal">
|
||||
<div className="category-modal-header">
|
||||
<h2>{isLmsMode ? translateString('Share with Course') : translateString('Add / Remove from Categories')}</h2>
|
||||
<h2>{translateString('Add / Remove from Categories')}</h2>
|
||||
<button className="category-modal-close" onClick={onCancel}>
|
||||
×
|
||||
</button>
|
||||
@@ -201,14 +192,14 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
|
||||
|
||||
<div className="category-modal-content">
|
||||
<div className="category-panel">
|
||||
<h3>{isLmsMode ? translateString('Courses') : translateString('Categories')}</h3>
|
||||
<h3>{translateString('Categories')}</h3>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
|
||||
<div className="loading-message">{translateString('Loading categories...')}</div>
|
||||
) : (
|
||||
<div className="category-list scrollable">
|
||||
{leftPanelCategories.length === 0 ? (
|
||||
<div className="empty-message">{isLmsMode ? translateString('All courses already added') : translateString('All categories already added')}</div>
|
||||
<div className="empty-message">{translateString('All categories already added')}</div>
|
||||
) : (
|
||||
leftPanelCategories.map((category) => (
|
||||
<div
|
||||
@@ -236,11 +227,11 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
|
||||
</h3>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
|
||||
<div className="loading-message">{translateString('Loading categories...')}</div>
|
||||
) : (
|
||||
<div className="category-list scrollable">
|
||||
{rightPanelCategories.length === 0 ? (
|
||||
<div className="empty-message">{isLmsMode ? translateString('No courses') : translateString('No categories')}</div>
|
||||
<div className="empty-message">{translateString('No categories')}</div>
|
||||
) : (
|
||||
rightPanelCategories.map((category) => {
|
||||
const isExisting = existingCategories.some((c) => c.uid === category.uid);
|
||||
@@ -260,7 +251,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
|
||||
removeCategoryFromAddList(category);
|
||||
}
|
||||
}}
|
||||
title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? (isLmsMode ? translateString('Remove course') : translateString('Remove category')) : translateString('Remove from list')}
|
||||
title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? translateString('Remove category') : translateString('Remove from list')}
|
||||
>
|
||||
{isMarkedForRemoval ? '↺' : '×'}
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import './BulkActionsDropdown.scss';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
|
||||
|
||||
interface BulkActionsDropdownProps {
|
||||
selectedCount: number;
|
||||
@@ -13,7 +12,7 @@ const BULK_ACTIONS = [
|
||||
{ value: 'add-remove-coeditors', label: translateString('Add / Remove Co-Editors'), enabled: true },
|
||||
{ value: 'add-remove-coowners', label: translateString('Add / Remove Co-Owners'), enabled: true },
|
||||
{ value: 'add-remove-playlist', label: translateString('Add to / Remove from Playlist'), enabled: true },
|
||||
{ value: 'add-remove-category', label: inEmbeddedApp() ? translateString('Share with Course') : translateString('Add to / Remove from Category'), enabled: true },
|
||||
{ value: 'add-remove-category', label: translateString('Add to / Remove from Category'), enabled: true },
|
||||
{ value: 'add-remove-tags', label: translateString('Add / Remove Tags'), enabled: true },
|
||||
{ value: 'enable-comments', label: translateString('Enable Comments'), enabled: true },
|
||||
{ value: 'disable-comments', label: translateString('Disable Comments'), enabled: true },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { translateString, inSelectMediaEmbedMode } from '../utils/helpers/';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
|
||||
interface MediaListHeaderProps {
|
||||
title?: string;
|
||||
@@ -11,12 +11,10 @@ interface MediaListHeaderProps {
|
||||
|
||||
export const MediaListHeader: React.FC<MediaListHeaderProps> = (props) => {
|
||||
const viewAllText = props.viewAllText || translateString('VIEW ALL');
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
return (
|
||||
<div className={(props.className ? props.className + ' ' : '') + 'media-list-header'} style={props.style}>
|
||||
<h2>{props.title}</h2>
|
||||
{!isSelectMediaMode && props.viewAllLink ? (
|
||||
{props.viewAllLink ? (
|
||||
<h3>
|
||||
{' '}
|
||||
<a href={props.viewAllLink} title={viewAllText}>
|
||||
|
||||
@@ -1,50 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveInteger, PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
|
||||
import { PositiveInteger, PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { MediaItemThumbnailLink, itemClassname } from './includes/items/';
|
||||
import { Item } from './Item';
|
||||
|
||||
export function MediaItem(props) {
|
||||
const type = props.type;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
// In embed mode, override components to remove links
|
||||
const ItemTitle = ({ title }) => (
|
||||
<h3>
|
||||
<span>{title}</span>
|
||||
</h3>
|
||||
);
|
||||
|
||||
const ItemMain = ({ children }) => <div className="item-main">{children}</div>;
|
||||
|
||||
const titleComponent = isSelectMediaMode
|
||||
? () => <ItemTitle title={props.title} />
|
||||
: titleComponentOrig;
|
||||
|
||||
const UnderThumbWrapper = isSelectMediaMode ? ItemMain : UnderThumbWrapperOrig;
|
||||
|
||||
function thumbnailComponent() {
|
||||
if (isSelectMediaMode) {
|
||||
// In embed mode, render thumbnail without link
|
||||
const thumbStyle = thumbnailUrl ? { backgroundImage: "url('" + thumbnailUrl + "')" } : null;
|
||||
return (
|
||||
<div
|
||||
key="item-thumb"
|
||||
className={'item-thumb' + (!thumbnailUrl ? ' no-thumb' : '')}
|
||||
style={thumbStyle}
|
||||
>
|
||||
{thumbnailUrl ? (
|
||||
<div key="item-type-icon" className="item-type-icon">
|
||||
<div></div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <MediaItemThumbnailLink src={thumbnailUrl} title={props.title} link={props.link} />;
|
||||
}
|
||||
|
||||
@@ -57,13 +25,11 @@ export function MediaItem(props) {
|
||||
const finalClassname = containerClassname +
|
||||
(props.showSelection ? ' with-selection' : '') +
|
||||
(props.isSelected ? ' selected' : '') +
|
||||
(props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
|
||||
(props.hasAnySelection ? ' has-any-selection' : '');
|
||||
|
||||
const handleItemClick = (e) => {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// In select media mode or if there's any selection active, clicking the item should toggle selection
|
||||
if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
|
||||
// If there's any selection active, clicking the item should toggle selection
|
||||
if (props.hasAnySelection && props.onCheckboxChange) {
|
||||
// Check if clicking on the checkbox itself, edit icon, or view icon
|
||||
if (e.target.closest('.item-selection-checkbox') ||
|
||||
e.target.closest('.item-edit-icon') ||
|
||||
@@ -93,24 +59,16 @@ export function MediaItem(props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSelectMediaMode && editMediaComponent()}
|
||||
{!isSelectMediaMode && viewMediaComponent()}
|
||||
{editMediaComponent()}
|
||||
{viewMediaComponent()}
|
||||
|
||||
{thumbnailComponent()}
|
||||
|
||||
{isSelectMediaMode ? (
|
||||
<UnderThumbWrapper>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
) : (
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
)}
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { MediaDurationInfo } from '../../utils/classes/';
|
||||
import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions';
|
||||
import { MediaItemDuration, MediaItemPlaylistIndex, itemClassname } from './includes/items/';
|
||||
@@ -9,26 +9,10 @@ import { MediaItem } from './MediaItem';
|
||||
|
||||
export function MediaItemAudio(props) {
|
||||
const type = props.type;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
// In embed mode, override components to remove links
|
||||
const ItemTitle = ({ title }) => (
|
||||
<h3>
|
||||
<span>{title}</span>
|
||||
</h3>
|
||||
);
|
||||
|
||||
const ItemMain = ({ children }) => <div className="item-main">{children}</div>;
|
||||
|
||||
const titleComponent = isSelectMediaMode
|
||||
? () => <ItemTitle title={props.title} />
|
||||
: titleComponentOrig;
|
||||
|
||||
const UnderThumbWrapper = isSelectMediaMode ? ItemMain : UnderThumbWrapperOrig;
|
||||
|
||||
const _MediaDurationInfo = new MediaDurationInfo();
|
||||
|
||||
_MediaDurationInfo.update(props.duration);
|
||||
@@ -38,21 +22,6 @@ export function MediaItemAudio(props) {
|
||||
const durationISO8601 = _MediaDurationInfo.ISO8601();
|
||||
|
||||
function thumbnailComponent() {
|
||||
if (isSelectMediaMode) {
|
||||
// In embed mode, render thumbnail without link
|
||||
return (
|
||||
<div
|
||||
key="item-thumb"
|
||||
className={'item-thumb' + (!thumbnailUrl ? ' no-thumb' : '')}
|
||||
style={!thumbnailUrl ? null : { backgroundImage: "url('" + thumbnailUrl + "')" }}
|
||||
>
|
||||
{props.inPlaylistView ? null : (
|
||||
<MediaItemDuration ariaLabel={duration} time={durationISO8601} text={durationStr} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
@@ -99,11 +68,11 @@ export function MediaItemAudio(props) {
|
||||
const finalClassname = containerClassname +
|
||||
(props.showSelection ? ' with-selection' : '') +
|
||||
(props.isSelected ? ' selected' : '') +
|
||||
(props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
|
||||
(props.hasAnySelection ? ' has-any-selection' : '');
|
||||
|
||||
const handleItemClick = (e) => {
|
||||
// In embed mode or if there's any selection active, clicking the item should toggle selection
|
||||
if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
|
||||
// If there's any selection active, clicking the item should toggle selection
|
||||
if (props.hasAnySelection && props.onCheckboxChange) {
|
||||
// Check if clicking on the checkbox itself, edit icon, or view icon
|
||||
if (e.target.closest('.item-selection-checkbox') ||
|
||||
e.target.closest('.item-edit-icon') ||
|
||||
@@ -135,24 +104,16 @@ export function MediaItemAudio(props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSelectMediaMode && editMediaComponent()}
|
||||
{!isSelectMediaMode && viewMediaComponent()}
|
||||
{editMediaComponent()}
|
||||
{viewMediaComponent()}
|
||||
|
||||
{thumbnailComponent()}
|
||||
|
||||
{isSelectMediaMode ? (
|
||||
<UnderThumbWrapper>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
) : (
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
)}
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
|
||||
{playlistOptionsComponent()}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useMediaItem } from '../../utils/hooks/';
|
||||
import { PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
|
||||
import { PositiveIntegerOrZero } from '../../utils/helpers/';
|
||||
import { MediaDurationInfo } from '../../utils/classes/';
|
||||
import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions.jsx';
|
||||
import { MediaItemVideoPlayer, MediaItemDuration, MediaItemVideoPreviewer, MediaItemPlaylistIndex, itemClassname } from './includes/items/';
|
||||
@@ -9,26 +9,10 @@ import { MediaItem } from './MediaItem';
|
||||
|
||||
export function MediaItemVideo(props) {
|
||||
const type = props.type;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] =
|
||||
useMediaItem({ ...props, type });
|
||||
|
||||
// In embed mode, override components to remove links
|
||||
const ItemTitle = ({ title }) => (
|
||||
<h3>
|
||||
<span>{title}</span>
|
||||
</h3>
|
||||
);
|
||||
|
||||
const ItemMain = ({ children }) => <div className="item-main">{children}</div>;
|
||||
|
||||
const titleComponent = isSelectMediaMode
|
||||
? () => <ItemTitle title={props.title} />
|
||||
: titleComponentOrig;
|
||||
|
||||
const UnderThumbWrapper = isSelectMediaMode ? ItemMain : UnderThumbWrapperOrig;
|
||||
|
||||
const _MediaDurationInfo = new MediaDurationInfo();
|
||||
|
||||
_MediaDurationInfo.update(props.duration);
|
||||
@@ -42,24 +26,6 @@ export function MediaItemVideo(props) {
|
||||
}
|
||||
|
||||
function thumbnailComponent() {
|
||||
if (isSelectMediaMode) {
|
||||
// In select media mode, render thumbnail without link
|
||||
return (
|
||||
<div
|
||||
key="item-thumb"
|
||||
className={'item-thumb' + (!thumbnailUrl ? ' no-thumb' : '')}
|
||||
style={!thumbnailUrl ? null : { backgroundImage: "url('" + thumbnailUrl + "')" }}
|
||||
>
|
||||
{props.inPlaylistView ? null : (
|
||||
<MediaItemDuration ariaLabel={duration} time={durationISO8601} text={durationStr} />
|
||||
)}
|
||||
{props.inPlaylistView || props.inPlaylistPage ? null : (
|
||||
<MediaItemVideoPreviewer url={props.preview_thumbnail} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const attr = {
|
||||
key: 'item-thumb',
|
||||
href: props.link,
|
||||
@@ -109,11 +75,11 @@ export function MediaItemVideo(props) {
|
||||
const finalClassname = containerClassname +
|
||||
(props.showSelection ? ' with-selection' : '') +
|
||||
(props.isSelected ? ' selected' : '') +
|
||||
(props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
|
||||
(props.hasAnySelection ? ' has-any-selection' : '');
|
||||
|
||||
const handleItemClick = (e) => {
|
||||
// In select media mode or if there's any selection active, clicking the item should toggle selection
|
||||
if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
|
||||
// If there's any selection active, clicking the item should toggle selection
|
||||
if (props.hasAnySelection && props.onCheckboxChange) {
|
||||
// Check if clicking on the checkbox itself, edit icon, or view icon
|
||||
if (e.target.closest('.item-selection-checkbox') ||
|
||||
e.target.closest('.item-edit-icon') ||
|
||||
@@ -145,27 +111,19 @@ export function MediaItemVideo(props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSelectMediaMode && editMediaComponent()}
|
||||
{!isSelectMediaMode && viewMediaComponent()}
|
||||
{editMediaComponent()}
|
||||
{viewMediaComponent()}
|
||||
|
||||
{props.hasMediaViewer ? videoViewerComponent() : thumbnailComponent()}
|
||||
|
||||
{isSelectMediaMode ? (
|
||||
<UnderThumbWrapper>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
) : (
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
)}
|
||||
|
||||
{playlistOptionsComponent()}
|
||||
<UnderThumbWrapper title={props.title} link={props.link}>
|
||||
{titleComponent()}
|
||||
{metaComponents()}
|
||||
{descriptionComponent()}
|
||||
</UnderThumbWrapper>
|
||||
</div>
|
||||
|
||||
{playlistOptionsComponent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,10 +79,6 @@ function EditMediaButton(props) {
|
||||
link = '/edit-media.html';
|
||||
}
|
||||
|
||||
if (link && inEmbeddedApp()) {
|
||||
link += '&mode=lms_embed_mode';
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={link} rel="nofollow" title={translateString('Edit media')} className="edit-media-icon">
|
||||
<i className="material-icons">edit</i>
|
||||
@@ -98,18 +94,11 @@ export default function ViewerInfoContent(props) {
|
||||
!PageStore.get('config-enabled').taxonomies.tags || PageStore.get('config-enabled').taxonomies.tags.enabled
|
||||
? metafield(MediaPageStore.get('media-tags'))
|
||||
: [];
|
||||
let mediaCategories = MediaPageStore.get('media-categories');
|
||||
|
||||
// Filter to show only LMS courses when in embed mode
|
||||
if (inEmbeddedApp()) {
|
||||
mediaCategories = mediaCategories.filter(cat => cat.is_lms_course === true);
|
||||
}
|
||||
|
||||
const categoriesContent = PageStore.get('config-options').pages.media.categoriesWithTitle
|
||||
? []
|
||||
: !PageStore.get('config-enabled').taxonomies.categories ||
|
||||
PageStore.get('config-enabled').taxonomies.categories.enabled
|
||||
? metafield(mediaCategories)
|
||||
? metafield(MediaPageStore.get('media-categories'))
|
||||
: [];
|
||||
|
||||
let summary = MediaPageStore.get('media-summary');
|
||||
@@ -231,13 +220,9 @@ export default function ViewerInfoContent(props) {
|
||||
<MediaMetaField
|
||||
value={categoriesContent}
|
||||
title={
|
||||
inEmbeddedApp()
|
||||
? (1 < categoriesContent.length
|
||||
? translateString('Courses')
|
||||
: translateString('Course'))
|
||||
: (1 < categoriesContent.length
|
||||
? translateString('Categories')
|
||||
: translateString('Category'))
|
||||
1 < categoriesContent.length
|
||||
? translateString('Categories')
|
||||
: translateString('Category')
|
||||
}
|
||||
id="categories"
|
||||
/>
|
||||
@@ -289,7 +274,7 @@ export default function ViewerInfoContent(props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommentsList />
|
||||
{!inEmbeddedApp() && <CommentsList />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { VideoViewerActions } from '../../../utils/actions/';
|
||||
import { SiteContext, SiteConsumer } from '../../../utils/contexts/';
|
||||
import { PageStore, MediaPageStore, VideoViewerStore } from '../../../utils/stores/';
|
||||
import { addClassname, removeClassname, formatInnerLink, inEmbeddedApp } from '../../../utils/helpers/';
|
||||
import { addClassname, removeClassname, formatInnerLink } from '../../../utils/helpers/';
|
||||
import { BrowserCache, UpNextLoaderView, MediaDurationInfo } from '../../../utils/classes/';
|
||||
import {
|
||||
orderedSupportedVideoFormats,
|
||||
@@ -176,13 +176,11 @@ export default class VideoViewer extends React.PureComponent {
|
||||
topLeftHtml = document.createElement('div');
|
||||
topLeftHtml.setAttribute('class', 'media-links-top-left');
|
||||
|
||||
const linkTarget = inEmbeddedApp() || window.location.href.indexOf('lms_embed_mode') > -1 ? '_self' : '_blank';
|
||||
|
||||
if (titleLink) {
|
||||
titleLink.setAttribute('class', 'title-link');
|
||||
titleLink.setAttribute('href', this.props.data.url);
|
||||
titleLink.setAttribute('title', this.props.data.title);
|
||||
titleLink.setAttribute('target', linkTarget);
|
||||
titleLink.setAttribute('target', '_blank');
|
||||
titleLink.innerHTML = this.props.data.title;
|
||||
}
|
||||
|
||||
@@ -193,7 +191,7 @@ export default class VideoViewer extends React.PureComponent {
|
||||
formatInnerLink(this.props.data.author_profile, this.props.siteUrl)
|
||||
);
|
||||
userThumbLink.setAttribute('title', this.props.data.author_name);
|
||||
userThumbLink.setAttribute('target', linkTarget);
|
||||
userThumbLink.setAttribute('target', '_blank');
|
||||
userThumbLink.setAttribute(
|
||||
'style',
|
||||
'background-image:url(' +
|
||||
|
||||
@@ -656,6 +656,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.fixed-nav {
|
||||
.profile-info-nav-wrap {
|
||||
padding-bottom: $_authorPage-navHeight;
|
||||
}
|
||||
|
||||
.profile-nav {
|
||||
z-index: 3;
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.visible-sidebar & {
|
||||
padding-left: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.sliding-sidebar & {
|
||||
transition-property: padding-left;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-main {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/'
|
||||
import { PageStore, ProfilePageStore } from '../../utils/stores/';
|
||||
import { PageActions, ProfilePageActions } from '../../utils/actions/';
|
||||
import { CircleIconButton, PopupMain } from '../_shared';
|
||||
import { translateString, inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode } from '../../utils/helpers/';
|
||||
import { translateString } from '../../utils/helpers/';
|
||||
|
||||
class ProfileSearchBar extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -372,47 +372,30 @@ class NavMenuInlineTabs extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// Append action=select_media to links when in select mode
|
||||
const mediaLink = isSelectMediaMode
|
||||
? `${LinksContext._currentValue.profile.media}${LinksContext._currentValue.profile.media.includes('?') ? '&' : '?'}action=select_media`
|
||||
: LinksContext._currentValue.profile.media;
|
||||
|
||||
const sharedByMeLink = isSelectMediaMode
|
||||
? `${LinksContext._currentValue.profile.shared_by_me}${LinksContext._currentValue.profile.shared_by_me.includes('?') ? '&' : '?'}action=select_media`
|
||||
: LinksContext._currentValue.profile.shared_by_me;
|
||||
|
||||
const sharedWithMeLink = isSelectMediaMode
|
||||
? `${LinksContext._currentValue.profile.shared_with_me}${LinksContext._currentValue.profile.shared_with_me.includes('?') ? '&' : '?'}action=select_media`
|
||||
: LinksContext._currentValue.profile.shared_with_me;
|
||||
|
||||
return (
|
||||
<nav ref="tabsNav" className="profile-nav items-list-outer list-inline list-slider">
|
||||
<div className="profile-nav-inner items-list-outer">
|
||||
{this.state.displayPrev ? this.previousBtn : null}
|
||||
|
||||
<ul className="items-list-wrap" ref="itemsListWrap">
|
||||
{!isSelectMediaMode && !inEmbeddedApp() ? (
|
||||
<InlineTab
|
||||
id="about"
|
||||
isActive={'about' === this.props.type}
|
||||
label={translateString('About')}
|
||||
link={LinksContext._currentValue.profile.about}
|
||||
/>
|
||||
) : null}
|
||||
<InlineTab
|
||||
id="about"
|
||||
isActive={'about' === this.props.type}
|
||||
label={translateString('About')}
|
||||
link={LinksContext._currentValue.profile.about}
|
||||
/>
|
||||
<InlineTab
|
||||
id="media"
|
||||
isActive={'media' === this.props.type}
|
||||
label={translateString(this.userIsAuthor ? 'Media I own' : 'Media')}
|
||||
link={mediaLink}
|
||||
link={LinksContext._currentValue.profile.media}
|
||||
/>
|
||||
{this.userIsAuthor ? (
|
||||
<InlineTab
|
||||
id="shared_by_me"
|
||||
isActive={'shared_by_me' === this.props.type}
|
||||
label={translateString('Shared by me')}
|
||||
link={sharedByMeLink}
|
||||
link={LinksContext._currentValue.profile.shared_by_me}
|
||||
/>
|
||||
) : null}
|
||||
{this.userIsAuthor ? (
|
||||
@@ -420,11 +403,11 @@ class NavMenuInlineTabs extends React.PureComponent {
|
||||
id="shared_with_me"
|
||||
isActive={'shared_with_me' === this.props.type}
|
||||
label={translateString('Shared with me')}
|
||||
link={sharedWithMeLink}
|
||||
link={LinksContext._currentValue.profile.shared_with_me}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isSelectMediaMode && MemberContext._currentValue.can.saveMedia ? (
|
||||
{MemberContext._currentValue.can.saveMedia ? (
|
||||
<InlineTab
|
||||
id="playlists"
|
||||
isActive={'playlists' === this.props.type}
|
||||
@@ -623,6 +606,12 @@ export default function ProfilePagesHeader(props) {
|
||||
const profilePageHeaderRef = useRef(null);
|
||||
const profileNavRef = useRef(null);
|
||||
|
||||
const [fixedNav, setFixedNav] = useState(false);
|
||||
|
||||
const positions = {
|
||||
profileNavTop: 0,
|
||||
};
|
||||
|
||||
const userIsAdmin = !MemberContext._currentValue.is.anonymous && MemberContext._currentValue.is.admin;
|
||||
const userIsAuthor =
|
||||
!MemberContext._currentValue.is.anonymous &&
|
||||
@@ -634,6 +623,18 @@ export default function ProfilePagesHeader(props) {
|
||||
userIsAuthor ||
|
||||
(!MemberContext._currentValue.is.anonymous && MemberContext._currentValue.can.deleteProfile);
|
||||
|
||||
function updateProfileNavTopPosition() {
|
||||
positions.profileHeaderTop = profilePageHeaderRef.current.offsetTop;
|
||||
positions.profileNavTop =
|
||||
positions.profileHeaderTop +
|
||||
profilePageHeaderRef.current.offsetHeight -
|
||||
profileNavRef.current.refs.tabsNav.offsetHeight;
|
||||
}
|
||||
|
||||
function updateFixedNavPosition() {
|
||||
setFixedNav(positions.profileHeaderTop + window.scrollY > positions.profileNavTop);
|
||||
}
|
||||
|
||||
function cancelProfileRemoval() {
|
||||
popupContentRef.current.toggle();
|
||||
}
|
||||
@@ -668,26 +669,42 @@ export default function ProfilePagesHeader(props) {
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
updateProfileNavTopPosition();
|
||||
updateFixedNavPosition();
|
||||
}
|
||||
|
||||
function onWindowScroll() {
|
||||
updateFixedNavPosition();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (userCanDeleteProfile) {
|
||||
ProfilePageStore.on('profile_delete', onProfileDelete);
|
||||
ProfilePageStore.on('profile_delete_fail', onProfileDeleteFail);
|
||||
}
|
||||
|
||||
PageStore.on('resize', onWindowResize);
|
||||
PageStore.on('changed_page_sidebar_visibility', onWindowResize);
|
||||
PageStore.on('window_scroll', onWindowScroll);
|
||||
|
||||
updateProfileNavTopPosition();
|
||||
updateFixedNavPosition();
|
||||
|
||||
return () => {
|
||||
if (userCanDeleteProfile) {
|
||||
ProfilePageStore.removeListener('profile_delete', onProfileDelete);
|
||||
ProfilePageStore.removeListener('profile_delete_fail', onProfileDeleteFail);
|
||||
}
|
||||
|
||||
PageStore.removeListener('resize', onWindowResize);
|
||||
PageStore.removeListener('changed_page_sidebar_visibility', onWindowResize);
|
||||
PageStore.removeListener('window_scroll', onWindowScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={profilePageHeaderRef}
|
||||
className={'profile-page-header'}
|
||||
{...(isSelectMediaMode() ? { 'data-action': 'select_media' } : {})}
|
||||
>
|
||||
<div ref={profilePageHeaderRef} className={'profile-page-header' + (fixedNav ? ' fixed-nav' : '')}>
|
||||
{!props.hideChannelBanner && (
|
||||
<span className="profile-banner-wrap">
|
||||
{props.author.banner_thumbnail_url ? (
|
||||
@@ -751,7 +768,7 @@ export default function ProfilePagesHeader(props) {
|
||||
)}
|
||||
|
||||
<div className="profile-info-nav-wrap">
|
||||
{!inEmbeddedApp() && (props.author.thumbnail_url || props.author.name) ? (
|
||||
{props.author.thumbnail_url || props.author.name ? (
|
||||
<div className="profile-info">
|
||||
<div className="profile-info-inner">
|
||||
<div>
|
||||
|
||||
@@ -3,14 +3,14 @@ import { ApiUrlConsumer } from '../utils/contexts/';
|
||||
import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
|
||||
import { Page } from './Page';
|
||||
import { translateString, inEmbeddedApp } from '../utils/helpers/';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
|
||||
interface CategoriesPageProps {
|
||||
id?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const CategoriesPage: React.FC<CategoriesPageProps> = ({ id = 'categories', title = inEmbeddedApp() ? translateString('Courses') : translateString('Categories') }) => (
|
||||
export const CategoriesPage: React.FC<CategoriesPageProps> = ({ id = 'categories', title = translateString('Categories') }) => (
|
||||
<Page id={id}>
|
||||
<ApiUrlConsumer>
|
||||
{(apiUrl) => (
|
||||
|
||||
@@ -55,7 +55,7 @@ export const HistoryPage: React.FC = () => {
|
||||
const anonymousPage = isAnonymous || !PageStore.get('config-options').pages.profile.includeHistory;
|
||||
|
||||
if (!anonymousPage) {
|
||||
addClassname(document.getElementById('page-history'), 'profile-page-history');
|
||||
addClassname(document.getElementById('page-history')!, 'profile-page-history');
|
||||
window.MediaCMS.profileId = username;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
<MediaListRow
|
||||
title={featured_title}
|
||||
style={!visibleFeatured ? { display: 'none' } : undefined}
|
||||
viewAllLink={featured_view_all_link ? links.featured : null}
|
||||
viewAllLink={featured_view_all_link ? links.featured : undefined}
|
||||
>
|
||||
<InlineSliderItemListAsync
|
||||
requestUrl={apiUrl.featured}
|
||||
@@ -93,7 +93,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
<MediaListRow
|
||||
title={recommended_title}
|
||||
style={!visibleRecommended ? { display: 'none' } : undefined}
|
||||
viewAllLink={recommended_view_all_link ? links.recommended : null}
|
||||
viewAllLink={recommended_view_all_link ? links.recommended : undefined}
|
||||
>
|
||||
<InlineSliderItemListAsync
|
||||
requestUrl={apiUrl.recommended}
|
||||
@@ -108,7 +108,7 @@ export const HomePage: React.FC<HomePageProps> = ({
|
||||
<MediaListRow
|
||||
title={latest_title}
|
||||
style={!visibleLatest ? { display: 'none' } : undefined}
|
||||
viewAllLink={latest_view_all_link ? links.latest : null}
|
||||
viewAllLink={latest_view_all_link ? links.latest : undefined}
|
||||
>
|
||||
<ItemListAsync
|
||||
pageItems={30}
|
||||
|
||||
@@ -55,7 +55,7 @@ export const LikedMediaPage: React.FC = () => {
|
||||
const anonymousPage = isAnonymous || !PageStore.get('config-options').pages.profile.includeLikedMedia;
|
||||
|
||||
if (!anonymousPage) {
|
||||
addClassname(document.getElementById('page-liked'), 'profile-page-liked');
|
||||
addClassname(document.getElementById('page-liked')!, 'profile-page-liked');
|
||||
window.MediaCMS.profileId = username;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { ApiUrlContext, LinksConsumer, MemberContext } from '../utils/contexts';
|
||||
import { PageStore, ProfilePageStore } from '../utils/stores';
|
||||
import { ProfilePageActions, PageActions } from '../utils/actions';
|
||||
import { inEmbeddedApp, inSelectMediaEmbedMode, translateString } from '../utils/helpers/';
|
||||
import { inEmbeddedApp, translateString } from '../utils/helpers/';
|
||||
import { MediaListWrapper } from '../components/MediaListWrapper';
|
||||
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
|
||||
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
|
||||
@@ -202,45 +202,13 @@ export class ProfileMediaPage extends Page {
|
||||
}
|
||||
|
||||
handleMediaSelection(mediaId, isSelected) {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
this.setState((prevState) => {
|
||||
const newSelectedMedia = new Set();
|
||||
|
||||
// In select media mode, only allow single selection
|
||||
if (isSelectMediaMode) {
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
console.log('Selected media item:', mediaId);
|
||||
|
||||
// Send postMessage to parent window (Moodle TinyMCE plugin)
|
||||
if (window.parent !== window) {
|
||||
// Construct the embed URL
|
||||
const baseUrl = window.location.origin;
|
||||
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
|
||||
|
||||
// Send message in the format expected by the Moodle plugin
|
||||
window.parent.postMessage({
|
||||
type: 'videoSelected',
|
||||
embedUrl: embedUrl,
|
||||
videoId: mediaId
|
||||
}, '*');
|
||||
|
||||
console.log('Sent postMessage to parent:', { embedUrl, videoId: mediaId });
|
||||
}
|
||||
}
|
||||
const newSelectedMedia = new Set(prevState.selectedMedia);
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
} else {
|
||||
// Normal mode: allow multiple selection
|
||||
newSelectedMedia.clear();
|
||||
prevState.selectedMedia.forEach((id) => newSelectedMedia.add(id));
|
||||
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
} else {
|
||||
newSelectedMedia.delete(mediaId);
|
||||
}
|
||||
newSelectedMedia.delete(mediaId);
|
||||
}
|
||||
|
||||
return { selectedMedia: newSelectedMedia };
|
||||
});
|
||||
}
|
||||
@@ -949,7 +917,6 @@ export class ProfileMediaPage extends Page {
|
||||
const authorData = ProfilePageStore.get('author-data');
|
||||
|
||||
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// Check if any filters are active (excluding default sort and tags)
|
||||
const hasActiveFilters =
|
||||
@@ -981,16 +948,15 @@ export class ProfileMediaPage extends Page {
|
||||
this.state.author ? (
|
||||
<ProfilePagesContent key="ProfilePagesContent">
|
||||
<MediaListWrapper
|
||||
title={inEmbeddedApp() ? undefined : this.state.title}
|
||||
title={this.state.title}
|
||||
className="items-list-ver"
|
||||
style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
|
||||
showBulkActions={!isSelectMediaMode && isMediaAuthor}
|
||||
showBulkActions={isMediaAuthor}
|
||||
selectedCount={this.state.selectedMedia.size}
|
||||
totalCount={this.state.availableMediaIds.length}
|
||||
onBulkAction={this.handleBulkAction}
|
||||
onSelectAll={this.handleSelectAll}
|
||||
onDeselectAll={this.handleDeselectAll}
|
||||
showAddMediaButton={!isSelectMediaMode && isMediaAuthor}
|
||||
showAddMediaButton={isMediaAuthor}
|
||||
>
|
||||
<ProfileMediaFilters
|
||||
hidden={this.state.hiddenFilters}
|
||||
@@ -1013,7 +979,7 @@ export class ProfileMediaPage extends Page {
|
||||
hideViews={!PageStore.get('config-media-item').displayViews}
|
||||
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
||||
canEdit={isMediaAuthor}
|
||||
showSelection={isMediaAuthor || isSelectMediaMode}
|
||||
showSelection={isMediaAuthor}
|
||||
hasAnySelection={this.state.selectedMedia.size > 0}
|
||||
selectedMedia={this.state.selectedMedia}
|
||||
onMediaSelection={this.handleMediaSelection}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFi
|
||||
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
||||
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
||||
import { BulkActionsModals } from '../components/BulkActionsModals';
|
||||
import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
|
||||
import { inEmbeddedApp, translateString } from '../utils/helpers';
|
||||
import { withBulkActions } from '../utils/hoc/withBulkActions';
|
||||
|
||||
import { Page } from './_Page';
|
||||
@@ -51,7 +51,6 @@ class ProfileSharedByMePage extends Page {
|
||||
availableTags: [],
|
||||
selectedTag: 'all',
|
||||
selectedSort: 'date_added_desc',
|
||||
selectedMedia: new Set(), // For select media mode
|
||||
};
|
||||
|
||||
this.authorDataLoad = this.authorDataLoad.bind(this);
|
||||
@@ -65,7 +64,6 @@ class ProfileSharedByMePage extends Page {
|
||||
this.onTagSelect = this.onTagSelect.bind(this);
|
||||
this.onSortSelect = this.onSortSelect.bind(this);
|
||||
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
||||
this.handleMediaSelection = this.handleMediaSelection.bind(this);
|
||||
|
||||
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
||||
}
|
||||
@@ -345,55 +343,10 @@ class ProfileSharedByMePage extends Page {
|
||||
}
|
||||
}
|
||||
|
||||
handleMediaSelection(mediaId, isSelected) {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
this.setState((prevState) => {
|
||||
const newSelectedMedia = new Set();
|
||||
|
||||
// In select media mode, only allow single selection
|
||||
if (isSelectMediaMode) {
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
console.log('Selected media item:', mediaId);
|
||||
|
||||
// Send postMessage to parent window (Moodle TinyMCE plugin)
|
||||
if (window.parent !== window) {
|
||||
// Construct the embed URL
|
||||
const baseUrl = window.location.origin;
|
||||
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
|
||||
|
||||
// Send message in the format expected by the Moodle plugin
|
||||
window.parent.postMessage({
|
||||
type: 'videoSelected',
|
||||
embedUrl: embedUrl,
|
||||
videoId: mediaId
|
||||
}, '*');
|
||||
|
||||
console.log('Sent postMessage to parent:', { embedUrl, videoId: mediaId });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal mode: should not reach here as bulk actions handle this
|
||||
newSelectedMedia.clear();
|
||||
prevState.selectedMedia.forEach((id) => newSelectedMedia.add(id));
|
||||
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
} else {
|
||||
newSelectedMedia.delete(mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedMedia: newSelectedMedia };
|
||||
});
|
||||
}
|
||||
|
||||
pageContent() {
|
||||
const authorData = ProfilePageStore.get('author-data');
|
||||
|
||||
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// Check if any filters are active
|
||||
const hasActiveFilters =
|
||||
@@ -422,12 +375,11 @@ class ProfileSharedByMePage extends Page {
|
||||
this.state.author ? (
|
||||
<ProfilePagesContent key="ProfilePagesContent">
|
||||
<MediaListWrapper
|
||||
title={inEmbeddedApp() ? undefined : this.state.title}
|
||||
title={this.state.title}
|
||||
className="items-list-ver"
|
||||
style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
|
||||
showBulkActions={!isSelectMediaMode && isMediaAuthor}
|
||||
selectedCount={isSelectMediaMode ? this.state.selectedMedia.size : this.props.bulkActions.selectedMedia.size}
|
||||
totalCount={isSelectMediaMode ? 0 : this.props.bulkActions.availableMediaIds.length}
|
||||
showBulkActions={isMediaAuthor}
|
||||
selectedCount={this.props.bulkActions.selectedMedia.size}
|
||||
totalCount={this.props.bulkActions.availableMediaIds.length}
|
||||
onBulkAction={this.props.bulkActions.handleBulkAction}
|
||||
onSelectAll={this.props.bulkActions.handleSelectAll}
|
||||
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
||||
@@ -444,19 +396,19 @@ class ProfileSharedByMePage extends Page {
|
||||
/>
|
||||
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
|
||||
<LazyLoadItemListAsync
|
||||
key={isSelectMediaMode ? this.state.requestUrl : `${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
|
||||
key={`${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
|
||||
requestUrl={this.state.requestUrl}
|
||||
hideAuthor={true}
|
||||
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
|
||||
hideViews={!PageStore.get('config-media-item').displayViews}
|
||||
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
||||
canEdit={!isSelectMediaMode && isMediaAuthor}
|
||||
canEdit={isMediaAuthor}
|
||||
onResponseDataLoaded={this.onResponseDataLoaded}
|
||||
showSelection={isMediaAuthor || isSelectMediaMode}
|
||||
hasAnySelection={isSelectMediaMode ? this.state.selectedMedia.size > 0 : this.props.bulkActions.selectedMedia.size > 0}
|
||||
selectedMedia={isSelectMediaMode ? this.state.selectedMedia : this.props.bulkActions.selectedMedia}
|
||||
onMediaSelection={isSelectMediaMode ? this.handleMediaSelection : this.props.bulkActions.handleMediaSelection}
|
||||
onItemsUpdate={!isSelectMediaMode ? this.props.bulkActions.handleItemsUpdate : undefined}
|
||||
showSelection={isMediaAuthor}
|
||||
hasAnySelection={this.props.bulkActions.selectedMedia.size > 0}
|
||||
selectedMedia={this.props.bulkActions.selectedMedia}
|
||||
onMediaSelection={this.props.bulkActions.handleMediaSelection}
|
||||
onItemsUpdate={this.props.bulkActions.handleItemsUpdate}
|
||||
/>
|
||||
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
|
||||
<EmptySharedByMe name={this.state.author.name} />
|
||||
@@ -464,7 +416,7 @@ class ProfileSharedByMePage extends Page {
|
||||
</MediaListWrapper>
|
||||
</ProfilePagesContent>
|
||||
) : null,
|
||||
this.state.author && isMediaAuthor && !isSelectMediaMode ? (
|
||||
this.state.author && isMediaAuthor ? (
|
||||
<BulkActionsModals
|
||||
key="BulkActionsModals"
|
||||
{...this.props.bulkActions}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
|
||||
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
|
||||
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
|
||||
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
|
||||
import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
|
||||
import { inEmbeddedApp, translateString } from '../utils/helpers';
|
||||
|
||||
import { Page } from './_Page';
|
||||
|
||||
@@ -49,7 +49,6 @@ export class ProfileSharedWithMePage extends Page {
|
||||
availableTags: [],
|
||||
selectedTag: 'all',
|
||||
selectedSort: 'date_added_desc',
|
||||
selectedMedia: new Set(), // For select media mode
|
||||
};
|
||||
|
||||
this.authorDataLoad = this.authorDataLoad.bind(this);
|
||||
@@ -63,7 +62,6 @@ export class ProfileSharedWithMePage extends Page {
|
||||
this.onTagSelect = this.onTagSelect.bind(this);
|
||||
this.onSortSelect = this.onSortSelect.bind(this);
|
||||
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
|
||||
this.handleMediaSelection = this.handleMediaSelection.bind(this);
|
||||
|
||||
ProfilePageStore.on('load-author-data', this.authorDataLoad);
|
||||
}
|
||||
@@ -343,55 +341,10 @@ export class ProfileSharedWithMePage extends Page {
|
||||
}
|
||||
}
|
||||
|
||||
handleMediaSelection(mediaId, isSelected) {
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
this.setState((prevState) => {
|
||||
const newSelectedMedia = new Set();
|
||||
|
||||
// In select media mode, only allow single selection
|
||||
if (isSelectMediaMode) {
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
console.log('Selected media item:', mediaId);
|
||||
|
||||
// Send postMessage to parent window (Moodle TinyMCE plugin)
|
||||
if (window.parent !== window) {
|
||||
// Construct the embed URL
|
||||
const baseUrl = window.location.origin;
|
||||
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
|
||||
|
||||
// Send message in the format expected by the Moodle plugin
|
||||
window.parent.postMessage({
|
||||
type: 'videoSelected',
|
||||
embedUrl: embedUrl,
|
||||
videoId: mediaId
|
||||
}, '*');
|
||||
|
||||
console.log('Sent postMessage to parent:', { embedUrl, videoId: mediaId });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal mode: no selection UI in this page normally
|
||||
newSelectedMedia.clear();
|
||||
prevState.selectedMedia.forEach((id) => newSelectedMedia.add(id));
|
||||
|
||||
if (isSelected) {
|
||||
newSelectedMedia.add(mediaId);
|
||||
} else {
|
||||
newSelectedMedia.delete(mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedMedia: newSelectedMedia };
|
||||
});
|
||||
}
|
||||
|
||||
pageContent() {
|
||||
const authorData = ProfilePageStore.get('author-data');
|
||||
|
||||
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
|
||||
const isSelectMediaMode = inSelectMediaEmbedMode();
|
||||
|
||||
// Check if any filters are active
|
||||
const hasActiveFilters =
|
||||
@@ -419,11 +372,7 @@ export class ProfileSharedWithMePage extends Page {
|
||||
) : null,
|
||||
this.state.author ? (
|
||||
<ProfilePagesContent key="ProfilePagesContent">
|
||||
<MediaListWrapper
|
||||
title={inEmbeddedApp() ? undefined : this.state.title}
|
||||
className="items-list-ver"
|
||||
style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
|
||||
>
|
||||
<MediaListWrapper title={this.state.title} className="items-list-ver">
|
||||
<ProfileMediaFilters
|
||||
hidden={this.state.hiddenFilters}
|
||||
tags={this.state.availableTags}
|
||||
@@ -444,10 +393,6 @@ export class ProfileSharedWithMePage extends Page {
|
||||
hideDate={!PageStore.get('config-media-item').displayPublishDate}
|
||||
canEdit={false}
|
||||
onResponseDataLoaded={this.onResponseDataLoaded}
|
||||
showSelection={isSelectMediaMode}
|
||||
hasAnySelection={this.state.selectedMedia.size > 0}
|
||||
selectedMedia={this.state.selectedMedia}
|
||||
onMediaSelection={this.handleMediaSelection}
|
||||
/>
|
||||
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
|
||||
<EmptySharedWithMe name={this.state.author.name} />
|
||||
|
||||
@@ -7,7 +7,7 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
|
||||
import { SearchMediaFiltersRow } from '../components/search-filters/SearchMediaFiltersRow';
|
||||
import { SearchResultsFilters } from '../components/search-filters/SearchResultsFilters';
|
||||
import { Page } from './_Page';
|
||||
import { translateString, inEmbeddedApp } from '../utils/helpers/';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
|
||||
export class SearchPage extends Page {
|
||||
constructor(props) {
|
||||
@@ -115,7 +115,7 @@ export class SearchPage extends Page {
|
||||
} else {
|
||||
if (this.state.searchCategories) {
|
||||
title = null === this.state.resultsCount || 0 === this.state.resultsCount ? 'No' : this.state.resultsCount;
|
||||
title += ' ' + translateString(inEmbeddedApp() ? 'media in course' : 'media in category') + ' "' + this.state.searchCategories + '"';
|
||||
title += ' ' + translateString('media in category') + ' "' + this.state.searchCategories + '"';
|
||||
} else if (this.state.searchTags) {
|
||||
title = null === this.state.resultsCount || 0 === this.state.resultsCount ? 'No' : this.state.resultsCount;
|
||||
title += ' ' + translateString('media in tag') + ' "' + this.state.searchTags + '"';
|
||||
|
||||
1
frontend/src/static/js/types/DeepPartial.ts
Normal file
1
frontend/src/static/js/types/DeepPartial.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
|
||||
212
frontend/src/static/js/types/GlobalMediaCMS.ts
Normal file
212
frontend/src/static/js/types/GlobalMediaCMS.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
type GlobalMediaCMSApi = {
|
||||
actions: string;
|
||||
categories: string;
|
||||
comments: string;
|
||||
history: string;
|
||||
liked: string;
|
||||
manage_comments: string;
|
||||
manage_media: string;
|
||||
manage_users: string;
|
||||
media: string;
|
||||
members: string;
|
||||
playlists: string;
|
||||
search: string;
|
||||
tags: string;
|
||||
};
|
||||
|
||||
type GlobalMediaCMSContents = {
|
||||
header: {
|
||||
right: string;
|
||||
onLogoRight: string;
|
||||
};
|
||||
notifications: {
|
||||
messages: {
|
||||
addToLiked: string;
|
||||
removeFromLiked: string;
|
||||
addToDisliked: string;
|
||||
removeFromDisliked: string;
|
||||
};
|
||||
};
|
||||
sidebar: {
|
||||
belowNavMenu: string;
|
||||
belowThemeSwitcher: string;
|
||||
footer: string;
|
||||
mainMenuExtraItems: { text: string; link: string; icon: string; className?: string }[]; // @todo: Check "className"
|
||||
navMenuItems: { text: string; link: string; icon: string; className?: string }[]; // @todo: Check "className"
|
||||
};
|
||||
uploader: {
|
||||
belowUploadArea: string;
|
||||
postUploadMessage: string;
|
||||
};
|
||||
};
|
||||
|
||||
type GlobalMediaCMSFeatures = {
|
||||
embeddedVideo: {
|
||||
initialDimensions: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
headerBar: {
|
||||
hideLogin: boolean;
|
||||
hideRegister: boolean;
|
||||
};
|
||||
sideBar: {
|
||||
hideHomeLink: boolean;
|
||||
hideTagsLink: boolean;
|
||||
hideCategoriesLink: boolean;
|
||||
};
|
||||
media: {
|
||||
actions: {
|
||||
share: boolean;
|
||||
report: boolean;
|
||||
like: boolean;
|
||||
dislike: boolean;
|
||||
download: boolean;
|
||||
comment: boolean;
|
||||
comment_mention: boolean;
|
||||
save: boolean;
|
||||
};
|
||||
shareOptions: ('embed' | 'email')[];
|
||||
};
|
||||
mediaItem: {
|
||||
hideDate: boolean;
|
||||
hideViews: boolean;
|
||||
hideAuthor: boolean;
|
||||
};
|
||||
playlists: {
|
||||
mediaTypes: ('audio' | 'video')[];
|
||||
};
|
||||
};
|
||||
|
||||
type GlobalCMSPages = {
|
||||
home: {
|
||||
sections: {
|
||||
latest: { title: string };
|
||||
featured: { title: string };
|
||||
recommended: { title: string };
|
||||
};
|
||||
};
|
||||
media: {
|
||||
categoriesWithTitle: boolean;
|
||||
htmlInDescription: boolean;
|
||||
hideViews: boolean;
|
||||
related: { initialSize: number };
|
||||
};
|
||||
profile: {
|
||||
htmlInDescription: boolean;
|
||||
includeHistory: boolean;
|
||||
includeLikedMedia: boolean;
|
||||
};
|
||||
search: { advancedFilters: boolean };
|
||||
};
|
||||
|
||||
type GlobalCMSSite = {
|
||||
api: string;
|
||||
devEnv: boolean;
|
||||
id: string;
|
||||
logo: {
|
||||
lightMode: { img: string; svg: string };
|
||||
darkMode: { img: string; svg: string };
|
||||
};
|
||||
pages: {
|
||||
featured: { enabled: boolean; title: string };
|
||||
latest: { enabled: boolean; title: string };
|
||||
members: { enabled: boolean; title: string };
|
||||
recommended: { enabled: boolean; title: string };
|
||||
};
|
||||
taxonomies: {
|
||||
categories: { enabled: boolean; title: string };
|
||||
tags: { enabled: boolean; title: string };
|
||||
};
|
||||
theme: {
|
||||
mode: 'light' | 'dark';
|
||||
switch: { enabled: boolean; position: 'header' | 'sidebar' };
|
||||
};
|
||||
title: string;
|
||||
url: string;
|
||||
useRoundedCorners: boolean;
|
||||
userPages: {
|
||||
history: { enabled: boolean; title: string };
|
||||
liked: { enabled: boolean; title: string };
|
||||
};
|
||||
version: string;
|
||||
};
|
||||
|
||||
type GlobalCMSUrl = {
|
||||
addMedia: string; // eg: "./add-media.html";
|
||||
admin: string; // eg: "/admin";
|
||||
categories: string; // eg: "./categories.html";
|
||||
changePassword: string; // eg: "./change-password.html";
|
||||
editChannel: string; // eg: "./edit-channel.html";
|
||||
editProfile: string; // eg: "./edit-profile.html";
|
||||
error404: string; // eg: "./error.html";
|
||||
featuredMedia: string; // eg: "./featured.html";
|
||||
history: string; // eg: "./history.html";
|
||||
home: string; // eg: "./index.html";
|
||||
latestMedia: string; // eg: "./latest.html";
|
||||
likedMedia: string; // eg: "./liked.html";
|
||||
manageComments: string; // eg: "./manage-comments.html";
|
||||
manageMedia: string; // eg: "./manage-media.html";
|
||||
manageUsers: string; // eg: "./manage-users.html";
|
||||
members: string; // eg: "./members.html";
|
||||
recommendedMedia: string; // eg: "./recommended.html";
|
||||
register: string; // eg: "./register.html";
|
||||
search: string; // eg: "./search.html";
|
||||
signin: string; // eg: "./signin.html";
|
||||
signout: string; // eg: "./signout.html";
|
||||
tags: string; // eg: "./tags.html";
|
||||
};
|
||||
|
||||
type GlobalCMSUser = {
|
||||
name: string;
|
||||
username: string;
|
||||
thumbnail: string;
|
||||
is: {
|
||||
admin: boolean;
|
||||
anonymous: boolean;
|
||||
};
|
||||
can: {
|
||||
// a
|
||||
addComment: boolean;
|
||||
addMedia: boolean;
|
||||
// c
|
||||
canSeeMembersPage: boolean;
|
||||
changePassword: boolean;
|
||||
contactUser: boolean;
|
||||
// d
|
||||
deleteComment: boolean;
|
||||
deleteMedia: boolean;
|
||||
deleteProfile: boolean;
|
||||
// e
|
||||
editMedia: boolean;
|
||||
editProfile: boolean;
|
||||
editSubtitle: boolean;
|
||||
// l
|
||||
// m
|
||||
manageComments: boolean;
|
||||
manageMedia: boolean;
|
||||
manageUsers: boolean;
|
||||
mentionComment: boolean;
|
||||
// r
|
||||
readComment: boolean;
|
||||
// u
|
||||
usersNeedsToBeApproved: boolean;
|
||||
};
|
||||
pages: {
|
||||
about: string;
|
||||
media: string;
|
||||
playlists: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GlobalMediaCMS = {
|
||||
api: GlobalMediaCMSApi;
|
||||
contents: GlobalMediaCMSContents;
|
||||
features: GlobalMediaCMSFeatures;
|
||||
pages: GlobalCMSPages;
|
||||
profileId?: string;
|
||||
site: GlobalCMSSite;
|
||||
url: GlobalCMSUrl;
|
||||
user: GlobalCMSUser;
|
||||
};
|
||||
200
frontend/src/static/js/types/MediaCMSConfig.ts
Normal file
200
frontend/src/static/js/types/MediaCMSConfig.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { GlobalMediaCMS } from './GlobalMediaCMS';
|
||||
|
||||
type MediaCMSConfigApi = {
|
||||
archive: {
|
||||
tags: string;
|
||||
categories: string;
|
||||
};
|
||||
featured: string;
|
||||
manage: {
|
||||
media: string;
|
||||
users: string;
|
||||
comments: string;
|
||||
};
|
||||
media: string;
|
||||
playlists: string;
|
||||
recommended: string;
|
||||
search: {
|
||||
query: string;
|
||||
titles: string;
|
||||
tag: string;
|
||||
category: string;
|
||||
};
|
||||
user: {
|
||||
liked: string;
|
||||
history: string;
|
||||
playlists: string;
|
||||
};
|
||||
users: string; // @todo: "users" or "members"?
|
||||
};
|
||||
|
||||
type MediaCMSConfigContents = Omit<GlobalMediaCMS['contents'], 'notifications' | 'sidebar'> & {
|
||||
sidebar: {
|
||||
belowNavMenu: GlobalMediaCMS['contents']['sidebar']['belowNavMenu'];
|
||||
belowThemeSwitcher: GlobalMediaCMS['contents']['sidebar']['belowThemeSwitcher'];
|
||||
footer: GlobalMediaCMS['contents']['sidebar']['footer'];
|
||||
mainMenuExtra: { items: GlobalMediaCMS['contents']['sidebar']['mainMenuExtraItems'] };
|
||||
navMenu: { items: GlobalMediaCMS['contents']['sidebar']['navMenuItems'] };
|
||||
};
|
||||
};
|
||||
|
||||
type MediaCMSConfigEnabled = Pick<GlobalMediaCMS['site'], 'taxonomies'> & {
|
||||
pages: GlobalMediaCMS['site']['pages'] & GlobalMediaCMS['site']['userPages'];
|
||||
};
|
||||
|
||||
type MediaCMSConfigMember = {
|
||||
name: GlobalMediaCMS['user']['name'] | null;
|
||||
username: GlobalMediaCMS['user']['username'] | null;
|
||||
thumbnail: GlobalMediaCMS['user']['thumbnail'] | null;
|
||||
is: GlobalMediaCMS['user']['is'];
|
||||
can: {
|
||||
// a
|
||||
addComment: boolean;
|
||||
addMedia: boolean;
|
||||
// c
|
||||
canSeeMembersPage: boolean; // @note: This sould be renamed
|
||||
changePassword: boolean;
|
||||
contactUser: boolean;
|
||||
// d
|
||||
deleteComment: boolean;
|
||||
deleteMedia: boolean;
|
||||
deleteProfile: boolean;
|
||||
dislikeMedia: boolean;
|
||||
downloadMedia: boolean;
|
||||
// e
|
||||
editMedia: boolean;
|
||||
editProfile: boolean;
|
||||
editSubtitle: boolean;
|
||||
// l
|
||||
likeMedia: boolean;
|
||||
login: boolean;
|
||||
// m
|
||||
manageComments: boolean;
|
||||
manageMedia: boolean;
|
||||
manageUsers: boolean;
|
||||
mentionComment: boolean;
|
||||
// r
|
||||
readComment: boolean;
|
||||
register: boolean;
|
||||
reportMedia: boolean;
|
||||
// s
|
||||
saveMedia: boolean;
|
||||
shareMedia: boolean;
|
||||
// u
|
||||
usersNeedsToBeApproved: boolean;
|
||||
};
|
||||
pages: {
|
||||
home: string | null; // @todo: Check this again
|
||||
about: GlobalMediaCMS['user']['pages']['about'] | null;
|
||||
media: GlobalMediaCMS['user']['pages']['media'] | null;
|
||||
playlists: GlobalMediaCMS['user']['pages']['playlists'] | null;
|
||||
};
|
||||
};
|
||||
|
||||
type MediaCMSConfigMedia = {
|
||||
item: {
|
||||
displayAuthor: boolean;
|
||||
displayViews: boolean;
|
||||
displayPublishDate: boolean;
|
||||
};
|
||||
share: { options: string[] };
|
||||
};
|
||||
|
||||
type MediaCMSConfigNotifications = GlobalMediaCMS['contents']['notifications'];
|
||||
|
||||
type MediaCMSConfigOptions = {
|
||||
pages: {
|
||||
home: GlobalMediaCMS['pages']['home'];
|
||||
search: GlobalMediaCMS['pages']['search'];
|
||||
media: Omit<GlobalMediaCMS['pages']['media'], 'hideViews'> & {
|
||||
displayViews: boolean;
|
||||
};
|
||||
profile: GlobalMediaCMS['pages']['profile'];
|
||||
};
|
||||
embedded: {
|
||||
video: {
|
||||
dimensions: {
|
||||
width: number;
|
||||
widthUnit: 'px';
|
||||
// widthUnit: 'px' | 'percent'; // @note: The unit value "percent" is not used
|
||||
height: number;
|
||||
heightUnit: 'px';
|
||||
// heightUnit: 'px' | 'percent'; // @note: The unit value "percent" is not used
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type MediaCMSConfigPlaylists = GlobalMediaCMS['features']['playlists'];
|
||||
|
||||
type MediaCMSConfigSidebar = GlobalMediaCMS['features']['sideBar'];
|
||||
|
||||
type MediaCMSConfigSite = {
|
||||
api: string;
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
useRoundedCorners: boolean;
|
||||
version: string;
|
||||
};
|
||||
|
||||
type MediaCMSConfigTheme = Pick<GlobalMediaCMS['site'], 'logo'> & GlobalMediaCMS['site']['theme'];
|
||||
|
||||
type MediaCMSConfigUrl = {
|
||||
admin: string; // eg: '/admin'
|
||||
archive: {
|
||||
categories: string; // eg: './categories.html'
|
||||
tags: string; // eg: './tags.html';
|
||||
};
|
||||
changePassword: string; // eg: './change-password.html';
|
||||
embed: string; // eg: 'http://localhost/embed?m=';
|
||||
error404: string; // eg: './error.html';
|
||||
featured: string; // eg: './featured.html';
|
||||
home: string; // eg: './index.html'
|
||||
latest: string; // eg: './latest.html';
|
||||
manage: {
|
||||
comments: string; // eg: './manage-comments.html'
|
||||
media: string; // eg: './manage-media.html';
|
||||
users: string; // eg: './manage-users.html';
|
||||
};
|
||||
members: string; // eg: './members.html';
|
||||
profile: {
|
||||
about: string; // eg: './profile-about.html';
|
||||
media: string; // eg: './profile-media.html';
|
||||
playlists: string; // eg: './profile-playlists.html';
|
||||
shared_by_me: string; // eg: './profile-media.html/shared_by_me';
|
||||
shared_with_me: string; // eg: './profile-media.html/shared_with_me';
|
||||
};
|
||||
recommended: string; // eg: './recommended.html';
|
||||
register: string; // eg: './register.html';
|
||||
search: {
|
||||
base: string; // eg: './search.html';
|
||||
category: string; // eg: './search.html?c=';
|
||||
query: string; // eg: './search.html?q=';
|
||||
tag: string; // eg: './search.html?t=';
|
||||
};
|
||||
signin: string; // eg: './signin.html';
|
||||
signout: string; // eg: './signout.html';
|
||||
user: {
|
||||
addMedia: string; // eg: './add-media.html';
|
||||
editChannel: string; // eg: './edit-channel.html';
|
||||
editProfile: string; // eg: './edit-profile.html';
|
||||
history: string; // eg: './history.html';
|
||||
liked: string; // eg: './liked.html';
|
||||
};
|
||||
};
|
||||
|
||||
export type MediaCMSConfig = {
|
||||
api: MediaCMSConfigApi;
|
||||
contents: MediaCMSConfigContents;
|
||||
enabled: MediaCMSConfigEnabled;
|
||||
member: MediaCMSConfigMember;
|
||||
media: MediaCMSConfigMedia;
|
||||
notifications: MediaCMSConfigNotifications;
|
||||
options: MediaCMSConfigOptions;
|
||||
playlists: MediaCMSConfigPlaylists;
|
||||
sidebar: MediaCMSConfigSidebar;
|
||||
site: MediaCMSConfigSite;
|
||||
theme: MediaCMSConfigTheme;
|
||||
url: MediaCMSConfigUrl;
|
||||
};
|
||||
3
frontend/src/static/js/types/index.ts
Normal file
3
frontend/src/static/js/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './DeepPartial';
|
||||
export * from './GlobalMediaCMS';
|
||||
export * from './MediaCMSConfig';
|
||||
@@ -1,90 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function loadMediaData() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'LOAD_MEDIA_DATA',
|
||||
});
|
||||
}
|
||||
|
||||
export function likeMedia() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'LIKE_MEDIA',
|
||||
});
|
||||
}
|
||||
|
||||
export function dislikeMedia() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'DISLIKE_MEDIA',
|
||||
});
|
||||
}
|
||||
|
||||
export function reportMedia(reportDescription) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REPORT_MEDIA',
|
||||
reportDescription: !!reportDescription ? reportDescription.replace(/\s/g, '') : '',
|
||||
});
|
||||
}
|
||||
|
||||
export function copyShareLink(inputElem) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'COPY_SHARE_LINK',
|
||||
inputElement: inputElem,
|
||||
});
|
||||
}
|
||||
|
||||
export function copyEmbedMediaCode(inputElem) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'COPY_EMBED_MEDIA_CODE',
|
||||
inputElement: inputElem,
|
||||
});
|
||||
}
|
||||
|
||||
export function removeMedia() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REMOVE_MEDIA',
|
||||
});
|
||||
}
|
||||
|
||||
export function submitComment(commentText) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SUBMIT_COMMENT',
|
||||
commentText,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteComment(commentId) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'DELETE_COMMENT',
|
||||
commentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function createPlaylist(playlist_data) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'CREATE_PLAYLIST',
|
||||
playlist_data,
|
||||
});
|
||||
}
|
||||
|
||||
export function addMediaToPlaylist(playlist_id, media_id) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'ADD_MEDIA_TO_PLAYLIST',
|
||||
playlist_id,
|
||||
media_id,
|
||||
});
|
||||
}
|
||||
|
||||
export function removeMediaFromPlaylist(playlist_id, media_id) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REMOVE_MEDIA_FROM_PLAYLIST',
|
||||
playlist_id,
|
||||
media_id,
|
||||
});
|
||||
}
|
||||
|
||||
export function addNewPlaylist(playlist_data) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'APPEND_NEW_PLAYLIST',
|
||||
playlist_data,
|
||||
});
|
||||
}
|
||||
63
frontend/src/static/js/utils/actions/MediaPageActions.ts
Executable file
63
frontend/src/static/js/utils/actions/MediaPageActions.ts
Executable file
@@ -0,0 +1,63 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function loadMediaData() {
|
||||
dispatcher.dispatch({ type: 'LOAD_MEDIA_DATA' });
|
||||
}
|
||||
|
||||
export function likeMedia() {
|
||||
dispatcher.dispatch({ type: 'LIKE_MEDIA' });
|
||||
}
|
||||
|
||||
export function dislikeMedia() {
|
||||
dispatcher.dispatch({ type: 'DISLIKE_MEDIA' });
|
||||
}
|
||||
|
||||
// @todo: Revisit this
|
||||
export function reportMedia(reportDescription?: string | null) {
|
||||
dispatcher.dispatch({
|
||||
type: 'REPORT_MEDIA',
|
||||
reportDescription: typeof reportDescription === 'string' ? reportDescription.replace(/\s/g, '') : '',
|
||||
});
|
||||
}
|
||||
|
||||
export function copyShareLink(inputElem: HTMLInputElement) {
|
||||
dispatcher.dispatch({ type: 'COPY_SHARE_LINK', inputElement: inputElem });
|
||||
}
|
||||
|
||||
export function copyEmbedMediaCode(inputElem: HTMLTextAreaElement) {
|
||||
dispatcher.dispatch({ type: 'COPY_EMBED_MEDIA_CODE', inputElement: inputElem });
|
||||
}
|
||||
|
||||
export function removeMedia() {
|
||||
dispatcher.dispatch({ type: 'REMOVE_MEDIA' });
|
||||
}
|
||||
|
||||
export function submitComment(commentText: string) {
|
||||
dispatcher.dispatch({ type: 'SUBMIT_COMMENT', commentText });
|
||||
}
|
||||
|
||||
export function deleteComment(commentId: string | number) {
|
||||
dispatcher.dispatch({ type: 'DELETE_COMMENT', commentId });
|
||||
}
|
||||
|
||||
export function createPlaylist(playlist_data: { title: string; description: string }) {
|
||||
dispatcher.dispatch({ type: 'CREATE_PLAYLIST', playlist_data });
|
||||
}
|
||||
|
||||
export function addMediaToPlaylist(playlist_id: string, media_id: string) {
|
||||
dispatcher.dispatch({ type: 'ADD_MEDIA_TO_PLAYLIST', playlist_id, media_id });
|
||||
}
|
||||
|
||||
export function removeMediaFromPlaylist(playlist_id: string, media_id: string) {
|
||||
dispatcher.dispatch({ type: 'REMOVE_MEDIA_FROM_PLAYLIST', playlist_id, media_id });
|
||||
}
|
||||
|
||||
export function addNewPlaylist(playlist_data: {
|
||||
playlist_id: string;
|
||||
add_date: Date; // @todo: Revisit this
|
||||
description: string;
|
||||
title: string;
|
||||
media_list: string[]; // @todo: Revisit this
|
||||
}) {
|
||||
dispatcher.dispatch({ type: 'APPEND_NEW_PLAYLIST', playlist_data });
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function initPage(page) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'INIT_PAGE',
|
||||
page,
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleMediaAutoPlay() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_AUTO_PLAY',
|
||||
});
|
||||
}
|
||||
|
||||
export function addNotification(notification, notificationId) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'ADD_NOTIFICATION',
|
||||
notification,
|
||||
notificationId,
|
||||
});
|
||||
}
|
||||
13
frontend/src/static/js/utils/actions/PageActions.ts
Executable file
13
frontend/src/static/js/utils/actions/PageActions.ts
Executable file
@@ -0,0 +1,13 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function initPage(page: string) {
|
||||
dispatcher.dispatch({ type: 'INIT_PAGE', page });
|
||||
}
|
||||
|
||||
export function toggleMediaAutoPlay() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_AUTO_PLAY' });
|
||||
}
|
||||
|
||||
export function addNotification(notification: string, notificationId: string) {
|
||||
dispatcher.dispatch({ type: 'ADD_NOTIFICATION', notification, notificationId });
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function loadPlaylistData() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'LOAD_PLAYLIST_DATA',
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleSave() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_SAVE',
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePlaylist(playlist_data) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'UPDATE_PLAYLIST',
|
||||
playlist_data,
|
||||
});
|
||||
}
|
||||
|
||||
export function removePlaylist() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REMOVE_PLAYLIST',
|
||||
});
|
||||
}
|
||||
|
||||
export function removedMediaFromPlaylist(media_id, playlist_id) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'MEDIA_REMOVED_FROM_PLAYLIST',
|
||||
media_id,
|
||||
playlist_id,
|
||||
});
|
||||
}
|
||||
|
||||
export function reorderedMediaInPlaylist(newMediaData) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'PLAYLIST_MEDIA_REORDERED',
|
||||
playlist_media: newMediaData,
|
||||
});
|
||||
}
|
||||
26
frontend/src/static/js/utils/actions/PlaylistPageActions.ts
Executable file
26
frontend/src/static/js/utils/actions/PlaylistPageActions.ts
Executable file
@@ -0,0 +1,26 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function loadPlaylistData() {
|
||||
dispatcher.dispatch({ type: 'LOAD_PLAYLIST_DATA' });
|
||||
}
|
||||
|
||||
export function toggleSave() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_SAVE' });
|
||||
}
|
||||
|
||||
export function updatePlaylist(playlist_data: { title: string; description: string }) {
|
||||
dispatcher.dispatch({ type: 'UPDATE_PLAYLIST', playlist_data });
|
||||
}
|
||||
|
||||
export function removePlaylist() {
|
||||
dispatcher.dispatch({ type: 'REMOVE_PLAYLIST' });
|
||||
}
|
||||
|
||||
export function removedMediaFromPlaylist(media_id: string, playlist_id: string) {
|
||||
dispatcher.dispatch({ type: 'MEDIA_REMOVED_FROM_PLAYLIST', media_id, playlist_id });
|
||||
}
|
||||
|
||||
// @todo: Revisit this
|
||||
export function reorderedMediaInPlaylist(newMediaData: { [k: string]: any; thumbnail_url: string; url: string }[]) {
|
||||
dispatcher.dispatch({ type: 'PLAYLIST_MEDIA_REORDERED', playlist_media: newMediaData });
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function toggleLoop() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_LOOP',
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleShuffle() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_SHUFFLE',
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleSave() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'TOGGLE_SAVE',
|
||||
});
|
||||
}
|
||||
13
frontend/src/static/js/utils/actions/PlaylistViewActions.ts
Executable file
13
frontend/src/static/js/utils/actions/PlaylistViewActions.ts
Executable file
@@ -0,0 +1,13 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function toggleLoop() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_LOOP' });
|
||||
}
|
||||
|
||||
export function toggleShuffle() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_SHUFFLE' });
|
||||
}
|
||||
|
||||
export function toggleSave() {
|
||||
dispatcher.dispatch({ type: 'TOGGLE_SAVE' });
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function load_author_data() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'LOAD_AUTHOR_DATA',
|
||||
});
|
||||
}
|
||||
|
||||
export function remove_profile() {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REMOVE_PROFILE',
|
||||
});
|
||||
}
|
||||
9
frontend/src/static/js/utils/actions/ProfilePageActions.ts
Executable file
9
frontend/src/static/js/utils/actions/ProfilePageActions.ts
Executable file
@@ -0,0 +1,9 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function load_author_data() {
|
||||
dispatcher.dispatch({ type: 'LOAD_AUTHOR_DATA' });
|
||||
}
|
||||
|
||||
export function remove_profile() {
|
||||
dispatcher.dispatch({ type: 'REMOVE_PROFILE' });
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function requestPredictions(query) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'REQUEST_PREDICTIONS',
|
||||
query,
|
||||
});
|
||||
}
|
||||
5
frontend/src/static/js/utils/actions/SearchFieldActions.ts
Executable file
5
frontend/src/static/js/utils/actions/SearchFieldActions.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function requestPredictions(query: string) {
|
||||
dispatcher.dispatch({ type: 'REQUEST_PREDICTIONS', query });
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import Dispatcher from '../dispatcher.js';
|
||||
|
||||
export function set_viewer_mode(inTheaterMode) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_VIEWER_MODE',
|
||||
inTheaterMode,
|
||||
});
|
||||
}
|
||||
|
||||
export function set_player_volume(playerVolume) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_PLAYER_VOLUME',
|
||||
playerVolume,
|
||||
});
|
||||
}
|
||||
|
||||
export function set_player_sound_muted(playerSoundMuted) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_PLAYER_SOUND_MUTED',
|
||||
playerSoundMuted,
|
||||
});
|
||||
}
|
||||
|
||||
export function set_video_quality(quality) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_VIDEO_QUALITY',
|
||||
quality,
|
||||
});
|
||||
}
|
||||
|
||||
export function set_video_playback_speed(playbackSpeed) {
|
||||
Dispatcher.dispatch({
|
||||
type: 'SET_VIDEO_PLAYBACK_SPEED',
|
||||
playbackSpeed,
|
||||
});
|
||||
}
|
||||
23
frontend/src/static/js/utils/actions/VideoViewerActions.ts
Executable file
23
frontend/src/static/js/utils/actions/VideoViewerActions.ts
Executable file
@@ -0,0 +1,23 @@
|
||||
import { dispatcher } from '../dispatcher';
|
||||
|
||||
export function set_viewer_mode(inTheaterMode: boolean) {
|
||||
dispatcher.dispatch({ type: 'SET_VIEWER_MODE', inTheaterMode });
|
||||
}
|
||||
|
||||
export function set_player_volume(playerVolume: number) {
|
||||
dispatcher.dispatch({ type: 'SET_PLAYER_VOLUME', playerVolume });
|
||||
}
|
||||
|
||||
export function set_player_sound_muted(playerSoundMuted: boolean) {
|
||||
dispatcher.dispatch({ type: 'SET_PLAYER_SOUND_MUTED', playerSoundMuted });
|
||||
}
|
||||
|
||||
export function set_video_quality(
|
||||
quality: 'auto' | number // @todo: Check this again
|
||||
) {
|
||||
dispatcher.dispatch({ type: 'SET_VIDEO_QUALITY', quality });
|
||||
}
|
||||
|
||||
export function set_video_playback_speed(playbackSpeed: number) {
|
||||
dispatcher.dispatch({ type: 'SET_VIDEO_PLAYBACK_SPEED', playbackSpeed });
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as months } from './months';
|
||||
export { default as weekdays } from './weekdays';
|
||||
2
frontend/src/static/js/utils/constants/index.ts
Normal file
2
frontend/src/static/js/utils/constants/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './months';
|
||||
export * from './weekdays';
|
||||
@@ -1,14 +0,0 @@
|
||||
export default [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
14
frontend/src/static/js/utils/constants/months.ts
Executable file
14
frontend/src/static/js/utils/constants/months.ts
Executable file
@@ -0,0 +1,14 @@
|
||||
export const months = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
] as const;
|
||||
@@ -1 +0,0 @@
|
||||
export default ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
1
frontend/src/static/js/utils/constants/weekdays.ts
Executable file
1
frontend/src/static/js/utils/constants/weekdays.ts
Executable file
@@ -0,0 +1 @@
|
||||
export const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] as const;
|
||||
@@ -1,5 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
|
||||
export const ApiUrlContext = createContext(mediacmsConfig(window.MediaCMS).api);
|
||||
export const ApiUrlConsumer = ApiUrlContext.Consumer;
|
||||
5
frontend/src/static/js/utils/contexts/ApiUrlContext.ts
Normal file
5
frontend/src/static/js/utils/contexts/ApiUrlContext.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const ApiUrlContext = createContext(mediacmsConfig(window.MediaCMS).api);
|
||||
export const ApiUrlConsumer = ApiUrlContext.Consumer;
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import { translateString } from '../../utils/helpers/';
|
||||
|
||||
const config = mediacmsConfig(window.MediaCMS);
|
||||
|
||||
const links = config.url;
|
||||
const theme = config.theme;
|
||||
const user = config.member;
|
||||
|
||||
const hasThemeSwitcher = theme.switch.enabled && 'header' === theme.switch.position;
|
||||
|
||||
function popupTopNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (!user.is.anonymous) {
|
||||
if (user.can.addMedia) {
|
||||
items.push({
|
||||
link: links.user.addMedia,
|
||||
icon: 'video_call',
|
||||
text: translateString('Upload media'),
|
||||
itemAttr: {
|
||||
className: 'visible-only-in-small',
|
||||
},
|
||||
});
|
||||
|
||||
if (user.pages.media) {
|
||||
items.push({
|
||||
link: user.pages.media,
|
||||
icon: 'video_library',
|
||||
text: translateString('My media'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
link: links.signout,
|
||||
icon: 'exit_to_app',
|
||||
text: translateString('Sign out'),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function popupMiddleNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (hasThemeSwitcher) {
|
||||
items.push({
|
||||
itemType: 'open-subpage',
|
||||
icon: 'brightness_4',
|
||||
iconPos: 'left',
|
||||
text: 'Switch theme',
|
||||
buttonAttr: {
|
||||
className: 'change-page',
|
||||
'data-page-id': 'switch-theme',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.is.anonymous) {
|
||||
if (user.can.login) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
icon: 'login',
|
||||
iconPos: 'left',
|
||||
text: translateString('Sign in'),
|
||||
link: links.signin,
|
||||
linkAttr: {
|
||||
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.can.register) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
icon: 'person_add',
|
||||
iconPos: 'left',
|
||||
text: translateString('Register'),
|
||||
link: links.register,
|
||||
linkAttr: {
|
||||
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
link: links.user.editProfile,
|
||||
icon: 'brush',
|
||||
text: translateString('Edit profile'),
|
||||
});
|
||||
|
||||
if (user.can.changePassword) {
|
||||
items.push({
|
||||
link: links.changePassword,
|
||||
icon: 'lock',
|
||||
text: translateString('Change password'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function popupBottomNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (user.is.admin) {
|
||||
items.push({
|
||||
link: links.admin,
|
||||
icon: 'admin_panel_settings',
|
||||
text: 'MediaCMS administration',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export const HeaderContext = createContext({
|
||||
hasThemeSwitcher,
|
||||
popupNavItems: {
|
||||
top: popupTopNavItems(),
|
||||
middle: popupMiddleNavItems(),
|
||||
bottom: popupBottomNavItems(),
|
||||
},
|
||||
});
|
||||
|
||||
export const HeaderConsumer = HeaderContext.Consumer;
|
||||
130
frontend/src/static/js/utils/contexts/HeaderContext.ts
Normal file
130
frontend/src/static/js/utils/contexts/HeaderContext.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
import { translateString } from '../helpers';
|
||||
|
||||
const config = mediacmsConfig(window.MediaCMS);
|
||||
|
||||
const links = config.url;
|
||||
const theme = config.theme;
|
||||
const user = config.member;
|
||||
|
||||
const hasThemeSwitcher = theme.switch.enabled && 'header' === theme.switch.position;
|
||||
|
||||
function popupTopNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (!user.is.anonymous) {
|
||||
if (user.can.addMedia) {
|
||||
items.push({
|
||||
link: links.user.addMedia,
|
||||
icon: 'video_call',
|
||||
text: translateString('Upload media'),
|
||||
itemAttr: {
|
||||
className: 'visible-only-in-small',
|
||||
},
|
||||
});
|
||||
|
||||
if (user.pages.media) {
|
||||
items.push({
|
||||
link: user.pages.media,
|
||||
icon: 'video_library',
|
||||
text: translateString('My media'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
link: links.signout,
|
||||
icon: 'exit_to_app',
|
||||
text: translateString('Sign out'),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function popupMiddleNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (hasThemeSwitcher) {
|
||||
items.push({
|
||||
itemType: 'open-subpage',
|
||||
icon: 'brightness_4',
|
||||
iconPos: 'left',
|
||||
text: 'Switch theme',
|
||||
buttonAttr: {
|
||||
className: 'change-page',
|
||||
'data-page-id': 'switch-theme',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.is.anonymous) {
|
||||
if (user.can.login) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
icon: 'login',
|
||||
iconPos: 'left',
|
||||
text: translateString('Sign in'),
|
||||
link: links.signin,
|
||||
linkAttr: {
|
||||
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (user.can.register) {
|
||||
items.push({
|
||||
itemType: 'link',
|
||||
icon: 'person_add',
|
||||
iconPos: 'left',
|
||||
text: translateString('Register'),
|
||||
link: links.register,
|
||||
linkAttr: {
|
||||
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
link: links.user.editProfile,
|
||||
icon: 'brush',
|
||||
text: translateString('Edit profile'),
|
||||
});
|
||||
|
||||
if (user.can.changePassword) {
|
||||
items.push({
|
||||
link: links.changePassword,
|
||||
icon: 'lock',
|
||||
text: translateString('Change password'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function popupBottomNavItems() {
|
||||
const items = [];
|
||||
|
||||
if (user.is.admin) {
|
||||
items.push({
|
||||
link: links.admin,
|
||||
icon: 'admin_panel_settings',
|
||||
text: 'MediaCMS administration',
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export const HeaderContext = createContext({
|
||||
hasThemeSwitcher,
|
||||
popupNavItems: {
|
||||
top: popupTopNavItems(),
|
||||
middle: popupMiddleNavItems(),
|
||||
bottom: popupBottomNavItems(),
|
||||
},
|
||||
});
|
||||
|
||||
export const HeaderConsumer = HeaderContext.Consumer;
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { BrowserCache } from '../classes/';
|
||||
import { PageStore } from '../stores/';
|
||||
import { addClassname, removeClassname, inEmbeddedApp } from '../helpers/';
|
||||
import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { BrowserCache } from '../classes';
|
||||
import { PageStore } from '../stores';
|
||||
import { addClassname, removeClassname, inEmbeddedApp } from '../helpers';
|
||||
import SiteContext from './SiteContext';
|
||||
|
||||
let slidingSidebarTimeout;
|
||||
let slidingSidebarTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
function onSidebarVisibilityChange(visibleSidebar) {
|
||||
clearTimeout(slidingSidebarTimeout);
|
||||
function onSidebarVisibilityChange(visibleSidebar: boolean) {
|
||||
if (slidingSidebarTimeout) {
|
||||
clearTimeout(slidingSidebarTimeout);
|
||||
}
|
||||
|
||||
addClassname(document.body, 'sliding-sidebar');
|
||||
|
||||
@@ -39,18 +41,29 @@ function onSidebarVisibilityChange(visibleSidebar) {
|
||||
}, 20);
|
||||
}
|
||||
|
||||
export const LayoutContext = createContext();
|
||||
export const LayoutContext = createContext({
|
||||
enabledSidebar: true,
|
||||
visibleSidebar: true,
|
||||
setVisibleSidebar: (_: boolean) => {},
|
||||
visibleMobileSearch: false,
|
||||
toggleMobileSearch: () => {},
|
||||
toggleSidebar: () => {},
|
||||
});
|
||||
|
||||
export const LayoutProvider = ({ children }) => {
|
||||
export const LayoutProvider = ({ children }: { children: ReactNode }) => {
|
||||
const site = useContext(SiteContext);
|
||||
const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
|
||||
const cache = BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
|
||||
|
||||
const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media', []);
|
||||
const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []);
|
||||
|
||||
const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
|
||||
|
||||
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-sidebar'));
|
||||
const [visibleSidebar, setVisibleSidebar] = useState<boolean>(
|
||||
cache instanceof Error
|
||||
? true // @todo: Check this again
|
||||
: cache.get('visible-sidebar')
|
||||
);
|
||||
const [visibleMobileSearch, setVisibleMobileSearch] = useState(false);
|
||||
|
||||
const toggleMobileSearch = () => {
|
||||
@@ -71,7 +84,9 @@ export const LayoutProvider = ({ children }) => {
|
||||
}
|
||||
|
||||
if (!isEmbeddedApp && !isMediaPage && 1023 < window.innerWidth) {
|
||||
cache.set('visible-sidebar', visibleSidebar);
|
||||
if (!(cache instanceof Error)) {
|
||||
cache.set('visible-sidebar', visibleSidebar);
|
||||
}
|
||||
}
|
||||
}, [isEmbeddedApp, isMediaPage, visibleSidebar]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const LinksContext = createContext(mediacmsConfig(window.MediaCMS).url);
|
||||
export const LinksConsumer = LinksContext.Consumer;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const MemberContext = createContext(mediacmsConfig(window.MediaCMS).member);
|
||||
export const MemberConsumer = MemberContext.Consumer;
|
||||
@@ -1,4 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
|
||||
export const PlaylistsContext = createContext(mediacmsConfig(window.MediaCMS).playlists);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const PlaylistsContext = createContext(mediacmsConfig(window.MediaCMS).playlists);
|
||||
@@ -1,5 +0,0 @@
|
||||
import React, { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config.js';
|
||||
|
||||
export const ShareOptionsContext = createContext(mediacmsConfig(window.MediaCMS).media.share.options);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import { config as mediacmsConfig } from '../settings/config';
|
||||
|
||||
export const ShareOptionsContext = createContext(mediacmsConfig(window.MediaCMS).media.share.options);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user