Compare commits

..

133 Commits

Author SHA1 Message Date
Markos Gogoulos
b3f7669f53 a 2026-03-15 17:59:04 +02:00
Markos Gogoulos
a09c5904e1 a 2026-03-15 17:52:47 +02:00
Markos Gogoulos
875f5f90d1 v 2026-03-15 17:51:34 +02:00
Markos Gogoulos
133c80ed7b a 2026-03-15 17:48:34 +02:00
Markos Gogoulos
3c8b83480f a 2026-03-15 17:40:45 +02:00
Markos Gogoulos
a79acb5ab0 all 2026-03-15 17:09:51 +02:00
Markos Gogoulos
c1ce525f24 all 2026-03-15 17:08:57 +02:00
Markos Gogoulos
10cc4eb069 1 2026-03-14 16:29:54 +02:00
Markos Gogoulos
715e6dec72 a 2026-03-14 16:00:35 +02:00
Markos Gogoulos
b157ecf5d3 al 2026-03-14 16:00:26 +02:00
Markos Gogoulos
e33144b8d0 all 2026-03-13 20:16:17 +02:00
Markos Gogoulos
6279f8bd60 w 2026-03-13 19:27:23 +02:00
Markos Gogoulos
0e0ab6e22c all 2026-03-13 19:27:16 +02:00
Markos Gogoulos
a40a0037bd a 2026-03-13 18:54:51 +02:00
Markos Gogoulos
04693f11c8 a 2026-03-13 18:44:21 +02:00
Markos Gogoulos
fb71e1ead4 all 2026-03-13 18:44:07 +02:00
Markos Gogoulos
cb9b4df3f6 wtv 2026-03-13 15:08:14 +02:00
Markos Gogoulos
22c9c391f5 foo 2026-03-13 15:08:01 +02:00
Markos Gogoulos
f3bc24a7a2 foo 2026-03-13 15:01:54 +02:00
Markos Gogoulos
d27c245f58 a 2026-03-13 14:45:57 +02:00
Markos Gogoulos
633e666cea all 2026-03-13 14:45:04 +02:00
Markos Gogoulos
0954b3ff0d a 2026-03-13 14:14:04 +02:00
Markos Gogoulos
8d0f4827ed f 2026-03-13 10:09:29 +02:00
Markos Gogoulos
e1e04ea93a new 2026-03-13 09:51:43 +02:00
Markos Gogoulos
0c8a464845 d 2026-03-09 20:27:32 +02:00
Markos Gogoulos
e0974433a4 a 2026-03-09 20:09:20 +02:00
Markos Gogoulos
e130c7403c py 2026-03-09 19:58:14 +02:00
Markos Gogoulos
182fb9599a st 2026-03-09 19:57:46 +02:00
Markos Gogoulos
eadaffb27c a 2026-03-06 18:30:34 +02:00
Markos Gogoulos
27bba37a55 wtv 2026-03-06 17:56:56 +02:00
Markos Gogoulos
90b480b4f8 nces 2026-03-06 17:56:27 +02:00
Markos Gogoulos
583a8c4512 chec 2026-03-06 17:19:05 +02:00
Markos Gogoulos
b806ffd7f5 a 2026-03-06 17:01:15 +02:00
Markos Gogoulos
a6f7ce71c6 a 2026-03-06 16:56:29 +02:00
Markos Gogoulos
ab5f86ee4d e 2026-03-06 16:30:29 +02:00
Markos Gogoulos
216365fdb6 a 2026-03-06 16:29:51 +02:00
Markos Gogoulos
66b19b99f2 all 2026-03-04 13:27:42 +02:00
Markos Gogoulos
692b6b5f09 fix 2026-03-04 13:09:19 +02:00
Markos Gogoulos
814a7d3cee all 2026-03-04 09:12:45 +02:00
Markos Gogoulos
31571fc4cb stf 2026-03-04 09:02:01 +02:00
Markos Gogoulos
edd40d45b4 all 2026-03-03 23:07:09 +02:00
Markos Gogoulos
45774fbc8c a few 2026-03-02 09:41:33 +02:00
Markos Gogoulos
5ac0515d05 p 2026-02-24 17:36:00 +02:00
Markos Gogoulos
492e3366c0 test 2026-02-24 13:33:28 +02:00
Markos Gogoulos
3f1ca5d02f logo 2026-02-24 12:49:06 +02:00
Markos Gogoulos
8bc09b69f7 f 2026-02-24 12:29:10 +02:00
Markos Gogoulos
7b8e4975ab a 2026-02-24 11:32:15 +02:00
Markos Gogoulos
df8701a515 p 2026-02-24 11:21:45 +02:00
Markos Gogoulos
2c1103e906 rem 2026-02-24 11:15:52 +02:00
Markos Gogoulos
1c50dcdff8 a 2026-02-24 11:13:18 +02:00
Markos Gogoulos
b8fee7d06a temp 2026-02-24 11:00:22 +02:00
Markos Gogoulos
1df6e0c10d temp 2026-02-24 10:57:56 +02:00
Markos Gogoulos
dc328cd33c all 2026-02-24 10:54:03 +02:00
Markos Gogoulos
b39789c2c4 wtv 2026-02-20 22:38:52 +02:00
Markos Gogoulos
86fa084391 a 2026-02-20 15:59:42 +02:00
Markos Gogoulos
533a30b6cb ver 2026-02-20 15:26:18 +02:00
Markos Gogoulos
cdbed4bc1d a 2026-02-20 13:28:26 +02:00
Markos Gogoulos
58d336478c this 2026-02-20 12:36:50 +02:00
Markos Gogoulos
d28fc114f3 a 2026-02-20 12:17:13 +02:00
Markos Gogoulos
0056e1ed8f wtv 2026-02-19 19:36:48 +02:00
Markos Gogoulos
2423e9517e s 2026-02-19 19:25:53 +02:00
Markos Gogoulos
699f4bd09d b 2026-02-19 18:15:35 +02:00
Markos Gogoulos
6071921908 foo 2026-02-19 16:41:05 +02:00
Markos Gogoulos
33da2242b4 wt 2026-02-19 15:25:48 +02:00
Markos Gogoulos
21ddd04165 a 2026-02-19 13:00:41 +02:00
Markos Gogoulos
96755af3b2 foo 2026-02-19 11:07:53 +02:00
Markos Gogoulos
17526aeecd foo 2026-02-19 11:07:30 +02:00
Markos Gogoulos
23c5c544ce a 2026-02-19 10:48:47 +02:00
Markos Gogoulos
ae9d614e84 a 2026-02-12 18:25:25 +02:00
Markos Gogoulos
e044187973 all 2026-02-12 18:12:34 +02:00
Markos Gogoulos
b5a76c53e1 a 2026-02-12 17:43:04 +02:00
Markos Gogoulos
86348f1747 file 2026-02-12 16:45:35 +02:00
Markos Gogoulos
c6be65edd6 f 2026-02-12 16:33:41 +02:00
Markos Gogoulos
4656ba1176 awesome 2026-02-12 14:24:45 +02:00
Markos Gogoulos
7e7cd10549 wtv 2026-02-12 13:20:35 +02:00
Markos Gogoulos
d5919e10af wtv 2026-02-12 13:01:03 +02:00
Markos Gogoulos
f16b61644a w 2026-02-12 12:43:28 +02:00
Markos Gogoulos
c486574eb0 w 2026-02-12 12:42:31 +02:00
Markos Gogoulos
364d22dd8e w 2026-02-12 12:42:26 +02:00
Markos Gogoulos
209cede39c wtv 2026-02-12 12:41:59 +02:00
Markos Gogoulos
9b8f63881a del 2026-02-12 12:36:01 +02:00
Markos Gogoulos
82fb2665e6 wtv 2026-02-12 11:34:50 +02:00
Markos Gogoulos
abd22426d7 wtv 2026-02-12 11:28:40 +02:00
Markos Gogoulos
86f4484c36 a 2026-02-11 21:34:52 +02:00
Markos Gogoulos
f226c2fe1e all 2026-02-11 21:09:15 +02:00
Markos Gogoulos
98111234f8 wtv 2026-02-10 22:33:41 +02:00
Markos Gogoulos
467e273ff5 plugin 2026-02-10 19:08:35 +02:00
Markos Gogoulos
166fb5c00b works 2026-02-10 17:08:31 +02:00
Markos Gogoulos
d3f722611d fix 2026-02-10 15:10:05 +02:00
Markos Gogoulos
fae5634729 fix 2026-02-10 15:03:00 +02:00
Markos Gogoulos
5cdcef2205 wtv 2026-02-10 09:56:01 +02:00
Markos Gogoulos
f6e44f8343 cate 2026-02-10 09:33:52 +02:00
Markos Gogoulos
1b3a58bc60 changes 2026-02-10 09:17:04 +02:00
Markos Gogoulos
befc1f0efa fixes 2026-02-10 09:16:55 +02:00
Markos Gogoulos
6075514ffa fix 2026-02-07 12:51:40 +02:00
Markos Gogoulos
9c7cc293ce merge main 2026-02-06 19:34:28 +02:00
semantic-release-bot
f7c675596f chore(release): 7.5.0 [skip ci]
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)

### Features

* bump version ([36d815c](36d815c0cf))
2026-02-06 17:26:12 +00:00
Markos Gogoulos
36d815c0cf feat: bump version 2026-02-06 19:25:31 +02:00
semantic-release-bot
8f28b00a63 chore(release): 7.4.0 [skip ci]
## [7.4.0](https://github.com/mediacms-io/mediacms/compare/v7.3.0...v7.4.0) (2026-02-06)

### Features

* Add video player context menu with share/embed options ([#1472](https://github.com/mediacms-io/mediacms/issues/1472)) ([74952f6](74952f68d7))
2026-02-06 17:24:27 +00:00
Yiannis Christodoulou
74952f68d7 feat: Add video player context menu with share/embed options (#1472) 2026-02-06 19:23:51 +02:00
semantic-release-bot
7950a4655a chore(release): 7.3.0 [skip ci]
## [7.3.0](https://github.com/mediacms-io/mediacms/compare/v7.2.0...v7.3.0) (2026-02-06)

### Features

* add package json for semantic release ([b405a04](b405a04e34))
* add semantic release github actions ([76a27ae](76a27ae256))
* frontend unit tests ([1c15880](1c15880ae3))
* Implement persistent "Embed Mode" to hide UI shell via Session Storage ([#1484](https://github.com/mediacms-io/mediacms/issues/1484)) ([223e870](223e87073f))
* Improve Visual Distinction Between Trim and Chapters Editors ([#1445](https://github.com/mediacms-io/mediacms/issues/1445)) ([d9b1d6c](d9b1d6cab1))
* semantic release ([b76282f](b76282f9e4))

### Bug Fixes

* add delay to task creation ([1b3cdfd](1b3cdfd302))
* Add regex denoter and improve celerybeat gitignore ([#1446](https://github.com/mediacms-io/mediacms/issues/1446)) ([90331f3](90331f3b4a))
* adjust poster url for audio ([01912ea](01912ea1f9))
* Chapter numbering and preserve custom titles on segment reorder ([#1435](https://github.com/mediacms-io/mediacms/issues/1435)) ([cd7dd4f](cd7dd4f72c))
* Show default chapter names in textarea instead of placeholder text ([#1428](https://github.com/mediacms-io/mediacms/issues/1428)) ([5eb6faf](5eb6fafb8c))
* static files ([#1429](https://github.com/mediacms-io/mediacms/issues/1429)) ([ba2c31b](ba2c31b1e6))

### Documentation

* update page link ([aeef828](aeef8284bf))
2026-02-06 16:56:49 +00:00
Markos Gogoulos
b76282f9e4 feat: semantic release 2026-02-06 18:56:13 +02:00
Markos Gogoulos
b405a04e34 feat: add package json for semantic release 2026-02-06 18:53:03 +02:00
Markos Gogoulos
76a27ae256 feat: add semantic release github actions 2026-02-06 18:40:50 +02:00
Markos Gogoulos
156e0bddd6 all 2026-02-03 19:25:59 +02:00
Markos Gogoulos
637e9eabea a 2026-02-03 19:23:25 +02:00
Markos Gogoulos
e12f361935 a 2026-02-03 19:23:02 +02:00
Markos Gogoulos
c4d569e7b0 a 2026-02-03 19:21:23 +02:00
Markos Gogoulos
24d15d2639 mpeee 2026-02-03 19:20:37 +02:00
Markos Gogoulos
1c0805748d wtv 2026-02-01 16:53:21 +02:00
Markos Gogoulos
8e7f1ef15d changes 2026-02-01 16:43:56 +02:00
Markos Gogoulos
d00b4ed3c5 wtv 2026-02-01 15:51:14 +02:00
Markos Gogoulos
7126cf1cbd wtv 2026-02-01 14:49:42 +02:00
Markos Gogoulos
33135f81e9 rem 2026-02-01 14:44:36 +02:00
Markos Gogoulos
cfd305d563 all 2026-02-01 14:42:28 +02:00
Markos Gogoulos
d4bad33592 rem 2026-02-01 14:41:01 +02:00
Markos Gogoulos
7c0ea60d0e rem 2026-02-01 14:40:41 +02:00
Markos Gogoulos
c6726d1b13 pl 2026-02-01 14:19:16 +02:00
Markos Gogoulos
7c04cc30fb r 2026-02-01 13:45:00 +02:00
Markos Gogoulos
f28ce5990b all 2026-02-01 13:38:48 +02:00
Markos Gogoulos
27828d798e all! 2026-02-01 13:24:16 +02:00
Markos Gogoulos
ba53467033 wtv 2026-02-01 13:21:39 +02:00
Markos Gogoulos
a77761ec35 embed mode 2026-01-31 15:36:04 +02:00
Markos Gogoulos
4d07ae64c6 Merge branch 'main' into feat-lti-integration-select 2026-01-31 15:33:27 +02:00
Markos Gogoulos
223e87073f feat: Implement persistent "Embed Mode" to hide UI shell via Session Storage (#1484)
* initial implementation

* updates in ViewerInfoVideoTitleBanner component

* Implement persistent "Embed Mode" to hide UI shell via Session Storage

---------

Co-authored-by: Yiannis <1515939+styiannis@users.noreply.github.com>
2026-01-31 15:27:40 +02:00
Markos Gogoulos
1a96ac0703 t 2026-01-31 14:20:04 +02:00
Markos Gogoulos
9e0290ae49 a 2026-01-31 14:16:27 +02:00
Markos Gogoulos
610a22935f tjis 2026-01-31 14:03:50 +02:00
Markos Gogoulos
81e3325687 a 2026-01-31 14:03:13 +02:00
Markos Gogoulos
16f7b010ab lti 2026-01-31 13:57:44 +02:00
Markos Gogoulos
68cb21ff8a rem 2026-01-31 13:57:36 +02:00
Markos Gogoulos
2ddc3b24d7 remove 2026-01-31 13:57:18 +02:00
Markos Gogoulos
63e96b327e remove 2026-01-31 13:57:11 +02:00
262 changed files with 28160 additions and 9411 deletions

View File

@@ -0,0 +1,22 @@
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- reopened
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
environment: dev
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

47
.github/workflows/semantic-release.yaml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Semantic Release
on:
push:
branches:
- main
permissions:
contents: write
issues: write
jobs:
semantic-release:
runs-on: ubuntu-latest
environment: dev
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.GA_DEPLOY_KEY }}
# use SSH url to ensure git commit using a deploy key bypasses the main
# branch protection rule
- name: Configure Git for SSH Push
run: git remote set-url origin "git@github.com:${{ github.repository }}.git"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Install Dependencies
run: npm clean-install
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
- name: Run Semantic Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,3 +1,4 @@
/templates/cms/* /templates/cms/*
/templates/*.html /templates/*.html
*.scss *.scss
/frontend/

100
.releaserc.json Normal file
View File

@@ -0,0 +1,100 @@
{
"branches": [
"main"
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "chore",
"hidden": true
},
{
"type": "docs",
"section": "Documentation"
},
{
"type": "style",
"hidden": true
},
{
"type": "refactor",
"section": "Refactors"
},
{
"type": "perf",
"section": "Performance"
},
{
"type": "test",
"hidden": true
},
{
"type": "depr",
"section": "Deprecations"
}
]
}
}
],
[
"semantic-release-replace-plugin",
{
"replacements": [
{
"files": [
"package.json"
],
"from": "\"version\": \".*\"",
"to": "\"version\": \"${nextRelease.version}\"",
"results": [
{
"file": "package.json",
"hasChanged": true,
"numMatches": 1,
"numReplacements": 1
}
],
"countMatches": true
}
]
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md",
"changelogTitle": "# Changelog"
}
],
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": [
"package.json",
"CHANGELOG.md"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}

37
CHANGELOG.md Normal file
View File

@@ -0,0 +1,37 @@
# Changelog
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)
### Features
* bump version ([36d815c](https://github.com/mediacms-io/mediacms/commit/36d815c0cfbe21d3136541d410d545742b9ebecd))
## [7.4.0](https://github.com/mediacms-io/mediacms/compare/v7.3.0...v7.4.0) (2026-02-06)
### Features
* Add video player context menu with share/embed options ([#1472](https://github.com/mediacms-io/mediacms/issues/1472)) ([74952f6](https://github.com/mediacms-io/mediacms/commit/74952f68d79bc67617edb38eac62d2f5e7457565))
## [7.3.0](https://github.com/mediacms-io/mediacms/compare/v7.2.0...v7.3.0) (2026-02-06)
### Features
* add package json for semantic release ([b405a04](https://github.com/mediacms-io/mediacms/commit/b405a04e346ca81b7d3f4e099eb984e7785cdd0f))
* add semantic release github actions ([76a27ae](https://github.com/mediacms-io/mediacms/commit/76a27ae25609178c1bd47c947b9f1a082c791d61))
* frontend unit tests ([1c15880](https://github.com/mediacms-io/mediacms/commit/1c15880ae3ef1ce77f53d5b473dfc0cc448b4977))
* Implement persistent "Embed Mode" to hide UI shell via Session Storage ([#1484](https://github.com/mediacms-io/mediacms/issues/1484)) ([223e870](https://github.com/mediacms-io/mediacms/commit/223e87073f7d5e44130c9976854cac670db0ae66))
* Improve Visual Distinction Between Trim and Chapters Editors ([#1445](https://github.com/mediacms-io/mediacms/issues/1445)) ([d9b1d6c](https://github.com/mediacms-io/mediacms/commit/d9b1d6cab1d2bdfc16f799a0a27b64313e2e0d22))
* semantic release ([b76282f](https://github.com/mediacms-io/mediacms/commit/b76282f9e465a39c2da5e9a22184d1db23de3f56))
### Bug Fixes
* add delay to task creation ([1b3cdfd](https://github.com/mediacms-io/mediacms/commit/1b3cdfd302abc5e69ebe01ca52b5091f3b24c0b2))
* Add regex denoter and improve celerybeat gitignore ([#1446](https://github.com/mediacms-io/mediacms/issues/1446)) ([90331f3](https://github.com/mediacms-io/mediacms/commit/90331f3b4a2a5737de9dd75ab45c096944813c42))
* adjust poster url for audio ([01912ea](https://github.com/mediacms-io/mediacms/commit/01912ea1f99ef43793a65712539d6264f1f6410f))
* Chapter numbering and preserve custom titles on segment reorder ([#1435](https://github.com/mediacms-io/mediacms/issues/1435)) ([cd7dd4f](https://github.com/mediacms-io/mediacms/commit/cd7dd4f72c9f0bac466c680f686a9ecfdd3a38dd))
* Show default chapter names in textarea instead of placeholder text ([#1428](https://github.com/mediacms-io/mediacms/issues/1428)) ([5eb6faf](https://github.com/mediacms-io/mediacms/commit/5eb6fafb8c6928b8bc3fe5f0c7af315273f78a55))
* static files ([#1429](https://github.com/mediacms-io/mediacms/issues/1429)) ([ba2c31b](https://github.com/mediacms-io/mediacms/commit/ba2c31b1e65b7f508dee598b1f2d86f01f9bf036))
### Documentation
* update page link ([aeef828](https://github.com/mediacms-io/mediacms/commit/aeef8284bfba2a9a7f69c684f96c54f0e0e0cf92))

1
LTI_README.md Normal file
View File

@@ -0,0 +1 @@
Django admin → /admin/lti/ltiplatform/ --> Change to: https://YOUR_MOODLE/filter/mediacms/lti_auth.php

View File

@@ -1,253 +0,0 @@
# MediaCMS LTI 1.3 Integration Setup Guide
This guide walks you through integrating MediaCMS with a Learning Management System (LMS) like Moodle using LTI 1.3.
## 1. Configure MediaCMS Settings
Add these settings to `cms/local_settings.py`:
```python
# Enable LTI integration
USE_LTI = True
# Enable RBAC for course-based access control
USE_RBAC = True
# Your production domain
FRONTEND_HOST = 'https://your-mediacms-domain.com'
ALLOWED_HOSTS = ['your-mediacms-domain.com', 'localhost']
```
**Note:** LTI-specific cookie settings (SESSION_COOKIE_SAMESITE='None', etc.) are automatically applied when `USE_LTI=True`.
## 2. MediaCMS Configuration
### A. Verify HTTPS Setup
Ensure your MediaCMS server is running on HTTPS. LTI 1.3 requires HTTPS for security and iframe embedding.
### B. Register Your LMS Platform
1. Access Django Admin: `https://your-mediacms-domain.com/admin/lti/ltiplatform/`
2. Add new LTI Platform with these settings:
**Basic Info:**
- **Name:** My LMS (or any descriptive name)
- **Platform ID (Issuer):** Get this from your LMS (e.g., `https://mylms.example.com`)
- **Client ID:** You'll get this from your LMS after registering MediaCMS as an external tool
**OIDC Endpoints (get from your LMS):**
- **Auth Login URL:** `https://mylms.example.com/mod/lti/auth.php`
- **Auth Token URL:** `https://mylms.example.com/mod/lti/token.php`
- **Key Set URL:** `https://mylms.example.com/mod/lti/certs.php`
**Deployment IDs:** Add the deployment ID(s) provided by your LMS as a JSON list, e.g., `["1"]`
**Features:**
- ✓ Enable NRPS (Names and Role Provisioning)
- ✓ Enable Deep Linking
- ✓ Auto-create categories
- ✓ Auto-create users
- ✓ Auto-sync roles
### C. Note MediaCMS URLs for LMS Configuration
You'll need these URLs when configuring your LMS:
- **Tool URL:** `https://your-mediacms-domain.com/lti/launch/`
- **OIDC Login URL:** `https://your-mediacms-domain.com/lti/oidc/login/`
- **JWK Set URL:** `https://your-mediacms-domain.com/lti/jwks/`
- **Redirection URI:** `https://your-mediacms-domain.com/lti/launch/`
- **Deep Linking URL:** `https://your-mediacms-domain.com/lti/select-media/`
## 3. LMS Configuration (Moodle Example)
### A. Register MediaCMS as External Tool
1. Navigate to: **Site administration → Plugins → Activity modules → External tool → Manage tools**
2. Click **Configure a tool manually** or add new tool
**Basic Settings:**
- **Tool name:** MediaCMS
- **Tool URL:** `https://your-mediacms-domain.com/lti/launch/`
- **LTI version:** LTI 1.3
- **Tool configuration usage:** Show in activity chooser
**URLs:**
- **Public keyset URL:** `https://your-mediacms-domain.com/lti/jwks/`
- **Initiate login URL:** `https://your-mediacms-domain.com/lti/oidc/login/`
- **Redirection URI(s):** `https://your-mediacms-domain.com/lti/launch/`
**Launch Settings:**
- **Default launch container:** Embed (without blocks) or New window
- **Accept grades from tool:** Optional
- **Share launcher's name:** Always ⚠️ **REQUIRED for user names**
- **Share launcher's email:** Always ⚠️ **REQUIRED for user emails**
> **Important:** MediaCMS creates user accounts automatically on first LTI launch. To ensure users have proper names and email addresses in MediaCMS, you **must** set both "Share launcher's name with tool" and "Share launcher's email with tool" to **Always** in the Privacy settings. Without these settings, users will be created with only a username based on their LTI user ID.
**Services:**
- ✓ IMS LTI Names and Role Provisioning (for roster sync)
- ✓ IMS LTI Deep Linking (for media selection)
**Tool Settings (Important for Deep Linking):**
-**Supports Deep Linking (Content-Item Message)** - Enable this to allow instructors to browse and select media from MediaCMS when adding activities
3. Save the tool configuration
### B. Copy Platform Details to MediaCMS
After saving, your LMS will provide:
- Platform ID (Issuer URL)
- Client ID
- Deployment ID
Copy these values back to the LTIPlatform configuration in MediaCMS admin (step 2B above).
### C. Using MediaCMS in Courses
**Option 1: Embed "My Media" view (Default)**
- In a course, add activity → External tool → MediaCMS
- Leave the custom URL blank (uses default launch URL)
- Students/teachers will see their MediaCMS profile in an iframe
**Option 2: Link to a Specific Video**
- Add activity → External tool → MediaCMS
- Activity name: "November 2020 Video" (or any descriptive name)
- In the activity settings, find **"Custom parameters"** (may be under "Privacy" or "Additional Settings")
- Add this parameter:
```
media_friendly_token=abc123def
```
- Replace `abc123def` with your video's token from MediaCMS (found in the URL: `/view?m=abc123def`)
- Students clicking this activity will go directly to that specific video
**Option 3: Link to Any MediaCMS Page**
- Add activity → External tool → MediaCMS
- In **"Custom parameters"**, add:
```
redirect_path=/featured
```
- Supported paths:
- `/featured` - Featured videos page
- `/latest` - Latest videos
- `/search/?q=keyword` - Search results
- `/category/category-name` - Specific category
- `/user/username` - User's profile
- Any other MediaCMS page path
**Option 4: Embed Specific Media via Deep Linking (Interactive)**
⚠️ **Prerequisite:** Ensure "Supports Deep Linking (Content-Item Message)" is enabled in the External Tool configuration (see section 3.A above)
When adding the activity to your course:
1. Add activity → External tool → MediaCMS
2. In the activity settings, enable **"Supports Deep Linking"** checkbox (may be under "Tool settings" or "Privacy" section)
3. Click **"Select content"** button → This launches the MediaCMS media browser
4. Browse and select media from MediaCMS (you can select multiple)
5. Click **"Add to course"** → Returns to Moodle with selected media configured
6. The activity will be automatically configured with the selected media's title and embed URL
7. Students clicking this activity will go directly to the selected media
### D. Custom Parameters - Complete Examples
**Example 1: Link to a specific video titled "Lecture 1 - Introduction"**
```
Activity Name: Lecture 1 - Introduction
Custom Parameters:
media_friendly_token=a1b2c3d4e5
```
**Example 2: Link to course-specific videos**
```
Activity Name: Course Videos
Custom Parameters:
redirect_path=/category/biology101
```
**Example 3: Link to search results for "genetics"**
```
Activity Name: Genetics Videos
Custom Parameters:
redirect_path=/search/?q=genetics
```
**Example 4: Link to featured content**
```
Activity Name: Featured Videos
Custom Parameters:
redirect_path=/featured
```
**Where to find Custom Parameters in Moodle:**
1. When creating/editing the External Tool activity
2. Expand **"Privacy"** section, or look for **"Additional Settings"**
3. Find the **"Custom parameters"** text field
4. Enter one parameter per line in the format: `key=value`
## 4. Testing Checklist
- [ ] HTTPS is working on MediaCMS
- [ ] `USE_LTI = True` in local_settings.py
- [ ] LTIPlatform configured in Django admin
- [ ] External tool registered in LMS
- [ ] Launch from LMS creates new user in MediaCMS
- [ ] Course is mapped to MediaCMS category
- [ ] Users are added to RBAC group with correct roles
- [ ] Media from course category is visible to course members
- [ ] Public media is accessible
- [ ] Private media from other courses is not accessible
## 5. Default Role Mappings
The system automatically maps LMS roles to MediaCMS:
- **Instructor/Teacher** → advancedUser (global) + manager (course group)
- **Student/Learner** → user (global) + member (course group)
- **Teaching Assistant** → user (global) + contributor (course group)
- **Administrator** → manager (global) + manager (course group)
You can customize these in Django admin under **LTI Role Mappings**.
## 6. User Creation and Authentication
### User Creation via LTI
When a user launches MediaCMS from your LMS for the first time, a MediaCMS account is automatically created with:
- **Username:** Generated from email (preferred) or name, or a unique ID if neither is available
- **Email:** From LTI claim (if shared by LMS)
- **Name:** From LTI given_name/family_name claims (if shared by LMS)
- **Roles:** Mapped from LTI roles to MediaCMS permissions
- **Course membership:** Automatically added to the RBAC group for the course
### Privacy Settings Are Critical
⚠️ **For proper user accounts, you must configure the LTI tool's privacy settings in Moodle:**
1. Edit the External Tool configuration in Moodle
2. Go to the **Privacy** section
3. Set **"Share launcher's name with tool"** to **Always**
4. Set **"Share launcher's email with tool"** to **Always**
Without these settings:
- Users will not have proper names in MediaCMS
- Users will not have email addresses
- Usernames will be generic hashes (e.g., `lti_user_abc123def`)
### Authentication
Users created through LTI integration do **not** have a password set. They can only access MediaCMS through LTI launches from your LMS. This is intentional for security.
If you need a user to have both LTI access and direct login capability, manually set a password using:
```bash
python manage.py changepassword <username>
```
## Need Help?
If you encounter issues, check:
- `/admin/lti/ltilaunchlog/` for launch attempt logs
- Django logs for detailed error messages
- Ensure HTTPS is properly configured (required for iframe cookies)
- Verify all URLs are correct and accessible
- Check that the Client ID and Deployment ID match between MediaCMS and your LMS

View File

@@ -665,6 +665,5 @@ if USE_LTI:
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'None' CSRF_COOKIE_SAMESITE = 'None'
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" # SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
# Use cached_db for reliability - stores in both cache AND database # Consider using cached_db for reliability if sessions are lost between many LTI launches
# This prevents session loss during multiple simultaneous LTI launches

View File

@@ -1 +1 @@
VERSION = "7.8124" VERSION = "8.93"

View File

@@ -73,3 +73,8 @@ Can be set through the SAML Configurations tab:
3. **Group Role Mapping**: Maps the role returned by SAML (as set in the SAML Configuration tab) with the role in groups that user will be added 3. **Group Role Mapping**: Maps the role returned by SAML (as set in the SAML Configuration tab) with the role in groups that user will be added
4. **Group mapping**: This creates groups associated with this IDP. Group ids as they come from SAML, associated with MediaCMS groups 4. **Group mapping**: This creates groups associated with this IDP. Group ids as they come from SAML, associated with MediaCMS groups
5. **Category Mapping**: This maps a group id (from SAML response) with a category in MediaCMS 5. **Category Mapping**: This maps a group id (from SAML response) with a category in MediaCMS
## More options
USER_SEARCH_FIELD = "name_username_email"
ALLOW_MEDIA_REPLACEMENT = True

View File

@@ -146,6 +146,8 @@ class CategoryAdmin(admin.ModelAdmin):
list_filter.insert(0, "is_rbac_category") list_filter.insert(0, "is_rbac_category")
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False): if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
list_filter.insert(-1, "identity_provider") list_filter.insert(-1, "identity_provider")
if getattr(settings, 'USE_LTI', False):
list_filter.append("is_lms_course")
return list_filter return list_filter
@@ -155,6 +157,8 @@ class CategoryAdmin(admin.ModelAdmin):
list_display.insert(-1, "is_rbac_category") list_display.insert(-1, "is_rbac_category")
if getattr(settings, 'USE_IDENTITY_PROVIDERS', False): if getattr(settings, 'USE_IDENTITY_PROVIDERS', False):
list_display.insert(-1, "identity_provider") list_display.insert(-1, "identity_provider")
if getattr(settings, 'USE_LTI', False):
list_display.insert(-1, "is_lms_course")
return list_display return list_display

View File

@@ -5,7 +5,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from .methods import get_next_state, is_mediacms_editor from .methods import get_next_state, is_mediacms_editor
from .models import MEDIA_STATES, Category, Media, Subtitle from .models import MEDIA_STATES, Category, Media, Subtitle, Tag
from .widgets import CategoryModalWidget from .widgets import CategoryModalWidget
@@ -123,10 +123,12 @@ class MediaPublishForm(forms.ModelForm):
widgets = { widgets = {
"category": CategoryModalWidget(), "category": CategoryModalWidget(),
"state": forms.RadioSelect(),
} }
def __init__(self, user, *args, **kwargs): def __init__(self, user, *args, **kwargs):
self.user = user self.user = user
self.request = kwargs.pop('request', None)
super(MediaPublishForm, self).__init__(*args, **kwargs) super(MediaPublishForm, self).__init__(*args, **kwargs)
self.has_custom_permissions = self.instance.permissions.exists() if self.instance.pk else False self.has_custom_permissions = self.instance.permissions.exists() if self.instance.pk else False
@@ -169,6 +171,16 @@ class MediaPublishForm(forms.ModelForm):
self.fields['category'].queryset = Category.objects.filter(id__in=combined_category_ids).order_by('title') 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 = FormHelper()
self.helper.form_tag = True self.helper.form_tag = True
self.helper.form_class = 'post-form' self.helper.form_class = 'post-form'
@@ -186,6 +198,22 @@ class MediaPublishForm(forms.ModelForm):
self.helper.layout.append(FormActions(Submit('submit', 'Publish Media', css_class='primaryAction'))) 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): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
state = cleaned_data.get("state") state = cleaned_data.get("state")
@@ -266,6 +294,10 @@ class MediaPublishForm(forms.ModelForm):
media = super(MediaPublishForm, self).save(*args, **kwargs) media = super(MediaPublishForm, self).save(*args, **kwargs)
for course in media.category.filter(is_lms_course=True):
tag, _ = Tag.objects.get_or_create(title=course.title[:100])
media.tags.add(tag)
return media return media

View File

@@ -57,4 +57,4 @@ def translate_string(language_code, string):
if not check_language_code(language_code): if not check_language_code(language_code):
return string return string
return translation_strings[language_code].get(string, string) return translation_strings[language_code].get(string) or string

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "حذف الوسائط", "DELETE MEDIA": "حذف الوسائط",
"DOWNLOAD": "تحميل", "DOWNLOAD": "تحميل",
"DURATION": "المدة", "DURATION": "المدة",
"Delete Comments": "",
"Delete Media": "حذف الوسائط", "Delete Media": "حذف الوسائط",
"Delete media": "حذف الوسائط", "Delete media": "حذف الوسائط",
"Disable Comments": "تعطيل التعليقات", "Disable Comments": "تعطيل التعليقات",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "فشل تغيير المالك. يرجى المحاولة مرة أخرى.", "Failed to change owner. Please try again.": "فشل تغيير المالك. يرجى المحاولة مرة أخرى.",
"Failed to copy media.": "فشل نسخ الوسائط.", "Failed to copy media.": "فشل نسخ الوسائط.",
"Failed to create playlist": "فشل إنشاء قائمة التشغيل", "Failed to create playlist": "فشل إنشاء قائمة التشغيل",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "فشل حذف الوسائط. يرجى المحاولة مرة أخرى.", "Failed to delete media. Please try again.": "فشل حذف الوسائط. يرجى المحاولة مرة أخرى.",
"Failed to disable comments.": "فشل تعطيل التعليقات.", "Failed to disable comments.": "فشل تعطيل التعليقات.",
"Failed to disable download.": "فشل تعطيل التنزيل.", "Failed to disable download.": "فشل تعطيل التنزيل.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "تصفية المستخدمين الموجودين...", "Filter existing users...": "تصفية المستخدمين الموجودين...",
"Filter playlists...": "تصفية قوائم التشغيل...", "Filter playlists...": "تصفية قوائم التشغيل...",
"Filters": "الفلاتر", "Filters": "الفلاتر",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "اذهب", "Go": "اذهب",
"History": "التاريخ", "History": "التاريخ",
"Home": "الرئيسية", "Home": "الرئيسية",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "إدارة التعليقات", "Manage comments": "إدارة التعليقات",
"Manage media": "إدارة الوسائط", "Manage media": "إدارة الوسائط",
"Manage users": "إدارة المستخدمين", "Manage users": "إدارة المستخدمين",
"Management": "",
"Media": "وسائط", "Media": "وسائط",
"Media I own": "الوسائط التي أمتلكها", "Media I own": "الوسائط التي أمتلكها",
"Media was edited": "تم تعديل الوسائط", "Media was edited": "تم تعديل الوسائط",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "لا توجد نتائج لـ", "No results for": "لا توجد نتائج لـ",
"No tags": "لا توجد علامات", "No tags": "لا توجد علامات",
"No users to add": "لا يوجد مستخدمون لإضافتهم", "No users to add": "لا يوجد مستخدمون لإضافتهم",
"Organization": "",
"PLAYLISTS": "قوائم التشغيل", "PLAYLISTS": "قوائم التشغيل",
"PUBLISH STATE": "حالة النشر", "PUBLISH STATE": "حالة النشر",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "نشر في", "Published on": "نشر في",
"Recent uploads": "التحميلات الأخيرة", "Recent uploads": "التحميلات الأخيرة",
"Recommended": "موصى به", "Recommended": "موصى به",
"Record": "",
"Record Screen": "تسجيل الشاشة", "Record Screen": "تسجيل الشاشة",
"Record Screen with Audio": "",
"Register": "تسجيل", "Register": "تسجيل",
"Remove category": "إزالة الفئة", "Remove category": "إزالة الفئة",
"Remove from list": "إزالة من القائمة", "Remove from list": "إزالة من القائمة",
"Remove tag": "إزالة العلامة", "Remove tag": "إزالة العلامة",
"Remove user": "إزالة المستخدم", "Remove user": "إزالة المستخدم",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "حفظ", "SAVE": "حفظ",
"SEARCH": "بحث", "SEARCH": "بحث",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "تحديد جميع الوسائط", "Select all media": "تحديد جميع الوسائط",
"Select publish state:": "اختر حالة النشر:", "Select publish state:": "اختر حالة النشر:",
"Selected": "محدد", "Selected": "محدد",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "مشاركة مني", "Shared by me": "مشاركة مني",
"Shared with me": "مشاركة معي", "Shared with me": "مشاركة معي",
"Sharing": "",
"Sign in": "تسجيل الدخول", "Sign in": "تسجيل الدخول",
"Sign out": "تسجيل الخروج", "Sign out": "تسجيل الخروج",
"Sort By": "ترتيب حسب", "Sort By": "ترتيب حسب",
"Start Recording": "بدء التسجيل", "Start Recording": "بدء التسجيل",
"Start uploading media and sharing your work. Media that you upload will show up here.": "ابدأ في تحميل الوسائط ومشاركة عملك. ستظهر الوسائط التي تحملها هنا.", "Start uploading media and sharing your work. Media that you upload will show up here.": "ابدأ في تحميل الوسائط ومشاركة عملك. ستظهر الوسائط التي تحملها هنا.",
"Stop Recording": "إيقاف التسجيل", "Stop Recording": "إيقاف التسجيل",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "إرسال", "Submit": "إرسال",
"Subtitle was added": "تمت إضافة الترجمة", "Subtitle was added": "تمت إضافة الترجمة",
"Subtitles": "ترجمات", "Subtitles": "ترجمات",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "تم تفعيل التعليقات بنجاح", "Successfully Enabled comments": "تم تفعيل التعليقات بنجاح",
"Successfully changed owner": "تم تغيير المالك بنجاح", "Successfully changed owner": "تم تغيير المالك بنجاح",
"Successfully deleted": "تم الحذف بنجاح", "Successfully deleted": "تم الحذف بنجاح",
"Successfully deleted comments": "",
"Successfully updated": "تم التحديث بنجاح", "Successfully updated": "تم التحديث بنجاح",
"Successfully updated categories": "تم تحديث الفئات بنجاح", "Successfully updated categories": "تم تحديث الفئات بنجاح",
"Successfully updated playlist membership": "تم تحديث عضوية قائمة التشغيل بنجاح", "Successfully updated playlist membership": "تم تحديث عضوية قائمة التشغيل بنجاح",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "رفع الوسائط", "Upload media": "رفع الوسائط",
"Uploads": "التحميلات", "Uploads": "التحميلات",
"Users": "المستخدمون", "Users": "المستخدمون",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "عرض الكل", "VIEW ALL": "عرض الكل",
"Video": "فيديو", "Video": "فيديو",
"View all": "عرض الكل", "View all": "عرض الكل",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "مرحباً", "Welcome": "مرحباً",
"You are going to copy": "سوف تقوم بالنسخ", "You are going to copy": "سوف تقوم بالنسخ",
"You are going to delete": "سوف تقوم بالحذف", "You are going to delete": "سوف تقوم بالحذف",
"You are going to delete all comments from": "",
"You are going to disable comments to": "سوف تقوم بتعطيل التعليقات لـ", "You are going to disable comments to": "سوف تقوم بتعطيل التعليقات لـ",
"You are going to disable download for": "سوف تقوم بتعطيل التنزيل لـ", "You are going to disable download for": "سوف تقوم بتعطيل التنزيل لـ",
"You are going to enable comments to": "سوف تقوم بتفعيل التعليقات لـ", "You are going to enable comments to": "سوف تقوم بتفعيل التعليقات لـ",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "মিডিয়া মুছুন", "DELETE MEDIA": "মিডিয়া মুছুন",
"DOWNLOAD": "ডাউনলোড", "DOWNLOAD": "ডাউনলোড",
"DURATION": "সময়কাল", "DURATION": "সময়কাল",
"Delete Comments": "",
"Delete Media": "", "Delete Media": "",
"Delete media": "মিডিয়া মুছুন", "Delete media": "মিডিয়া মুছুন",
"Disable Comments": "", "Disable Comments": "",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "", "Failed to change owner. Please try again.": "",
"Failed to copy media.": "মিডিয়া কপি করতে ব্যর্থ হয়েছে।", "Failed to copy media.": "মিডিয়া কপি করতে ব্যর্থ হয়েছে।",
"Failed to create playlist": "", "Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "মিডিয়া মুছতে ব্যর্থ হয়েছে। দয়া করে আবার চেষ্টা করুন।", "Failed to delete media. Please try again.": "মিডিয়া মুছতে ব্যর্থ হয়েছে। দয়া করে আবার চেষ্টা করুন।",
"Failed to disable comments.": "মন্তব্য নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।", "Failed to disable comments.": "মন্তব্য নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
"Failed to disable download.": "ডাউনলোড নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।", "Failed to disable download.": "ডাউনলোড নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "", "Filter existing users...": "",
"Filter playlists...": "", "Filter playlists...": "",
"Filters": "ফিল্টার", "Filters": "ফিল্টার",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "যাও", "Go": "যাও",
"History": "ইতিহাস", "History": "ইতিহাস",
"Home": "বাড়ি", "Home": "বাড়ি",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "মন্তব্য পরিচালনা করুন", "Manage comments": "মন্তব্য পরিচালনা করুন",
"Manage media": "মিডিয়া পরিচালনা করুন", "Manage media": "মিডিয়া পরিচালনা করুন",
"Manage users": "ব্যবহারকারীদের পরিচালনা করুন", "Manage users": "ব্যবহারকারীদের পরিচালনা করুন",
"Management": "",
"Media": "মিডিয়া", "Media": "মিডিয়া",
"Media I own": "", "Media I own": "",
"Media was edited": "মিডিয়া সম্পাদিত হয়েছে", "Media was edited": "মিডিয়া সম্পাদিত হয়েছে",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "এর জন্য কোন ফলাফল নেই", "No results for": "এর জন্য কোন ফলাফল নেই",
"No tags": "", "No tags": "",
"No users to add": "", "No users to add": "",
"Organization": "",
"PLAYLISTS": "প্লেলিস্ট", "PLAYLISTS": "প্লেলিস্ট",
"PUBLISH STATE": "প্রকাশের অবস্থা", "PUBLISH STATE": "প্রকাশের অবস্থা",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "প্রকাশিত", "Published on": "প্রকাশিত",
"Recent uploads": "সাম্প্রতিক আপলোড", "Recent uploads": "সাম্প্রতিক আপলোড",
"Recommended": "প্রস্তাবিত", "Recommended": "প্রস্তাবিত",
"Record": "",
"Record Screen": "স্ক্রিন রেকর্ড করুন", "Record Screen": "স্ক্রিন রেকর্ড করুন",
"Record Screen with Audio": "",
"Register": "নিবন্ধন করুন", "Register": "নিবন্ধন করুন",
"Remove category": "", "Remove category": "",
"Remove from list": "", "Remove from list": "",
"Remove tag": "", "Remove tag": "",
"Remove user": "", "Remove user": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "সংরক্ষণ করুন", "SAVE": "সংরক্ষণ করুন",
"SEARCH": "অনুসন্ধান", "SEARCH": "অনুসন্ধান",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "", "Select all media": "",
"Select publish state:": "", "Select publish state:": "",
"Selected": "", "Selected": "",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "আমার দ্বারা শেয়ার করা", "Shared by me": "আমার দ্বারা শেয়ার করা",
"Shared with me": "আমার সাথে শেয়ার করা", "Shared with me": "আমার সাথে শেয়ার করা",
"Sharing": "",
"Sign in": "সাইন ইন করুন", "Sign in": "সাইন ইন করুন",
"Sign out": "সাইন আউট করুন", "Sign out": "সাইন আউট করুন",
"Sort By": "সাজান", "Sort By": "সাজান",
"Start Recording": "রেকর্ডিং শুরু করুন", "Start Recording": "রেকর্ডিং শুরু করুন",
"Start uploading media and sharing your work. Media that you upload will show up here.": "মিডিয়া আপলোড করা এবং আপনার কাজ শেয়ার করা শুরু করুন। আপনি যে মিডিয়া আপলোড করবেন তা এখানে প্রদর্শিত হবে।", "Start uploading media and sharing your work. Media that you upload will show up here.": "মিডিয়া আপলোড করা এবং আপনার কাজ শেয়ার করা শুরু করুন। আপনি যে মিডিয়া আপলোড করবেন তা এখানে প্রদর্শিত হবে।",
"Stop Recording": "রেকর্ডিং বন্ধ করুন", "Stop Recording": "রেকর্ডিং বন্ধ করুন",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "", "Submit": "",
"Subtitle was added": "সাবটাইটেল যোগ করা হয়েছে", "Subtitle was added": "সাবটাইটেল যোগ করা হয়েছে",
"Subtitles": "সাবটাইটেল", "Subtitles": "সাবটাইটেল",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "মন্তব্য সফলভাবে সক্রিয় হয়েছে", "Successfully Enabled comments": "মন্তব্য সফলভাবে সক্রিয় হয়েছে",
"Successfully changed owner": "", "Successfully changed owner": "",
"Successfully deleted": "সফলভাবে মুছে ফেলা হয়েছে", "Successfully deleted": "সফলভাবে মুছে ফেলা হয়েছে",
"Successfully deleted comments": "",
"Successfully updated": "", "Successfully updated": "",
"Successfully updated categories": "", "Successfully updated categories": "",
"Successfully updated playlist membership": "", "Successfully updated playlist membership": "",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "মিডিয়া আপলোড করুন", "Upload media": "মিডিয়া আপলোড করুন",
"Uploads": "আপলোডসমূহ", "Uploads": "আপলোডসমূহ",
"Users": "", "Users": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "সব দেখুন", "VIEW ALL": "সব দেখুন",
"Video": "ভিডিও", "Video": "ভিডিও",
"View all": "সব দেখুন", "View all": "সব দেখুন",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "স্বাগতম", "Welcome": "স্বাগতম",
"You are going to copy": "আপনি কপি করতে চলেছেন", "You are going to copy": "আপনি কপি করতে চলেছেন",
"You are going to delete": "আপনি মুছে ফেলতে চলেছেন", "You are going to delete": "আপনি মুছে ফেলতে চলেছেন",
"You are going to delete all comments from": "",
"You are going to disable comments to": "আপনি মন্তব্য নিষ্ক্রিয় করতে চলেছেন", "You are going to disable comments to": "আপনি মন্তব্য নিষ্ক্রিয় করতে চলেছেন",
"You are going to disable download for": "আপনি ডাউনলোড নিষ্ক্রিয় করতে চলেছেন", "You are going to disable download for": "আপনি ডাউনলোড নিষ্ক্রিয় করতে চলেছেন",
"You are going to enable comments to": "আপনি মন্তব্য সক্রিয় করতে চলেছেন", "You are going to enable comments to": "আপনি মন্তব্য সক্রিয় করতে চলেছেন",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "SLET MEDIE", "DELETE MEDIA": "SLET MEDIE",
"DOWNLOAD": "HENT", "DOWNLOAD": "HENT",
"DURATION": "VARIGHED", "DURATION": "VARIGHED",
"Delete Comments": "",
"Delete Media": "Slet Medie", "Delete Media": "Slet Medie",
"Delete media": "Slet medie", "Delete media": "Slet medie",
"Disable Comments": "Deaktiver Kommentarer", "Disable Comments": "Deaktiver Kommentarer",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Ændring af ejer mislykkedes. Prøv venligst igen.", "Failed to change owner. Please try again.": "Ændring af ejer mislykkedes. Prøv venligst igen.",
"Failed to copy media.": "Kopiering af medie mislykkedes.", "Failed to copy media.": "Kopiering af medie mislykkedes.",
"Failed to create playlist": "Oprettelse af playliste mislykkedes", "Failed to create playlist": "Oprettelse af playliste mislykkedes",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Sletning af medie mislykkedes. Prøv venligst igen.", "Failed to delete media. Please try again.": "Sletning af medie mislykkedes. Prøv venligst igen.",
"Failed to disable comments.": "Deaktivering af kommentarer mislykkedes.", "Failed to disable comments.": "Deaktivering af kommentarer mislykkedes.",
"Failed to disable download.": "Deaktivering af download mislykkedes.", "Failed to disable download.": "Deaktivering af download mislykkedes.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filtrer eksisterende brugere...", "Filter existing users...": "Filtrer eksisterende brugere...",
"Filter playlists...": "Filtrer playlister...", "Filter playlists...": "Filtrer playlister...",
"Filters": "Filtre", "Filters": "Filtre",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Vælg", "Go": "Vælg",
"History": "Historik", "History": "Historik",
"Home": "Hjem", "Home": "Hjem",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Administrer kommentarer", "Manage comments": "Administrer kommentarer",
"Manage media": "Administrer medier", "Manage media": "Administrer medier",
"Manage users": "Administrer brugere", "Manage users": "Administrer brugere",
"Management": "",
"Media": "Medier", "Media": "Medier",
"Media I own": "Medier jeg ejer", "Media I own": "Medier jeg ejer",
"Media was edited": "Mediet er blevet redigeret", "Media was edited": "Mediet er blevet redigeret",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Ingen resultater for", "No results for": "Ingen resultater for",
"No tags": "Ingen tags", "No tags": "Ingen tags",
"No users to add": "Ingen brugere at tilføje", "No users to add": "Ingen brugere at tilføje",
"Organization": "",
"PLAYLISTS": "PLAYLISTER", "PLAYLISTS": "PLAYLISTER",
"PUBLISH STATE": "PUBLICERINGSSTATUS", "PUBLISH STATE": "PUBLICERINGSSTATUS",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Udgivet på", "Published on": "Udgivet på",
"Recent uploads": "Nylige uploads", "Recent uploads": "Nylige uploads",
"Recommended": "Anbefalet", "Recommended": "Anbefalet",
"Record": "",
"Record Screen": "Optag skærm", "Record Screen": "Optag skærm",
"Record Screen with Audio": "",
"Register": "Registrer", "Register": "Registrer",
"Remove category": "Fjern kategori", "Remove category": "Fjern kategori",
"Remove from list": "Fjern fra liste", "Remove from list": "Fjern fra liste",
"Remove tag": "Fjern tag", "Remove tag": "Fjern tag",
"Remove user": "Fjern bruger", "Remove user": "Fjern bruger",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "GEM", "SAVE": "GEM",
"SEARCH": "SØG", "SEARCH": "SØG",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Vælg alle medier", "Select all media": "Vælg alle medier",
"Select publish state:": "Vælg publiceringsstatus:", "Select publish state:": "Vælg publiceringsstatus:",
"Selected": "Valgt", "Selected": "Valgt",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Delt af mig", "Shared by me": "Delt af mig",
"Shared with me": "Delt med mig", "Shared with me": "Delt med mig",
"Sharing": "",
"Sign in": "Log ind", "Sign in": "Log ind",
"Sign out": "Log ud", "Sign out": "Log ud",
"Sort By": "Sorter efter", "Sort By": "Sorter efter",
"Start Recording": "Start optagelse", "Start Recording": "Start optagelse",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Begynd at uploade medier og dele dit arbejde. Medier, du uploader, vil blive vist her.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Begynd at uploade medier og dele dit arbejde. Medier, du uploader, vil blive vist her.",
"Stop Recording": "Stop optagelse", "Stop Recording": "Stop optagelse",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Indsend", "Submit": "Indsend",
"Subtitle was added": "Undertekster tilføjet", "Subtitle was added": "Undertekster tilføjet",
"Subtitles": "Undertekster", "Subtitles": "Undertekster",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Kommentarer aktiveret med succes", "Successfully Enabled comments": "Kommentarer aktiveret med succes",
"Successfully changed owner": "Ejer ændret med succes", "Successfully changed owner": "Ejer ændret med succes",
"Successfully deleted": "Slettet med succes", "Successfully deleted": "Slettet med succes",
"Successfully deleted comments": "",
"Successfully updated": "Opdateret med succes", "Successfully updated": "Opdateret med succes",
"Successfully updated categories": "Kategorier opdateret med succes", "Successfully updated categories": "Kategorier opdateret med succes",
"Successfully updated playlist membership": "Playlistemedlemskab opdateret med succes", "Successfully updated playlist membership": "Playlistemedlemskab opdateret med succes",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Upload medie", "Upload media": "Upload medie",
"Uploads": "Uploads", "Uploads": "Uploads",
"Users": "Brugere", "Users": "Brugere",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "SE ALLE", "VIEW ALL": "SE ALLE",
"Video": "Video", "Video": "Video",
"View all": "Se alle", "View all": "Se alle",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Velkommen", "Welcome": "Velkommen",
"You are going to copy": "Du er ved at kopiere", "You are going to copy": "Du er ved at kopiere",
"You are going to delete": "Du er ved at slette", "You are going to delete": "Du er ved at slette",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Du er ved at deaktivere kommentarer til", "You are going to disable comments to": "Du er ved at deaktivere kommentarer til",
"You are going to disable download for": "Du er ved at deaktivere download for", "You are going to disable download for": "Du er ved at deaktivere download for",
"You are going to enable comments to": "Du er ved at aktivere kommentarer til", "You are going to enable comments to": "Du er ved at aktivere kommentarer til",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "MEDIEN LÖSCHEN", "DELETE MEDIA": "MEDIEN LÖSCHEN",
"DOWNLOAD": "HERUNTERLADEN", "DOWNLOAD": "HERUNTERLADEN",
"DURATION": "DAUER", "DURATION": "DAUER",
"Delete Comments": "",
"Delete Media": "Medien löschen", "Delete Media": "Medien löschen",
"Delete media": "Medien löschen", "Delete media": "Medien löschen",
"Disable Comments": "Kommentare deaktivieren", "Disable Comments": "Kommentare deaktivieren",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Fehler beim Ändern des Eigentümers. Bitte versuchen Sie es erneut.", "Failed to change owner. Please try again.": "Fehler beim Ändern des Eigentümers. Bitte versuchen Sie es erneut.",
"Failed to copy media.": "Fehler beim Kopieren der Medien.", "Failed to copy media.": "Fehler beim Kopieren der Medien.",
"Failed to create playlist": "Fehler beim Erstellen der Playlist", "Failed to create playlist": "Fehler beim Erstellen der Playlist",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Fehler beim Löschen der Medien. Bitte versuchen Sie es erneut.", "Failed to delete media. Please try again.": "Fehler beim Löschen der Medien. Bitte versuchen Sie es erneut.",
"Failed to disable comments.": "Fehler beim Deaktivieren der Kommentare.", "Failed to disable comments.": "Fehler beim Deaktivieren der Kommentare.",
"Failed to disable download.": "Fehler beim Deaktivieren des Downloads.", "Failed to disable download.": "Fehler beim Deaktivieren des Downloads.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Vorhandene Benutzer filtern...", "Filter existing users...": "Vorhandene Benutzer filtern...",
"Filter playlists...": "Playlists filtern...", "Filter playlists...": "Playlists filtern...",
"Filters": "Filter", "Filters": "Filter",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Los", "Go": "Los",
"History": "Verlauf", "History": "Verlauf",
"Home": "Startseite", "Home": "Startseite",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Kommentare verwalten", "Manage comments": "Kommentare verwalten",
"Manage media": "Medien verwalten", "Manage media": "Medien verwalten",
"Manage users": "Benutzer verwalten", "Manage users": "Benutzer verwalten",
"Management": "",
"Media": "Medien", "Media": "Medien",
"Media I own": "Medien, die mir gehören", "Media I own": "Medien, die mir gehören",
"Media was edited": "Medien wurden bearbeitet", "Media was edited": "Medien wurden bearbeitet",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Keine Ergebnisse für", "No results for": "Keine Ergebnisse für",
"No tags": "Keine Tags", "No tags": "Keine Tags",
"No users to add": "Keine Benutzer hinzuzufügen", "No users to add": "Keine Benutzer hinzuzufügen",
"Organization": "",
"PLAYLISTS": "PLAYLISTS", "PLAYLISTS": "PLAYLISTS",
"PUBLISH STATE": "VERÖFFENTLICHUNGSSTATUS", "PUBLISH STATE": "VERÖFFENTLICHUNGSSTATUS",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Veröffentlicht am", "Published on": "Veröffentlicht am",
"Recent uploads": "Neue Uploads", "Recent uploads": "Neue Uploads",
"Recommended": "Empfohlen", "Recommended": "Empfohlen",
"Record": "",
"Record Screen": "Bildschirm aufnehmen", "Record Screen": "Bildschirm aufnehmen",
"Record Screen with Audio": "",
"Register": "Registrieren", "Register": "Registrieren",
"Remove category": "Kategorie entfernen", "Remove category": "Kategorie entfernen",
"Remove from list": "Aus Liste entfernen", "Remove from list": "Aus Liste entfernen",
"Remove tag": "Tag entfernen", "Remove tag": "Tag entfernen",
"Remove user": "Benutzer entfernen", "Remove user": "Benutzer entfernen",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "SPEICHERN", "SAVE": "SPEICHERN",
"SEARCH": "SUCHE", "SEARCH": "SUCHE",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Alle Medien auswählen", "Select all media": "Alle Medien auswählen",
"Select publish state:": "Veröffentlichungsstatus auswählen:", "Select publish state:": "Veröffentlichungsstatus auswählen:",
"Selected": "Ausgewählt", "Selected": "Ausgewählt",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Von mir geteilt", "Shared by me": "Von mir geteilt",
"Shared with me": "Mit mir geteilt", "Shared with me": "Mit mir geteilt",
"Sharing": "",
"Sign in": "Anmelden", "Sign in": "Anmelden",
"Sign out": "Abmelden", "Sign out": "Abmelden",
"Sort By": "Sortieren nach", "Sort By": "Sortieren nach",
"Start Recording": "Aufnahme starten", "Start Recording": "Aufnahme starten",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Beginnen Sie mit dem Hochladen von Medien und dem Teilen Ihrer Arbeit. Hochgeladene Medien werden hier angezeigt.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Beginnen Sie mit dem Hochladen von Medien und dem Teilen Ihrer Arbeit. Hochgeladene Medien werden hier angezeigt.",
"Stop Recording": "Aufnahme stoppen", "Stop Recording": "Aufnahme stoppen",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Absenden", "Submit": "Absenden",
"Subtitle was added": "Untertitel wurde hinzugefügt", "Subtitle was added": "Untertitel wurde hinzugefügt",
"Subtitles": "Untertitel", "Subtitles": "Untertitel",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Kommentare erfolgreich aktiviert", "Successfully Enabled comments": "Kommentare erfolgreich aktiviert",
"Successfully changed owner": "Eigentümer erfolgreich geändert", "Successfully changed owner": "Eigentümer erfolgreich geändert",
"Successfully deleted": "Erfolgreich gelöscht", "Successfully deleted": "Erfolgreich gelöscht",
"Successfully deleted comments": "",
"Successfully updated": "Erfolgreich aktualisiert", "Successfully updated": "Erfolgreich aktualisiert",
"Successfully updated categories": "Kategorien erfolgreich aktualisiert", "Successfully updated categories": "Kategorien erfolgreich aktualisiert",
"Successfully updated playlist membership": "Playlist-Mitgliedschaft erfolgreich aktualisiert", "Successfully updated playlist membership": "Playlist-Mitgliedschaft erfolgreich aktualisiert",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Medien hochladen", "Upload media": "Medien hochladen",
"Uploads": "Uploads", "Uploads": "Uploads",
"Users": "Benutzer", "Users": "Benutzer",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "ALLE ANZEIGEN", "VIEW ALL": "ALLE ANZEIGEN",
"Video": "Video", "Video": "Video",
"View all": "Alle anzeigen", "View all": "Alle anzeigen",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Willkommen", "Welcome": "Willkommen",
"You are going to copy": "Sie werden kopieren", "You are going to copy": "Sie werden kopieren",
"You are going to delete": "Sie werden löschen", "You are going to delete": "Sie werden löschen",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Sie werden Kommentare deaktivieren für", "You are going to disable comments to": "Sie werden Kommentare deaktivieren für",
"You are going to disable download for": "Sie werden Download deaktivieren für", "You are going to disable download for": "Sie werden Download deaktivieren für",
"You are going to enable comments to": "Sie werden Kommentare aktivieren für", "You are going to enable comments to": "Sie werden Kommentare aktivieren für",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "ΔΙΑΓΡΑΦΗ ΑΡΧΕΙΟΥ", "DELETE MEDIA": "ΔΙΑΓΡΑΦΗ ΑΡΧΕΙΟΥ",
"DOWNLOAD": "ΚΑΤΕΒΑΣΜΑ", "DOWNLOAD": "ΚΑΤΕΒΑΣΜΑ",
"DURATION": "ΔΙΑΡΚΕΙΑ", "DURATION": "ΔΙΑΡΚΕΙΑ",
"Delete Comments": "",
"Delete Media": "Διαγραφή Αρχείου", "Delete Media": "Διαγραφή Αρχείου",
"Delete media": "Διαγραφή αρχείου", "Delete media": "Διαγραφή αρχείου",
"Disable Comments": "Απενεργοποίηση Σχολίων", "Disable Comments": "Απενεργοποίηση Σχολίων",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Αποτυχία αλλαγής ιδιοκτήτη. Παρακαλώ δοκιμάστε ξανά.", "Failed to change owner. Please try again.": "Αποτυχία αλλαγής ιδιοκτήτη. Παρακαλώ δοκιμάστε ξανά.",
"Failed to copy media.": "Αποτυχία αντιγραφής αρχείου.", "Failed to copy media.": "Αποτυχία αντιγραφής αρχείου.",
"Failed to create playlist": "Αποτυχία δημιουργίας λίστας", "Failed to create playlist": "Αποτυχία δημιουργίας λίστας",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Αποτυχία διαγραφής αρχείου. Παρακαλώ δοκιμάστε ξανά.", "Failed to delete media. Please try again.": "Αποτυχία διαγραφής αρχείου. Παρακαλώ δοκιμάστε ξανά.",
"Failed to disable comments.": "Αποτυχία απενεργοποίησης σχολίων.", "Failed to disable comments.": "Αποτυχία απενεργοποίησης σχολίων.",
"Failed to disable download.": "Αποτυχία απενεργοποίησης λήψης.", "Failed to disable download.": "Αποτυχία απενεργοποίησης λήψης.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Φιλτράρισμα υπαρχόντων χρηστών...", "Filter existing users...": "Φιλτράρισμα υπαρχόντων χρηστών...",
"Filter playlists...": "Φιλτράρισμα λιστών...", "Filter playlists...": "Φιλτράρισμα λιστών...",
"Filters": "Φίλτρα", "Filters": "Φίλτρα",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Μετάβαση", "Go": "Μετάβαση",
"History": "Ιστορικό", "History": "Ιστορικό",
"Home": "Αρχική", "Home": "Αρχική",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Διαχείριση σχολίων", "Manage comments": "Διαχείριση σχολίων",
"Manage media": "Διαχείριση αρχείων", "Manage media": "Διαχείριση αρχείων",
"Manage users": "Διαχείριση χρηστών", "Manage users": "Διαχείριση χρηστών",
"Management": "",
"Media": "Αρχεία", "Media": "Αρχεία",
"Media I own": "Δικά μου αρχεία", "Media I own": "Δικά μου αρχεία",
"Media was edited": "Το αρχείο επεξεργάστηκε", "Media was edited": "Το αρχείο επεξεργάστηκε",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Δεν υπάρχουν αποτελέσματα για", "No results for": "Δεν υπάρχουν αποτελέσματα για",
"No tags": "Δεν υπάρχουν ετικέτες", "No tags": "Δεν υπάρχουν ετικέτες",
"No users to add": "Δεν υπάρχουν χρήστες για προσθήκη", "No users to add": "Δεν υπάρχουν χρήστες για προσθήκη",
"Organization": "",
"PLAYLISTS": "ΛΙΣΤΕΣ", "PLAYLISTS": "ΛΙΣΤΕΣ",
"PUBLISH STATE": "ΚΑΤΑΣΤΑΣΗ ΔΗΜΟΣΙΕΥΣΗΣ", "PUBLISH STATE": "ΚΑΤΑΣΤΑΣΗ ΔΗΜΟΣΙΕΥΣΗΣ",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Δημοσιεύτηκε στις", "Published on": "Δημοσιεύτηκε στις",
"Recent uploads": "Πρόσφατα ανεβάσματα", "Recent uploads": "Πρόσφατα ανεβάσματα",
"Recommended": "Προτεινόμενα", "Recommended": "Προτεινόμενα",
"Record": "",
"Record Screen": "Καταγραφή οθόνης", "Record Screen": "Καταγραφή οθόνης",
"Record Screen with Audio": "",
"Register": "Εγγραφή", "Register": "Εγγραφή",
"Remove category": "Αφαίρεση κατηγορίας", "Remove category": "Αφαίρεση κατηγορίας",
"Remove from list": "Αφαίρεση από λίστα", "Remove from list": "Αφαίρεση από λίστα",
"Remove tag": "Αφαίρεση ετικέτας", "Remove tag": "Αφαίρεση ετικέτας",
"Remove user": "Αφαίρεση χρήστη", "Remove user": "Αφαίρεση χρήστη",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "ΑΠΟΘΗΚΕΥΣΗ", "SAVE": "ΑΠΟΘΗΚΕΥΣΗ",
"SEARCH": "ΑΝΑΖΗΤΗΣΗ", "SEARCH": "ΑΝΑΖΗΤΗΣΗ",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Επιλογή όλων των αρχείων", "Select all media": "Επιλογή όλων των αρχείων",
"Select publish state:": "Επιλέξτε κατάσταση δημοσίευσης:", "Select publish state:": "Επιλέξτε κατάσταση δημοσίευσης:",
"Selected": "Επιλεγμένα", "Selected": "Επιλεγμένα",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Κοινοποιήθηκαν από εμένα", "Shared by me": "Κοινοποιήθηκαν από εμένα",
"Shared with me": "Κοινοποιήθηκαν σε εμένα", "Shared with me": "Κοινοποιήθηκαν σε εμένα",
"Sharing": "",
"Sign in": "Σύνδεση", "Sign in": "Σύνδεση",
"Sign out": "Αποσύνδεση", "Sign out": "Αποσύνδεση",
"Sort By": "Ταξινόμηση", "Sort By": "Ταξινόμηση",
"Start Recording": "Έναρξη εγγραφής", "Start Recording": "Έναρξη εγγραφής",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Ξεκινήστε να ανεβάζετε αρχεία και να κοινοποιείτε τη δουλειά σας. Τα αρχεία που ανεβάζετε θα εμφανίζονται εδώ.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Ξεκινήστε να ανεβάζετε αρχεία και να κοινοποιείτε τη δουλειά σας. Τα αρχεία που ανεβάζετε θα εμφανίζονται εδώ.",
"Stop Recording": "Διακοπή εγγραφής", "Stop Recording": "Διακοπή εγγραφής",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Υποβολή", "Submit": "Υποβολή",
"Subtitle was added": "Οι υπότιτλοι προστέθηκαν", "Subtitle was added": "Οι υπότιτλοι προστέθηκαν",
"Subtitles": "Υπότιτλοι", "Subtitles": "Υπότιτλοι",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Τα σχόλια ενεργοποιήθηκαν με επιτυχία", "Successfully Enabled comments": "Τα σχόλια ενεργοποιήθηκαν με επιτυχία",
"Successfully changed owner": "Ο ιδιοκτήτης άλλαξε με επιτυχία", "Successfully changed owner": "Ο ιδιοκτήτης άλλαξε με επιτυχία",
"Successfully deleted": "Διαγράφηκε με επιτυχία", "Successfully deleted": "Διαγράφηκε με επιτυχία",
"Successfully deleted comments": "",
"Successfully updated": "Ενημερώθηκε με επιτυχία", "Successfully updated": "Ενημερώθηκε με επιτυχία",
"Successfully updated categories": "Οι κατηγορίες ενημερώθηκαν με επιτυχία", "Successfully updated categories": "Οι κατηγορίες ενημερώθηκαν με επιτυχία",
"Successfully updated playlist membership": "Η συμμετοχή στη λίστα ενημερώθηκε με επιτυχία", "Successfully updated playlist membership": "Η συμμετοχή στη λίστα ενημερώθηκε με επιτυχία",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Ανέβασμα αρχείων", "Upload media": "Ανέβασμα αρχείων",
"Uploads": "Ανεβάσματα", "Uploads": "Ανεβάσματα",
"Users": "Χρήστες", "Users": "Χρήστες",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "ΔΕΣ ΤΑ ΟΛΑ", "VIEW ALL": "ΔΕΣ ΤΑ ΟΛΑ",
"Video": "Βίντεο", "Video": "Βίντεο",
"View all": "Δες τα όλα", "View all": "Δες τα όλα",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Καλώς ήρθατε", "Welcome": "Καλώς ήρθατε",
"You are going to copy": "Πρόκειται να αντιγράψετε", "You are going to copy": "Πρόκειται να αντιγράψετε",
"You are going to delete": "Πρόκειται να διαγράψετε", "You are going to delete": "Πρόκειται να διαγράψετε",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Πρόκειται να απενεργοποιήσετε τα σχόλια για", "You are going to disable comments to": "Πρόκειται να απενεργοποιήσετε τα σχόλια για",
"You are going to disable download for": "Πρόκειται να απενεργοποιήσετε τη λήψη για", "You are going to disable download for": "Πρόκειται να απενεργοποιήσετε τη λήψη για",
"You are going to enable comments to": "Πρόκειται να ενεργοποιήσετε τα σχόλια για", "You are going to enable comments to": "Πρόκειται να ενεργοποιήσετε τα σχόλια για",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE": "", "DELETE": "",
"DELETE MEDIA": "", "DELETE MEDIA": "",
"Delete media": "", "Delete media": "",
"Delete Comments": "",
"Delete Media": "", "Delete Media": "",
"Disable Comments": "", "Disable Comments": "",
"Disable Download": "", "Disable Download": "",
@@ -71,6 +72,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "", "Failed to change owner. Please try again.": "",
"Failed to copy media.": "", "Failed to copy media.": "",
"Failed to create playlist": "", "Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "", "Failed to delete media. Please try again.": "",
"Failed to disable comments.": "", "Failed to disable comments.": "",
"Failed to disable download.": "", "Failed to disable download.": "",
@@ -102,6 +104,9 @@ translation_strings = {
"Filter existing users...": "", "Filter existing users...": "",
"Filter playlists...": "", "Filter playlists...": "",
"Filters": "", "Filters": "",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "", "Go": "",
"History": "", "History": "",
"Home": "", "Home": "",
@@ -117,6 +122,7 @@ translation_strings = {
"Loading existing users...": "", "Loading existing users...": "",
"Loading playlists...": "", "Loading playlists...": "",
"Loading tags...": "", "Loading tags...": "",
"Management": "",
"Manage": "", "Manage": "",
"Manage comments": "", "Manage comments": "",
"Manage media": "", "Manage media": "",
@@ -143,6 +149,7 @@ translation_strings = {
"No results for": "", "No results for": "",
"No tags": "", "No tags": "",
"No users to add": "", "No users to add": "",
"Organization": "",
"or": "", "or": "",
"Pdf": "", "Pdf": "",
"PLAYLISTS": "", "PLAYLISTS": "",
@@ -162,8 +169,13 @@ translation_strings = {
"Published": "", "Published": "",
"Published on": "", "Published on": "",
"Recent uploads": "", "Recent uploads": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Recommended": "", "Recommended": "",
"Record": "",
"Record Screen": "", "Record Screen": "",
"Record Screen with Audio": "",
"Register": "", "Register": "",
"Replace": "", "Replace": "",
"Remove category": "", "Remove category": "",
@@ -171,6 +183,7 @@ translation_strings = {
"Remove tag": "", "Remove tag": "",
"Remove user": "", "Remove user": "",
"results for": "", "results for": "",
"Settings": "",
"SAVE": "", "SAVE": "",
"SEARCH": "", "SEARCH": "",
"Search": "", "Search": "",
@@ -184,6 +197,12 @@ translation_strings = {
"Selected": "", "Selected": "",
"selected": "", "selected": "",
"SHARE": "", "SHARE": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Sharing": "",
"Share with": "",
"Shared by me": "", "Shared by me": "",
"Shared with me": "", "Shared with me": "",
"SHOW MORE": "", "SHOW MORE": "",
@@ -194,6 +213,7 @@ translation_strings = {
"Start Recording": "", "Start Recording": "",
"Start uploading media and sharing your work. Media that you upload will show up here.": "", "Start uploading media and sharing your work. Media that you upload will show up here.": "",
"Stop Recording": "", "Stop Recording": "",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"SUBMIT": "", "SUBMIT": "",
"Submit": "", "Submit": "",
"Subtitle was added": "", "Subtitle was added": "",
@@ -201,6 +221,7 @@ translation_strings = {
"Successfully changed owner": "", "Successfully changed owner": "",
"Successfully Copied": "", "Successfully Copied": "",
"Successfully deleted": "", "Successfully deleted": "",
"Successfully deleted comments": "",
"Successfully Disabled comments": "", "Successfully Disabled comments": "",
"Successfully Disabled Download": "", "Successfully Disabled Download": "",
"Successfully Enabled comments": "", "Successfully Enabled comments": "",
@@ -240,6 +261,9 @@ translation_strings = {
"UPLOAD MEDIA": "", "UPLOAD MEDIA": "",
"Upload media": "", "Upload media": "",
"Uploads": "", "Uploads": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"Users": "", "Users": "",
"Video": "", "Video": "",
"view": "", "view": "",
@@ -252,6 +276,7 @@ translation_strings = {
"yet": "", "yet": "",
"You are going to copy": "", "You are going to copy": "",
"You are going to delete": "", "You are going to delete": "",
"You are going to delete all comments from": "",
"You are going to disable comments to": "", "You are going to disable comments to": "",
"You are going to disable download for": "", "You are going to disable download for": "",
"You are going to enable comments to": "", "You are going to enable comments to": "",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "ELIMINAR MEDIOS", "DELETE MEDIA": "ELIMINAR MEDIOS",
"DOWNLOAD": "DESCARGAR", "DOWNLOAD": "DESCARGAR",
"DURATION": "DURACIÓN", "DURATION": "DURACIÓN",
"Delete Comments": "",
"Delete Media": "Eliminar Medio", "Delete Media": "Eliminar Medio",
"Delete media": "Eliminar medios", "Delete media": "Eliminar medios",
"Disable Comments": "Deshabilitar Comentarios", "Disable Comments": "Deshabilitar Comentarios",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Error al cambiar propietario. Por favor, inténtelo de nuevo.", "Failed to change owner. Please try again.": "Error al cambiar propietario. Por favor, inténtelo de nuevo.",
"Failed to copy media.": "Error al copiar medios.", "Failed to copy media.": "Error al copiar medios.",
"Failed to create playlist": "Error al crear lista de reproducción", "Failed to create playlist": "Error al crear lista de reproducción",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Error al eliminar medios. Por favor, inténtelo de nuevo.", "Failed to delete media. Please try again.": "Error al eliminar medios. Por favor, inténtelo de nuevo.",
"Failed to disable comments.": "Error al deshabilitar comentarios.", "Failed to disable comments.": "Error al deshabilitar comentarios.",
"Failed to disable download.": "Error al deshabilitar descarga.", "Failed to disable download.": "Error al deshabilitar descarga.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filtrar usuarios existentes...", "Filter existing users...": "Filtrar usuarios existentes...",
"Filter playlists...": "Filtrar listas de reproducción...", "Filter playlists...": "Filtrar listas de reproducción...",
"Filters": "Filtros", "Filters": "Filtros",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Ir", "Go": "Ir",
"History": "Historial", "History": "Historial",
"Home": "Inicio", "Home": "Inicio",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Gestionar comentarios", "Manage comments": "Gestionar comentarios",
"Manage media": "Gestionar medios", "Manage media": "Gestionar medios",
"Manage users": "Gestionar usuarios", "Manage users": "Gestionar usuarios",
"Management": "",
"Media": "Medios", "Media": "Medios",
"Media I own": "Medios que poseo", "Media I own": "Medios que poseo",
"Media was edited": "El medio fue editado", "Media was edited": "El medio fue editado",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "No hay resultados para", "No results for": "No hay resultados para",
"No tags": "Sin etiquetas", "No tags": "Sin etiquetas",
"No users to add": "No hay usuarios para agregar", "No users to add": "No hay usuarios para agregar",
"Organization": "",
"PLAYLISTS": "LISTAS DE REPRODUCCIÓN", "PLAYLISTS": "LISTAS DE REPRODUCCIÓN",
"PUBLISH STATE": "ESTADO DE PUBLICACIÓN", "PUBLISH STATE": "ESTADO DE PUBLICACIÓN",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Publicado en", "Published on": "Publicado en",
"Recent uploads": "Subidas recientes", "Recent uploads": "Subidas recientes",
"Recommended": "Recomendado", "Recommended": "Recomendado",
"Record": "",
"Record Screen": "Grabar pantalla", "Record Screen": "Grabar pantalla",
"Record Screen with Audio": "",
"Register": "Registrarse", "Register": "Registrarse",
"Remove category": "Eliminar categoría", "Remove category": "Eliminar categoría",
"Remove from list": "Eliminar de la lista", "Remove from list": "Eliminar de la lista",
"Remove tag": "Eliminar etiqueta", "Remove tag": "Eliminar etiqueta",
"Remove user": "Eliminar usuario", "Remove user": "Eliminar usuario",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "GUARDAR", "SAVE": "GUARDAR",
"SEARCH": "BUSCAR", "SEARCH": "BUSCAR",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Seleccionar todos los medios", "Select all media": "Seleccionar todos los medios",
"Select publish state:": "Seleccionar estado de publicación:", "Select publish state:": "Seleccionar estado de publicación:",
"Selected": "Seleccionado", "Selected": "Seleccionado",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Compartido por mí", "Shared by me": "Compartido por mí",
"Shared with me": "Compartido conmigo", "Shared with me": "Compartido conmigo",
"Sharing": "",
"Sign in": "Iniciar sesión", "Sign in": "Iniciar sesión",
"Sign out": "Cerrar sesión", "Sign out": "Cerrar sesión",
"Sort By": "Ordenar por", "Sort By": "Ordenar por",
"Start Recording": "Iniciar grabación", "Start Recording": "Iniciar grabación",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Comience a subir medios y compartir su trabajo. Los medios que suba aparecerán aquí.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Comience a subir medios y compartir su trabajo. Los medios que suba aparecerán aquí.",
"Stop Recording": "Detener grabación", "Stop Recording": "Detener grabación",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Enviar", "Submit": "Enviar",
"Subtitle was added": "El subtítulo fue agregado", "Subtitle was added": "El subtítulo fue agregado",
"Subtitles": "Subtítulos", "Subtitles": "Subtítulos",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Comentarios habilitados exitosamente", "Successfully Enabled comments": "Comentarios habilitados exitosamente",
"Successfully changed owner": "Propietario cambiado exitosamente", "Successfully changed owner": "Propietario cambiado exitosamente",
"Successfully deleted": "Eliminado exitosamente", "Successfully deleted": "Eliminado exitosamente",
"Successfully deleted comments": "",
"Successfully updated": "Actualizado exitosamente", "Successfully updated": "Actualizado exitosamente",
"Successfully updated categories": "Categorías actualizadas exitosamente", "Successfully updated categories": "Categorías actualizadas exitosamente",
"Successfully updated playlist membership": "Membresía de lista de reproducción actualizada exitosamente", "Successfully updated playlist membership": "Membresía de lista de reproducción actualizada exitosamente",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Subir medios", "Upload media": "Subir medios",
"Uploads": "Subidas", "Uploads": "Subidas",
"Users": "Usuarios", "Users": "Usuarios",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "VER TODO", "VIEW ALL": "VER TODO",
"Video": "Video", "Video": "Video",
"View all": "Ver todo", "View all": "Ver todo",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Bienvenido", "Welcome": "Bienvenido",
"You are going to copy": "Vas a copiar", "You are going to copy": "Vas a copiar",
"You are going to delete": "Vas a eliminar", "You are going to delete": "Vas a eliminar",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Vas a deshabilitar comentarios de", "You are going to disable comments to": "Vas a deshabilitar comentarios de",
"You are going to disable download for": "Vas a deshabilitar descarga de", "You are going to disable download for": "Vas a deshabilitar descarga de",
"You are going to enable comments to": "Vas a habilitar comentarios de", "You are going to enable comments to": "Vas a habilitar comentarios de",

View File

@@ -49,6 +49,7 @@ translation_strings = {
"DELETE MEDIA": "SUPPRIMER LE MÉDIA", "DELETE MEDIA": "SUPPRIMER LE MÉDIA",
"DOWNLOAD": "TÉLÉCHARGER", "DOWNLOAD": "TÉLÉCHARGER",
"DURATION": "DURÉE", "DURATION": "DURÉE",
"Delete Comments": "",
"Delete Media": "Supprimer le média", "Delete Media": "Supprimer le média",
"Delete media": "Supprimer le média", "Delete media": "Supprimer le média",
"Disable Comments": "Désactiver les commentaires", "Disable Comments": "Désactiver les commentaires",
@@ -71,6 +72,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Échec du changement de propriétaire. Veuillez réessayer.", "Failed to change owner. Please try again.": "Échec du changement de propriétaire. Veuillez réessayer.",
"Failed to copy media.": "Échec de la copie du média.", "Failed to copy media.": "Échec de la copie du média.",
"Failed to create playlist": "Échec de la création de la playlist", "Failed to create playlist": "Échec de la création de la playlist",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Échec de la suppression du média. Veuillez réessayer.", "Failed to delete media. Please try again.": "Échec de la suppression du média. Veuillez réessayer.",
"Failed to disable comments.": "Échec de la désactivation des commentaires.", "Failed to disable comments.": "Échec de la désactivation des commentaires.",
"Failed to disable download.": "Échec de la désactivation du téléchargement.", "Failed to disable download.": "Échec de la désactivation du téléchargement.",
@@ -102,6 +104,9 @@ translation_strings = {
"Filter existing users...": "Filtrer les utilisateurs existants...", "Filter existing users...": "Filtrer les utilisateurs existants...",
"Filter playlists...": "Filtrer les playlists...", "Filter playlists...": "Filtrer les playlists...",
"Filters": "Filtres", "Filters": "Filtres",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Aller", "Go": "Aller",
"History": "Historique", "History": "Historique",
"Home": "Accueil", "Home": "Accueil",
@@ -122,6 +127,7 @@ translation_strings = {
"Manage comments": "Gérer les commentaires", "Manage comments": "Gérer les commentaires",
"Manage media": "Gérer les médias", "Manage media": "Gérer les médias",
"Manage users": "Gérer les utilisateurs", "Manage users": "Gérer les utilisateurs",
"Management": "",
"Media": "Média", "Media": "Média",
"Media I own": "Médias que je possède", "Media I own": "Médias que je possède",
"Media was edited": "Le média a été modifié", "Media was edited": "Le média a été modifié",
@@ -138,6 +144,7 @@ translation_strings = {
"No results for": "Aucun résultat pour", "No results for": "Aucun résultat pour",
"No tags": "Aucun tag", "No tags": "Aucun tag",
"No users to add": "Aucun utilisateur à ajouter", "No users to add": "Aucun utilisateur à ajouter",
"Organization": "",
"PLAYLISTS": "PLAYLISTS", "PLAYLISTS": "PLAYLISTS",
"PUBLISH STATE": "ÉTAT DE PUBLICATION", "PUBLISH STATE": "ÉTAT DE PUBLICATION",
"Pdf": "PDF", "Pdf": "PDF",
@@ -157,12 +164,17 @@ translation_strings = {
"Published on": "Publié le", "Published on": "Publié le",
"Recent uploads": "Téléchargements récents", "Recent uploads": "Téléchargements récents",
"Recommended": "Recommandé", "Recommended": "Recommandé",
"Record": "",
"Record Screen": "Enregistrer l'écran", "Record Screen": "Enregistrer l'écran",
"Record Screen with Audio": "",
"Register": "S'inscrire", "Register": "S'inscrire",
"Remove category": "Supprimer la catégorie", "Remove category": "Supprimer la catégorie",
"Remove from list": "Supprimer de la liste", "Remove from list": "Supprimer de la liste",
"Remove tag": "Supprimer le tag", "Remove tag": "Supprimer le tag",
"Remove user": "Supprimer l'utilisateur", "Remove user": "Supprimer l'utilisateur",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "ENREGISTRER", "SAVE": "ENREGISTRER",
"SEARCH": "RECHERCHER", "SEARCH": "RECHERCHER",
@@ -179,14 +191,22 @@ translation_strings = {
"Select all media": "Sélectionner tous les médias", "Select all media": "Sélectionner tous les médias",
"Select publish state:": "Sélectionner l'état de publication:", "Select publish state:": "Sélectionner l'état de publication:",
"Selected": "Sélectionné", "Selected": "Sélectionné",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Partagé par moi", "Shared by me": "Partagé par moi",
"Shared with me": "Partagé avec moi", "Shared with me": "Partagé avec moi",
"Sharing": "",
"Sign in": "Se connecter", "Sign in": "Se connecter",
"Sign out": "Se déconnecter", "Sign out": "Se déconnecter",
"Sort By": "Trier par", "Sort By": "Trier par",
"Start Recording": "Commencer l'enregistrement", "Start Recording": "Commencer l'enregistrement",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Commencez à télécharger des médias et à partager votre travail. Les médias que vous téléchargez apparaîtront ici.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Commencez à télécharger des médias et à partager votre travail. Les médias que vous téléchargez apparaîtront ici.",
"Stop Recording": "Arrêter l'enregistrement", "Stop Recording": "Arrêter l'enregistrement",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Soumettre", "Submit": "Soumettre",
"Subtitle was added": "Le sous-titre a été ajouté", "Subtitle was added": "Le sous-titre a été ajouté",
"Subtitles": "Sous-titres", "Subtitles": "Sous-titres",
@@ -197,6 +217,7 @@ translation_strings = {
"Successfully Enabled comments": "Commentaires activés avec succès", "Successfully Enabled comments": "Commentaires activés avec succès",
"Successfully changed owner": "Propriétaire changé avec succès", "Successfully changed owner": "Propriétaire changé avec succès",
"Successfully deleted": "Supprimé avec succès", "Successfully deleted": "Supprimé avec succès",
"Successfully deleted comments": "",
"Successfully updated": "Mis à jour avec succès", "Successfully updated": "Mis à jour avec succès",
"Successfully updated categories": "Catégories mises à jour avec succès", "Successfully updated categories": "Catégories mises à jour avec succès",
"Successfully updated playlist membership": "Adhésion à la playlist mise à jour avec succès", "Successfully updated playlist membership": "Adhésion à la playlist mise à jour avec succès",
@@ -233,6 +254,9 @@ translation_strings = {
"Upload media": "Télécharger des médias", "Upload media": "Télécharger des médias",
"Uploads": "Téléchargements", "Uploads": "Téléchargements",
"Users": "Utilisateurs", "Users": "Utilisateurs",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "VOIR TOUT", "VIEW ALL": "VOIR TOUT",
"Video": "Vidéo", "Video": "Vidéo",
"View all": "Voir tout", "View all": "Voir tout",
@@ -241,6 +265,7 @@ translation_strings = {
"Welcome": "Bienvenue", "Welcome": "Bienvenue",
"You are going to copy": "Vous allez copier", "You are going to copy": "Vous allez copier",
"You are going to delete": "Vous allez supprimer", "You are going to delete": "Vous allez supprimer",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Vous allez désactiver les commentaires de", "You are going to disable comments to": "Vous allez désactiver les commentaires de",
"You are going to disable download for": "Vous allez désactiver le téléchargement de", "You are going to disable download for": "Vous allez désactiver le téléchargement de",
"You are going to enable comments to": "Vous allez activer les commentaires de", "You are going to enable comments to": "Vous allez activer les commentaires de",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "מחק מדיה", "DELETE MEDIA": "מחק מדיה",
"DOWNLOAD": "הורד", "DOWNLOAD": "הורד",
"DURATION": "משך", "DURATION": "משך",
"Delete Comments": "",
"Delete Media": "", "Delete Media": "",
"Delete media": "מחק מדיה", "Delete media": "מחק מדיה",
"Disable Comments": "", "Disable Comments": "",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "", "Failed to change owner. Please try again.": "",
"Failed to copy media.": "העתקת המדיה נכשלה.", "Failed to copy media.": "העתקת המדיה נכשלה.",
"Failed to create playlist": "", "Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "מחיקת המדיה נכשלה. אנא נסה שוב.", "Failed to delete media. Please try again.": "מחיקת המדיה נכשלה. אנא נסה שוב.",
"Failed to disable comments.": "ביטול התגובות נכשל.", "Failed to disable comments.": "ביטול התגובות נכשל.",
"Failed to disable download.": "ביטול ההורדה נכשל.", "Failed to disable download.": "ביטול ההורדה נכשל.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "", "Filter existing users...": "",
"Filter playlists...": "", "Filter playlists...": "",
"Filters": "מסננים", "Filters": "מסננים",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "בצע", "Go": "בצע",
"History": "היסטוריה", "History": "היסטוריה",
"Home": "דף הבית", "Home": "דף הבית",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "ניהול תגובות", "Manage comments": "ניהול תגובות",
"Manage media": "ניהול מדיה", "Manage media": "ניהול מדיה",
"Manage users": "ניהול משתמשים", "Manage users": "ניהול משתמשים",
"Management": "",
"Media": "מדיה", "Media": "מדיה",
"Media I own": "", "Media I own": "",
"Media was edited": "המדיה נערכה", "Media was edited": "המדיה נערכה",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "אין תוצאות עבור", "No results for": "אין תוצאות עבור",
"No tags": "", "No tags": "",
"No users to add": "", "No users to add": "",
"Organization": "",
"PLAYLISTS": "פלייליסטים", "PLAYLISTS": "פלייליסטים",
"PUBLISH STATE": "מצב פרסום", "PUBLISH STATE": "מצב פרסום",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "פורסם בתאריך", "Published on": "פורסם בתאריך",
"Recent uploads": "העלאות אחרונות", "Recent uploads": "העלאות אחרונות",
"Recommended": "מומלץ", "Recommended": "מומלץ",
"Record": "",
"Record Screen": "הקלטת מסך", "Record Screen": "הקלטת מסך",
"Record Screen with Audio": "",
"Register": "הרשמה", "Register": "הרשמה",
"Remove category": "", "Remove category": "",
"Remove from list": "", "Remove from list": "",
"Remove tag": "", "Remove tag": "",
"Remove user": "", "Remove user": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "שמור", "SAVE": "שמור",
"SEARCH": "חפש", "SEARCH": "חפש",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "", "Select all media": "",
"Select publish state:": "", "Select publish state:": "",
"Selected": "", "Selected": "",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "שותף על ידי", "Shared by me": "שותף על ידי",
"Shared with me": "שותף איתי", "Shared with me": "שותף איתי",
"Sharing": "",
"Sign in": "התחבר", "Sign in": "התחבר",
"Sign out": "התנתק", "Sign out": "התנתק",
"Sort By": "מיין לפי", "Sort By": "מיין לפי",
"Start Recording": "התחל הקלטה", "Start Recording": "התחל הקלטה",
"Start uploading media and sharing your work. Media that you upload will show up here.": "התחל להעלות מדיה ולשתף את עבודתך. המדיה שתעלה תופיע כאן.", "Start uploading media and sharing your work. Media that you upload will show up here.": "התחל להעלות מדיה ולשתף את עבודתך. המדיה שתעלה תופיע כאן.",
"Stop Recording": "עצור הקלטה", "Stop Recording": "עצור הקלטה",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "", "Submit": "",
"Subtitle was added": "הכתובית נוספה", "Subtitle was added": "הכתובית נוספה",
"Subtitles": "כתוביות", "Subtitles": "כתוביות",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "התגובות הופעלו בהצלחה", "Successfully Enabled comments": "התגובות הופעלו בהצלחה",
"Successfully changed owner": "", "Successfully changed owner": "",
"Successfully deleted": "נמחק בהצלחה", "Successfully deleted": "נמחק בהצלחה",
"Successfully deleted comments": "",
"Successfully updated": "", "Successfully updated": "",
"Successfully updated categories": "", "Successfully updated categories": "",
"Successfully updated playlist membership": "", "Successfully updated playlist membership": "",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "העלה מדיה", "Upload media": "העלה מדיה",
"Uploads": "העלאות", "Uploads": "העלאות",
"Users": "", "Users": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "הצג הכל", "VIEW ALL": "הצג הכל",
"Video": "וידאו", "Video": "וידאו",
"View all": "הצג הכל", "View all": "הצג הכל",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "ברוך הבא", "Welcome": "ברוך הבא",
"You are going to copy": "אתה עומד להעתיק", "You are going to copy": "אתה עומד להעתיק",
"You are going to delete": "אתה עומד למחוק", "You are going to delete": "אתה עומד למחוק",
"You are going to delete all comments from": "",
"You are going to disable comments to": "אתה עומד לבטל תגובות ל", "You are going to disable comments to": "אתה עומד לבטל תגובות ל",
"You are going to disable download for": "אתה עומד לבטל הורדה עבור", "You are going to disable download for": "אתה עומד לבטל הורדה עבור",
"You are going to enable comments to": "אתה עומד להפעיל תגובות ל", "You are going to enable comments to": "אתה עומד להפעיל תגובות ל",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "मीडिया हटाएं", "DELETE MEDIA": "मीडिया हटाएं",
"DOWNLOAD": "डाउनलोड करें", "DOWNLOAD": "डाउनलोड करें",
"DURATION": "अवधि", "DURATION": "अवधि",
"Delete Comments": "",
"Delete Media": "मीडिया हटाएं", "Delete Media": "मीडिया हटाएं",
"Delete media": "मीडिया हटाएं", "Delete media": "मीडिया हटाएं",
"Disable Comments": "टिप्पणियां अक्षम करें", "Disable Comments": "टिप्पणियां अक्षम करें",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "स्वामी बदलने में विफल। कृपया पुनः प्रयास करें।", "Failed to change owner. Please try again.": "स्वामी बदलने में विफल। कृपया पुनः प्रयास करें।",
"Failed to copy media.": "मीडिया कॉपी करने में विफल।", "Failed to copy media.": "मीडिया कॉपी करने में विफल।",
"Failed to create playlist": "प्लेलिस्ट बनाने में विफल", "Failed to create playlist": "प्लेलिस्ट बनाने में विफल",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "मीडिया हटाने में विफल। कृपया पुनः प्रयास करें।", "Failed to delete media. Please try again.": "मीडिया हटाने में विफल। कृपया पुनः प्रयास करें।",
"Failed to disable comments.": "टिप्पणियों को अक्षम करने में विफल।", "Failed to disable comments.": "टिप्पणियों को अक्षम करने में विफल।",
"Failed to disable download.": "डाउनलोड अक्षम करने में विफल।", "Failed to disable download.": "डाउनलोड अक्षम करने में विफल।",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "मौजूदा उपयोगकर्ताओं को फ़िल्टर करें...", "Filter existing users...": "मौजूदा उपयोगकर्ताओं को फ़िल्टर करें...",
"Filter playlists...": "प्लेलिस्ट फ़िल्टर करें...", "Filter playlists...": "प्लेलिस्ट फ़िल्टर करें...",
"Filters": "फ़िल्टर", "Filters": "फ़िल्टर",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "जाएं", "Go": "जाएं",
"History": "इतिहास", "History": "इतिहास",
"Home": "मुख्य पृष्ठ", "Home": "मुख्य पृष्ठ",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "टिप्पणियाँ प्रबंधित करें", "Manage comments": "टिप्पणियाँ प्रबंधित करें",
"Manage media": "मीडिया प्रबंधित करें", "Manage media": "मीडिया प्रबंधित करें",
"Manage users": "उपयोगकर्ताओं को प्रबंधित करें", "Manage users": "उपयोगकर्ताओं को प्रबंधित करें",
"Management": "",
"Media": "मीडिया", "Media": "मीडिया",
"Media I own": "मेरे स्वामित्व वाली मीडिया", "Media I own": "मेरे स्वामित्व वाली मीडिया",
"Media was edited": "मीडिया संपादित किया गया था", "Media was edited": "मीडिया संपादित किया गया था",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "के लिए कोई परिणाम नहीं", "No results for": "के लिए कोई परिणाम नहीं",
"No tags": "कोई टैग नहीं", "No tags": "कोई टैग नहीं",
"No users to add": "जोड़ने के लिए कोई उपयोगकर्ता नहीं", "No users to add": "जोड़ने के लिए कोई उपयोगकर्ता नहीं",
"Organization": "",
"PLAYLISTS": "प्लेलिस्ट", "PLAYLISTS": "प्लेलिस्ट",
"PUBLISH STATE": "प्रकाशन स्थिति", "PUBLISH STATE": "प्रकाशन स्थिति",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "पर प्रकाशित", "Published on": "पर प्रकाशित",
"Recent uploads": "हाल के अपलोड", "Recent uploads": "हाल के अपलोड",
"Recommended": "अनुशंसित", "Recommended": "अनुशंसित",
"Record": "",
"Record Screen": "स्क्रीन रिकॉर्ड करें", "Record Screen": "स्क्रीन रिकॉर्ड करें",
"Record Screen with Audio": "",
"Register": "पंजीकरण करें", "Register": "पंजीकरण करें",
"Remove category": "श्रेणी हटाएं", "Remove category": "श्रेणी हटाएं",
"Remove from list": "सूची से हटाएं", "Remove from list": "सूची से हटाएं",
"Remove tag": "टैग हटाएं", "Remove tag": "टैग हटाएं",
"Remove user": "उपयोगकर्ता हटाएं", "Remove user": "उपयोगकर्ता हटाएं",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "सहेजें", "SAVE": "सहेजें",
"SEARCH": "खोजें", "SEARCH": "खोजें",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "सभी मीडिया चुनें", "Select all media": "सभी मीडिया चुनें",
"Select publish state:": "प्रकाशन स्थिति चुनें:", "Select publish state:": "प्रकाशन स्थिति चुनें:",
"Selected": "चयनित", "Selected": "चयनित",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "मेरे द्वारा साझा किया गया", "Shared by me": "मेरे द्वारा साझा किया गया",
"Shared with me": "मेरे साथ साझा किया गया", "Shared with me": "मेरे साथ साझा किया गया",
"Sharing": "",
"Sign in": "साइन इन करें", "Sign in": "साइन इन करें",
"Sign out": "साइन आउट करें", "Sign out": "साइन आउट करें",
"Sort By": "इसके अनुसार क्रमबद्ध करें", "Sort By": "इसके अनुसार क्रमबद्ध करें",
"Start Recording": "रिकॉर्डिंग प्रारंभ करें", "Start Recording": "रिकॉर्डिंग प्रारंभ करें",
"Start uploading media and sharing your work. Media that you upload will show up here.": "मीडिया अपलोड करना और अपना काम साझा करना शुरू करें। आपके द्वारा अपलोड किया गया मीडिया यहां दिखाई देगा।", "Start uploading media and sharing your work. Media that you upload will show up here.": "मीडिया अपलोड करना और अपना काम साझा करना शुरू करें। आपके द्वारा अपलोड किया गया मीडिया यहां दिखाई देगा।",
"Stop Recording": "रिकॉर्डिंग रोकें", "Stop Recording": "रिकॉर्डिंग रोकें",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "प्रस्तुत करें", "Submit": "प्रस्तुत करें",
"Subtitle was added": "उपशीर्षक जोड़ा गया", "Subtitle was added": "उपशीर्षक जोड़ा गया",
"Subtitles": "उपशीर्षक", "Subtitles": "उपशीर्षक",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "टिप्पणियां सफलतापूर्वक सक्षम की गईं", "Successfully Enabled comments": "टिप्पणियां सफलतापूर्वक सक्षम की गईं",
"Successfully changed owner": "स्वामी सफलतापूर्वक बदला गया", "Successfully changed owner": "स्वामी सफलतापूर्वक बदला गया",
"Successfully deleted": "सफलतापूर्वक हटाया गया", "Successfully deleted": "सफलतापूर्वक हटाया गया",
"Successfully deleted comments": "",
"Successfully updated": "सफलतापूर्वक अपडेट किया गया", "Successfully updated": "सफलतापूर्वक अपडेट किया गया",
"Successfully updated categories": "श्रेणियां सफलतापूर्वक अपडेट की गईं", "Successfully updated categories": "श्रेणियां सफलतापूर्वक अपडेट की गईं",
"Successfully updated playlist membership": "प्लेलिस्ट सदस्यता सफलतापूर्वक अपडेट की गई", "Successfully updated playlist membership": "प्लेलिस्ट सदस्यता सफलतापूर्वक अपडेट की गई",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "मीडिया अपलोड करें", "Upload media": "मीडिया अपलोड करें",
"Uploads": "अपलोड", "Uploads": "अपलोड",
"Users": "उपयोगकर्ता", "Users": "उपयोगकर्ता",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "सभी देखें", "VIEW ALL": "सभी देखें",
"Video": "वीडियो", "Video": "वीडियो",
"View all": "सभी देखें", "View all": "सभी देखें",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "स्वागत है", "Welcome": "स्वागत है",
"You are going to copy": "आप कॉपी करने जा रहे हैं", "You are going to copy": "आप कॉपी करने जा रहे हैं",
"You are going to delete": "आप हटाने जा रहे हैं", "You are going to delete": "आप हटाने जा रहे हैं",
"You are going to delete all comments from": "",
"You are going to disable comments to": "आप टिप्पणियों को अक्षम करने जा रहे हैं", "You are going to disable comments to": "आप टिप्पणियों को अक्षम करने जा रहे हैं",
"You are going to disable download for": "आप डाउनलोड को अक्षम करने जा रहे हैं", "You are going to disable download for": "आप डाउनलोड को अक्षम करने जा रहे हैं",
"You are going to enable comments to": "आप टिप्पणियों को सक्षम करने जा रहे हैं", "You are going to enable comments to": "आप टिप्पणियों को सक्षम करने जा रहे हैं",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "HAPUS MEDIA", "DELETE MEDIA": "HAPUS MEDIA",
"DOWNLOAD": "UNDUH", "DOWNLOAD": "UNDUH",
"DURATION": "DURASI", "DURATION": "DURASI",
"Delete Comments": "",
"Delete Media": "Hapus Media", "Delete Media": "Hapus Media",
"Delete media": "Hapus media", "Delete media": "Hapus media",
"Disable Comments": "Nonaktifkan Komentar", "Disable Comments": "Nonaktifkan Komentar",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Gagal mengganti pemilik. Silakan coba lagi.", "Failed to change owner. Please try again.": "Gagal mengganti pemilik. Silakan coba lagi.",
"Failed to copy media.": "Gagal menyalin media.", "Failed to copy media.": "Gagal menyalin media.",
"Failed to create playlist": "Gagal membuat daftar putar", "Failed to create playlist": "Gagal membuat daftar putar",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Gagal menghapus media. Silakan coba lagi.", "Failed to delete media. Please try again.": "Gagal menghapus media. Silakan coba lagi.",
"Failed to disable comments.": "Gagal menonaktifkan komentar.", "Failed to disable comments.": "Gagal menonaktifkan komentar.",
"Failed to disable download.": "Gagal menonaktifkan unduhan.", "Failed to disable download.": "Gagal menonaktifkan unduhan.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filter pengguna yang ada...", "Filter existing users...": "Filter pengguna yang ada...",
"Filter playlists...": "Filter daftar putar...", "Filter playlists...": "Filter daftar putar...",
"Filters": "Filter", "Filters": "Filter",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Pergi", "Go": "Pergi",
"History": "Riwayat", "History": "Riwayat",
"Home": "Beranda", "Home": "Beranda",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Kelola komentar", "Manage comments": "Kelola komentar",
"Manage media": "Kelola media", "Manage media": "Kelola media",
"Manage users": "Kelola pengguna", "Manage users": "Kelola pengguna",
"Management": "",
"Media": "Media", "Media": "Media",
"Media I own": "Media yang saya miliki", "Media I own": "Media yang saya miliki",
"Media was edited": "Media telah diedit", "Media was edited": "Media telah diedit",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Tidak ada hasil untuk", "No results for": "Tidak ada hasil untuk",
"No tags": "Tidak ada tag", "No tags": "Tidak ada tag",
"No users to add": "Tidak ada pengguna untuk ditambahkan", "No users to add": "Tidak ada pengguna untuk ditambahkan",
"Organization": "",
"PLAYLISTS": "DAFTAR PUTAR", "PLAYLISTS": "DAFTAR PUTAR",
"PUBLISH STATE": "STATUS PUBLIKASI", "PUBLISH STATE": "STATUS PUBLIKASI",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Diterbitkan pada", "Published on": "Diterbitkan pada",
"Recent uploads": "Unggahan terbaru", "Recent uploads": "Unggahan terbaru",
"Recommended": "Direkomendasikan", "Recommended": "Direkomendasikan",
"Record": "",
"Record Screen": "Rekam Layar", "Record Screen": "Rekam Layar",
"Record Screen with Audio": "",
"Register": "Daftar", "Register": "Daftar",
"Remove category": "Hapus kategori", "Remove category": "Hapus kategori",
"Remove from list": "Hapus dari daftar", "Remove from list": "Hapus dari daftar",
"Remove tag": "Hapus tag", "Remove tag": "Hapus tag",
"Remove user": "Hapus pengguna", "Remove user": "Hapus pengguna",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "SIMPAN", "SAVE": "SIMPAN",
"SEARCH": "CARI", "SEARCH": "CARI",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Pilih semua media", "Select all media": "Pilih semua media",
"Select publish state:": "Pilih status publikasi:", "Select publish state:": "Pilih status publikasi:",
"Selected": "Dipilih", "Selected": "Dipilih",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Dibagikan oleh saya", "Shared by me": "Dibagikan oleh saya",
"Shared with me": "Dibagikan dengan saya", "Shared with me": "Dibagikan dengan saya",
"Sharing": "",
"Sign in": "Masuk", "Sign in": "Masuk",
"Sign out": "Keluar", "Sign out": "Keluar",
"Sort By": "Urutkan Berdasarkan", "Sort By": "Urutkan Berdasarkan",
"Start Recording": "Mulai Merekam", "Start Recording": "Mulai Merekam",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Mulai mengunggah media dan berbagi karya Anda. Media yang Anda unggah akan muncul di sini.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Mulai mengunggah media dan berbagi karya Anda. Media yang Anda unggah akan muncul di sini.",
"Stop Recording": "Hentikan Perekaman", "Stop Recording": "Hentikan Perekaman",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Kirim", "Submit": "Kirim",
"Subtitle was added": "Subtitle telah ditambahkan", "Subtitle was added": "Subtitle telah ditambahkan",
"Subtitles": "Subtitel", "Subtitles": "Subtitel",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Komentar berhasil diaktifkan", "Successfully Enabled comments": "Komentar berhasil diaktifkan",
"Successfully changed owner": "Berhasil mengganti pemilik", "Successfully changed owner": "Berhasil mengganti pemilik",
"Successfully deleted": "Berhasil dihapus", "Successfully deleted": "Berhasil dihapus",
"Successfully deleted comments": "",
"Successfully updated": "Berhasil diperbarui", "Successfully updated": "Berhasil diperbarui",
"Successfully updated categories": "Kategori berhasil diperbarui", "Successfully updated categories": "Kategori berhasil diperbarui",
"Successfully updated playlist membership": "Keanggotaan daftar putar berhasil diperbarui", "Successfully updated playlist membership": "Keanggotaan daftar putar berhasil diperbarui",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Unggah media", "Upload media": "Unggah media",
"Uploads": "Unggahan", "Uploads": "Unggahan",
"Users": "Pengguna", "Users": "Pengguna",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "LIHAT SEMUA", "VIEW ALL": "LIHAT SEMUA",
"Video": "Video", "Video": "Video",
"View all": "Lihat semua", "View all": "Lihat semua",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Selamat datang", "Welcome": "Selamat datang",
"You are going to copy": "Anda akan menyalin", "You are going to copy": "Anda akan menyalin",
"You are going to delete": "Anda akan menghapus", "You are going to delete": "Anda akan menghapus",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Anda akan menonaktifkan komentar untuk", "You are going to disable comments to": "Anda akan menonaktifkan komentar untuk",
"You are going to disable download for": "Anda akan menonaktifkan unduhan untuk", "You are going to disable download for": "Anda akan menonaktifkan unduhan untuk",
"You are going to enable comments to": "Anda akan mengaktifkan komentar untuk", "You are going to enable comments to": "Anda akan mengaktifkan komentar untuk",

View File

@@ -49,6 +49,7 @@ translation_strings = {
"DELETE MEDIA": "ELIMINA MEDIA", "DELETE MEDIA": "ELIMINA MEDIA",
"DOWNLOAD": "SCARICA", "DOWNLOAD": "SCARICA",
"DURATION": "DURATA", "DURATION": "DURATA",
"Delete Comments": "",
"Delete Media": "Elimina Media", "Delete Media": "Elimina Media",
"Delete media": "Elimina media", "Delete media": "Elimina media",
"Disable Comments": "Disabilita Commenti", "Disable Comments": "Disabilita Commenti",
@@ -71,6 +72,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Impossibile cambiare proprietario. Riprova.", "Failed to change owner. Please try again.": "Impossibile cambiare proprietario. Riprova.",
"Failed to copy media.": "Impossibile copiare il media.", "Failed to copy media.": "Impossibile copiare il media.",
"Failed to create playlist": "Impossibile creare playlist", "Failed to create playlist": "Impossibile creare playlist",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Impossibile eliminare il media. Riprova.", "Failed to delete media. Please try again.": "Impossibile eliminare il media. Riprova.",
"Failed to disable comments.": "Impossibile disabilitare i commenti.", "Failed to disable comments.": "Impossibile disabilitare i commenti.",
"Failed to disable download.": "Impossibile disabilitare il download.", "Failed to disable download.": "Impossibile disabilitare il download.",
@@ -102,6 +104,9 @@ translation_strings = {
"Filter existing users...": "Filtra utenti esistenti...", "Filter existing users...": "Filtra utenti esistenti...",
"Filter playlists...": "Filtra playlist...", "Filter playlists...": "Filtra playlist...",
"Filters": "Filtri", "Filters": "Filtri",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Vai", "Go": "Vai",
"History": "Cronologia", "History": "Cronologia",
"Home": "Home", "Home": "Home",
@@ -122,6 +127,7 @@ translation_strings = {
"Manage comments": "Gestisci i commenti", "Manage comments": "Gestisci i commenti",
"Manage media": "Gestisci i media", "Manage media": "Gestisci i media",
"Manage users": "Gestisci gli utenti", "Manage users": "Gestisci gli utenti",
"Management": "",
"Media": "Media", "Media": "Media",
"Media I own": "Media di mia proprietà", "Media I own": "Media di mia proprietà",
"Media was edited": "Il media è stato modificato", "Media was edited": "Il media è stato modificato",
@@ -138,6 +144,7 @@ translation_strings = {
"No results for": "Nessun risultato per", "No results for": "Nessun risultato per",
"No tags": "Nessun tag", "No tags": "Nessun tag",
"No users to add": "Nessun utente da aggiungere", "No users to add": "Nessun utente da aggiungere",
"Organization": "",
"PLAYLISTS": "PLAYLIST", "PLAYLISTS": "PLAYLIST",
"PUBLISH STATE": "STATO DI PUBBLICAZIONE", "PUBLISH STATE": "STATO DI PUBBLICAZIONE",
"Pdf": "PDF", "Pdf": "PDF",
@@ -157,12 +164,17 @@ translation_strings = {
"Published on": "Pubblicato il", "Published on": "Pubblicato il",
"Recent uploads": "Caricamenti recenti", "Recent uploads": "Caricamenti recenti",
"Recommended": "Raccomandati", "Recommended": "Raccomandati",
"Record": "",
"Record Screen": "Registra schermo", "Record Screen": "Registra schermo",
"Record Screen with Audio": "",
"Register": "Registrati", "Register": "Registrati",
"Remove category": "Rimuovi categoria", "Remove category": "Rimuovi categoria",
"Remove from list": "Rimuovi dalla lista", "Remove from list": "Rimuovi dalla lista",
"Remove tag": "Rimuovi tag", "Remove tag": "Rimuovi tag",
"Remove user": "Rimuovi utente", "Remove user": "Rimuovi utente",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "SALVA", "SAVE": "SALVA",
"SEARCH": "CERCA", "SEARCH": "CERCA",
@@ -179,14 +191,22 @@ translation_strings = {
"Select all media": "Seleziona tutti i media", "Select all media": "Seleziona tutti i media",
"Select publish state:": "Seleziona stato di pubblicazione:", "Select publish state:": "Seleziona stato di pubblicazione:",
"Selected": "Selezionato", "Selected": "Selezionato",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Condiviso da me", "Shared by me": "Condiviso da me",
"Shared with me": "Condiviso con me", "Shared with me": "Condiviso con me",
"Sharing": "",
"Sign in": "Login", "Sign in": "Login",
"Sign out": "Logout", "Sign out": "Logout",
"Sort By": "Ordina per", "Sort By": "Ordina per",
"Start Recording": "Inizia registrazione", "Start Recording": "Inizia registrazione",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Inizia a caricare media e condividere il tuo lavoro. I media caricati appariranno qui.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Inizia a caricare media e condividere il tuo lavoro. I media caricati appariranno qui.",
"Stop Recording": "Interrompi registrazione", "Stop Recording": "Interrompi registrazione",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Invia", "Submit": "Invia",
"Subtitle was added": "I sottotitoli sono stati aggiunti", "Subtitle was added": "I sottotitoli sono stati aggiunti",
"Subtitles": "Sottotitoli", "Subtitles": "Sottotitoli",
@@ -197,6 +217,7 @@ translation_strings = {
"Successfully Enabled comments": "Commenti abilitati con successo", "Successfully Enabled comments": "Commenti abilitati con successo",
"Successfully changed owner": "Proprietario cambiato con successo", "Successfully changed owner": "Proprietario cambiato con successo",
"Successfully deleted": "Eliminato con successo", "Successfully deleted": "Eliminato con successo",
"Successfully deleted comments": "",
"Successfully updated": "Aggiornato con successo", "Successfully updated": "Aggiornato con successo",
"Successfully updated categories": "Categorie aggiornate con successo", "Successfully updated categories": "Categorie aggiornate con successo",
"Successfully updated playlist membership": "Appartenenza alla playlist aggiornata con successo", "Successfully updated playlist membership": "Appartenenza alla playlist aggiornata con successo",
@@ -233,6 +254,9 @@ translation_strings = {
"Upload media": "Carica i media", "Upload media": "Carica i media",
"Uploads": "Caricamenti", "Uploads": "Caricamenti",
"Users": "Utenti", "Users": "Utenti",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "MOSTRA TUTTI", "VIEW ALL": "MOSTRA TUTTI",
"Video": "Video", "Video": "Video",
"View all": "Mostra tutti", "View all": "Mostra tutti",
@@ -241,6 +265,7 @@ translation_strings = {
"Welcome": "Benvenuto", "Welcome": "Benvenuto",
"You are going to copy": "Stai per copiare", "You are going to copy": "Stai per copiare",
"You are going to delete": "Stai per eliminare", "You are going to delete": "Stai per eliminare",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Stai per disabilitare i commenti di", "You are going to disable comments to": "Stai per disabilitare i commenti di",
"You are going to disable download for": "Stai per disabilitare il download di", "You are going to disable download for": "Stai per disabilitare il download di",
"You are going to enable comments to": "Stai per abilitare i commenti di", "You are going to enable comments to": "Stai per abilitare i commenti di",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "メディアを削除", "DELETE MEDIA": "メディアを削除",
"DOWNLOAD": "ダウンロード", "DOWNLOAD": "ダウンロード",
"DURATION": "期間", "DURATION": "期間",
"Delete Comments": "",
"Delete Media": "メディアを削除", "Delete Media": "メディアを削除",
"Delete media": "メディアを削除", "Delete media": "メディアを削除",
"Disable Comments": "コメントを無効化", "Disable Comments": "コメントを無効化",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "所有者の変更に失敗しました。もう一度お試しください。", "Failed to change owner. Please try again.": "所有者の変更に失敗しました。もう一度お試しください。",
"Failed to copy media.": "メディアのコピーに失敗しました。", "Failed to copy media.": "メディアのコピーに失敗しました。",
"Failed to create playlist": "プレイリストの作成に失敗しました", "Failed to create playlist": "プレイリストの作成に失敗しました",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "メディアの削除に失敗しました。もう一度お試しください。", "Failed to delete media. Please try again.": "メディアの削除に失敗しました。もう一度お試しください。",
"Failed to disable comments.": "コメントの無効化に失敗しました。", "Failed to disable comments.": "コメントの無効化に失敗しました。",
"Failed to disable download.": "ダウンロードの無効化に失敗しました。", "Failed to disable download.": "ダウンロードの無効化に失敗しました。",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "既存ユーザーをフィルター...", "Filter existing users...": "既存ユーザーをフィルター...",
"Filter playlists...": "プレイリストをフィルター...", "Filter playlists...": "プレイリストをフィルター...",
"Filters": "フィルター", "Filters": "フィルター",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "行く", "Go": "行く",
"History": "履歴", "History": "履歴",
"Home": "ホーム", "Home": "ホーム",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "コメントを管理", "Manage comments": "コメントを管理",
"Manage media": "メディアを管理", "Manage media": "メディアを管理",
"Manage users": "ユーザーを管理", "Manage users": "ユーザーを管理",
"Management": "",
"Media": "メディア", "Media": "メディア",
"Media I own": "自分が所有するメディア", "Media I own": "自分が所有するメディア",
"Media was edited": "メディアが編集されました", "Media was edited": "メディアが編集されました",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "の結果はありません", "No results for": "の結果はありません",
"No tags": "タグなし", "No tags": "タグなし",
"No users to add": "追加するユーザーなし", "No users to add": "追加するユーザーなし",
"Organization": "",
"PLAYLISTS": "プレイリスト", "PLAYLISTS": "プレイリスト",
"PUBLISH STATE": "公開状態", "PUBLISH STATE": "公開状態",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "公開日", "Published on": "公開日",
"Recent uploads": "最近のアップロード", "Recent uploads": "最近のアップロード",
"Recommended": "おすすめ", "Recommended": "おすすめ",
"Record": "",
"Record Screen": "画面を録画", "Record Screen": "画面を録画",
"Record Screen with Audio": "",
"Register": "登録", "Register": "登録",
"Remove category": "カテゴリーを削除", "Remove category": "カテゴリーを削除",
"Remove from list": "リストから削除", "Remove from list": "リストから削除",
"Remove tag": "タグを削除", "Remove tag": "タグを削除",
"Remove user": "ユーザーを削除", "Remove user": "ユーザーを削除",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "保存", "SAVE": "保存",
"SEARCH": "検索", "SEARCH": "検索",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "すべてのメディアを選択", "Select all media": "すべてのメディアを選択",
"Select publish state:": "公開状態を選択:", "Select publish state:": "公開状態を選択:",
"Selected": "選択済み", "Selected": "選択済み",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "自分が共有", "Shared by me": "自分が共有",
"Shared with me": "共有されたもの", "Shared with me": "共有されたもの",
"Sharing": "",
"Sign in": "サインイン", "Sign in": "サインイン",
"Sign out": "サインアウト", "Sign out": "サインアウト",
"Sort By": "並び替え", "Sort By": "並び替え",
"Start Recording": "録画開始", "Start Recording": "録画開始",
"Start uploading media and sharing your work. Media that you upload will show up here.": "メディアをアップロードして作品を共有しましょう。アップロードしたメディアはここに表示されます。", "Start uploading media and sharing your work. Media that you upload will show up here.": "メディアをアップロードして作品を共有しましょう。アップロードしたメディアはここに表示されます。",
"Stop Recording": "録画停止", "Stop Recording": "録画停止",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "送信", "Submit": "送信",
"Subtitle was added": "字幕が追加されました", "Subtitle was added": "字幕が追加されました",
"Subtitles": "字幕", "Subtitles": "字幕",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "コメントが正常に有効化されました", "Successfully Enabled comments": "コメントが正常に有効化されました",
"Successfully changed owner": "所有者が正常に変更されました", "Successfully changed owner": "所有者が正常に変更されました",
"Successfully deleted": "正常に削除されました", "Successfully deleted": "正常に削除されました",
"Successfully deleted comments": "",
"Successfully updated": "正常に更新されました", "Successfully updated": "正常に更新されました",
"Successfully updated categories": "カテゴリーが正常に更新されました", "Successfully updated categories": "カテゴリーが正常に更新されました",
"Successfully updated playlist membership": "プレイリストメンバーシップが正常に更新されました", "Successfully updated playlist membership": "プレイリストメンバーシップが正常に更新されました",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "メディアをアップロード", "Upload media": "メディアをアップロード",
"Uploads": "アップロード", "Uploads": "アップロード",
"Users": "ユーザー", "Users": "ユーザー",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "すべて表示", "VIEW ALL": "すべて表示",
"Video": "ビデオ", "Video": "ビデオ",
"View all": "すべて表示", "View all": "すべて表示",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "ようこそ", "Welcome": "ようこそ",
"You are going to copy": "コピーします", "You are going to copy": "コピーします",
"You are going to delete": "削除します", "You are going to delete": "削除します",
"You are going to delete all comments from": "",
"You are going to disable comments to": "コメントを無効化します", "You are going to disable comments to": "コメントを無効化します",
"You are going to disable download for": "ダウンロードを無効化します", "You are going to disable download for": "ダウンロードを無効化します",
"You are going to enable comments to": "コメントを有効化します", "You are going to enable comments to": "コメントを有効化します",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "미디어 삭제", "DELETE MEDIA": "미디어 삭제",
"DOWNLOAD": "다운로드", "DOWNLOAD": "다운로드",
"DURATION": "재생 시간", "DURATION": "재생 시간",
"Delete Comments": "",
"Delete Media": "미디어 삭제", "Delete Media": "미디어 삭제",
"Delete media": "미디어 삭제", "Delete media": "미디어 삭제",
"Disable Comments": "댓글 비활성화", "Disable Comments": "댓글 비활성화",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "소유자 변경에 실패했습니다. 다시 시도해주세요.", "Failed to change owner. Please try again.": "소유자 변경에 실패했습니다. 다시 시도해주세요.",
"Failed to copy media.": "미디어 복사에 실패했습니다.", "Failed to copy media.": "미디어 복사에 실패했습니다.",
"Failed to create playlist": "재생 목록 만들기 실패", "Failed to create playlist": "재생 목록 만들기 실패",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "미디어 삭제에 실패했습니다. 다시 시도해주세요.", "Failed to delete media. Please try again.": "미디어 삭제에 실패했습니다. 다시 시도해주세요.",
"Failed to disable comments.": "댓글 비활성화에 실패했습니다.", "Failed to disable comments.": "댓글 비활성화에 실패했습니다.",
"Failed to disable download.": "다운로드 비활성화에 실패했습니다.", "Failed to disable download.": "다운로드 비활성화에 실패했습니다.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "기존 사용자 필터링...", "Filter existing users...": "기존 사용자 필터링...",
"Filter playlists...": "재생 목록 필터링...", "Filter playlists...": "재생 목록 필터링...",
"Filters": "필터", "Filters": "필터",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "이동", "Go": "이동",
"History": "기록", "History": "기록",
"Home": "", "Home": "",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "댓글 관리", "Manage comments": "댓글 관리",
"Manage media": "미디어 관리", "Manage media": "미디어 관리",
"Manage users": "사용자 관리", "Manage users": "사용자 관리",
"Management": "",
"Media": "미디어", "Media": "미디어",
"Media I own": "내가 소유한 미디어", "Media I own": "내가 소유한 미디어",
"Media was edited": "미디어가 편집되었습니다", "Media was edited": "미디어가 편집되었습니다",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "결과 없음", "No results for": "결과 없음",
"No tags": "태그 없음", "No tags": "태그 없음",
"No users to add": "추가할 사용자 없음", "No users to add": "추가할 사용자 없음",
"Organization": "",
"PLAYLISTS": "재생 목록", "PLAYLISTS": "재생 목록",
"PUBLISH STATE": "게시 상태", "PUBLISH STATE": "게시 상태",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "게시일", "Published on": "게시일",
"Recent uploads": "최근 업로드", "Recent uploads": "최근 업로드",
"Recommended": "추천", "Recommended": "추천",
"Record": "",
"Record Screen": "화면 녹화", "Record Screen": "화면 녹화",
"Record Screen with Audio": "",
"Register": "등록", "Register": "등록",
"Remove category": "카테고리 제거", "Remove category": "카테고리 제거",
"Remove from list": "목록에서 제거", "Remove from list": "목록에서 제거",
"Remove tag": "태그 제거", "Remove tag": "태그 제거",
"Remove user": "사용자 제거", "Remove user": "사용자 제거",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "저장", "SAVE": "저장",
"SEARCH": "검색", "SEARCH": "검색",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "모든 미디어 선택", "Select all media": "모든 미디어 선택",
"Select publish state:": "게시 상태 선택:", "Select publish state:": "게시 상태 선택:",
"Selected": "선택됨", "Selected": "선택됨",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "내가 공유함", "Shared by me": "내가 공유함",
"Shared with me": "나와 공유됨", "Shared with me": "나와 공유됨",
"Sharing": "",
"Sign in": "로그인", "Sign in": "로그인",
"Sign out": "로그아웃", "Sign out": "로그아웃",
"Sort By": "정렬", "Sort By": "정렬",
"Start Recording": "녹화 시작", "Start Recording": "녹화 시작",
"Start uploading media and sharing your work. Media that you upload will show up here.": "미디어를 업로드하고 작업을 공유하세요. 업로드한 미디어가 여기에 표시됩니다.", "Start uploading media and sharing your work. Media that you upload will show up here.": "미디어를 업로드하고 작업을 공유하세요. 업로드한 미디어가 여기에 표시됩니다.",
"Stop Recording": "녹화 중지", "Stop Recording": "녹화 중지",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "제출", "Submit": "제출",
"Subtitle was added": "자막이 추가되었습니다", "Subtitle was added": "자막이 추가되었습니다",
"Subtitles": "자막", "Subtitles": "자막",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "댓글이 활성화되었습니다", "Successfully Enabled comments": "댓글이 활성화되었습니다",
"Successfully changed owner": "소유자가 변경되었습니다", "Successfully changed owner": "소유자가 변경되었습니다",
"Successfully deleted": "삭제 성공", "Successfully deleted": "삭제 성공",
"Successfully deleted comments": "",
"Successfully updated": "업데이트 성공", "Successfully updated": "업데이트 성공",
"Successfully updated categories": "카테고리가 업데이트되었습니다", "Successfully updated categories": "카테고리가 업데이트되었습니다",
"Successfully updated playlist membership": "재생 목록 멤버십이 업데이트되었습니다", "Successfully updated playlist membership": "재생 목록 멤버십이 업데이트되었습니다",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "미디어 업로드", "Upload media": "미디어 업로드",
"Uploads": "업로드", "Uploads": "업로드",
"Users": "사용자", "Users": "사용자",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "모두 보기", "VIEW ALL": "모두 보기",
"Video": "비디오", "Video": "비디오",
"View all": "모두 보기", "View all": "모두 보기",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "환영합니다", "Welcome": "환영합니다",
"You are going to copy": "복사하려고 합니다", "You are going to copy": "복사하려고 합니다",
"You are going to delete": "삭제하려고 합니다", "You are going to delete": "삭제하려고 합니다",
"You are going to delete all comments from": "",
"You are going to disable comments to": "댓글을 비활성화하려고 합니다", "You are going to disable comments to": "댓글을 비활성화하려고 합니다",
"You are going to disable download for": "다운로드를 비활성화하려고 합니다", "You are going to disable download for": "다운로드를 비활성화하려고 합니다",
"You are going to enable comments to": "댓글을 활성화하려고 합니다", "You are going to enable comments to": "댓글을 활성화하려고 합니다",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "MEDIA VERWIJDEREN", "DELETE MEDIA": "MEDIA VERWIJDEREN",
"DOWNLOAD": "DOWNLOADEN", "DOWNLOAD": "DOWNLOADEN",
"DURATION": "DUUR", "DURATION": "DUUR",
"Delete Comments": "",
"Delete Media": "Media verwijderen", "Delete Media": "Media verwijderen",
"Delete media": "Media verwijderen", "Delete media": "Media verwijderen",
"Disable Comments": "Reacties uitschakelen", "Disable Comments": "Reacties uitschakelen",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Eigenaar wijzigen mislukt. Probeer het opnieuw.", "Failed to change owner. Please try again.": "Eigenaar wijzigen mislukt. Probeer het opnieuw.",
"Failed to copy media.": "Media kopiëren mislukt.", "Failed to copy media.": "Media kopiëren mislukt.",
"Failed to create playlist": "Afspeellijst maken mislukt", "Failed to create playlist": "Afspeellijst maken mislukt",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Media verwijderen mislukt. Probeer het opnieuw.", "Failed to delete media. Please try again.": "Media verwijderen mislukt. Probeer het opnieuw.",
"Failed to disable comments.": "Reacties uitschakelen mislukt.", "Failed to disable comments.": "Reacties uitschakelen mislukt.",
"Failed to disable download.": "Download uitschakelen mislukt.", "Failed to disable download.": "Download uitschakelen mislukt.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filter bestaande gebruikers...", "Filter existing users...": "Filter bestaande gebruikers...",
"Filter playlists...": "Filter afspeellijsten...", "Filter playlists...": "Filter afspeellijsten...",
"Filters": "Filters", "Filters": "Filters",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Ga", "Go": "Ga",
"History": "Geschiedenis", "History": "Geschiedenis",
"Home": "Home", "Home": "Home",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Reacties beheren", "Manage comments": "Reacties beheren",
"Manage media": "Media beheren", "Manage media": "Media beheren",
"Manage users": "Gebruikers beheren", "Manage users": "Gebruikers beheren",
"Management": "",
"Media": "Media", "Media": "Media",
"Media I own": "Media die ik bezit", "Media I own": "Media die ik bezit",
"Media was edited": "Media is bewerkt", "Media was edited": "Media is bewerkt",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Geen resultaten voor", "No results for": "Geen resultaten voor",
"No tags": "Geen tags", "No tags": "Geen tags",
"No users to add": "Geen gebruikers om toe te voegen", "No users to add": "Geen gebruikers om toe te voegen",
"Organization": "",
"PLAYLISTS": "AFSPEELLIJSTEN", "PLAYLISTS": "AFSPEELLIJSTEN",
"PUBLISH STATE": "PUBLICATIESTATUS", "PUBLISH STATE": "PUBLICATIESTATUS",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Gepubliceerd op", "Published on": "Gepubliceerd op",
"Recent uploads": "Recente uploads", "Recent uploads": "Recente uploads",
"Recommended": "Aanbevolen", "Recommended": "Aanbevolen",
"Record": "",
"Record Screen": "Scherm opnemen", "Record Screen": "Scherm opnemen",
"Record Screen with Audio": "",
"Register": "Registreren", "Register": "Registreren",
"Remove category": "Categorie verwijderen", "Remove category": "Categorie verwijderen",
"Remove from list": "Verwijderen uit lijst", "Remove from list": "Verwijderen uit lijst",
"Remove tag": "Tag verwijderen", "Remove tag": "Tag verwijderen",
"Remove user": "Gebruiker verwijderen", "Remove user": "Gebruiker verwijderen",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "OPSLAAN", "SAVE": "OPSLAAN",
"SEARCH": "ZOEKEN", "SEARCH": "ZOEKEN",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Alle media selecteren", "Select all media": "Alle media selecteren",
"Select publish state:": "Selecteer publicatiestatus:", "Select publish state:": "Selecteer publicatiestatus:",
"Selected": "Geselecteerd", "Selected": "Geselecteerd",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Gedeeld door mij", "Shared by me": "Gedeeld door mij",
"Shared with me": "Gedeeld met mij", "Shared with me": "Gedeeld met mij",
"Sharing": "",
"Sign in": "Inloggen", "Sign in": "Inloggen",
"Sign out": "Uitloggen", "Sign out": "Uitloggen",
"Sort By": "Sorteer op", "Sort By": "Sorteer op",
"Start Recording": "Opname starten", "Start Recording": "Opname starten",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Begin met het uploaden van media en het delen van uw werk. Media die u uploadt, verschijnt hier.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Begin met het uploaden van media en het delen van uw werk. Media die u uploadt, verschijnt hier.",
"Stop Recording": "Opname stoppen", "Stop Recording": "Opname stoppen",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Indienen", "Submit": "Indienen",
"Subtitle was added": "Ondertitel is toegevoegd", "Subtitle was added": "Ondertitel is toegevoegd",
"Subtitles": "Ondertitels", "Subtitles": "Ondertitels",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Reacties succesvol ingeschakeld", "Successfully Enabled comments": "Reacties succesvol ingeschakeld",
"Successfully changed owner": "Eigenaar succesvol gewijzigd", "Successfully changed owner": "Eigenaar succesvol gewijzigd",
"Successfully deleted": "Succesvol verwijderd", "Successfully deleted": "Succesvol verwijderd",
"Successfully deleted comments": "",
"Successfully updated": "Succesvol bijgewerkt", "Successfully updated": "Succesvol bijgewerkt",
"Successfully updated categories": "Categorieën succesvol bijgewerkt", "Successfully updated categories": "Categorieën succesvol bijgewerkt",
"Successfully updated playlist membership": "Afspeellijstlidmaatschap succesvol bijgewerkt", "Successfully updated playlist membership": "Afspeellijstlidmaatschap succesvol bijgewerkt",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Media uploaden", "Upload media": "Media uploaden",
"Uploads": "Uploads", "Uploads": "Uploads",
"Users": "Gebruikers", "Users": "Gebruikers",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "BEKIJK ALLES", "VIEW ALL": "BEKIJK ALLES",
"Video": "Video", "Video": "Video",
"View all": "Bekijk alles", "View all": "Bekijk alles",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Welkom", "Welcome": "Welkom",
"You are going to copy": "Je gaat kopiëren", "You are going to copy": "Je gaat kopiëren",
"You are going to delete": "Je gaat verwijderen", "You are going to delete": "Je gaat verwijderen",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Je gaat reacties uitschakelen voor", "You are going to disable comments to": "Je gaat reacties uitschakelen voor",
"You are going to disable download for": "Je gaat download uitschakelen voor", "You are going to disable download for": "Je gaat download uitschakelen voor",
"You are going to enable comments to": "Je gaat reacties inschakelen voor", "You are going to enable comments to": "Je gaat reacties inschakelen voor",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "EXCLUIR MÍDIA", "DELETE MEDIA": "EXCLUIR MÍDIA",
"DOWNLOAD": "BAIXAR", "DOWNLOAD": "BAIXAR",
"DURATION": "DURAÇÃO", "DURATION": "DURAÇÃO",
"Delete Comments": "",
"Delete Media": "Excluir mídia", "Delete Media": "Excluir mídia",
"Delete media": "Excluir mídia", "Delete media": "Excluir mídia",
"Disable Comments": "Desativar comentários", "Disable Comments": "Desativar comentários",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Falha ao mudar proprietário. Por favor, tente novamente.", "Failed to change owner. Please try again.": "Falha ao mudar proprietário. Por favor, tente novamente.",
"Failed to copy media.": "Falha ao copiar mídia.", "Failed to copy media.": "Falha ao copiar mídia.",
"Failed to create playlist": "Falha ao criar playlist", "Failed to create playlist": "Falha ao criar playlist",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Falha ao excluir mídia. Por favor, tente novamente.", "Failed to delete media. Please try again.": "Falha ao excluir mídia. Por favor, tente novamente.",
"Failed to disable comments.": "Falha ao desativar comentários.", "Failed to disable comments.": "Falha ao desativar comentários.",
"Failed to disable download.": "Falha ao desativar download.", "Failed to disable download.": "Falha ao desativar download.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filtrar usuários existentes...", "Filter existing users...": "Filtrar usuários existentes...",
"Filter playlists...": "Filtrar playlists...", "Filter playlists...": "Filtrar playlists...",
"Filters": "Filtros", "Filters": "Filtros",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Ir", "Go": "Ir",
"History": "Histórico", "History": "Histórico",
"Home": "Início", "Home": "Início",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Gerenciar comentários", "Manage comments": "Gerenciar comentários",
"Manage media": "Gerenciar mídia", "Manage media": "Gerenciar mídia",
"Manage users": "Gerenciar usuários", "Manage users": "Gerenciar usuários",
"Management": "",
"Media": "Mídia", "Media": "Mídia",
"Media I own": "Mídia que possuo", "Media I own": "Mídia que possuo",
"Media was edited": "Mídia foi editada", "Media was edited": "Mídia foi editada",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Nenhum resultado para", "No results for": "Nenhum resultado para",
"No tags": "Nenhuma tag", "No tags": "Nenhuma tag",
"No users to add": "Nenhum usuário para adicionar", "No users to add": "Nenhum usuário para adicionar",
"Organization": "",
"PLAYLISTS": "PLAYLISTS", "PLAYLISTS": "PLAYLISTS",
"PUBLISH STATE": "ESTADO DE PUBLICAÇÃO", "PUBLISH STATE": "ESTADO DE PUBLICAÇÃO",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Publicado em", "Published on": "Publicado em",
"Recent uploads": "Uploads recentes", "Recent uploads": "Uploads recentes",
"Recommended": "Recomendado", "Recommended": "Recomendado",
"Record": "",
"Record Screen": "Gravar tela", "Record Screen": "Gravar tela",
"Record Screen with Audio": "",
"Register": "Registrar", "Register": "Registrar",
"Remove category": "Remover categoria", "Remove category": "Remover categoria",
"Remove from list": "Remover da lista", "Remove from list": "Remover da lista",
"Remove tag": "Remover tag", "Remove tag": "Remover tag",
"Remove user": "Remover usuário", "Remove user": "Remover usuário",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "SALVAR", "SAVE": "SALVAR",
"SEARCH": "PESQUISAR", "SEARCH": "PESQUISAR",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Selecionar todas as mídias", "Select all media": "Selecionar todas as mídias",
"Select publish state:": "Selecionar estado de publicação:", "Select publish state:": "Selecionar estado de publicação:",
"Selected": "Selecionado", "Selected": "Selecionado",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Compartilhado por mim", "Shared by me": "Compartilhado por mim",
"Shared with me": "Compartilhado comigo", "Shared with me": "Compartilhado comigo",
"Sharing": "",
"Sign in": "Entrar", "Sign in": "Entrar",
"Sign out": "Sair", "Sign out": "Sair",
"Sort By": "Ordenar por", "Sort By": "Ordenar por",
"Start Recording": "Iniciar gravação", "Start Recording": "Iniciar gravação",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Comece a fazer upload de mídia e compartilhar seu trabalho. A mídia que você fizer upload aparecerá aqui.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Comece a fazer upload de mídia e compartilhar seu trabalho. A mídia que você fizer upload aparecerá aqui.",
"Stop Recording": "Parar gravação", "Stop Recording": "Parar gravação",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Enviar", "Submit": "Enviar",
"Subtitle was added": "Legenda foi adicionada", "Subtitle was added": "Legenda foi adicionada",
"Subtitles": "Legendas", "Subtitles": "Legendas",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Comentários ativados com sucesso", "Successfully Enabled comments": "Comentários ativados com sucesso",
"Successfully changed owner": "Proprietário alterado com sucesso", "Successfully changed owner": "Proprietário alterado com sucesso",
"Successfully deleted": "Excluído com sucesso", "Successfully deleted": "Excluído com sucesso",
"Successfully deleted comments": "",
"Successfully updated": "Atualizado com sucesso", "Successfully updated": "Atualizado com sucesso",
"Successfully updated categories": "Categorias atualizadas com sucesso", "Successfully updated categories": "Categorias atualizadas com sucesso",
"Successfully updated playlist membership": "Associação da playlist atualizada com sucesso", "Successfully updated playlist membership": "Associação da playlist atualizada com sucesso",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Carregar mídia", "Upload media": "Carregar mídia",
"Uploads": "Uploads", "Uploads": "Uploads",
"Users": "Usuários", "Users": "Usuários",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "VER TODOS", "VIEW ALL": "VER TODOS",
"Video": "Vídeo", "Video": "Vídeo",
"View all": "Ver todos", "View all": "Ver todos",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Bem-vindo", "Welcome": "Bem-vindo",
"You are going to copy": "Você vai copiar", "You are going to copy": "Você vai copiar",
"You are going to delete": "Você vai excluir", "You are going to delete": "Você vai excluir",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Você vai desativar comentários de", "You are going to disable comments to": "Você vai desativar comentários de",
"You are going to disable download for": "Você vai desativar download de", "You are going to disable download for": "Você vai desativar download de",
"You are going to enable comments to": "Você vai ativar comentários de", "You are going to enable comments to": "Você vai ativar comentários de",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "УДАЛИТЬ МЕДИА", "DELETE MEDIA": "УДАЛИТЬ МЕДИА",
"DOWNLOAD": "СКАЧАТЬ", "DOWNLOAD": "СКАЧАТЬ",
"DURATION": "ДЛИТЕЛЬНОСТЬ", "DURATION": "ДЛИТЕЛЬНОСТЬ",
"Delete Comments": "",
"Delete Media": "Удалить медиа", "Delete Media": "Удалить медиа",
"Delete media": "Удалить медиа", "Delete media": "Удалить медиа",
"Disable Comments": "Отключить комментарии", "Disable Comments": "Отключить комментарии",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Не удалось изменить владельца. Пожалуйста, попробуйте снова.", "Failed to change owner. Please try again.": "Не удалось изменить владельца. Пожалуйста, попробуйте снова.",
"Failed to copy media.": "Не удалось скопировать медиа.", "Failed to copy media.": "Не удалось скопировать медиа.",
"Failed to create playlist": "Не удалось создать плейлист", "Failed to create playlist": "Не удалось создать плейлист",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Не удалось удалить медиа. Пожалуйста, попробуйте снова.", "Failed to delete media. Please try again.": "Не удалось удалить медиа. Пожалуйста, попробуйте снова.",
"Failed to disable comments.": "Не удалось отключить комментарии.", "Failed to disable comments.": "Не удалось отключить комментарии.",
"Failed to disable download.": "Не удалось отключить загрузку.", "Failed to disable download.": "Не удалось отключить загрузку.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Фильтровать существующих пользователей...", "Filter existing users...": "Фильтровать существующих пользователей...",
"Filter playlists...": "Фильтровать плейлисты...", "Filter playlists...": "Фильтровать плейлисты...",
"Filters": "Фильтры", "Filters": "Фильтры",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Перейти", "Go": "Перейти",
"History": "История", "History": "История",
"Home": "Главная", "Home": "Главная",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Управление комментариями", "Manage comments": "Управление комментариями",
"Manage media": "Управление медиа", "Manage media": "Управление медиа",
"Manage users": "Управление пользователями", "Manage users": "Управление пользователями",
"Management": "",
"Media": "Медиа", "Media": "Медиа",
"Media I own": "Медиа, которыми я владею", "Media I own": "Медиа, которыми я владею",
"Media was edited": "Медиа было отредактировано", "Media was edited": "Медиа было отредактировано",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Нет результатов для", "No results for": "Нет результатов для",
"No tags": "Нет тегов", "No tags": "Нет тегов",
"No users to add": "Нет пользователей для добавления", "No users to add": "Нет пользователей для добавления",
"Organization": "",
"PLAYLISTS": "ПЛЕЙЛИСТЫ", "PLAYLISTS": "ПЛЕЙЛИСТЫ",
"PUBLISH STATE": "СОСТОЯНИЕ ПУБЛИКАЦИИ", "PUBLISH STATE": "СОСТОЯНИЕ ПУБЛИКАЦИИ",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Опубликовано", "Published on": "Опубликовано",
"Recent uploads": "Недавние загрузки", "Recent uploads": "Недавние загрузки",
"Recommended": "Рекомендуемое", "Recommended": "Рекомендуемое",
"Record": "",
"Record Screen": "Запись экрана", "Record Screen": "Запись экрана",
"Record Screen with Audio": "",
"Register": "Регистрация", "Register": "Регистрация",
"Remove category": "Удалить категорию", "Remove category": "Удалить категорию",
"Remove from list": "Удалить из списка", "Remove from list": "Удалить из списка",
"Remove tag": "Удалить тег", "Remove tag": "Удалить тег",
"Remove user": "Удалить пользователя", "Remove user": "Удалить пользователя",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "СОХРАНИТЬ", "SAVE": "СОХРАНИТЬ",
"SEARCH": "ПОИСК", "SEARCH": "ПОИСК",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Выбрать все медиа", "Select all media": "Выбрать все медиа",
"Select publish state:": "Выберите состояние публикации:", "Select publish state:": "Выберите состояние публикации:",
"Selected": "Выбрано", "Selected": "Выбрано",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Мной поделено", "Shared by me": "Мной поделено",
"Shared with me": "Поделено со мной", "Shared with me": "Поделено со мной",
"Sharing": "",
"Sign in": "Войти", "Sign in": "Войти",
"Sign out": "Выйти", "Sign out": "Выйти",
"Sort By": "Сортировать по", "Sort By": "Сортировать по",
"Start Recording": "Начать запись", "Start Recording": "Начать запись",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Начните загружать медиа и делиться своей работой. Загруженные медиа появятся здесь.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Начните загружать медиа и делиться своей работой. Загруженные медиа появятся здесь.",
"Stop Recording": "Остановить запись", "Stop Recording": "Остановить запись",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Отправить", "Submit": "Отправить",
"Subtitle was added": "Субтитры были добавлены", "Subtitle was added": "Субтитры были добавлены",
"Subtitles": "Субтитры", "Subtitles": "Субтитры",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Комментарии успешно включены", "Successfully Enabled comments": "Комментарии успешно включены",
"Successfully changed owner": "Владелец успешно изменен", "Successfully changed owner": "Владелец успешно изменен",
"Successfully deleted": "Успешно удалено", "Successfully deleted": "Успешно удалено",
"Successfully deleted comments": "",
"Successfully updated": "Успешно обновлено", "Successfully updated": "Успешно обновлено",
"Successfully updated categories": "Категории успешно обновлены", "Successfully updated categories": "Категории успешно обновлены",
"Successfully updated playlist membership": "Членство в плейлисте успешно обновлено", "Successfully updated playlist membership": "Членство в плейлисте успешно обновлено",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Загрузить медиа", "Upload media": "Загрузить медиа",
"Uploads": "Загрузки", "Uploads": "Загрузки",
"Users": "Пользователи", "Users": "Пользователи",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "ПОКАЗАТЬ ВСЕ", "VIEW ALL": "ПОКАЗАТЬ ВСЕ",
"Video": "Видео", "Video": "Видео",
"View all": "Показать все", "View all": "Показать все",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Добро пожаловать", "Welcome": "Добро пожаловать",
"You are going to copy": "Вы собираетесь скопировать", "You are going to copy": "Вы собираетесь скопировать",
"You are going to delete": "Вы собираетесь удалить", "You are going to delete": "Вы собираетесь удалить",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Вы собираетесь отключить комментарии для", "You are going to disable comments to": "Вы собираетесь отключить комментарии для",
"You are going to disable download for": "Вы собираетесь отключить загрузку для", "You are going to disable download for": "Вы собираетесь отключить загрузку для",
"You are going to enable comments to": "Вы собираетесь включить комментарии для", "You are going to enable comments to": "Вы собираетесь включить комментарии для",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "IZBRIŠI MEDIJ", "DELETE MEDIA": "IZBRIŠI MEDIJ",
"DOWNLOAD": "PRENESI", "DOWNLOAD": "PRENESI",
"DURATION": "TRAJANJE", "DURATION": "TRAJANJE",
"Delete Comments": "",
"Delete Media": "Izbriši Medij", "Delete Media": "Izbriši Medij",
"Delete media": "Izbriši medij", "Delete media": "Izbriši medij",
"Disable Comments": "Onemogoči Komentarje", "Disable Comments": "Onemogoči Komentarje",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Spreminjanje lastnika ni uspelo. Prosim poskusite ponovno.", "Failed to change owner. Please try again.": "Spreminjanje lastnika ni uspelo. Prosim poskusite ponovno.",
"Failed to copy media.": "Kopiranje medija ni uspelo.", "Failed to copy media.": "Kopiranje medija ni uspelo.",
"Failed to create playlist": "Ustvarjanje seznama predvajanja ni uspelo", "Failed to create playlist": "Ustvarjanje seznama predvajanja ni uspelo",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Brisanje medija ni uspelo. Prosim poskusite ponovno.", "Failed to delete media. Please try again.": "Brisanje medija ni uspelo. Prosim poskusite ponovno.",
"Failed to disable comments.": "Onemogočanje komentarjev ni uspelo.", "Failed to disable comments.": "Onemogočanje komentarjev ni uspelo.",
"Failed to disable download.": "Onemogočanje prenosa ni uspelo.", "Failed to disable download.": "Onemogočanje prenosa ni uspelo.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Filtriraj obstoječe uporabnike...", "Filter existing users...": "Filtriraj obstoječe uporabnike...",
"Filter playlists...": "Filtriraj sezname predvajanja...", "Filter playlists...": "Filtriraj sezname predvajanja...",
"Filters": "Filtri", "Filters": "Filtri",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Pojdi", "Go": "Pojdi",
"History": "Zgodovina", "History": "Zgodovina",
"Home": "Domov", "Home": "Domov",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Upravljaj komentarje", "Manage comments": "Upravljaj komentarje",
"Manage media": "Upravljaj medije", "Manage media": "Upravljaj medije",
"Manage users": "Upravljaj uporabnike", "Manage users": "Upravljaj uporabnike",
"Management": "",
"Media": "Mediji", "Media": "Mediji",
"Media I own": "Mediji, ki jih posedujam", "Media I own": "Mediji, ki jih posedujam",
"Media was edited": "Medij je bil urejen", "Media was edited": "Medij je bil urejen",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Ni rezultatov za", "No results for": "Ni rezultatov za",
"No tags": "Brez oznak", "No tags": "Brez oznak",
"No users to add": "Ni uporabnikov za dodajanje", "No users to add": "Ni uporabnikov za dodajanje",
"Organization": "",
"PLAYLISTS": "SEZNAMI PREDVAJANJA", "PLAYLISTS": "SEZNAMI PREDVAJANJA",
"PUBLISH STATE": "STANJE OBJAVE", "PUBLISH STATE": "STANJE OBJAVE",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Objavljeno", "Published on": "Objavljeno",
"Recent uploads": "Nedavne naložitve", "Recent uploads": "Nedavne naložitve",
"Recommended": "Priporočeno", "Recommended": "Priporočeno",
"Record": "",
"Record Screen": "Snemanje zaslona", "Record Screen": "Snemanje zaslona",
"Record Screen with Audio": "",
"Register": "Registracija", "Register": "Registracija",
"Remove category": "Odstrani kategorijo", "Remove category": "Odstrani kategorijo",
"Remove from list": "Odstrani s seznama", "Remove from list": "Odstrani s seznama",
"Remove tag": "Odstrani oznako", "Remove tag": "Odstrani oznako",
"Remove user": "Odstrani uporabnika", "Remove user": "Odstrani uporabnika",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "SHRANI", "SAVE": "SHRANI",
"SEARCH": "ISKANJE", "SEARCH": "ISKANJE",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Izberi vse medije", "Select all media": "Izberi vse medije",
"Select publish state:": "Izberi stanje objave:", "Select publish state:": "Izberi stanje objave:",
"Selected": "Izbrano", "Selected": "Izbrano",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Deljeno z moje strani", "Shared by me": "Deljeno z moje strani",
"Shared with me": "Deljeno z mano", "Shared with me": "Deljeno z mano",
"Sharing": "",
"Sign in": "Prijava", "Sign in": "Prijava",
"Sign out": "Odjava", "Sign out": "Odjava",
"Sort By": "Razvrsti po", "Sort By": "Razvrsti po",
"Start Recording": "Začni snemanje", "Start Recording": "Začni snemanje",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Začnite nalagati medije in deliti svoje delo. Mediji, ki jih naložite, bodo prikazani tukaj.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Začnite nalagati medije in deliti svoje delo. Mediji, ki jih naložite, bodo prikazani tukaj.",
"Stop Recording": "Ustavi snemanje", "Stop Recording": "Ustavi snemanje",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Pošlji", "Submit": "Pošlji",
"Subtitle was added": "Podnapisi so bili dodani", "Subtitle was added": "Podnapisi so bili dodani",
"Subtitles": "Podnapisi", "Subtitles": "Podnapisi",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Komentarji uspešno omogočeni", "Successfully Enabled comments": "Komentarji uspešno omogočeni",
"Successfully changed owner": "Lastnik uspešno spremenjen", "Successfully changed owner": "Lastnik uspešno spremenjen",
"Successfully deleted": "Uspešno izbrisano", "Successfully deleted": "Uspešno izbrisano",
"Successfully deleted comments": "",
"Successfully updated": "Uspešno posodobljeno", "Successfully updated": "Uspešno posodobljeno",
"Successfully updated categories": "Kategorije uspešno posodobljene", "Successfully updated categories": "Kategorije uspešno posodobljene",
"Successfully updated playlist membership": "Članstvo seznama predvajanja uspešno posodobljeno", "Successfully updated playlist membership": "Članstvo seznama predvajanja uspešno posodobljeno",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Naloži medij", "Upload media": "Naloži medij",
"Uploads": "Naloženi", "Uploads": "Naloženi",
"Users": "Uporabniki", "Users": "Uporabniki",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "PRIKAŽI VSE", "VIEW ALL": "PRIKAŽI VSE",
"Video": "Video", "Video": "Video",
"View all": "Prikaži vse", "View all": "Prikaži vse",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Dobrodošli", "Welcome": "Dobrodošli",
"You are going to copy": "Kopirate", "You are going to copy": "Kopirate",
"You are going to delete": "Brišete", "You are going to delete": "Brišete",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Onemogočate komentarje za", "You are going to disable comments to": "Onemogočate komentarje za",
"You are going to disable download for": "Onemogočate prenos za", "You are going to disable download for": "Onemogočate prenos za",
"You are going to enable comments to": "Omogočate komentarje za", "You are going to enable comments to": "Omogočate komentarje za",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "MEDYAYI SİL", "DELETE MEDIA": "MEDYAYI SİL",
"DOWNLOAD": "İNDİR", "DOWNLOAD": "İNDİR",
"DURATION": "SÜRE", "DURATION": "SÜRE",
"Delete Comments": "",
"Delete Media": "Medyayı Sil", "Delete Media": "Medyayı Sil",
"Delete media": "Medyayı sil", "Delete media": "Medyayı sil",
"Disable Comments": "Yorumları Devre Dışı Bırak", "Disable Comments": "Yorumları Devre Dışı Bırak",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "Sahip değiştirilemedi. Lütfen tekrar deneyin.", "Failed to change owner. Please try again.": "Sahip değiştirilemedi. Lütfen tekrar deneyin.",
"Failed to copy media.": "Medya kopyalanamadı.", "Failed to copy media.": "Medya kopyalanamadı.",
"Failed to create playlist": "Çalma listesi oluşturulamadı", "Failed to create playlist": "Çalma listesi oluşturulamadı",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "Medya silinemedi. Lütfen tekrar deneyin.", "Failed to delete media. Please try again.": "Medya silinemedi. Lütfen tekrar deneyin.",
"Failed to disable comments.": "Yorumlar devre dışı bırakılamadı.", "Failed to disable comments.": "Yorumlar devre dışı bırakılamadı.",
"Failed to disable download.": "İndirme devre dışı bırakılamadı.", "Failed to disable download.": "İndirme devre dışı bırakılamadı.",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "Mevcut kullanıcıları filtrele...", "Filter existing users...": "Mevcut kullanıcıları filtrele...",
"Filter playlists...": "Çalma listelerini filtrele...", "Filter playlists...": "Çalma listelerini filtrele...",
"Filters": "Filtreler", "Filters": "Filtreler",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "Git", "Go": "Git",
"History": "Geçmiş", "History": "Geçmiş",
"Home": "Ana Sayfa", "Home": "Ana Sayfa",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "Yorumları yönet", "Manage comments": "Yorumları yönet",
"Manage media": "Medyayı yönet", "Manage media": "Medyayı yönet",
"Manage users": "Kullanıcıları yönet", "Manage users": "Kullanıcıları yönet",
"Management": "",
"Media": "Medya", "Media": "Medya",
"Media I own": "Sahip olduğum medya", "Media I own": "Sahip olduğum medya",
"Media was edited": "Medya düzenlendi", "Media was edited": "Medya düzenlendi",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "Sonuç bulunamadı", "No results for": "Sonuç bulunamadı",
"No tags": "Etiket yok", "No tags": "Etiket yok",
"No users to add": "Eklenecek kullanıcı yok", "No users to add": "Eklenecek kullanıcı yok",
"Organization": "",
"PLAYLISTS": "ÇALMA LİSTELERİ", "PLAYLISTS": "ÇALMA LİSTELERİ",
"PUBLISH STATE": "YAYINLANMA DURUMU", "PUBLISH STATE": "YAYINLANMA DURUMU",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "Yayınlanma tarihi", "Published on": "Yayınlanma tarihi",
"Recent uploads": "Son yüklemeler", "Recent uploads": "Son yüklemeler",
"Recommended": "Önerilen", "Recommended": "Önerilen",
"Record": "",
"Record Screen": "Ekranı Kaydet", "Record Screen": "Ekranı Kaydet",
"Record Screen with Audio": "",
"Register": "Kayıt Ol", "Register": "Kayıt Ol",
"Remove category": "Kategoriyi kaldır", "Remove category": "Kategoriyi kaldır",
"Remove from list": "Listeden kaldır", "Remove from list": "Listeden kaldır",
"Remove tag": "Etiketi kaldır", "Remove tag": "Etiketi kaldır",
"Remove user": "Kullanıcıyı kaldır", "Remove user": "Kullanıcıyı kaldır",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "KAYDET", "SAVE": "KAYDET",
"SEARCH": "ARA", "SEARCH": "ARA",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "Tüm medyayı seç", "Select all media": "Tüm medyayı seç",
"Select publish state:": "Yayınlanma durumunu seç:", "Select publish state:": "Yayınlanma durumunu seç:",
"Selected": "Seçildi", "Selected": "Seçildi",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "Paylaştıklarım", "Shared by me": "Paylaştıklarım",
"Shared with me": "Benimle paylaşılanlar", "Shared with me": "Benimle paylaşılanlar",
"Sharing": "",
"Sign in": "Giriş Yap", "Sign in": "Giriş Yap",
"Sign out": "Çıkış Yap", "Sign out": "Çıkış Yap",
"Sort By": "Sırala", "Sort By": "Sırala",
"Start Recording": "Kaydı Başlat", "Start Recording": "Kaydı Başlat",
"Start uploading media and sharing your work. Media that you upload will show up here.": "Medya yüklemeye ve çalışmanızı paylaşmaya başlayın. Yüklediğiniz medya burada görünecektir.", "Start uploading media and sharing your work. Media that you upload will show up here.": "Medya yüklemeye ve çalışmanızı paylaşmaya başlayın. Yüklediğiniz medya burada görünecektir.",
"Stop Recording": "Kaydı Durdur", "Stop Recording": "Kaydı Durdur",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "Gönder", "Submit": "Gönder",
"Subtitle was added": "Alt yazı eklendi", "Subtitle was added": "Alt yazı eklendi",
"Subtitles": "Altyazılar", "Subtitles": "Altyazılar",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "Yorumlar başarıyla etkinleştirildi", "Successfully Enabled comments": "Yorumlar başarıyla etkinleştirildi",
"Successfully changed owner": "Sahip başarıyla değiştirildi", "Successfully changed owner": "Sahip başarıyla değiştirildi",
"Successfully deleted": "Başarıyla silindi", "Successfully deleted": "Başarıyla silindi",
"Successfully deleted comments": "",
"Successfully updated": "Başarıyla güncellendi", "Successfully updated": "Başarıyla güncellendi",
"Successfully updated categories": "Kategoriler başarıyla güncellendi", "Successfully updated categories": "Kategoriler başarıyla güncellendi",
"Successfully updated playlist membership": "Çalma listesi üyeliği başarıyla güncellendi", "Successfully updated playlist membership": "Çalma listesi üyeliği başarıyla güncellendi",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "Medya yükle", "Upload media": "Medya yükle",
"Uploads": "Yüklemeler", "Uploads": "Yüklemeler",
"Users": "Kullanıcılar", "Users": "Kullanıcılar",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "HEPSİNİ GÖR", "VIEW ALL": "HEPSİNİ GÖR",
"Video": "Video", "Video": "Video",
"View all": "Hepsini gör", "View all": "Hepsini gör",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "Hoş geldiniz", "Welcome": "Hoş geldiniz",
"You are going to copy": "Kopyalayacaksınız", "You are going to copy": "Kopyalayacaksınız",
"You are going to delete": "Sileceksiniz", "You are going to delete": "Sileceksiniz",
"You are going to delete all comments from": "",
"You are going to disable comments to": "Yorumları devre dışı bırakacaksınız", "You are going to disable comments to": "Yorumları devre dışı bırakacaksınız",
"You are going to disable download for": "İndirmeyi devre dışı bırakacaksınız", "You are going to disable download for": "İndirmeyi devre dışı bırakacaksınız",
"You are going to enable comments to": "Yorumları etkinleştireceksiniz", "You are going to enable comments to": "Yorumları etkinleştireceksiniz",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "میڈیا حذف کریں", "DELETE MEDIA": "میڈیا حذف کریں",
"DOWNLOAD": "ڈاؤن لوڈ", "DOWNLOAD": "ڈاؤن لوڈ",
"DURATION": "دورانیہ", "DURATION": "دورانیہ",
"Delete Comments": "",
"Delete Media": "میڈیا حذف کریں", "Delete Media": "میڈیا حذف کریں",
"Delete media": "میڈیا حذف کریں", "Delete media": "میڈیا حذف کریں",
"Disable Comments": "تبصرے غیر فعال کریں", "Disable Comments": "تبصرے غیر فعال کریں",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "مالک تبدیل کرنے میں ناکام۔ براہ کرم دوبارہ کوشش کریں۔", "Failed to change owner. Please try again.": "مالک تبدیل کرنے میں ناکام۔ براہ کرم دوبارہ کوشش کریں۔",
"Failed to copy media.": "میڈیا کاپی کرنے میں ناکام۔", "Failed to copy media.": "میڈیا کاپی کرنے میں ناکام۔",
"Failed to create playlist": "پلے لسٹ بنانے میں ناکام", "Failed to create playlist": "پلے لسٹ بنانے میں ناکام",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "میڈیا حذف کرنے میں ناکام۔ براہ کرم دوبارہ کوشش کریں۔", "Failed to delete media. Please try again.": "میڈیا حذف کرنے میں ناکام۔ براہ کرم دوبارہ کوشش کریں۔",
"Failed to disable comments.": "تبصرے غیر فعال کرنے میں ناکام۔", "Failed to disable comments.": "تبصرے غیر فعال کرنے میں ناکام۔",
"Failed to disable download.": "ڈاؤن لوڈ غیر فعال کرنے میں ناکام۔", "Failed to disable download.": "ڈاؤن لوڈ غیر فعال کرنے میں ناکام۔",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "موجودہ صارفین فلٹر کریں...", "Filter existing users...": "موجودہ صارفین فلٹر کریں...",
"Filter playlists...": "پلے لسٹس فلٹر کریں...", "Filter playlists...": "پلے لسٹس فلٹر کریں...",
"Filters": "فلٹرز", "Filters": "فلٹرز",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "جائیں", "Go": "جائیں",
"History": "تاریخ", "History": "تاریخ",
"Home": "ہوم", "Home": "ہوم",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "تبصرے منظم کریں", "Manage comments": "تبصرے منظم کریں",
"Manage media": "میڈیا منظم کریں", "Manage media": "میڈیا منظم کریں",
"Manage users": "صارفین منظم کریں", "Manage users": "صارفین منظم کریں",
"Management": "",
"Media": "میڈیا", "Media": "میڈیا",
"Media I own": "", "Media I own": "",
"Media was edited": "میڈیا ترمیم کیا گیا", "Media was edited": "میڈیا ترمیم کیا گیا",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "کے لئے کوئی نتائج نہیں", "No results for": "کے لئے کوئی نتائج نہیں",
"No tags": "کوئی ٹیگز نہیں", "No tags": "کوئی ٹیگز نہیں",
"No users to add": "شامل کرنے کے لیے کوئی صارف نہیں", "No users to add": "شامل کرنے کے لیے کوئی صارف نہیں",
"Organization": "",
"PLAYLISTS": "پلے لسٹس", "PLAYLISTS": "پلے لسٹس",
"PUBLISH STATE": "اشاعت کی حالت", "PUBLISH STATE": "اشاعت کی حالت",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "پر شائع ہوا", "Published on": "پر شائع ہوا",
"Recent uploads": "حالیہ اپ لوڈز", "Recent uploads": "حالیہ اپ لوڈز",
"Recommended": "تجویز کردہ", "Recommended": "تجویز کردہ",
"Record": "",
"Record Screen": "اسکرین ریکارڈ کریں", "Record Screen": "اسکرین ریکارڈ کریں",
"Record Screen with Audio": "",
"Register": "رجسٹر کریں", "Register": "رجسٹر کریں",
"Remove category": "قسم ہٹائیں", "Remove category": "قسم ہٹائیں",
"Remove from list": "فہرست سے ہٹائیں", "Remove from list": "فہرست سے ہٹائیں",
"Remove tag": "ٹیگ ہٹائیں", "Remove tag": "ٹیگ ہٹائیں",
"Remove user": "صارف ہٹائیں", "Remove user": "صارف ہٹائیں",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "محفوظ کریں", "SAVE": "محفوظ کریں",
"SEARCH": "تلاش کریں", "SEARCH": "تلاش کریں",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "تمام میڈیا منتخب کریں", "Select all media": "تمام میڈیا منتخب کریں",
"Select publish state:": "اشاعت کی حالت منتخب کریں:", "Select publish state:": "اشاعت کی حالت منتخب کریں:",
"Selected": "منتخب شدہ", "Selected": "منتخب شدہ",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "میری طرف سے شیئر کیا گیا", "Shared by me": "میری طرف سے شیئر کیا گیا",
"Shared with me": "میرے ساتھ شیئر کیا گیا", "Shared with me": "میرے ساتھ شیئر کیا گیا",
"Sharing": "",
"Sign in": "سائن ان کریں", "Sign in": "سائن ان کریں",
"Sign out": "سائن آؤٹ کریں", "Sign out": "سائن آؤٹ کریں",
"Sort By": "ترتیب دیں", "Sort By": "ترتیب دیں",
"Start Recording": "ریکارڈنگ شروع کریں", "Start Recording": "ریکارڈنگ شروع کریں",
"Start uploading media and sharing your work. Media that you upload will show up here.": "میڈیا اپ لوڈ کرنا اور اپنا کام شیئر کرنا شروع کریں۔ آپ جو میڈیا اپ لوڈ کرتے ہیں وہ یہاں ظاہر ہوگا۔", "Start uploading media and sharing your work. Media that you upload will show up here.": "میڈیا اپ لوڈ کرنا اور اپنا کام شیئر کرنا شروع کریں۔ آپ جو میڈیا اپ لوڈ کرتے ہیں وہ یہاں ظاہر ہوگا۔",
"Stop Recording": "ریکارڈنگ روکیں", "Stop Recording": "ریکارڈنگ روکیں",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "جمع کرائیں", "Submit": "جمع کرائیں",
"Subtitle was added": "سب ٹائٹل شامل کیا گیا", "Subtitle was added": "سب ٹائٹل شامل کیا گیا",
"Subtitles": "سب ٹائٹلز", "Subtitles": "سب ٹائٹلز",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "تبصرے کامیابی سے فعال ہو گئے", "Successfully Enabled comments": "تبصرے کامیابی سے فعال ہو گئے",
"Successfully changed owner": "مالک کامیابی سے تبدیل ہو گیا", "Successfully changed owner": "مالک کامیابی سے تبدیل ہو گیا",
"Successfully deleted": "کامیابی سے حذف ہو گیا", "Successfully deleted": "کامیابی سے حذف ہو گیا",
"Successfully deleted comments": "",
"Successfully updated": "کامیابی سے اپ ڈیٹ ہو گیا", "Successfully updated": "کامیابی سے اپ ڈیٹ ہو گیا",
"Successfully updated categories": "اقسام کامیابی سے اپ ڈیٹ ہو گئیں", "Successfully updated categories": "اقسام کامیابی سے اپ ڈیٹ ہو گئیں",
"Successfully updated playlist membership": "پلے لسٹ ممبرشپ کامیابی سے اپ ڈیٹ ہو گئی", "Successfully updated playlist membership": "پلے لسٹ ممبرشپ کامیابی سے اپ ڈیٹ ہو گئی",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "میڈیا اپ لوڈ کریں", "Upload media": "میڈیا اپ لوڈ کریں",
"Uploads": "اپ لوڈز", "Uploads": "اپ لوڈز",
"Users": "صارفین", "Users": "صارفین",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "سب دیکھیں", "VIEW ALL": "سب دیکھیں",
"Video": "ویڈیو", "Video": "ویڈیو",
"View all": "سب دیکھیں", "View all": "سب دیکھیں",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "خوش آمدید", "Welcome": "خوش آمدید",
"You are going to copy": "آپ کاپی کرنے جا رہے ہیں", "You are going to copy": "آپ کاپی کرنے جا رہے ہیں",
"You are going to delete": "آپ حذف کرنے جا رہے ہیں", "You are going to delete": "آپ حذف کرنے جا رہے ہیں",
"You are going to delete all comments from": "",
"You are going to disable comments to": "آپ تبصرے غیر فعال کرنے جا رہے ہیں", "You are going to disable comments to": "آپ تبصرے غیر فعال کرنے جا رہے ہیں",
"You are going to disable download for": "آپ ڈاؤن لوڈ غیر فعال کرنے جا رہے ہیں", "You are going to disable download for": "آپ ڈاؤن لوڈ غیر فعال کرنے جا رہے ہیں",
"You are going to enable comments to": "آپ تبصرے فعال کرنے جا رہے ہیں", "You are going to enable comments to": "آپ تبصرے فعال کرنے جا رہے ہیں",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "删除媒体", "DELETE MEDIA": "删除媒体",
"DOWNLOAD": "下载", "DOWNLOAD": "下载",
"DURATION": "时长", "DURATION": "时长",
"Delete Comments": "",
"Delete Media": "", "Delete Media": "",
"Delete media": "删除媒体", "Delete media": "删除媒体",
"Disable Comments": "", "Disable Comments": "",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "", "Failed to change owner. Please try again.": "",
"Failed to copy media.": "复制媒体失败。", "Failed to copy media.": "复制媒体失败。",
"Failed to create playlist": "", "Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "删除媒体失败。请重试。", "Failed to delete media. Please try again.": "删除媒体失败。请重试。",
"Failed to disable comments.": "禁用评论失败。", "Failed to disable comments.": "禁用评论失败。",
"Failed to disable download.": "禁用下载失败。", "Failed to disable download.": "禁用下载失败。",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "", "Filter existing users...": "",
"Filter playlists...": "", "Filter playlists...": "",
"Filters": "筛选", "Filters": "筛选",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "", "Go": "",
"History": "历史", "History": "历史",
"Home": "主页", "Home": "主页",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "管理评论", "Manage comments": "管理评论",
"Manage media": "管理媒体", "Manage media": "管理媒体",
"Manage users": "管理用户", "Manage users": "管理用户",
"Management": "",
"Media": "媒体", "Media": "媒体",
"Media I own": "", "Media I own": "",
"Media was edited": "媒体已编辑", "Media was edited": "媒体已编辑",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "没有结果", "No results for": "没有结果",
"No tags": "", "No tags": "",
"No users to add": "", "No users to add": "",
"Organization": "",
"PLAYLISTS": "播放列表", "PLAYLISTS": "播放列表",
"PUBLISH STATE": "发布状态", "PUBLISH STATE": "发布状态",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "发布于", "Published on": "发布于",
"Recent uploads": "最近上传", "Recent uploads": "最近上传",
"Recommended": "推荐", "Recommended": "推荐",
"Record": "",
"Record Screen": "录制屏幕", "Record Screen": "录制屏幕",
"Record Screen with Audio": "",
"Register": "注册", "Register": "注册",
"Remove category": "", "Remove category": "",
"Remove from list": "", "Remove from list": "",
"Remove tag": "", "Remove tag": "",
"Remove user": "", "Remove user": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "保存", "SAVE": "保存",
"SEARCH": "搜索", "SEARCH": "搜索",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "", "Select all media": "",
"Select publish state:": "", "Select publish state:": "",
"Selected": "", "Selected": "",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "我分享的", "Shared by me": "我分享的",
"Shared with me": "分享给我的", "Shared with me": "分享给我的",
"Sharing": "",
"Sign in": "登录", "Sign in": "登录",
"Sign out": "登出", "Sign out": "登出",
"Sort By": "排序方式", "Sort By": "排序方式",
"Start Recording": "开始录制", "Start Recording": "开始录制",
"Start uploading media and sharing your work. Media that you upload will show up here.": "开始上传媒体并分享您的作品。您上传的媒体将显示在这里。", "Start uploading media and sharing your work. Media that you upload will show up here.": "开始上传媒体并分享您的作品。您上传的媒体将显示在这里。",
"Stop Recording": "停止录制", "Stop Recording": "停止录制",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "", "Submit": "",
"Subtitle was added": "字幕已添加", "Subtitle was added": "字幕已添加",
"Subtitles": "字幕", "Subtitles": "字幕",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "评论已成功启用", "Successfully Enabled comments": "评论已成功启用",
"Successfully changed owner": "", "Successfully changed owner": "",
"Successfully deleted": "删除成功", "Successfully deleted": "删除成功",
"Successfully deleted comments": "",
"Successfully updated": "", "Successfully updated": "",
"Successfully updated categories": "", "Successfully updated categories": "",
"Successfully updated playlist membership": "", "Successfully updated playlist membership": "",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "上传媒体", "Upload media": "上传媒体",
"Uploads": "上传", "Uploads": "上传",
"Users": "", "Users": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "查看全部", "VIEW ALL": "查看全部",
"Video": "视频", "Video": "视频",
"View all": "查看全部", "View all": "查看全部",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "欢迎", "Welcome": "欢迎",
"You are going to copy": "您将复制", "You are going to copy": "您将复制",
"You are going to delete": "您将删除", "You are going to delete": "您将删除",
"You are going to delete all comments from": "",
"You are going to disable comments to": "您将禁用评论", "You are going to disable comments to": "您将禁用评论",
"You are going to disable download for": "您将禁用下载", "You are going to disable download for": "您将禁用下载",
"You are going to enable comments to": "您将启用评论", "You are going to enable comments to": "您将启用评论",

View File

@@ -48,6 +48,7 @@ translation_strings = {
"DELETE MEDIA": "刪除影片", "DELETE MEDIA": "刪除影片",
"DOWNLOAD": "下載", "DOWNLOAD": "下載",
"DURATION": "時長", "DURATION": "時長",
"Delete Comments": "",
"Delete Media": "", "Delete Media": "",
"Delete media": "刪除媒體", "Delete media": "刪除媒體",
"Disable Comments": "", "Disable Comments": "",
@@ -70,6 +71,7 @@ translation_strings = {
"Failed to change owner. Please try again.": "", "Failed to change owner. Please try again.": "",
"Failed to copy media.": "複製媒體失敗。", "Failed to copy media.": "複製媒體失敗。",
"Failed to create playlist": "", "Failed to create playlist": "",
"Failed to delete comments.": "",
"Failed to delete media. Please try again.": "刪除媒體失敗。請再試一次。", "Failed to delete media. Please try again.": "刪除媒體失敗。請再試一次。",
"Failed to disable comments.": "停用留言失敗。", "Failed to disable comments.": "停用留言失敗。",
"Failed to disable download.": "停用下載失敗。", "Failed to disable download.": "停用下載失敗。",
@@ -101,6 +103,9 @@ translation_strings = {
"Filter existing users...": "", "Filter existing users...": "",
"Filter playlists...": "", "Filter playlists...": "",
"Filters": "篩選器", "Filters": "篩選器",
"Give users editor permissions to your media by adding them to the below list.": "",
"Give users owner permissions to your media, except for deleting the media, by adding them to the below list.": "",
"Give users viewer permissions to your media by adding them to the below list.": "",
"Go": "執行", "Go": "執行",
"History": "觀看紀錄", "History": "觀看紀錄",
"Home": "首頁", "Home": "首頁",
@@ -121,6 +126,7 @@ translation_strings = {
"Manage comments": "留言管理", "Manage comments": "留言管理",
"Manage media": "媒體管理", "Manage media": "媒體管理",
"Manage users": "使用者管理", "Manage users": "使用者管理",
"Management": "",
"Media": "媒體", "Media": "媒體",
"Media I own": "", "Media I own": "",
"Media was edited": "媒體已更新", "Media was edited": "媒體已更新",
@@ -137,6 +143,7 @@ translation_strings = {
"No results for": "查無相關結果:", "No results for": "查無相關結果:",
"No tags": "", "No tags": "",
"No users to add": "", "No users to add": "",
"Organization": "",
"PLAYLISTS": "播放清單", "PLAYLISTS": "播放清單",
"PUBLISH STATE": "發布狀態", "PUBLISH STATE": "發布狀態",
"Pdf": "PDF", "Pdf": "PDF",
@@ -156,12 +163,17 @@ translation_strings = {
"Published on": "發布日期為", "Published on": "發布日期為",
"Recent uploads": "最近上傳", "Recent uploads": "最近上傳",
"Recommended": "推薦內容", "Recommended": "推薦內容",
"Record": "",
"Record Screen": "螢幕錄製", "Record Screen": "螢幕錄製",
"Record Screen with Audio": "",
"Register": "註冊", "Register": "註冊",
"Remove category": "", "Remove category": "",
"Remove from list": "", "Remove from list": "",
"Remove tag": "", "Remove tag": "",
"Remove user": "", "Remove user": "",
"Remove users from the list to remove editor permissions": "",
"Remove users from the list to remove owner permissions": "",
"Remove users from the list to remove viewer permissions": "",
"Replace": "", "Replace": "",
"SAVE": "儲存", "SAVE": "儲存",
"SEARCH": "搜尋", "SEARCH": "搜尋",
@@ -178,14 +190,22 @@ translation_strings = {
"Select all media": "", "Select all media": "",
"Select publish state:": "", "Select publish state:": "",
"Selected": "", "Selected": "",
"Settings": "",
"Share with": "",
"Share with Co-Editors": "",
"Share with Co-Owners": "",
"Share with Co-Viewers": "",
"Share with Course Members": "",
"Shared by me": "我分享的", "Shared by me": "我分享的",
"Shared with me": "與我分享", "Shared with me": "與我分享",
"Sharing": "",
"Sign in": "登入", "Sign in": "登入",
"Sign out": "登出", "Sign out": "登出",
"Sort By": "排序方式", "Sort By": "排序方式",
"Start Recording": "開始錄製", "Start Recording": "開始錄製",
"Start uploading media and sharing your work. Media that you upload will show up here.": "開始上傳媒體並分享您的作品。您上傳的媒體將顯示在此處。", "Start uploading media and sharing your work. Media that you upload will show up here.": "開始上傳媒體並分享您的作品。您上傳的媒體將顯示在此處。",
"Stop Recording": "停止錄製", "Stop Recording": "停止錄製",
"Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)": "",
"Submit": "", "Submit": "",
"Subtitle was added": "字幕已新增", "Subtitle was added": "字幕已新增",
"Subtitles": "字幕", "Subtitles": "字幕",
@@ -196,6 +216,7 @@ translation_strings = {
"Successfully Enabled comments": "成功啟用留言", "Successfully Enabled comments": "成功啟用留言",
"Successfully changed owner": "", "Successfully changed owner": "",
"Successfully deleted": "成功刪除", "Successfully deleted": "成功刪除",
"Successfully deleted comments": "",
"Successfully updated": "", "Successfully updated": "",
"Successfully updated categories": "", "Successfully updated categories": "",
"Successfully updated playlist membership": "", "Successfully updated playlist membership": "",
@@ -232,6 +253,9 @@ translation_strings = {
"Upload media": "上傳媒體", "Upload media": "上傳媒體",
"Uploads": "上傳內容", "Uploads": "上傳內容",
"Users": "", "Users": "",
"Users can edit your media via: My Media > Shared with Me > particular media > Edit...": "",
"Users can manage your media via: My Media > Shared with Me > particular media > ...": "",
"Users can view your media via: My Media > Shared with Me > particular media > ...": "",
"VIEW ALL": "查看全部", "VIEW ALL": "查看全部",
"Video": "影片", "Video": "影片",
"View all": "瀏覽全部", "View all": "瀏覽全部",
@@ -240,6 +264,7 @@ translation_strings = {
"Welcome": "歡迎", "Welcome": "歡迎",
"You are going to copy": "您即將複製", "You are going to copy": "您即將複製",
"You are going to delete": "您即將刪除", "You are going to delete": "您即將刪除",
"You are going to delete all comments from": "",
"You are going to disable comments to": "您即將停用留言", "You are going to disable comments to": "您即將停用留言",
"You are going to disable download for": "您即將停用下載", "You are going to disable download for": "您即將停用下載",
"You are going to enable comments to": "您即將啟用留言", "You are going to enable comments to": "您即將啟用留言",

View File

@@ -736,7 +736,7 @@ class Media(models.Model):
ret = [] ret = []
for cat in self.category.all(): for cat in self.category.all():
ret.append({"title": cat.title, "url": cat.get_absolute_url()}) ret.append({"title": cat.title, "url": cat.get_absolute_url(), "is_lms_course": cat.is_lms_course})
return ret return ret
@property @property

View File

@@ -226,6 +226,7 @@ class CategorySerializer(serializers.ModelSerializer):
"media_count", "media_count",
"user", "user",
"thumbnail_url", "thumbnail_url",
"is_lms_course",
) )

View File

@@ -47,7 +47,14 @@ class CategoryListContributor(APIView):
"""List categories where user has contributor access""" """List categories where user has contributor access"""
@swagger_auto_schema( @swagger_auto_schema(
manual_parameters=[], 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'], tags=['Categories'],
operation_summary='Lists Categories for Contributors', operation_summary='Lists Categories for Contributors',
operation_description='Lists all categories where the user has contributor access', operation_description='Lists all categories where the user has contributor access',
@@ -61,15 +68,17 @@ class CategoryListContributor(APIView):
categories = Category.objects.none() categories = Category.objects.none()
# Get global/public categories (non-RBAC) # Filter for LMS courses only if requested
public_categories = Category.objects.filter(is_rbac_category=False).prefetch_related("user") 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 # Get RBAC categories where user has contributor access
if getattr(settings, 'USE_RBAC', False): if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_contributor() rbac_categories = request.user.get_rbac_categories_as_contributor()
categories = public_categories.union(rbac_categories) categories = categories.union(rbac_categories)
else:
categories = public_categories
categories = categories.order_by("title") categories = categories.order_by("title")

View File

@@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
from django.contrib.postgres.search import SearchQuery from django.contrib.postgres.search import SearchQuery
from django.db.models import Count, Q from django.db.models import Count, Prefetch, Q, prefetch_related_objects
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
@@ -35,6 +35,7 @@ from ..methods import (
) )
from ..models import ( from ..models import (
Category, Category,
Comment,
EncodeProfile, EncodeProfile,
Media, Media,
MediaPermission, MediaPermission,
@@ -112,6 +113,8 @@ class MediaList(APIView):
upload_date = params.get('upload_date', '').strip() upload_date = params.get('upload_date', '').strip()
duration = params.get('duration', '').strip() duration = params.get('duration', '').strip()
publish_state = params.get('publish_state', '').strip() publish_state = params.get('publish_state', '').strip()
shared_user = params.get('shared_user', '').strip()
shared_group = params.get('shared_group', '').strip()
query = params.get("q", "").strip().lower() query = params.get("q", "").strip().lower()
parsed_combined = False parsed_combined = False
@@ -152,6 +155,7 @@ class MediaList(APIView):
gte = datetime(year, 1, 1) gte = datetime(year, 1, 1)
already_sorted = False already_sorted = False
include_sharing_info = False
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
if show_param == "recommended": if show_param == "recommended":
@@ -164,7 +168,15 @@ class MediaList(APIView):
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
media = Media.objects.none() media = Media.objects.none()
else: else:
media = Media.objects.filter(permissions__owner_user=self.request.user).prefetch_related("user", "tags").distinct() base_queryset = Media.objects.prefetch_related("user", "tags")
conditions = Q(permissions__owner_user=self.request.user)
if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_contributor()
conditions |= Q(category__in=rbac_categories, user=self.request.user)
media = base_queryset.filter(conditions).distinct()
include_sharing_info = True
elif show_param == "shared_with_me": elif show_param == "shared_with_me":
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
media = Media.objects.none() media = Media.objects.none()
@@ -178,12 +190,14 @@ class MediaList(APIView):
rbac_categories = request.user.get_rbac_categories_as_member() rbac_categories = request.user.get_rbac_categories_as_member()
conditions |= Q(category__in=rbac_categories) conditions |= Q(category__in=rbac_categories)
media = base_queryset.filter(conditions).distinct() media = base_queryset.filter(conditions).exclude(user=request.user).distinct()
elif author_param: elif author_param:
user_queryset = User.objects.all() user_queryset = User.objects.all()
user = get_object_or_404(user_queryset, username=author_param) user = get_object_or_404(user_queryset, username=author_param)
if self.request.user == user or is_mediacms_editor(self.request.user): if self.request.user == user or is_mediacms_editor(self.request.user):
media = Media.objects.filter(user=user).prefetch_related("user", "tags") media = Media.objects.filter(user=user).prefetch_related("user", "tags")
if self.request.user == user:
include_sharing_info = True
else: else:
media = self._get_media_queryset(request, user) media = self._get_media_queryset(request, user)
already_sorted = True already_sorted = True
@@ -234,6 +248,12 @@ class MediaList(APIView):
elif publish_state in ['private', 'public', 'unlisted']: elif publish_state in ['private', 'public', 'unlisted']:
media = media.filter(state=publish_state) media = media.filter(state=publish_state)
if shared_user and include_sharing_info:
media = media.filter(permissions__user__username=shared_user).distinct()
if shared_group and include_sharing_info:
media = media.filter(category__is_rbac_category=True, category__rbac_groups__name=shared_group).distinct()
if not already_sorted: if not already_sorted:
media = media.order_by(f"{ordering}{sort_by}") media = media.order_by(f"{ordering}{sort_by}")
@@ -243,6 +263,15 @@ class MediaList(APIView):
page = paginator.paginate_queryset(media, request) page = paginator.paginate_queryset(media, request)
prefetch_related_objects(page, 'tags')
if include_sharing_info:
prefetch_related_objects(
page,
Prefetch('permissions', queryset=MediaPermission.objects.select_related('user')),
Prefetch('category', queryset=Category.objects.filter(is_rbac_category=True).prefetch_related('rbac_groups'), to_attr='rbac_categories_prefetched'),
)
serializer = MediaSerializer(page, many=True, context={"request": request}) serializer = MediaSerializer(page, many=True, context={"request": request})
tags_set = set() tags_set = set()
@@ -253,6 +282,19 @@ class MediaList(APIView):
response = paginator.get_paginated_response(serializer.data) response = paginator.get_paginated_response(serializer.data)
response.data['tags'] = tags response.data['tags'] = tags
if include_sharing_info:
shared_users = {}
shared_groups = {}
for media_obj in page:
for perm in media_obj.permissions.all():
shared_users[perm.user.username] = {"username": perm.user.username, "name": perm.user.name or perm.user.username}
for cat in getattr(media_obj, 'rbac_categories_prefetched', []):
for group in cat.rbac_groups.all():
shared_groups[group.name] = {"name": group.name}
response.data['shared_users'] = list(shared_users.values())
response.data['shared_groups'] = list(shared_groups.values())
return response return response
@swagger_auto_schema( @swagger_auto_schema(
@@ -295,6 +337,7 @@ class MediaBulkUserActions(APIView):
enum=[ enum=[
"enable_comments", "enable_comments",
"disable_comments", "disable_comments",
"delete_comments",
"delete_media", "delete_media",
"enable_download", "enable_download",
"disable_download", "disable_download",
@@ -379,6 +422,10 @@ class MediaBulkUserActions(APIView):
media.update(enable_comments=False) media.update(enable_comments=False)
return Response({"detail": f"Comments disabled for {media.count()} media items"}) return Response({"detail": f"Comments disabled for {media.count()} media items"})
elif action == "delete_comments":
deleted_count, _ = Comment.objects.filter(media__in=media).delete()
return Response({"detail": f"{deleted_count} comments deleted"})
elif action == "delete_media": elif action == "delete_media":
count = media.count() count = media.count()
media.delete() media.delete()
@@ -492,8 +539,9 @@ class MediaBulkUserActions(APIView):
users = ( users = (
MediaPermission.objects.filter(media__in=media, permission=ownership_type) MediaPermission.objects.filter(media__in=media, permission=ownership_type)
.exclude(user=request.user)
.values('user__name', 'user__username') .values('user__name', 'user__username')
.annotate(media_count=Count('media', distinct=True)) .annotate(media_count=Count('media'))
.filter(media_count=media_count) .filter(media_count=media_count)
) )
@@ -574,19 +622,44 @@ class MediaBulkUserActions(APIView):
elif action == "add_to_category": elif action == "add_to_category":
category_uids = request.data.get('category_uids', []) category_uids = request.data.get('category_uids', [])
if not category_uids: lti_context_id = request.data.get('lti_context_id')
return Response({"detail": "category_uids is required for add_to_category action"}, status=status.HTTP_400_BAD_REQUEST)
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) 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 categories: if not categories:
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST) return Response({"detail": "No matching categories found or access denied"}, status=status.HTTP_400_BAD_REQUEST)
added_count = 0 added_count = 0
tag = None
for category in categories: for category in categories:
if category.is_lms_course:
tag, _ = Tag.objects.get_or_create(title=category.title[:100])
for m in media: for m in media:
if not m.category.filter(uid=category.uid).exists(): if not m.category.filter(uid=category.uid).exists():
m.category.add(category) m.category.add(category)
added_count += 1 added_count += 1
if tag:
m.tags.add(tag)
return Response({"detail": f"Added {added_count} media items to {categories.count()} categories"}) return Response({"detail": f"Added {added_count} media items to {categories.count()} categories"})
@@ -600,11 +673,17 @@ class MediaBulkUserActions(APIView):
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST) return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
removed_count = 0 removed_count = 0
tag = None
for category in categories: for category in categories:
if category.is_lms_course:
tag = Tag.objects.filter(title=category.title[:100]).first()
for m in media: for m in media:
if m.category.filter(uid=category.uid).exists(): if m.category.filter(uid=category.uid).exists():
m.category.remove(category) m.category.remove(category)
removed_count += 1 removed_count += 1
if tag:
m.tags.remove(tag)
return Response({"detail": f"Removed {removed_count} media items from {categories.count()} categories"}) return Response({"detail": f"Removed {removed_count} media items from {categories.count()} categories"})
@@ -697,9 +776,6 @@ class MediaDetail(APIView):
return media return media
serializer = SingleMediaSerializer(media, context={"request": request}) serializer = SingleMediaSerializer(media, context={"request": request})
if media.state == "private":
related_media = []
else:
related_media = show_related_media(media, request=request, limit=100) related_media = show_related_media(media, request=request, limit=100)
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request}) related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
related_media = related_media_serializer.data related_media = related_media_serializer.data

View File

@@ -350,13 +350,13 @@ def publish_media(request):
return HttpResponseRedirect(media.get_absolute_url()) return HttpResponseRedirect(media.get_absolute_url())
if request.method == "POST": if request.method == "POST":
form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media) form = MediaPublishForm(request.user, request.POST, request.FILES, instance=media, request=request)
if form.is_valid(): if form.is_valid():
media = form.save() media = form.save()
messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited")) messages.add_message(request, messages.INFO, translate_string(request.LANGUAGE_CODE, "Media was edited"))
return HttpResponseRedirect(media.get_absolute_url()) return HttpResponseRedirect(media.get_absolute_url())
else: else:
form = MediaPublishForm(request.user, instance=media) form = MediaPublishForm(request.user, instance=media, request=request)
return render( return render(
request, request,

View File

@@ -3,6 +3,8 @@ import json
from django import forms from django import forms
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from .models import Category
class CategoryModalWidget(forms.SelectMultiple): class CategoryModalWidget(forms.SelectMultiple):
"""Two-panel category selector with modal""" """Two-panel category selector with modal"""
@@ -12,28 +14,42 @@ class CategoryModalWidget(forms.SelectMultiple):
js = ('js/category_modal.js',) js = ('js/category_modal.js',)
def render(self, name, value, attrs=None, renderer=None): def render(self, name, value, attrs=None, renderer=None):
is_lms_mode = getattr(self, 'is_lms_mode', False)
# Get all categories as JSON # Get all categories as JSON
categories = [] categories = []
for opt_value, opt_label in self.choices: for opt_value, opt_label in self.choices:
if opt_value: # Skip empty choice if opt_value: # Skip empty choice
categories.append({'id': str(opt_value), 'title': str(opt_label)}) # 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})
all_categories_json = json.dumps(categories) all_categories_json = json.dumps(categories)
selected_ids_json = json.dumps([str(v) for v in (value or [])]) 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}"> html = f'''<div class="category-widget" data-name="{name}">
<div class="category-content"> <div class="category-content">
<div class="category-panel"> <div class="category-panel">
<input type="text" class="category-search" placeholder="Search categories..."> <input type="text" class="category-search" placeholder="{search_placeholder}">
<div class="category-list scrollable" data-panel="left"></div> <div class="category-list scrollable" data-panel="left"></div>
</div> </div>
<div class="category-panel"> <div class="category-panel">
<h3>Selected Categories</h3> <h3>{selected_header}</h3>
<div class="category-list scrollable" data-panel="right"></div> <div class="category-list scrollable" data-panel="right"></div>
</div> </div>
</div> </div>
<div class="hidden-inputs"></div> <div class="hidden-inputs"></div>
<script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json}}}</script> <script type="application/json" class="category-data">{{"all":{all_categories_json},"selected":{selected_ids_json},"lms_mode":{lms_mode_json}}}</script>
</div>''' </div>'''
return mark_safe(html) return mark_safe(html)

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en" style="height: 100%">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Embedded Video - Full Screen</title>
<style>
body {
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
overflow: hidden;
}
</style>
</head>
<body>
<iframe
src="https://demo.mediacms.io/embed?m=zK2nirNLC"
style="
width: 100%;
max-width: calc(100vh * 16 / 9);
aspect-ratio: 16 / 9;
display: block;
margin: auto;
border: 0;
"
allowfullscreen
></iframe>
</body>
</html>

View File

@@ -204,6 +204,54 @@ class SeekIndicator extends Component {
</div> </div>
`; `;
textEl.textContent = 'Pause'; textEl.textContent = 'Pause';
} else if (direction === 'copy-url') {
iconEl.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
<div style="
width: ${circleSize};
height: ${circleSize};
border-radius: 50%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
border: none;
outline: none;
box-sizing: border-box;
overflow: hidden;
">
<svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
</div>
</div>
`;
textEl.textContent = '';
} else if (direction === 'copy-embed') {
iconEl.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
<div style="
width: ${circleSize};
height: ${circleSize};
border-radius: 50%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
border: none;
outline: none;
box-sizing: border-box;
overflow: hidden;
">
<svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
<path d="M16 18l6-6-6-6"/>
<path d="M8 6l-6 6 6 6"/>
</svg>
</div>
</div>
`;
textEl.textContent = '';
} }
// Clear any text content in the text element // Clear any text content in the text element
@@ -239,6 +287,11 @@ class SeekIndicator extends Component {
this.showTimeout = setTimeout(() => { this.showTimeout = setTimeout(() => {
this.hide(); this.hide();
}, 500); }, 500);
} else if (direction === 'copy-url' || direction === 'copy-embed') {
// Copy operations: 500ms (same as play/pause)
this.showTimeout = setTimeout(() => {
this.hide();
}, 500);
} }
} }

View File

@@ -14,10 +14,22 @@ class EmbedInfoOverlay extends Component {
this.authorThumbnail = options.authorThumbnail || ''; this.authorThumbnail = options.authorThumbnail || '';
this.videoTitle = options.videoTitle || 'Video'; this.videoTitle = options.videoTitle || 'Video';
this.videoUrl = options.videoUrl || ''; this.videoUrl = options.videoUrl || '';
this.showTitle = options.showTitle !== undefined ? options.showTitle : true;
this.showRelated = options.showRelated !== undefined ? options.showRelated : true;
this.showUserAvatar = options.showUserAvatar !== undefined ? options.showUserAvatar : true;
this.linkTitle = options.linkTitle !== undefined ? options.linkTitle : true;
// Initialize after player is ready // Initialize after player is ready
this.player().ready(() => { this.player().ready(() => {
if (this.showTitle) {
this.createOverlay(); this.createOverlay();
} else {
// Hide overlay element if showTitle is false
const overlay = this.el();
overlay.style.display = 'none';
overlay.style.opacity = '0';
overlay.style.visibility = 'hidden';
}
}); });
} }
@@ -49,7 +61,7 @@ class EmbedInfoOverlay extends Component {
`; `;
// Create avatar container // Create avatar container
if (this.authorThumbnail) { if (this.authorThumbnail && this.showUserAvatar) {
const avatarContainer = document.createElement('div'); const avatarContainer = document.createElement('div');
avatarContainer.className = 'embed-avatar-container'; avatarContainer.className = 'embed-avatar-container';
avatarContainer.style.cssText = ` avatarContainer.style.cssText = `
@@ -125,7 +137,7 @@ class EmbedInfoOverlay extends Component {
overflow: hidden; overflow: hidden;
`; `;
if (this.videoUrl) { if (this.videoUrl && this.linkTitle) {
const titleLink = document.createElement('a'); const titleLink = document.createElement('a');
titleLink.href = this.videoUrl; titleLink.href = this.videoUrl;
titleLink.target = '_blank'; titleLink.target = '_blank';
@@ -186,10 +198,16 @@ class EmbedInfoOverlay extends Component {
const player = this.player(); const player = this.player();
const overlay = this.el(); const overlay = this.el();
// If showTitle is false, ensure overlay is hidden
if (!this.showTitle) {
overlay.style.display = 'none';
overlay.style.opacity = '0';
overlay.style.visibility = 'hidden';
return;
}
// Sync overlay visibility with control bar visibility // Sync overlay visibility with control bar visibility
const updateOverlayVisibility = () => { const updateOverlayVisibility = () => {
const controlBar = player.getChild('controlBar');
if (!player.hasStarted()) { if (!player.hasStarted()) {
// Show overlay when video hasn't started (poster is showing) - like before // Show overlay when video hasn't started (poster is showing) - like before
overlay.style.opacity = '1'; overlay.style.opacity = '1';

View File

@@ -0,0 +1,47 @@
.video-context-menu {
position: fixed;
background-color: #282828;
border-radius: 4px;
padding: 4px 0;
min-width: 240px;
z-index: 10000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
.video-context-menu-item {
display: flex;
align-items: center;
padding: 10px 16px;
color: #ffffff;
cursor: pointer;
transition: background-color 0.15s ease;
font-size: 14px;
user-select: none;
}
.video-context-menu-item:hover {
background-color: #3d3d3d;
}
.video-context-menu-item:active {
background-color: #4a4a4a;
}
.video-context-menu-icon {
width: 18px;
height: 18px;
margin-right: 12px;
flex-shrink: 0;
stroke: currentColor;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.video-context-menu-item span {
flex: 1;
white-space: nowrap;
}

View File

@@ -0,0 +1,85 @@
import React, { useEffect, useRef } from 'react';
import './VideoContextMenu.css';
function VideoContextMenu({ visible, position, onClose, onCopyVideoUrl, onCopyVideoUrlAtTime, onCopyEmbedCode }) {
const menuRef = useRef(null);
useEffect(() => {
if (visible && menuRef.current) {
// Position the menu
menuRef.current.style.left = `${position.x}px`;
menuRef.current.style.top = `${position.y}px`;
// Adjust if menu goes off screen
const rect = menuRef.current.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (rect.right > windowWidth) {
menuRef.current.style.left = `${position.x - rect.width}px`;
}
if (rect.bottom > windowHeight) {
menuRef.current.style.top = `${position.y - rect.height}px`;
}
}
}, [visible, position]);
useEffect(() => {
const handleClickOutside = (e) => {
if (visible && menuRef.current && !menuRef.current.contains(e.target)) {
onClose();
}
};
const handleEscape = (e) => {
if (e.key === 'Escape' && visible) {
onClose();
}
};
if (visible) {
// Use capture phase to catch events earlier, before they can be stopped
// Listen to both mousedown and click to ensure we catch all clicks
document.addEventListener('mousedown', handleClickOutside, true);
document.addEventListener('click', handleClickOutside, true);
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside, true);
document.removeEventListener('click', handleClickOutside, true);
document.removeEventListener('keydown', handleEscape);
};
}, [visible, onClose]);
if (!visible) return null;
return (
<div ref={menuRef} className="video-context-menu" onClick={(e) => e.stopPropagation()}>
<div className="video-context-menu-item" onClick={onCopyVideoUrl}>
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span>Copy video URL</span>
</div>
<div className="video-context-menu-item" onClick={onCopyVideoUrlAtTime}>
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span>Copy video URL at current time</span>
</div>
<div className="video-context-menu-item" onClick={onCopyEmbedCode}>
<svg className="video-context-menu-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 18l6-6-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 6l-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span>Copy embed code</span>
</div>
</div>
);
}
export default VideoContextMenu;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useMemo } from 'react'; import React, { useEffect, useRef, useMemo, useState, useCallback } from 'react';
import videojs from 'video.js'; import videojs from 'video.js';
import 'video.js/dist/video-js.css'; import 'video.js/dist/video-js.css';
import '../../styles/embed.css'; import '../../styles/embed.css';
@@ -17,6 +17,7 @@ import CustomRemainingTime from '../controls/CustomRemainingTime';
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay'; import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
import CustomSettingsMenu from '../controls/CustomSettingsMenu'; import CustomSettingsMenu from '../controls/CustomSettingsMenu';
import SeekIndicator from '../controls/SeekIndicator'; import SeekIndicator from '../controls/SeekIndicator';
import VideoContextMenu from '../overlays/VideoContextMenu';
import UserPreferences from '../../utils/UserPreferences'; import UserPreferences from '../../utils/UserPreferences';
import PlayerConfig from '../../config/playerConfig'; import PlayerConfig from '../../config/playerConfig';
import { AutoplayHandler } from '../../utils/AutoplayHandler'; import { AutoplayHandler } from '../../utils/AutoplayHandler';
@@ -169,7 +170,7 @@ const enableStandardButtonTooltips = (player) => {
}, 500); // Delay to ensure all components are ready }, 500); // Delay to ensure all components are ready
}; };
function VideoJSPlayer({ videoId = 'default-video' }) { function VideoJSPlayer({ videoId = 'default-video', showTitle = true, showRelated = true, showUserAvatar = true, linkTitle = true, urlTimestamp = null }) {
const videoRef = useRef(null); const videoRef = useRef(null);
const playerRef = useRef(null); // Track the player instance const playerRef = useRef(null); // Track the player instance
const userPreferences = useRef(new UserPreferences()); // User preferences instance const userPreferences = useRef(new UserPreferences()); // User preferences instance
@@ -177,25 +178,17 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
const keyboardHandler = useRef(null); // Keyboard handler instance const keyboardHandler = useRef(null); // Keyboard handler instance
const playbackEventHandler = useRef(null); // Playback event handler instance const playbackEventHandler = useRef(null); // Playback event handler instance
// Context menu state
const [contextMenuVisible, setContextMenuVisible] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
// Check if this is an embed player (disable next video and autoplay features) // Check if this is an embed player (disable next video and autoplay features)
const isEmbedPlayer = videoId === 'video-embed'; const isEmbedPlayer = videoId === 'video-embed';
// Utility function to detect touch devices
const isTouchDevice = useMemo(() => {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
}, []);
// Utility function to detect iOS devices
const isIOS = useMemo(() => {
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
);
}, []);
// Environment-based development mode configuration // Environment-based development mode configuration
const isDevMode = import.meta.env.VITE_DEV_MODE === 'true' || window.location.hostname.includes('vercel.app'); const isDevMode = import.meta.env.VITE_DEV_MODE === 'true' || window.location.hostname.includes('vercel.app');
// Safely access window.MEDIA_DATA with fallback using useMemo
// Read options from window.MEDIA_DATA if available (for consistency with embed logic)
const mediaData = useMemo( const mediaData = useMemo(
() => () =>
typeof window !== 'undefined' && window.MEDIA_DATA typeof window !== 'undefined' && window.MEDIA_DATA
@@ -214,12 +207,37 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
}, },
siteUrl: 'https://deic.mediacms.io', siteUrl: 'https://deic.mediacms.io',
nextLink: 'https://deic.mediacms.io/view?m=elygiagorgechania', nextLink: 'https://deic.mediacms.io/view?m=elygiagorgechania',
urlAutoplay: true,
urlMuted: false,
}, },
[] []
); );
// Helper to get effective value (prop or MEDIA_DATA or default)
const getOption = (propKey, mediaDataKey, defaultValue) => {
if (isEmbedPlayer) {
if (mediaData[mediaDataKey] !== undefined) return mediaData[mediaDataKey];
}
return propKey !== undefined ? propKey : defaultValue;
};
const finalShowTitle = getOption(showTitle, 'showTitle', true);
const finalShowRelated = getOption(showRelated, 'showRelated', true);
const finalShowUserAvatar = getOption(showUserAvatar, 'showUserAvatar', true);
const finalLinkTitle = getOption(linkTitle, 'linkTitle', true);
const finalTimestamp = getOption(urlTimestamp, 'urlTimestamp', null);
// Utility function to detect touch devices
const isTouchDevice = useMemo(() => {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
}, []);
// Utility function to detect iOS devices
const isIOS = useMemo(() => {
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
);
}, []);
// Define chapters as JSON object // Define chapters as JSON object
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON // Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
// CONDITIONAL LOGIC: // CONDITIONAL LOGIC:
@@ -531,8 +549,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
isPlayList: mediaData?.isPlayList, isPlayList: mediaData?.isPlayList,
related_media: mediaData.data?.related_media || [], related_media: mediaData.data?.related_media || [],
nextLink: mediaData?.nextLink || null, nextLink: mediaData?.nextLink || null,
urlAutoplay: mediaData?.urlAutoplay || true,
urlMuted: mediaData?.urlMuted || false,
sources: getVideoSources(), sources: getVideoSources(),
}; };
@@ -738,6 +754,212 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
} }
}; };
// Context menu handlers
const handleContextMenu = useCallback((e) => {
// Only handle if clicking on video player area
const target = e.target;
const isVideoPlayerArea =
target.closest('.video-js') ||
target.classList.contains('vjs-tech') ||
target.tagName === 'VIDEO' ||
target.closest('video');
if (isVideoPlayerArea) {
e.preventDefault();
e.stopPropagation();
setContextMenuPosition({ x: e.clientX, y: e.clientY });
setContextMenuVisible(true);
}
}, []);
const closeContextMenu = () => {
setContextMenuVisible(false);
};
// Helper function to get media ID
const getMediaId = () => {
if (typeof window !== 'undefined' && window.MEDIA_DATA?.data?.friendly_token) {
return window.MEDIA_DATA.data.friendly_token;
}
if (mediaData?.data?.friendly_token) {
return mediaData.data.friendly_token;
}
// Try to get from URL (works for both main page and embed page)
if (typeof window !== 'undefined') {
const urlParams = new URLSearchParams(window.location.search);
const mediaIdFromUrl = urlParams.get('m');
if (mediaIdFromUrl) {
return mediaIdFromUrl;
}
// Also check if we're on an embed page with media ID in path
const pathMatch = window.location.pathname.match(/\/embed\/([^/?]+)/);
if (pathMatch) {
return pathMatch[1];
}
}
return currentVideo.id || 'default-video';
};
// Helper function to get base origin URL (handles embed mode)
const getBaseOrigin = () => {
if (typeof window !== 'undefined') {
// In embed mode, try to get origin from parent window if possible
// Otherwise use current window origin
try {
// Check if we're in an iframe and can access parent
if (window.parent !== window && window.parent.location.origin) {
return window.parent.location.origin;
}
} catch {
// Cross-origin iframe, use current origin
}
return window.location.origin;
}
return mediaData.siteUrl || 'https://deic.mediacms.io';
};
// Helper function to get embed URL
const getEmbedUrl = () => {
const mediaId = getMediaId();
const origin = getBaseOrigin();
// Try to get embed URL from config or construct it
if (typeof window !== 'undefined' && window.MediaCMS?.config?.url?.embed) {
return window.MediaCMS.config.url.embed + mediaId;
}
// Fallback: construct embed URL (check if current URL is embed format)
if (typeof window !== 'undefined' && window.location.pathname.includes('/embed')) {
// If we're already on an embed page, use current URL format
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('m', mediaId);
return currentUrl.toString();
}
// Default embed URL format
return `${origin}/embed?m=${mediaId}`;
};
// Copy video URL to clipboard
const handleCopyVideoUrl = async () => {
const mediaId = getMediaId();
const origin = getBaseOrigin();
const videoUrl = `${origin}/view?m=${mediaId}`;
// Show copy icon
if (customComponents.current?.seekIndicator) {
customComponents.current.seekIndicator.show('copy-url');
}
try {
await navigator.clipboard.writeText(videoUrl);
closeContextMenu();
// You can add a notification here if needed
} catch (err) {
console.error('Failed to copy video URL:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = videoUrl;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
closeContextMenu();
}
};
// Copy video URL at current time to clipboard
const handleCopyVideoUrlAtTime = async () => {
if (!playerRef.current) {
closeContextMenu();
return;
}
const currentTime = Math.floor(playerRef.current.currentTime() || 0);
const mediaId = getMediaId();
const origin = getBaseOrigin();
const videoUrl = `${origin}/view?m=${mediaId}&t=${currentTime}`;
// Show copy icon
if (customComponents.current?.seekIndicator) {
customComponents.current.seekIndicator.show('copy-url');
}
try {
await navigator.clipboard.writeText(videoUrl);
closeContextMenu();
} catch (err) {
console.error('Failed to copy video URL at time:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = videoUrl;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
closeContextMenu();
}
};
// Copy embed code to clipboard
const handleCopyEmbedCode = async () => {
const embedUrl = getEmbedUrl();
const embedCode = `<iframe width="560" height="315" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`;
// Show copy embed icon
if (customComponents.current?.seekIndicator) {
customComponents.current.seekIndicator.show('copy-embed');
}
try {
await navigator.clipboard.writeText(embedCode);
closeContextMenu();
} catch (err) {
console.error('Failed to copy embed code:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = embedCode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
closeContextMenu();
}
};
// Add context menu handler directly to video element and document (works before and after Video.js initialization)
useEffect(() => {
const videoElement = videoRef.current;
// Attach to document with capture to catch all contextmenu events, then filter
const documentHandler = (e) => {
// Check if the event originated from within the video player
const target = e.target;
const playerWrapper =
videoElement?.closest('.video-js') || document.querySelector(`#${videoId}`)?.closest('.video-js');
if (playerWrapper && (playerWrapper.contains(target) || target === playerWrapper)) {
handleContextMenu(e);
}
};
// Use capture phase on document to catch before anything else
document.addEventListener('contextmenu', documentHandler, true);
// Also attach directly to video element
if (videoElement) {
videoElement.addEventListener('contextmenu', handleContextMenu, true);
}
return () => {
document.removeEventListener('contextmenu', documentHandler, true);
if (videoElement) {
videoElement.removeEventListener('contextmenu', handleContextMenu, true);
}
};
}, [handleContextMenu, videoId]);
useEffect(() => { useEffect(() => {
// Only initialize if we don't already have a player and element exists // Only initialize if we don't already have a player and element exists
if (videoRef.current && !playerRef.current) { if (videoRef.current && !playerRef.current) {
@@ -1078,6 +1300,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
currentVideo, currentVideo,
relatedVideos, relatedVideos,
goToNextVideo, goToNextVideo,
showRelated: finalShowRelated,
showUserAvatar: finalShowUserAvatar,
linkTitle: finalLinkTitle,
}); });
customComponents.current.endScreenHandler = endScreenHandler; // Store for cleanup customComponents.current.endScreenHandler = endScreenHandler; // Store for cleanup
@@ -1098,8 +1323,8 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
} }
// Handle URL timestamp parameter // Handle URL timestamp parameter
if (mediaData.urlTimestamp !== null && mediaData.urlTimestamp >= 0) { if (finalTimestamp !== null && finalTimestamp >= 0) {
const timestamp = mediaData.urlTimestamp; const timestamp = finalTimestamp;
// Wait for video metadata to be loaded before seeking // Wait for video metadata to be loaded before seeking
if (playerRef.current.readyState() >= 1) { if (playerRef.current.readyState() >= 1) {
@@ -1997,6 +2222,10 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
authorThumbnail: currentVideo.author_thumbnail, authorThumbnail: currentVideo.author_thumbnail,
videoTitle: currentVideo.title, videoTitle: currentVideo.title,
videoUrl: currentVideo.url, videoUrl: currentVideo.url,
showTitle: finalShowTitle,
showRelated: finalShowRelated,
showUserAvatar: finalShowUserAvatar,
linkTitle: finalLinkTitle,
}); });
} }
// END: Add Embed Info Overlay Component // END: Add Embed Info Overlay Component
@@ -2083,20 +2312,72 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// Make the video element focusable // Make the video element focusable
const videoElement = playerRef.current.el(); const videoElement = playerRef.current.el();
videoElement.setAttribute('tabindex', '0'); videoElement.setAttribute('tabindex', '0');
if (!isEmbedPlayer) {
videoElement.focus(); videoElement.focus();
} }
// Add context menu (right-click) handler to the player wrapper and video element
// Attach to player wrapper (this catches all clicks on the player)
videoElement.addEventListener('contextmenu', handleContextMenu, true);
// Also try to attach to the actual video tech element
const attachContextMenu = () => {
const techElement =
playerRef.current.el().querySelector('.vjs-tech') ||
playerRef.current.el().querySelector('video') ||
(playerRef.current.tech() && playerRef.current.tech().el());
if (techElement && techElement !== videoRef.current && techElement !== videoElement) {
// Use capture phase to catch before Video.js might prevent it
techElement.addEventListener('contextmenu', handleContextMenu, true);
return true;
}
return false;
};
// Try to attach immediately
attachContextMenu();
// Also try after a short delay in case elements aren't ready yet
setTimeout(() => {
attachContextMenu();
}, 100);
// Also try when video is loaded
playerRef.current.one('loadedmetadata', () => {
attachContextMenu();
});
}
}); });
} }
//}, 0); //}, 0);
} }
// Cleanup: Remove context menu event listener
return () => {
if (playerRef.current && playerRef.current.el()) {
const playerEl = playerRef.current.el();
playerEl.removeEventListener('contextmenu', handleContextMenu, true);
const techElement =
playerEl.querySelector('.vjs-tech') ||
playerEl.querySelector('video') ||
(playerRef.current.tech() && playerRef.current.tech().el());
if (techElement) {
techElement.removeEventListener('contextmenu', handleContextMenu, true);
}
}
};
}, []); }, []);
return ( return (
<>
<video <video
ref={videoRef} ref={videoRef}
id={videoId} id={videoId}
controls={true} controls={true}
className={`video-js vjs-fluid vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`} className={`video-js ${isEmbedPlayer ? 'vjs-fill' : 'vjs-fluid'} vjs-default-skin${currentVideo.useRoundedCorners ? ' video-js-rounded-corners' : ''}`}
preload="auto" preload="auto"
poster={currentVideo.poster} poster={currentVideo.poster}
tabIndex="0" tabIndex="0"
@@ -2129,6 +2410,15 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
chaptersData.length > 0 && chaptersData.length > 0 &&
(console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */} (console.log('chaptersData', chaptersData), (<track kind="chapters" src="/sample-chapters.vtt" />))} */}
</video> </video>
<VideoContextMenu
visible={contextMenuVisible}
position={contextMenuPosition}
onClose={closeContextMenu}
onCopyVideoUrl={handleCopyVideoUrl}
onCopyVideoUrlAtTime={handleCopyVideoUrlAtTime}
onCopyEmbedCode={handleCopyEmbedCode}
/>
</>
); );
} }

View File

@@ -63,7 +63,17 @@ export class EndScreenHandler {
} }
handleVideoEnded() { handleVideoEnded() {
const { isEmbedPlayer, userPreferences, mediaData, currentVideo, relatedVideos, goToNextVideo } = this.options; const {
isEmbedPlayer,
userPreferences,
mediaData,
currentVideo,
relatedVideos,
goToNextVideo,
showRelated,
showUserAvatar,
linkTitle,
} = this.options;
// For embed players, show big play button when video ends // For embed players, show big play button when video ends
if (isEmbedPlayer) { if (isEmbedPlayer) {
@@ -73,6 +83,34 @@ export class EndScreenHandler {
} }
} }
// If showRelated is false, we don't show the end screen or autoplay countdown
if (showRelated === false) {
// But we still want to keep the control bar visible and hide the poster
setTimeout(() => {
if (this.player && !this.player.isDisposed()) {
const playerEl = this.player.el();
if (playerEl) {
// Hide poster elements
const posterElements = playerEl.querySelectorAll('.vjs-poster');
posterElements.forEach((posterEl) => {
posterEl.style.display = 'none';
posterEl.style.visibility = 'hidden';
posterEl.style.opacity = '0';
});
// Keep control bar visible
const controlBar = this.player.getChild('controlBar');
if (controlBar) {
controlBar.show();
controlBar.el().style.opacity = '1';
controlBar.el().style.pointerEvents = 'auto';
}
}
}
}, 50);
return;
}
// Keep controls active after video ends // Keep controls active after video ends
setTimeout(() => { setTimeout(() => {
if (this.player && !this.player.isDisposed()) { if (this.player && !this.player.isDisposed()) {

View File

@@ -54,7 +54,7 @@
.category-modal-header { .category-modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
padding: 20px 24px; padding: 20px 24px;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
@@ -74,6 +74,19 @@
} }
} }
.category-modal-subtitle {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
font-size: 13px;
color: #777;
.dark_theme & {
color: #aaa;
}
}
.category-modal-close { .category-modal-close {
background: none; background: none;
border: none; border: none;

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import './BulkActionCategoryModal.scss'; import './BulkActionCategoryModal.scss';
import { translateString } from '../utils/helpers/'; import { translateString } from '../utils/helpers/';
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
interface Category { interface Category {
title: string; title: string;
@@ -24,6 +25,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
onError, onError,
csrfToken, csrfToken,
}) => { }) => {
const isLmsMode = inEmbeddedApp();
const [existingCategories, setExistingCategories] = useState<Category[]>([]); const [existingCategories, setExistingCategories] = useState<Category[]>([]);
const [allCategories, setAllCategories] = useState<Category[]>([]); const [allCategories, setAllCategories] = useState<Category[]>([]);
const [categoriesToAdd, setCategoriesToAdd] = useState<Category[]>([]); const [categoriesToAdd, setCategoriesToAdd] = useState<Category[]>([]);
@@ -66,20 +68,27 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
const existingData = await existingResponse.json(); const existingData = await existingResponse.json();
const existing = existingData.results || []; const existing = existingData.results || [];
// Fetch all categories // Fetch all categories (or LMS courses only in embed mode)
const allResponse = await fetch('/api/v1/categories'); const categoriesUrl = isLmsMode
? '/api/v1/categories/contributor?lms_courses_only=true'
: '/api/v1/categories';
const allResponse = await fetch(categoriesUrl);
if (!allResponse.ok) { if (!allResponse.ok) {
throw new Error(translateString('Failed to fetch all categories')); throw new Error(isLmsMode ? translateString('Failed to fetch courses') : translateString('Failed to fetch all categories'));
} }
const allData = await allResponse.json(); const allData = await allResponse.json();
const all = allData.results || allData; const all = allData.results || allData;
setExistingCategories(existing); // 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);
setAllCategories(all); setAllCategories(all);
} catch (error) { } catch (error) {
console.error('Error fetching categories:', error); console.error('Error fetching categories:', error);
onError(translateString('Failed to load categories')); onError(isLmsMode ? translateString('Failed to load courses') : translateString('Failed to load categories'));
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -126,7 +135,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
}); });
if (!addResponse.ok) { if (!addResponse.ok) {
throw new Error(translateString('Failed to add categories')); throw new Error(isLmsMode ? translateString('Failed to add courses') : translateString('Failed to add categories'));
} }
} }
@@ -147,15 +156,15 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
}); });
if (!removeResponse.ok) { if (!removeResponse.ok) {
throw new Error(translateString('Failed to remove categories')); throw new Error(isLmsMode ? translateString('Failed to remove courses') : translateString('Failed to remove categories'));
} }
} }
onSuccess(translateString('Successfully updated categories')); onSuccess(isLmsMode ? translateString('Successfully updated courses') : translateString('Successfully updated categories'));
onCancel(); onCancel();
} catch (error) { } catch (error) {
console.error('Error processing categories:', error); console.error('Error processing categories:', error);
onError(translateString('Failed to update categories. Please try again.')); onError(isLmsMode ? translateString('Failed to update courses. Please try again.') : translateString('Failed to update categories. Please try again.'));
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
@@ -184,7 +193,14 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
<div className="category-modal-overlay"> <div className="category-modal-overlay">
<div className="category-modal"> <div className="category-modal">
<div className="category-modal-header"> <div className="category-modal-header">
<h2>{translateString('Add / Remove from Categories')}</h2> <div>
<h2>{isLmsMode ? translateString('Share with Course') : translateString('Add / Remove from Categories')}</h2>
{isLmsMode && (
<div className="category-modal-subtitle">
<span>{translateString('Students will get viewer permissions, while lecturers will get co-owner permissions (same as owner, but cannot delete the media)')}</span>
</div>
)}
</div>
<button className="category-modal-close" onClick={onCancel}> <button className="category-modal-close" onClick={onCancel}>
× ×
</button> </button>
@@ -192,14 +208,14 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
<div className="category-modal-content"> <div className="category-modal-content">
<div className="category-panel"> <div className="category-panel">
<h3>{translateString('Categories')}</h3> <h3>{isLmsMode ? translateString('Courses') : translateString('Categories')}</h3>
{isLoading ? ( {isLoading ? (
<div className="loading-message">{translateString('Loading categories...')}</div> <div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
) : ( ) : (
<div className="category-list scrollable"> <div className="category-list scrollable">
{leftPanelCategories.length === 0 ? ( {leftPanelCategories.length === 0 ? (
<div className="empty-message">{translateString('All categories already added')}</div> <div className="empty-message">{isLmsMode ? translateString('All courses already added') : translateString('All categories already added')}</div>
) : ( ) : (
leftPanelCategories.map((category) => ( leftPanelCategories.map((category) => (
<div <div
@@ -227,11 +243,11 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
</h3> </h3>
{isLoading ? ( {isLoading ? (
<div className="loading-message">{translateString('Loading categories...')}</div> <div className="loading-message">{isLmsMode ? translateString('Loading courses...') : translateString('Loading categories...')}</div>
) : ( ) : (
<div className="category-list scrollable"> <div className="category-list scrollable">
{rightPanelCategories.length === 0 ? ( {rightPanelCategories.length === 0 ? (
<div className="empty-message">{translateString('No categories')}</div> <div className="empty-message">{isLmsMode ? translateString('No courses') : translateString('No categories')}</div>
) : ( ) : (
rightPanelCategories.map((category) => { rightPanelCategories.map((category) => {
const isExisting = existingCategories.some((c) => c.uid === category.uid); const isExisting = existingCategories.some((c) => c.uid === category.uid);
@@ -251,7 +267,7 @@ export const BulkActionCategoryModal: React.FC<BulkActionCategoryModalProps> = (
removeCategoryFromAddList(category); removeCategoryFromAddList(category);
} }
}} }}
title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? translateString('Remove category') : translateString('Remove from list')} title={isMarkedForRemoval ? translateString('Undo removal') : isExisting ? (isLmsMode ? translateString('Remove course') : translateString('Remove category')) : translateString('Remove from list')}
> >
{isMarkedForRemoval ? '↺' : '×'} {isMarkedForRemoval ? '↺' : '×'}
</button> </button>

View File

@@ -47,7 +47,7 @@ export const BulkActionChangeOwnerModal: React.FC<BulkActionChangeOwnerModalProp
} }
try { try {
const response = await fetch(`/api/v1/users?name=${encodeURIComponent(name)}`); const response = await fetch(`/api/v1/users?name=${encodeURIComponent(name)}&exclude_self=True`);
if (!response.ok) { if (!response.ok) {
throw new Error(translateString('Failed to search users')); throw new Error(translateString('Failed to search users'));
} }

View File

@@ -54,7 +54,7 @@
.permission-modal-header { .permission-modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
padding: 20px 24px; padding: 20px 24px;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
@@ -102,6 +102,19 @@
} }
} }
.permission-modal-subtitle {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
font-size: 13px;
color: #777;
.dark_theme & {
color: #aaa;
}
}
.permission-modal-content { .permission-modal-content {
display: flex; display: flex;
gap: 24px; gap: 24px;

View File

@@ -226,7 +226,42 @@ export const BulkActionPermissionModal: React.FC<BulkActionPermissionModalProps>
<div className="permission-modal-overlay"> <div className="permission-modal-overlay">
<div className="permission-modal"> <div className="permission-modal">
<div className="permission-modal-header"> <div className="permission-modal-header">
<h2>{translateString('Manage')} {permissionLabel}</h2> <div>
<h2>{translateString('Share with')} {permissionLabel}</h2>
{permissionType === 'viewer' && (
<div className="permission-modal-subtitle">
<span>{translateString('Give users viewer permissions to your media by adding them to the below list.')}</span>
<span
className="info-tooltip"
title={translateString("Users can view your media via: My Media > Shared with Me > particular media > ...")}
>
i
</span>
</div>
)}
{permissionType === 'editor' && (
<div className="permission-modal-subtitle">
<span>{translateString('Give users editor permissions to your media by adding them to the below list.')}</span>
<span
className="info-tooltip"
title={translateString("Users can edit your media via: My Media > Shared with Me > particular media > Edit...")}
>
i
</span>
</div>
)}
{permissionType === 'owner' && (
<div className="permission-modal-subtitle">
<span>{translateString('Give users owner permissions to your media, except for deleting the media, by adding them to the below list.')}</span>
<span
className="info-tooltip"
title={translateString("Users can manage your media via: My Media > Shared with Me > particular media > ...")}
>
i
</span>
</div>
)}
</div>
<button className="permission-modal-close" onClick={onCancel}> <button className="permission-modal-close" onClick={onCancel}>
× ×
</button> </button>
@@ -284,6 +319,21 @@ export const BulkActionPermissionModal: React.FC<BulkActionPermissionModalProps>
? ?
</span> </span>
)} )}
{permissionType === 'viewer' && (
<span className="info-tooltip" title={translateString('Remove users from the list to remove viewer permissions')}>
i
</span>
)}
{permissionType === 'editor' && (
<span className="info-tooltip" title={translateString('Remove users from the list to remove editor permissions')}>
i
</span>
)}
{permissionType === 'owner' && (
<span className="info-tooltip" title={translateString('Remove users from the list to remove owner permissions')}>
i
</span>
)}
</h3> </h3>
<div className="search-box"> <div className="search-box">
<input <input

View File

@@ -40,6 +40,12 @@
color: #666; color: #666;
} }
optgroup {
color: #000;
font-weight: 700;
background-color: white;
}
option { option {
padding: 10px; padding: 10px;
font-weight: normal; font-weight: normal;
@@ -77,6 +83,11 @@
color: #aaa; color: #aaa;
} }
optgroup {
color: #fff;
background-color: #2a2a2a;
}
option { option {
background-color: #2a2a2a; background-color: #2a2a2a;
color: #fff; color: #fff;

View File

@@ -1,30 +1,64 @@
import React from 'react'; import React from 'react';
import './BulkActionsDropdown.scss'; import './BulkActionsDropdown.scss';
import { translateString } from '../utils/helpers/'; import { translateString } from '../utils/helpers/';
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
interface BulkActionsDropdownProps { interface BulkActionsDropdownProps {
selectedCount: number; selectedCount: number;
onActionSelect: (action: string) => void; onActionSelect: (action: string) => void;
} }
const BULK_ACTIONS = [ interface BulkAction {
{ value: 'add-remove-coviewers', label: translateString('Add / Remove Co-Viewers'), enabled: true }, value: string;
{ value: 'add-remove-coeditors', label: translateString('Add / Remove Co-Editors'), enabled: true }, label: string;
{ value: 'add-remove-coowners', label: translateString('Add / Remove Co-Owners'), enabled: true }, enabled: boolean;
}
interface BulkActionGroup {
label: string;
actions: BulkAction[];
}
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect }) => {
const isLmsMode = inEmbeddedApp();
const BULK_ACTION_GROUPS: BulkActionGroup[] = [
{
label: translateString('Sharing'),
actions: [
{ value: 'add-remove-coviewers', label: translateString('Share with Co-Viewers'), enabled: true },
{ value: 'add-remove-coeditors', label: translateString('Share with Co-Editors'), enabled: true },
{ value: 'add-remove-coowners', label: translateString('Share with Co-Owners'), enabled: true },
{ value: 'add-remove-category', label: isLmsMode ? translateString('Share with Course Members') : translateString('Add / Remove from Categories'), enabled: true },
],
},
{
label: translateString('Organization'),
actions: [
{ value: 'add-remove-playlist', label: translateString('Add to / Remove from Playlist'), enabled: true }, { value: 'add-remove-playlist', label: translateString('Add to / Remove from Playlist'), 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: 'add-remove-tags', label: translateString('Add / Remove Tags'), enabled: true },
],
},
{
label: translateString('Settings'),
actions: [
{ value: 'enable-comments', label: translateString('Enable Comments'), enabled: true }, { value: 'enable-comments', label: translateString('Enable Comments'), enabled: true },
{ value: 'disable-comments', label: translateString('Disable Comments'), enabled: true }, { value: 'disable-comments', label: translateString('Disable Comments'), enabled: true },
{ value: 'delete-comments', label: translateString('Delete Comments'), enabled: true },
{ value: 'enable-download', label: translateString('Enable Download'), enabled: true }, { value: 'enable-download', label: translateString('Enable Download'), enabled: true },
{ value: 'disable-download', label: translateString('Disable Download'), enabled: true }, { value: 'disable-download', label: translateString('Disable Download'), enabled: true },
],
},
{
label: translateString('Management'),
actions: [
{ value: 'publish-state', label: translateString('Publish State'), enabled: true }, { value: 'publish-state', label: translateString('Publish State'), enabled: true },
{ value: 'change-owner', label: translateString('Change Owner'), enabled: true }, { value: 'change-owner', label: translateString('Change Owner'), enabled: true },
{ value: 'copy-media', label: translateString('Copy Media'), enabled: true }, { value: 'copy-media', label: translateString('Copy Media'), enabled: true },
{ value: 'delete-media', label: translateString('Delete Media'), enabled: true }, { value: 'delete-media', label: translateString('Delete Media'), enabled: true },
],
},
]; ];
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect }) => {
const noSelection = selectedCount === 0; const noSelection = selectedCount === 0;
@@ -58,11 +92,15 @@ export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ select
<option value="" disabled> <option value="" disabled>
{displayText} {displayText}
</option> </option>
{BULK_ACTIONS.map((action) => ( {BULK_ACTION_GROUPS.map((group) => (
<optgroup key={group.label} label={group.label}>
{group.actions.map((action) => (
<option key={action.value} value={action.value} disabled={noSelection || !action.enabled}> <option key={action.value} value={action.value} disabled={noSelection || !action.enabled}>
{action.label} {action.label}
</option> </option>
))} ))}
</optgroup>
))}
</select> </select>
</div> </div>
); );

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { translateString } from '../utils/helpers/'; import { translateString, inSelectMediaEmbedMode } from '../utils/helpers/';
interface MediaListHeaderProps { interface MediaListHeaderProps {
title?: string; title?: string;
@@ -11,10 +11,12 @@ interface MediaListHeaderProps {
export const MediaListHeader: React.FC<MediaListHeaderProps> = (props) => { export const MediaListHeader: React.FC<MediaListHeaderProps> = (props) => {
const viewAllText = props.viewAllText || translateString('VIEW ALL'); const viewAllText = props.viewAllText || translateString('VIEW ALL');
const isSelectMediaMode = inSelectMediaEmbedMode();
return ( return (
<div className={(props.className ? props.className + ' ' : '') + 'media-list-header'} style={props.style}> <div className={(props.className ? props.className + ' ' : '') + 'media-list-header'} style={props.style}>
<h2>{props.title}</h2> <h2>{props.title}</h2>
{props.viewAllLink ? ( {!isSelectMediaMode && props.viewAllLink ? (
<h3> <h3>
{' '} {' '}
<a href={props.viewAllLink} title={viewAllText}> <a href={props.viewAllLink} title={viewAllText}>

View File

@@ -16,8 +16,17 @@
margin-bottom: 16px; margin-bottom: 16px;
.add-media-button { .add-media-button {
position: relative;
margin-left: auto; margin-left: auto;
.popup {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
z-index: 99;
}
a { a {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -2,8 +2,9 @@ import React from 'react';
import { MediaListRow } from './MediaListRow'; import { MediaListRow } from './MediaListRow';
import { BulkActionsDropdown } from './BulkActionsDropdown'; import { BulkActionsDropdown } from './BulkActionsDropdown';
import { SelectAllCheckbox } from './SelectAllCheckbox'; import { SelectAllCheckbox } from './SelectAllCheckbox';
import { CircleIconButton, MaterialIcon } from './_shared'; import { CircleIconButton, MaterialIcon, PopupMain, NavigationMenuList } from './_shared';
import { LinksConsumer } from '../utils/contexts'; import { LinksConsumer } from '../utils/contexts';
import { usePopup } from '../utils/hooks';
import { translateString } from '../utils/helpers/'; import { translateString } from '../utils/helpers/';
import './MediaListWrapper.scss'; import './MediaListWrapper.scss';
@@ -37,12 +38,29 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
onSelectAll = () => {}, onSelectAll = () => {},
onDeselectAll = () => {}, onDeselectAll = () => {},
showAddMediaButton = false, showAddMediaButton = false,
}) => ( }) => {
const [popupContentRef, PopupContent, PopupTrigger] = usePopup() as [any, any, any];
return (
<div className={(className ? className + ' ' : '') + 'media-list-wrapper'} style={style}> <div className={(className ? className + ' ' : '') + 'media-list-wrapper'} style={style}>
<MediaListRow title={title} viewAllLink={viewAllLink} viewAllText={viewAllText}> <MediaListRow title={title} viewAllLink={viewAllLink} viewAllText={viewAllText}>
{showBulkActions && ( {showBulkActions && (
<LinksConsumer> <LinksConsumer>
{(links) => ( {(links) => {
const uploadMenuItems = [
{
link: links.user.addMedia,
icon: 'upload',
text: translateString('Upload'),
},
{
link: '/record_screen',
icon: 'videocam',
text: translateString('Record'),
},
];
return (
<div className="bulk-actions-container"> <div className="bulk-actions-container">
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} /> <BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} />
@@ -55,18 +73,25 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
</div> </div>
{showAddMediaButton && ( {showAddMediaButton && (
<div className="add-media-button"> <div className="add-media-button">
<a href={links.user.addMedia} title={translateString('Add media')}> <PopupTrigger contentRef={popupContentRef}>
<CircleIconButton> <CircleIconButton title={translateString('Add media')}>
<MaterialIcon type="video_call" /> <MaterialIcon type="video_call" />
</CircleIconButton> </CircleIconButton>
</a> </PopupTrigger>
<PopupContent contentRef={popupContentRef}>
<PopupMain>
<NavigationMenuList items={uploadMenuItems} />
</PopupMain>
</PopupContent>
</div> </div>
)} )}
</div> </div>
)} );
}}
</LinksConsumer> </LinksConsumer>
)} )}
{children || null} {children || null}
</MediaListRow> </MediaListRow>
</div> </div>
); );
};

View File

@@ -31,8 +31,11 @@ const VideoJSEmbed = ({
poster, poster,
previewSprite, previewSprite,
subtitlesInfo, subtitlesInfo,
enableAutoplay,
inEmbed, inEmbed,
showTitle,
showRelated,
showUserAvatar,
linkTitle,
hasTheaterMode, hasTheaterMode,
hasNextLink, hasNextLink,
nextLink, nextLink,
@@ -62,8 +65,10 @@ const VideoJSEmbed = ({
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// Get URL parameters for autoplay, muted, and timestamp // Get URL parameters for autoplay, muted, and timestamp
const urlTimestamp = getUrlParameter('t'); const urlTimestamp = getUrlParameter('t');
const urlAutoplay = getUrlParameter('autoplay');
const urlMuted = getUrlParameter('muted'); const urlMuted = getUrlParameter('muted');
const urlShowRelated = getUrlParameter('showRelated');
const urlShowUserAvatar = getUrlParameter('showUserAvatar');
const urlLinkTitle = getUrlParameter('linkTitle');
window.MEDIA_DATA = { window.MEDIA_DATA = {
data: data || {}, data: data || {},
@@ -71,7 +76,7 @@ const VideoJSEmbed = ({
version: version, version: version,
isPlayList: isPlayList, isPlayList: isPlayList,
playerVolume: playerVolume || 0.5, playerVolume: playerVolume || 0.5,
playerSoundMuted: playerSoundMuted || (urlMuted === '1'), playerSoundMuted: urlMuted === '1',
videoQuality: videoQuality || 'auto', videoQuality: videoQuality || 'auto',
videoPlaybackSpeed: videoPlaybackSpeed || 1, videoPlaybackSpeed: videoPlaybackSpeed || 1,
inTheaterMode: inTheaterMode || false, inTheaterMode: inTheaterMode || false,
@@ -83,8 +88,11 @@ const VideoJSEmbed = ({
poster: poster || '', poster: poster || '',
previewSprite: previewSprite || null, previewSprite: previewSprite || null,
subtitlesInfo: subtitlesInfo || [], subtitlesInfo: subtitlesInfo || [],
enableAutoplay: enableAutoplay || (urlAutoplay === '1'),
inEmbed: inEmbed || false, inEmbed: inEmbed || false,
showTitle: showTitle || false,
showRelated: showRelated !== undefined ? showRelated : (urlShowRelated === '1' || urlShowRelated === 'true' || urlShowRelated === null),
showUserAvatar: showUserAvatar !== undefined ? showUserAvatar : (urlShowUserAvatar === '1' || urlShowUserAvatar === 'true' || urlShowUserAvatar === null),
linkTitle: linkTitle !== undefined ? linkTitle : (urlLinkTitle === '1' || urlLinkTitle === 'true' || urlLinkTitle === null),
hasTheaterMode: hasTheaterMode || false, hasTheaterMode: hasTheaterMode || false,
hasNextLink: hasNextLink || false, hasNextLink: hasNextLink || false,
nextLink: nextLink || null, nextLink: nextLink || null,
@@ -92,8 +100,10 @@ const VideoJSEmbed = ({
errorMessage: errorMessage || '', errorMessage: errorMessage || '',
// URL parameters // URL parameters
urlTimestamp: urlTimestamp ? parseInt(urlTimestamp, 10) : null, urlTimestamp: urlTimestamp ? parseInt(urlTimestamp, 10) : null,
urlAutoplay: urlAutoplay === '1',
urlMuted: urlMuted === '1', urlMuted: urlMuted === '1',
urlShowRelated: urlShowRelated === '1' || urlShowRelated === 'true',
urlShowUserAvatar: urlShowUserAvatar === '1' || urlShowUserAvatar === 'true',
urlLinkTitle: urlLinkTitle === '1' || urlLinkTitle === 'true',
onClickNextCallback: onClickNextCallback || null, onClickNextCallback: onClickNextCallback || null,
onClickPreviousCallback: onClickPreviousCallback || null, onClickPreviousCallback: onClickPreviousCallback || null,
onStateUpdateCallback: onStateUpdateCallback || null, onStateUpdateCallback: onStateUpdateCallback || null,
@@ -176,12 +186,18 @@ const VideoJSEmbed = ({
// Scroll to the video player with smooth behavior // Scroll to the video player with smooth behavior
const videoElement = document.querySelector(inEmbedRef.current ? '#video-embed' : '#video-main'); const videoElement = document.querySelector(inEmbedRef.current ? '#video-embed' : '#video-main');
if (videoElement) { if (videoElement) {
const urlScroll = getUrlParameter('scroll');
const isIframe = window.parent !== window;
// Only scroll if not in an iframe, OR if explicitly requested via scroll=1 parameter
if (!isIframe || urlScroll === '1' || urlScroll === 'true') {
videoElement.scrollIntoView({ videoElement.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'center', block: 'center',
inline: 'nearest' inline: 'nearest'
}); });
} }
}
} else { } else {
console.warn('VideoJS player not found for timestamp navigation'); console.warn('VideoJS player not found for timestamp navigation');
} }
@@ -220,7 +236,14 @@ const VideoJSEmbed = ({
return ( return (
<div className="video-js-wrapper" ref={containerRef}> <div className="video-js-wrapper" ref={containerRef}>
{inEmbed ? <div id="video-js-root-embed" className="video-js-root-embed" /> : <div id="video-js-root-main" className="video-js-root-main" />} {inEmbed ? (
<div
id="video-js-root-embed"
className="video-js-root-embed"
/>
) : (
<div id="video-js-root-main" className="video-js-root-main" />
)}
</div> </div>
); );
}; };

View File

@@ -1,18 +1,50 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useMediaItem } from '../../utils/hooks/'; import { useMediaItem } from '../../utils/hooks/';
import { PositiveInteger, PositiveIntegerOrZero } from '../../utils/helpers/'; import { PositiveInteger, PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
import { MediaItemThumbnailLink, itemClassname } from './includes/items/'; import { MediaItemThumbnailLink, itemClassname } from './includes/items/';
import { Item } from './Item'; import { Item } from './Item';
export function MediaItem(props) { export function MediaItem(props) {
const type = props.type; const type = props.type;
const isSelectMediaMode = inSelectMediaEmbedMode();
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] = const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
useMediaItem({ ...props, type }); 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() { 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} />; return <MediaItemThumbnailLink src={thumbnailUrl} title={props.title} link={props.link} />;
} }
@@ -25,11 +57,13 @@ export function MediaItem(props) {
const finalClassname = containerClassname + const finalClassname = containerClassname +
(props.showSelection ? ' with-selection' : '') + (props.showSelection ? ' with-selection' : '') +
(props.isSelected ? ' selected' : '') + (props.isSelected ? ' selected' : '') +
(props.hasAnySelection ? ' has-any-selection' : ''); (props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
const handleItemClick = (e) => { const handleItemClick = (e) => {
// If there's any selection active, clicking the item should toggle selection const isSelectMediaMode = inSelectMediaEmbedMode();
if (props.hasAnySelection && props.onCheckboxChange) {
// In select media mode or if there's any selection active, clicking the item should toggle selection
if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
// Check if clicking on the checkbox itself, edit icon, or view icon // Check if clicking on the checkbox itself, edit icon, or view icon
if (e.target.closest('.item-selection-checkbox') || if (e.target.closest('.item-selection-checkbox') ||
e.target.closest('.item-edit-icon') || e.target.closest('.item-edit-icon') ||
@@ -59,16 +93,24 @@ export function MediaItem(props) {
</div> </div>
)} )}
{editMediaComponent()} {!isSelectMediaMode && editMediaComponent()}
{viewMediaComponent()} {!isSelectMediaMode && viewMediaComponent()}
{thumbnailComponent()} {thumbnailComponent()}
{isSelectMediaMode ? (
<UnderThumbWrapper>
{titleComponent()}
{metaComponents()}
{descriptionComponent()}
</UnderThumbWrapper>
) : (
<UnderThumbWrapper title={props.title} link={props.link}> <UnderThumbWrapper title={props.title} link={props.link}>
{titleComponent()} {titleComponent()}
{metaComponents()} {metaComponents()}
{descriptionComponent()} {descriptionComponent()}
</UnderThumbWrapper> </UnderThumbWrapper>
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useMediaItem } from '../../utils/hooks/'; import { useMediaItem } from '../../utils/hooks/';
import { PositiveIntegerOrZero } from '../../utils/helpers/'; import { PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
import { MediaDurationInfo } from '../../utils/classes/'; import { MediaDurationInfo } from '../../utils/classes/';
import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions'; import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions';
import { MediaItemDuration, MediaItemPlaylistIndex, itemClassname } from './includes/items/'; import { MediaItemDuration, MediaItemPlaylistIndex, itemClassname } from './includes/items/';
@@ -9,10 +9,26 @@ import { MediaItem } from './MediaItem';
export function MediaItemAudio(props) { export function MediaItemAudio(props) {
const type = props.type; const type = props.type;
const isSelectMediaMode = inSelectMediaEmbedMode();
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] = const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
useMediaItem({ ...props, type }); 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(); const _MediaDurationInfo = new MediaDurationInfo();
_MediaDurationInfo.update(props.duration); _MediaDurationInfo.update(props.duration);
@@ -22,6 +38,21 @@ export function MediaItemAudio(props) {
const durationISO8601 = _MediaDurationInfo.ISO8601(); const durationISO8601 = _MediaDurationInfo.ISO8601();
function thumbnailComponent() { 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 = { const attr = {
key: 'item-thumb', key: 'item-thumb',
href: props.link, href: props.link,
@@ -68,11 +99,11 @@ export function MediaItemAudio(props) {
const finalClassname = containerClassname + const finalClassname = containerClassname +
(props.showSelection ? ' with-selection' : '') + (props.showSelection ? ' with-selection' : '') +
(props.isSelected ? ' selected' : '') + (props.isSelected ? ' selected' : '') +
(props.hasAnySelection ? ' has-any-selection' : ''); (props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
const handleItemClick = (e) => { const handleItemClick = (e) => {
// If there's any selection active, clicking the item should toggle selection // In embed mode or if there's any selection active, clicking the item should toggle selection
if (props.hasAnySelection && props.onCheckboxChange) { if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
// Check if clicking on the checkbox itself, edit icon, or view icon // Check if clicking on the checkbox itself, edit icon, or view icon
if (e.target.closest('.item-selection-checkbox') || if (e.target.closest('.item-selection-checkbox') ||
e.target.closest('.item-edit-icon') || e.target.closest('.item-edit-icon') ||
@@ -104,16 +135,24 @@ export function MediaItemAudio(props) {
</div> </div>
)} )}
{editMediaComponent()} {!isSelectMediaMode && editMediaComponent()}
{viewMediaComponent()} {!isSelectMediaMode && viewMediaComponent()}
{thumbnailComponent()} {thumbnailComponent()}
{isSelectMediaMode ? (
<UnderThumbWrapper>
{titleComponent()}
{metaComponents()}
{descriptionComponent()}
</UnderThumbWrapper>
) : (
<UnderThumbWrapper title={props.title} link={props.link}> <UnderThumbWrapper title={props.title} link={props.link}>
{titleComponent()} {titleComponent()}
{metaComponents()} {metaComponents()}
{descriptionComponent()} {descriptionComponent()}
</UnderThumbWrapper> </UnderThumbWrapper>
)}
{playlistOptionsComponent()} {playlistOptionsComponent()}
</div> </div>

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useMediaItem } from '../../utils/hooks/'; import { useMediaItem } from '../../utils/hooks/';
import { PositiveIntegerOrZero } from '../../utils/helpers/'; import { PositiveIntegerOrZero, inSelectMediaEmbedMode } from '../../utils/helpers/';
import { MediaDurationInfo } from '../../utils/classes/'; import { MediaDurationInfo } from '../../utils/classes/';
import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions.jsx'; import { MediaPlaylistOptions } from '../media-playlist-options/MediaPlaylistOptions.jsx';
import { MediaItemVideoPlayer, MediaItemDuration, MediaItemVideoPreviewer, MediaItemPlaylistIndex, itemClassname } from './includes/items/'; import { MediaItemVideoPlayer, MediaItemDuration, MediaItemVideoPreviewer, MediaItemPlaylistIndex, itemClassname } from './includes/items/';
@@ -9,10 +9,26 @@ import { MediaItem } from './MediaItem';
export function MediaItemVideo(props) { export function MediaItemVideo(props) {
const type = props.type; const type = props.type;
const isSelectMediaMode = inSelectMediaEmbedMode();
const [titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper, editMediaComponent, metaComponents, viewMediaComponent] = const [titleComponentOrig, descriptionComponent, thumbnailUrl, UnderThumbWrapperOrig, editMediaComponent, metaComponents, viewMediaComponent] =
useMediaItem({ ...props, type }); 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(); const _MediaDurationInfo = new MediaDurationInfo();
_MediaDurationInfo.update(props.duration); _MediaDurationInfo.update(props.duration);
@@ -26,6 +42,24 @@ export function MediaItemVideo(props) {
} }
function thumbnailComponent() { 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 = { const attr = {
key: 'item-thumb', key: 'item-thumb',
href: props.link, href: props.link,
@@ -75,11 +109,11 @@ export function MediaItemVideo(props) {
const finalClassname = containerClassname + const finalClassname = containerClassname +
(props.showSelection ? ' with-selection' : '') + (props.showSelection ? ' with-selection' : '') +
(props.isSelected ? ' selected' : '') + (props.isSelected ? ' selected' : '') +
(props.hasAnySelection ? ' has-any-selection' : ''); (props.hasAnySelection || isSelectMediaMode ? ' has-any-selection' : '');
const handleItemClick = (e) => { const handleItemClick = (e) => {
// If there's any selection active, clicking the item should toggle selection // In select media mode or if there's any selection active, clicking the item should toggle selection
if (props.hasAnySelection && props.onCheckboxChange) { if ((isSelectMediaMode || props.hasAnySelection) && props.onCheckboxChange) {
// Check if clicking on the checkbox itself, edit icon, or view icon // Check if clicking on the checkbox itself, edit icon, or view icon
if (e.target.closest('.item-selection-checkbox') || if (e.target.closest('.item-selection-checkbox') ||
e.target.closest('.item-edit-icon') || e.target.closest('.item-edit-icon') ||
@@ -111,20 +145,28 @@ export function MediaItemVideo(props) {
</div> </div>
)} )}
{editMediaComponent()} {!isSelectMediaMode && editMediaComponent()}
{viewMediaComponent()} {!isSelectMediaMode && viewMediaComponent()}
{props.hasMediaViewer ? videoViewerComponent() : thumbnailComponent()} {props.hasMediaViewer ? videoViewerComponent() : thumbnailComponent()}
{isSelectMediaMode ? (
<UnderThumbWrapper>
{titleComponent()}
{metaComponents()}
{descriptionComponent()}
</UnderThumbWrapper>
) : (
<UnderThumbWrapper title={props.title} link={props.link}> <UnderThumbWrapper title={props.title} link={props.link}>
{titleComponent()} {titleComponent()}
{metaComponents()} {metaComponents()}
{descriptionComponent()} {descriptionComponent()}
</UnderThumbWrapper> </UnderThumbWrapper>
</div> )}
{playlistOptionsComponent()} {playlistOptionsComponent()}
</div> </div>
</div>
); );
} }

View File

@@ -4,10 +4,32 @@ import { LinksContext, SiteConsumer } from '../../utils/contexts/';
import { PageStore, MediaPageStore } from '../../utils/stores/'; import { PageStore, MediaPageStore } from '../../utils/stores/';
import { PageActions, MediaPageActions } from '../../utils/actions/'; import { PageActions, MediaPageActions } from '../../utils/actions/';
import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/'; import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/';
import VideoViewer from '../media-viewer/VideoViewer';
const EMBED_OPTIONS_STORAGE_KEY = 'mediacms_embed_options';
function loadEmbedOptions() {
try {
const saved = localStorage.getItem(EMBED_OPTIONS_STORAGE_KEY);
if (saved) {
return JSON.parse(saved);
}
} catch (e) {
// Ignore localStorage errors
}
return null;
}
function saveEmbedOptions(options) {
try {
localStorage.setItem(EMBED_OPTIONS_STORAGE_KEY, JSON.stringify(options));
} catch (e) {
// Ignore localStorage errors
}
}
export function MediaShareEmbed(props) { export function MediaShareEmbed(props) {
const embedVideoDimensions = PageStore.get('config-options').embedded.video.dimensions; const embedVideoDimensions = PageStore.get('config-options').embedded.video.dimensions;
const savedOptions = loadEmbedOptions();
const links = useContext(LinksContext); const links = useContext(LinksContext);
@@ -18,12 +40,19 @@ export function MediaShareEmbed(props) {
const onRightBottomRef = useRef(null); const onRightBottomRef = useRef(null);
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 144 + 56); const [maxHeight, setMaxHeight] = useState(window.innerHeight - 144 + 56);
const [keepAspectRatio, setKeepAspectRatio] = useState(false); const [keepAspectRatio, setKeepAspectRatio] = useState(savedOptions?.keepAspectRatio ?? true);
const [aspectRatio, setAspectRatio] = useState('16:9'); const [showTitle, setShowTitle] = useState(savedOptions?.showTitle ?? true);
const [embedWidthValue, setEmbedWidthValue] = useState(embedVideoDimensions.width); const [showRelated, setShowRelated] = useState(savedOptions?.showRelated ?? true);
const [embedWidthUnit, setEmbedWidthUnit] = useState(embedVideoDimensions.widthUnit); const [showUserAvatar, setShowUserAvatar] = useState(savedOptions?.showUserAvatar ?? true);
const [embedHeightValue, setEmbedHeightValue] = useState(embedVideoDimensions.height); const [linkTitle, setLinkTitle] = useState(savedOptions?.linkTitle ?? true);
const [embedHeightUnit, setEmbedHeightUnit] = useState(embedVideoDimensions.heightUnit); const [responsive, setResponsive] = useState(savedOptions?.responsive ?? false);
const [startAt, setStartAt] = useState(false);
const [startTime, setStartTime] = useState('0:00');
const [aspectRatio, setAspectRatio] = useState(savedOptions?.aspectRatio ?? '16:9');
const [embedWidthValue, setEmbedWidthValue] = useState(savedOptions?.embedWidthValue ?? embedVideoDimensions.width);
const [embedWidthUnit, setEmbedWidthUnit] = useState(savedOptions?.embedWidthUnit ?? embedVideoDimensions.widthUnit);
const [embedHeightValue, setEmbedHeightValue] = useState(savedOptions?.embedHeightValue ?? embedVideoDimensions.height);
const [embedHeightUnit, setEmbedHeightUnit] = useState(savedOptions?.embedHeightUnit ?? embedVideoDimensions.heightUnit);
const [rightMiddlePositionTop, setRightMiddlePositionTop] = useState(60); const [rightMiddlePositionTop, setRightMiddlePositionTop] = useState(60);
const [rightMiddlePositionBottom, setRightMiddlePositionBottom] = useState(60); const [rightMiddlePositionBottom, setRightMiddlePositionBottom] = useState(60);
const [unitOptions, setUnitOptions] = useState([ const [unitOptions, setUnitOptions] = useState([
@@ -71,36 +100,65 @@ export function MediaShareEmbed(props) {
setEmbedHeightUnit(newVal); setEmbedHeightUnit(newVal);
} }
function onKeepAspectRatioChange() { function onShowTitleChange() {
const newVal = !keepAspectRatio; setShowTitle(!showTitle);
}
function onShowRelatedChange() {
setShowRelated(!showRelated);
}
function onShowUserAvatarChange() {
setShowUserAvatar(!showUserAvatar);
}
function onLinkTitleChange() {
setLinkTitle(!linkTitle);
}
function onResponsiveChange() {
const nextResponsive = !responsive;
setResponsive(nextResponsive);
if (!nextResponsive) {
if (aspectRatio !== 'custom') {
const arr = aspectRatio.split(':'); const arr = aspectRatio.split(':');
const x = arr[0]; const x = arr[0];
const y = arr[1]; const y = arr[1];
setKeepAspectRatio(newVal); setKeepAspectRatio(true);
setEmbedWidthUnit(newVal ? 'px' : embedWidthUnit); setEmbedHeightValue(parseInt((embedWidthValue * y) / x, 10));
setEmbedHeightUnit(newVal ? 'px' : embedHeightUnit); } else {
setEmbedHeightValue(newVal ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue); setKeepAspectRatio(false);
setUnitOptions( }
newVal } else {
? [{ key: 'px', label: 'px' }] setKeepAspectRatio(false);
: [ }
{ key: 'px', label: 'px' }, }
{ key: 'percent', label: '%' },
] function onStartAtChange() {
); setStartAt(!startAt);
}
function onStartTimeChange(e) {
setStartTime(e.target.value);
} }
function onAspectRatioChange() { function onAspectRatioChange() {
const newVal = aspectRatioValueRef.current.value; const newVal = aspectRatioValueRef.current.value;
if (newVal === 'custom') {
setAspectRatio(newVal);
setKeepAspectRatio(false);
} else {
const arr = newVal.split(':'); const arr = newVal.split(':');
const x = arr[0]; const x = arr[0];
const y = arr[1]; const y = arr[1];
setAspectRatio(newVal); setAspectRatio(newVal);
setEmbedHeightValue(keepAspectRatio ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue); setKeepAspectRatio(true);
setEmbedHeightValue(parseInt((embedWidthValue * y) / x, 10));
}
} }
function onWindowResize() { function onWindowResize() {
@@ -130,13 +188,88 @@ export function MediaShareEmbed(props) {
}; };
}, []); }, []);
// Save embed options to localStorage when they change (except startAt/startTime)
useEffect(() => {
saveEmbedOptions({
showTitle,
showRelated,
showUserAvatar,
linkTitle,
responsive,
aspectRatio,
embedWidthValue,
embedWidthUnit,
embedHeightValue,
embedHeightUnit,
keepAspectRatio,
});
}, [showTitle, showRelated, showUserAvatar, linkTitle, responsive, aspectRatio, embedWidthValue, embedWidthUnit, embedHeightValue, embedHeightUnit, keepAspectRatio]);
function getEmbedCode() {
const mediaId = MediaPageStore.get('media-id');
const params = new URLSearchParams();
if (showTitle) params.set('showTitle', '1');
else params.set('showTitle', '0');
if (showRelated) params.set('showRelated', '1');
else params.set('showRelated', '0');
if (showUserAvatar) params.set('showUserAvatar', '1');
else params.set('showUserAvatar', '0');
if (linkTitle) params.set('linkTitle', '1');
else params.set('linkTitle', '0');
if (startAt && startTime) {
const parts = startTime.split(':').reverse();
let seconds = 0;
if (parts[0]) seconds += parseInt(parts[0], 10) || 0;
if (parts[1]) seconds += (parseInt(parts[1], 10) || 0) * 60;
if (parts[2]) seconds += (parseInt(parts[2], 10) || 0) * 3600;
if (seconds > 0) params.set('t', seconds);
}
const separator = links.embed.includes('?') ? '&' : '?';
const finalUrl = `${links.embed}${mediaId}${separator}${params.toString()}`;
if (responsive) {
if (aspectRatio === 'custom') {
// Use current width/height values to calculate aspect ratio for custom
const ratio = `${embedWidthValue} / ${embedHeightValue}`;
const maxWidth = `calc(100vh * ${embedWidthValue} / ${embedHeightValue})`;
return `<iframe src="${finalUrl}" style="width:100%;max-width:${maxWidth};aspect-ratio:${ratio};display:block;margin:auto;border:0;" allowFullScreen></iframe>`;
}
const arr = aspectRatio.split(':');
const ratio = `${arr[0]} / ${arr[1]}`;
const maxWidth = `calc(100vh * ${arr[0]} / ${arr[1]})`;
return `<iframe src="${finalUrl}" style="width:100%;max-width:${maxWidth};aspect-ratio:${ratio};display:block;margin:auto;border:0;" allowFullScreen></iframe>`;
}
const width = 'percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue;
const height = 'percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue;
return `<iframe width="${width}" height="${height}" src="${finalUrl}" frameBorder="0" allowFullScreen></iframe>`;
}
return ( return (
<div className="share-embed" style={{ maxHeight: maxHeight + 'px' }}> <div className="share-embed" style={{ maxHeight: maxHeight + 'px' }}>
<div className="share-embed-inner"> <div className="share-embed-inner">
<div className="on-left"> <div className="on-left">
<div className="media-embed-wrap"> <div className="media-embed-wrap">
<SiteConsumer> <SiteConsumer>
{(site) => <VideoViewer data={MediaPageStore.get('media-data')} siteUrl={site.url} inEmbed={true} />} {(site) => {
const previewUrl = `${links.embed + MediaPageStore.get('media-id')}&showTitle=${showTitle ? '1' : '0'}&showRelated=${showRelated ? '1' : '0'}&showUserAvatar=${showUserAvatar ? '1' : '0'}&linkTitle=${linkTitle ? '1' : '0'}${startAt ? '&t=' + (startTime.split(':').reverse().reduce((acc, cur, i) => acc + (parseInt(cur, 10) || 0) * Math.pow(60, i), 0)) : ''}`;
const style = {};
style.width = '100%';
style.height = '480px';
style.overflow = 'hidden';
return (
<div style={style}>
<iframe width="100%" height="100%" src={previewUrl} frameBorder="0" allowFullScreen></iframe>
</div>
);
}}
</SiteConsumer> </SiteConsumer>
</div> </div>
</div> </div>
@@ -158,16 +291,7 @@ export function MediaShareEmbed(props) {
> >
<textarea <textarea
readOnly readOnly
value={ value={getEmbedCode()}
'<iframe width="' +
('percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue) +
'" height="' +
('percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue) +
'" src="' +
links.embed +
MediaPageStore.get('media-id') +
'" frameborder="0" allowfullscreen></iframe>'
}
></textarea> ></textarea>
<div className="iframe-config"> <div className="iframe-config">
@@ -179,34 +303,79 @@ export function MediaShareEmbed(props) {
</div>*/} </div>*/}
<div className="option-content"> <div className="option-content">
<div className="ratio-options"> <div className="ratio-options" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 10px' }}>
<div className="options-group"> <div className="options-group">
<label style={{ minHeight: '36px' }}> <label style={{ minHeight: '36px', whiteSpace: 'nowrap' }}>
<input type="checkbox" checked={keepAspectRatio} onChange={onKeepAspectRatioChange} /> <input type="checkbox" checked={showTitle} onChange={onShowTitleChange} />
Keep aspect ratio Show title
</label> </label>
</div> </div>
{!keepAspectRatio ? null : (
<div className="options-group"> <div className="options-group">
<select ref={aspectRatioValueRef} onChange={onAspectRatioChange} value={aspectRatio}> <label style={{ minHeight: '36px', whiteSpace: 'nowrap', opacity: showTitle ? 1 : 0.5 }}>
<optgroup label="Horizontal orientation"> <input type="checkbox" checked={linkTitle} onChange={onLinkTitleChange} disabled={!showTitle} />
Link title
</label>
</div>
<div className="options-group">
<label style={{ minHeight: '36px', whiteSpace: 'nowrap' }}>
<input type="checkbox" checked={showRelated} onChange={onShowRelatedChange} />
Show related
</label>
</div>
<div className="options-group">
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', opacity: showTitle ? 1 : 0.5 }}>
<input type="checkbox" checked={showUserAvatar} onChange={onShowUserAvatarChange} disabled={!showTitle} />
Show user avatar
</label>
</div>
<div className="options-group" style={{ display: 'flex', alignItems: 'center' }}>
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', marginRight: '10px' }}>
<input type="checkbox" checked={responsive} onChange={onResponsiveChange} />
Responsive
</label>
</div>
<div className="options-group" style={{ display: 'flex', alignItems: 'center' }}>
<label style={{ minHeight: '36px', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', marginRight: '10px' }}>
<input type="checkbox" checked={startAt} onChange={onStartAtChange} />
Start at
</label>
{startAt && (
<input
type="text"
value={startTime}
onChange={onStartTimeChange}
style={{ width: '60px', height: '28px', fontSize: '12px', padding: '2px 5px' }}
/>
)}
</div>
<div className="options-group" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div style={{ fontSize: '12px', marginBottom: '4px', color: 'rgba(0,0,0,0.6)' }}>Aspect Ratio</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<select
ref={aspectRatioValueRef}
onChange={onAspectRatioChange}
value={aspectRatio}
style={{ height: '28px', fontSize: '12px' }}
>
<option value="16:9">16:9</option> <option value="16:9">16:9</option>
<option value="4:3">4:3</option> <option value="4:3">4:3</option>
<option value="3:2">3:2</option> <option value="3:2">3:2</option>
</optgroup> <option value="custom">Custom</option>
<optgroup label="Vertical orientation">
<option value="9:16">9:16</option>
<option value="3:4">3:4</option>
<option value="2:3">2:3</option>
</optgroup>
</select> </select>
</div> </div>
)} </div>
</div> </div>
<br /> <br />
{!responsive && (
<>
<div className="options-group"> <div className="options-group">
<NumericInputWithUnit <NumericInputWithUnit
valueCallback={onEmbedWidthValueChange} valueCallback={onEmbedWidthValueChange}
@@ -232,6 +401,8 @@ export function MediaShareEmbed(props) {
units={unitOptions} units={unitOptions}
/> />
</div> </div>
</>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
@use "sass:math"; @use "sass:math";
@import '../../../css/includes/_variables.scss'; @import "../../../css/includes/_variables.scss";
@import '../../../css/includes/_variables_dimensions.scss'; @import "../../../css/includes/_variables_dimensions.scss";
.visible-sidebar .page-main-wrap { .visible-sidebar .page-main-wrap {
padding-left: 0; padding-left: 0;
@@ -119,7 +119,7 @@
background-color: var(--media-actions-share-copy-field-bg-color); background-color: var(--media-actions-share-copy-field-bg-color);
} }
input[type='text'] { input[type="text"] {
color: var(--media-actions-share-copy-field-input-text-color); color: var(--media-actions-share-copy-field-input-text-color);
} }
} }
@@ -180,7 +180,7 @@
color: var(--report-form-field-label-text-color); color: var(--report-form-field-label-text-color);
} }
input[type='text'], input[type="text"],
textarea { textarea {
color: var(--report-form-field-input-text-color); color: var(--report-form-field-input-text-color);
border-color: var(--report-form-field-input-border-color); border-color: var(--report-form-field-input-border-color);
@@ -479,7 +479,7 @@
&.audio-player-container { &.audio-player-container {
&:before { &:before {
content: '\E3A1'; content: "\E3A1";
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@@ -490,12 +490,11 @@
line-height: 1; line-height: 1;
padding: 0; padding: 0;
font-family: 'Material Icons'; font-family: "Material Icons";
text-decoration: none; text-decoration: none;
color: #888; color: #888;
} }
.vjs-big-play-button { .vjs-big-play-button {
} }
@@ -514,6 +513,7 @@
} }
} }
.viewer-image-container { .viewer-image-container {
position: relative; position: relative;
display: block; display: block;
@@ -550,8 +550,6 @@
max-width: 90%; max-width: 90%;
} }
.slideshow-image img { .slideshow-image img {
display: block; display: block;
width: auto; width: auto;
@@ -560,7 +558,9 @@
max-height: 90vh; max-height: 90vh;
border-radius: 0; border-radius: 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: transform 60s ease-in-out, opacity 60 ease-in-out; transition:
transform 60s ease-in-out,
opacity 60 ease-in-out;
} }
.slideshow-title { .slideshow-title {
@@ -572,7 +572,6 @@
z-index: 1200; z-index: 1200;
} }
.arrow { .arrow {
position: absolute; position: absolute;
display: flex; display: flex;
@@ -590,7 +589,9 @@
padding: 10px; padding: 10px;
border-radius: 50%; border-radius: 50%;
z-index: 1000; z-index: 1000;
transition: background-color 0.2s ease, transform 0.2s ease; transition:
background-color 0.2s ease,
transform 0.2s ease;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
} }
@@ -685,18 +686,19 @@
width: 100%; // Default width for mobile width: 100%; // Default width for mobile
height: 400px; // Default height for mobile height: 400px; // Default height for mobile
@media (min-width: 768px) and (max-width: 1023px) { // Tablets @media (min-width: 768px) and (max-width: 1023px) {
// Tablets
width: 90%; width: 90%;
height: 600px; height: 600px;
} }
@media (min-width: 1024px) { // Desktop @media (min-width: 1024px) {
// Desktop
width: 85%; width: 85%;
height: 900px; height: 900px;
} }
} }
.viewer-container .player-container.viewer-pdf-container, .viewer-container .player-container.viewer-pdf-container,
.viewer-container .player-container.viewer-attachment-container { .viewer-container .player-container.viewer-attachment-container {
background-color: var(--item-thumb-bg-color); background-color: var(--item-thumb-bg-color);
@@ -785,78 +787,6 @@
} }
} }
.edit-media-dropdown {
position: relative;
display: inline-block;
.popup-fullscreen-overlay {
display: none;
}
.popup-main {
position: absolute;
top: calc(100% + 8px);
right: 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
z-index: 1000;
.dark_theme & {
background: #2d2d2d;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
}
.edit-options {
.navigation-menu-list {
padding: 8px 0;
li {
list-style: none;
margin: 0;
a {
display: flex;
align-items: center;
padding: 12px 16px;
text-decoration: none;
color: #333;
transition: background-color 0.2s ease;
.dark_theme & {
color: #e0e0e0;
}
&:hover {
background-color: rgba(0, 153, 51, 0.1);
.dark_theme & {
background-color: rgba(102, 187, 102, 0.2);
}
}
.material-icons {
margin-right: 12px;
font-size: 20px;
color: rgba(0, 153, 51, 0.9);
.dark_theme & {
color: rgba(102, 187, 102, 0.9);
}
}
span {
font-size: 14px;
font-weight: 500;
}
}
}
}
}
}
.remove-media-icon { .remove-media-icon {
background-color: rgba(220, 53, 69, 0.9); background-color: rgba(220, 53, 69, 0.9);
@@ -1078,7 +1008,7 @@
&.like, &.like,
&.dislike { &.dislike {
&:before { &:before {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: -4px; left: -4px;
@@ -1218,7 +1148,7 @@
border-radius: 2px; border-radius: 2px;
} }
input[type='text'] { input[type="text"] {
width: 100%; width: 100%;
height: 42px; height: 42px;
padding: 1px 0 1px 16px; padding: 1px 0 1px 16px;
@@ -1278,13 +1208,18 @@
width: 220px; width: 220px;
} }
box-shadow: 0 16px 24px 2px rgba(#000, 0.14), 0 6px 30px 5px rgba(#000, 0.12), box-shadow:
0 16px 24px 2px rgba(#000, 0.14),
0 6px 30px 5px rgba(#000, 0.12),
0 8px 10px -5px rgba(#000, 0.4); 0 8px 10px -5px rgba(#000, 0.4);
&.main-options, &.main-options,
&.video-download-options { &.video-download-options {
width: 240px; width: 240px;
box-shadow: 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12), 0 3px 1px -2px rgba(#000, 0.2); box-shadow:
0 2px 2px 0 rgba(#000, 0.14),
0 1px 5px 0 rgba(#000, 0.12),
0 3px 1px -2px rgba(#000, 0.2);
} }
} }
} }
@@ -1342,7 +1277,9 @@
padding: 24px; padding: 24px;
text-align: initial; text-align: initial;
box-shadow: rgba(#000, 0.14) 0px 16px 24px 2px, rgba(#000, 0.12) 0px 6px 30px 5px, box-shadow:
rgba(#000, 0.14) 0px 16px 24px 2px,
rgba(#000, 0.12) 0px 6px 30px 5px,
rgba(#000, 0.4) 0px 8px 10px; rgba(#000, 0.4) 0px 8px 10px;
} }
} }
@@ -1375,13 +1312,18 @@
width: 220px; width: 220px;
} }
box-shadow: 0 16px 24px 2px rgba(#000, 0.14), 0 6px 30px 5px rgba(#000, 0.12), box-shadow:
0 16px 24px 2px rgba(#000, 0.14),
0 6px 30px 5px rgba(#000, 0.12),
0 8px 10px -5px rgba(#000, 0.4); 0 8px 10px -5px rgba(#000, 0.4);
&.main-options, &.main-options,
&.video-download-options { &.video-download-options {
width: 240px; width: 240px;
box-shadow: 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12), 0 3px 1px -2px rgba(#000, 0.2); box-shadow:
0 2px 2px 0 rgba(#000, 0.14),
0 1px 5px 0 rgba(#000, 0.12),
0 3px 1px -2px rgba(#000, 0.2);
.popup-main { .popup-main {
min-height: 0; min-height: 0;
@@ -1484,7 +1426,7 @@
font-weight: 500; font-weight: 500;
} }
input[type='text'], input[type="text"],
textarea { textarea {
min-width: 100%; min-width: 100%;
width: 100%; width: 100%;
@@ -1507,7 +1449,7 @@
cursor: not-allowed; cursor: not-allowed;
} }
input[type='text'] { input[type="text"] {
font-size: 14px; font-size: 14px;
} }
@@ -1804,7 +1746,7 @@
border-width: 0 0 1px; border-width: 0 0 1px;
&:after { &:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -5px; bottom: -5px;
right: 0; right: 0;
@@ -1860,7 +1802,7 @@
max-height: 100%; max-height: 100%;
padding: 16px; padding: 16px;
cursor: text; cursor: text;
font-family: 'Roboto Mono', monospace; font-family: "Roboto Mono", monospace;
font-size: 14px; font-size: 14px;
line-height: 1.714285714; line-height: 1.714285714;
outline: 0; outline: 0;
@@ -1894,7 +1836,7 @@
vertical-align: top; vertical-align: top;
input { input {
&[type='checkbox'] { &[type="checkbox"] {
margin-left: 0; margin-left: 0;
} }
} }
@@ -1906,7 +1848,7 @@
width: 100%; width: 100%;
input { input {
&[type='checkbox'] { &[type="checkbox"] {
margin-left: 0; margin-left: 0;
} }
} }
@@ -1982,9 +1924,21 @@
} }
} }
.media-embed-wrap {
display: block;
width: 100%;
height: 100%;
background: #000;
.media-embed-wrap { .media-embed-wrap {
display: block; display: block;
.player-container,
.player-container-inner {
width: 100%;
height: 100%;
padding-top: 0;
background: #000;
}
.player-container, .player-container,
.player-container-inner { .player-container-inner {
width: 100%; width: 100%;
@@ -1998,6 +1952,10 @@
.circle-icon-button { .circle-icon-button {
} }
.video-js.vjs-mediacms {
padding-top: 0;
}
}
.video-js.vjs-mediacms { .video-js.vjs-mediacms {
padding-top: math.div(9, 16) * 100%; padding-top: math.div(9, 16) * 100%;
} }
@@ -2051,8 +2009,8 @@
} }
.item-date:before { .item-date:before {
content: ''; content: "";
content: '\2022'; content: "\2022";
margin: 0 4px; margin: 0 4px;
} }
@@ -2089,14 +2047,14 @@
margin-right: 4px; margin-right: 4px;
&:after { &:after {
content: ','; content: ",";
} }
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
&:after { &:after {
content: ''; content: "";
} }
} }
} }

View File

@@ -3,8 +3,8 @@ import { SiteContext } from '../../utils/contexts/';
import { useUser, usePopup } from '../../utils/hooks/'; import { useUser, usePopup } from '../../utils/hooks/';
import { PageStore, MediaPageStore } from '../../utils/stores/'; import { PageStore, MediaPageStore } from '../../utils/stores/';
import { PageActions, MediaPageActions } from '../../utils/actions/'; import { PageActions, MediaPageActions } from '../../utils/actions/';
import { formatInnerLink, publishedOnDate } from '../../utils/helpers/'; import { formatInnerLink, inEmbeddedApp, publishedOnDate } from '../../utils/helpers/';
import { PopupMain, CircleIconButton, MaterialIcon, NavigationMenuList, NavigationContentApp } from '../_shared/'; import { PopupMain } from '../_shared/';
import CommentsList from '../comments/Comments'; import CommentsList from '../comments/Comments';
import { replaceString } from '../../utils/helpers/'; import { replaceString } from '../../utils/helpers/';
import { translateString } from '../../utils/helpers/'; import { translateString } from '../../utils/helpers/';
@@ -72,103 +72,21 @@ function MediaMetaField(props) {
); );
} }
function getEditMenuItems() {
const items = [];
const friendlyToken = window.MediaCMS.mediaId;
const mediaData = MediaPageStore.get('media-data');
const mediaType = mediaData ? mediaData.media_type : null;
const isVideoOrAudio = mediaType === 'video' || mediaType === 'audio';
const allowMediaReplacement = window.MediaCMS.features.media.actions.allowMediaReplacement;
// Edit metadata - always available
items.push({
itemType: 'link',
link: `/edit?m=${friendlyToken}`,
text: translateString('Edit metadata'),
icon: 'edit',
});
// Trim - only for video/audio
if (isVideoOrAudio) {
items.push({
itemType: 'link',
link: `/edit_video?m=${friendlyToken}`,
text: translateString('Trim'),
icon: 'content_cut',
});
}
// Captions - only for video/audio
if (isVideoOrAudio) {
items.push({
itemType: 'link',
link: `/add_subtitle?m=${friendlyToken}`,
text: translateString('Captions'),
icon: 'closed_caption',
});
}
// Chapters - only for video/audio
if (isVideoOrAudio) {
items.push({
itemType: 'link',
link: `/edit_chapters?m=${friendlyToken}`,
text: 'Chapters',
icon: 'menu_book',
});
}
// Publish - always available
items.push({
itemType: 'link',
link: `/publish?m=${friendlyToken}`,
text: translateString('Publish'),
icon: 'publish',
});
// Replace - only if enabled
if (allowMediaReplacement) {
items.push({
itemType: 'link',
link: `/replace_media?m=${friendlyToken}`,
text: translateString('Replace'),
icon: 'swap_horiz',
});
}
return items;
}
function EditMediaButton(props) { function EditMediaButton(props) {
const [popupContentRef, PopupContent, PopupTrigger] = usePopup(); let link = props.link;
const menuItems = getEditMenuItems(); if (window.MediaCMS.site.devEnv) {
link = '/edit-media.html';
}
const popupPages = { if (link && inEmbeddedApp()) {
main: ( link += '&mode=lms_embed_mode';
<div className="edit-options"> }
<PopupMain>
<NavigationMenuList items={menuItems} />
</PopupMain>
</div>
),
};
return ( return (
<div className="edit-media-dropdown"> <a href={link} rel="nofollow" title={translateString('Edit media')} className="edit-media-icon">
<PopupTrigger contentRef={popupContentRef}>
<button className="edit-media-icon" title={translateString('Edit media')}>
<i className="material-icons">edit</i> <i className="material-icons">edit</i>
</button> </a>
</PopupTrigger>
<PopupContent contentRef={popupContentRef}>
<NavigationContentApp
initPage="main"
focusFirstItemOnPageChange={false}
pages={popupPages}
/>
</PopupContent>
</div>
); );
} }
@@ -180,11 +98,18 @@ export default function ViewerInfoContent(props) {
!PageStore.get('config-enabled').taxonomies.tags || PageStore.get('config-enabled').taxonomies.tags.enabled !PageStore.get('config-enabled').taxonomies.tags || PageStore.get('config-enabled').taxonomies.tags.enabled
? metafield(MediaPageStore.get('media-tags')) ? 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 const categoriesContent = PageStore.get('config-options').pages.media.categoriesWithTitle
? [] ? []
: !PageStore.get('config-enabled').taxonomies.categories || : !PageStore.get('config-enabled').taxonomies.categories ||
PageStore.get('config-enabled').taxonomies.categories.enabled PageStore.get('config-enabled').taxonomies.categories.enabled
? metafield(MediaPageStore.get('media-categories')) ? metafield(mediaCategories)
: []; : [];
let summary = MediaPageStore.get('media-summary'); let summary = MediaPageStore.get('media-summary');
@@ -211,7 +136,9 @@ export default function ViewerInfoContent(props) {
PageActions.addNotification('Media removed. Redirecting...', 'mediaDelete'); PageActions.addNotification('Media removed. Redirecting...', 'mediaDelete');
setTimeout(function () { setTimeout(function () {
window.location.href = window.location.href =
SiteContext._currentValue.url + '/' + MediaPageStore.get('media-data').author_profile.replace(/^\//g, ''); SiteContext._currentValue.url +
'/' +
MediaPageStore.get('media-data').author_profile.replace(/^\//g, '');
}, 2000); }, 2000);
}, 100); }, 100);
@@ -271,7 +198,12 @@ export default function ViewerInfoContent(props) {
{void 0 === PageStore.get('config-media-item').displayAuthor || {void 0 === PageStore.get('config-media-item').displayAuthor ||
null === PageStore.get('config-media-item').displayAuthor || null === PageStore.get('config-media-item').displayAuthor ||
!!PageStore.get('config-media-item').displayAuthor ? ( !!PageStore.get('config-media-item').displayAuthor ? (
<MediaAuthorBanner link={authorLink} thumb={authorThumb} name={props.author.name} published={props.published} /> <MediaAuthorBanner
link={authorLink}
thumb={authorThumb}
name={props.author.name}
published={props.published}
/>
) : null} ) : null}
<div className="media-content-banner"> <div className="media-content-banner">
@@ -298,14 +230,24 @@ export default function ViewerInfoContent(props) {
{categoriesContent.length ? ( {categoriesContent.length ? (
<MediaMetaField <MediaMetaField
value={categoriesContent} value={categoriesContent}
title={1 < categoriesContent.length ? translateString('Categories') : translateString('Category')} title={
inEmbeddedApp()
? (1 < categoriesContent.length
? translateString('Courses')
: translateString('Course'))
: (1 < categoriesContent.length
? translateString('Categories')
: translateString('Category'))
}
id="categories" id="categories"
/> />
) : null} ) : null}
{userCan.editMedia ? ( {userCan.editMedia ? (
<div className="media-author-actions"> <div className="media-author-actions">
{userCan.editMedia ? <EditMediaButton /> : null} {userCan.editMedia ? (
<EditMediaButton link={MediaPageStore.get('media-data').edit_url} />
) : null}
{userCan.deleteMedia ? ( {userCan.deleteMedia ? (
<PopupTrigger contentRef={popupContentRef}> <PopupTrigger contentRef={popupContentRef}>
@@ -320,14 +262,22 @@ export default function ViewerInfoContent(props) {
<PopupMain> <PopupMain>
<div className="popup-message"> <div className="popup-message">
<span className="popup-message-title">Media removal</span> <span className="popup-message-title">Media removal</span>
<span className="popup-message-main">You're willing to remove media permanently?</span> <span className="popup-message-main">
You're willing to remove media permanently?
</span>
</div> </div>
<hr /> <hr />
<span className="popup-message-bottom"> <span className="popup-message-bottom">
<button className="button-link cancel-comment-removal" onClick={cancelMediaRemoval}> <button
className="button-link cancel-comment-removal"
onClick={cancelMediaRemoval}
>
CANCEL CANCEL
</button> </button>
<button className="button-link proceed-comment-removal" onClick={proceedMediaRemoval}> <button
className="button-link proceed-comment-removal"
onClick={proceedMediaRemoval}
>
PROCEED PROCEED
</button> </button>
</span> </span>

View File

@@ -183,8 +183,7 @@ export default class ViewerInfoTitleBanner extends React.PureComponent {
{MemberContext._currentValue.can.shareMedia ? <MediaShareButton isVideo={false} /> : null} {MemberContext._currentValue.can.shareMedia ? <MediaShareButton isVideo={false} /> : null}
{!MemberContext._currentValue.is.anonymous && {!MemberContext._currentValue.is.anonymous &&
MemberContext._currentValue.can.saveMedia && MemberContext._currentValue.can.saveMedia ? (
-1 < PlaylistsContext._currentValue.mediaTypes.indexOf(MediaPageStore.get('media-type')) ? (
<MediaSaveButton /> <MediaSaveButton />
) : null} ) : null}

View File

@@ -1,8 +1,16 @@
import React from 'react'; import React from 'react';
import { formatViewsNumber } from '../../utils/helpers/'; import { formatViewsNumber, inEmbeddedApp } from '../../utils/helpers/';
import { PageStore, MediaPageStore } from '../../utils/stores/'; import { PageStore, MediaPageStore } from '../../utils/stores/';
import { MemberContext, PlaylistsContext } from '../../utils/contexts/'; import { MemberContext, PlaylistsContext } from '../../utils/contexts/';
import { MediaLikeIcon, MediaDislikeIcon, OtherMediaDownloadLink, VideoMediaDownloadLink, MediaSaveButton, MediaShareButton, MediaMoreOptionsIcon } from '../media-actions/'; import {
MediaLikeIcon,
MediaDislikeIcon,
OtherMediaDownloadLink,
VideoMediaDownloadLink,
MediaSaveButton,
MediaShareButton,
MediaMoreOptionsIcon,
} from '../media-actions/';
import ViewerInfoTitleBanner from './ViewerInfoTitleBanner'; import ViewerInfoTitleBanner from './ViewerInfoTitleBanner';
import { translateString } from '../../utils/helpers/'; import { translateString } from '../../utils/helpers/';
@@ -74,7 +82,8 @@ export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
{displayViews ? ( {displayViews ? (
<div className="media-views"> <div className="media-views">
{formatViewsNumber(this.props.views, true)} {1 >= this.props.views ? translateString('view') : translateString('views')} {formatViewsNumber(this.props.views, true)}{' '}
{1 >= this.props.views ? translateString('view') : translateString('views')}
</div> </div>
) : null} ) : null}
@@ -82,11 +91,12 @@ export default class ViewerInfoVideoTitleBanner extends ViewerInfoTitleBanner {
<div> <div>
{MemberContext._currentValue.can.likeMedia ? <MediaLikeIcon /> : null} {MemberContext._currentValue.can.likeMedia ? <MediaLikeIcon /> : null}
{MemberContext._currentValue.can.dislikeMedia ? <MediaDislikeIcon /> : null} {MemberContext._currentValue.can.dislikeMedia ? <MediaDislikeIcon /> : null}
{MemberContext._currentValue.can.shareMedia ? <MediaShareButton isVideo={true} /> : null} {!inEmbeddedApp() && MemberContext._currentValue.can.shareMedia ? (
<MediaShareButton isVideo={true} />
) : null}
{!MemberContext._currentValue.is.anonymous && {!MemberContext._currentValue.is.anonymous &&
MemberContext._currentValue.can.saveMedia && MemberContext._currentValue.can.saveMedia ? (
-1 < PlaylistsContext._currentValue.mediaTypes.indexOf(MediaPageStore.get('media-type')) ? (
<MediaSaveButton /> <MediaSaveButton />
) : null} ) : null}

View File

@@ -1,7 +1,4 @@
import React from 'react'; import React from 'react';
import { MediaPageStore } from '../../utils/stores/';
import { AutoPlay } from './AutoPlay';
import { RelatedMedia } from './RelatedMedia';
import PlaylistView from './PlaylistView'; import PlaylistView from './PlaylistView';
export default class ViewerSidebar extends React.PureComponent { export default class ViewerSidebar extends React.PureComponent {
@@ -12,8 +9,6 @@ export default class ViewerSidebar extends React.PureComponent {
playlistData: props.playlistData, playlistData: props.playlistData,
isPlaylistPage: !!props.playlistData, isPlaylistPage: !!props.playlistData,
activeItem: 0, activeItem: 0,
mediaType: MediaPageStore.get('media-type'),
chapters: MediaPageStore.get('media-data')?.chapters
}; };
if (props.playlistData) { if (props.playlistData) {
@@ -28,18 +23,6 @@ export default class ViewerSidebar extends React.PureComponent {
} }
} }
this.onMediaLoad = this.onMediaLoad.bind(this);
}
componentDidMount() {
MediaPageStore.on('loaded_media_data', this.onMediaLoad);
}
onMediaLoad() {
this.setState({
mediaType: MediaPageStore.get('media-type'),
chapters: MediaPageStore.get('media-data')?.chapter_data || []
});
} }
render() { render() {
@@ -47,10 +30,7 @@ export default class ViewerSidebar extends React.PureComponent {
<div className="viewer-sidebar"> <div className="viewer-sidebar">
{this.state.isPlaylistPage ? ( {this.state.isPlaylistPage ? (
<PlaylistView activeItem={this.state.activeItem} playlistData={this.props.playlistData} /> <PlaylistView activeItem={this.state.activeItem} playlistData={this.props.playlistData} />
) : 'video' === this.state.mediaType || 'audio' === this.state.mediaType ? (
<AutoPlay />
) : null} ) : null}
<RelatedMedia hideFirst={!this.state.isPlaylistPage} />
</div> </div>
); );
} }

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { VideoViewerActions } from '../../../utils/actions/'; import { VideoViewerActions } from '../../../utils/actions/';
import { SiteContext, SiteConsumer } from '../../../utils/contexts/'; import { SiteContext, SiteConsumer } from '../../../utils/contexts/';
import { PageStore, MediaPageStore, VideoViewerStore } from '../../../utils/stores/'; import { PageStore, MediaPageStore, VideoViewerStore } from '../../../utils/stores/';
import { addClassname, removeClassname, formatInnerLink } from '../../../utils/helpers/'; import { addClassname, removeClassname, formatInnerLink, inEmbeddedApp } from '../../../utils/helpers/';
import { BrowserCache, UpNextLoaderView, MediaDurationInfo } from '../../../utils/classes/'; import { BrowserCache, UpNextLoaderView, MediaDurationInfo } from '../../../utils/classes/';
import { import {
orderedSupportedVideoFormats, orderedSupportedVideoFormats,
@@ -176,11 +176,13 @@ export default class VideoViewer extends React.PureComponent {
topLeftHtml = document.createElement('div'); topLeftHtml = document.createElement('div');
topLeftHtml.setAttribute('class', 'media-links-top-left'); topLeftHtml.setAttribute('class', 'media-links-top-left');
const linkTarget = inEmbeddedApp() || window.location.href.indexOf('lms_embed_mode') > -1 ? '_self' : '_blank';
if (titleLink) { if (titleLink) {
titleLink.setAttribute('class', 'title-link'); titleLink.setAttribute('class', 'title-link');
titleLink.setAttribute('href', this.props.data.url); titleLink.setAttribute('href', this.props.data.url);
titleLink.setAttribute('title', this.props.data.title); titleLink.setAttribute('title', this.props.data.title);
titleLink.setAttribute('target', '_blank'); titleLink.setAttribute('target', linkTarget);
titleLink.innerHTML = this.props.data.title; titleLink.innerHTML = this.props.data.title;
} }
@@ -191,7 +193,7 @@ export default class VideoViewer extends React.PureComponent {
formatInnerLink(this.props.data.author_profile, this.props.siteUrl) formatInnerLink(this.props.data.author_profile, this.props.siteUrl)
); );
userThumbLink.setAttribute('title', this.props.data.author_name); userThumbLink.setAttribute('title', this.props.data.author_name);
userThumbLink.setAttribute('target', '_blank'); userThumbLink.setAttribute('target', linkTarget);
userThumbLink.setAttribute( userThumbLink.setAttribute(
'style', 'style',
'background-image:url(' + 'background-image:url(' +
@@ -410,8 +412,12 @@ export default class VideoViewer extends React.PureComponent {
poster: this.videoPoster, poster: this.videoPoster,
previewSprite: previewSprite, previewSprite: previewSprite,
subtitlesInfo: this.props.data.subtitles_info, subtitlesInfo: this.props.data.subtitles_info,
enableAutoplay: !this.props.inEmbed,
inEmbed: this.props.inEmbed, inEmbed: this.props.inEmbed,
showTitle: this.props.showTitle,
showRelated: this.props.showRelated,
showUserAvatar: this.props.showUserAvatar,
linkTitle: this.props.linkTitle,
urlTimestamp: this.props.timestamp,
hasTheaterMode: !this.props.inEmbed, hasTheaterMode: !this.props.inEmbed,
hasNextLink: !!nextLink, hasNextLink: !!nextLink,
nextLink: nextLink, nextLink: nextLink,
@@ -435,9 +441,19 @@ export default class VideoViewer extends React.PureComponent {
VideoViewer.defaultProps = { VideoViewer.defaultProps = {
inEmbed: !0, inEmbed: !0,
showTitle: !0,
showRelated: !0,
showUserAvatar: !0,
linkTitle: !0,
timestamp: null,
siteUrl: PropTypes.string.isRequired, siteUrl: PropTypes.string.isRequired,
}; };
VideoViewer.propTypes = { VideoViewer.propTypes = {
inEmbed: PropTypes.bool, inEmbed: PropTypes.bool,
showTitle: PropTypes.bool,
showRelated: PropTypes.bool,
showUserAvatar: PropTypes.bool,
linkTitle: PropTypes.bool,
timestamp: PropTypes.number,
}; };

View File

@@ -88,7 +88,7 @@ function UploadMediaButton({ user, links }) {
{ {
link: '/record_screen', link: '/record_screen',
icon: 'videocam', icon: 'videocam',
text: translateString('Record Screen'), text: translateString('Record'),
}, },
]; ];

View File

@@ -23,6 +23,11 @@
transition-property: padding-left; transition-property: padding-left;
transition-duration: 0.2s; transition-duration: 0.2s;
} }
.embedded-app & {
padding-top: 0;
padding-left: 0;
}
} }
#page-profile-media, #page-profile-media,

View File

@@ -576,6 +576,7 @@
// Ensure icon buttons are visible on mobile // Ensure icon buttons are visible on mobile
&.media-search, &.media-search,
&.media-filters-toggle, &.media-filters-toggle,
&.media-sharing-toggle,
&.media-tags-toggle, &.media-tags-toggle,
&.media-sorting-toggle { &.media-sorting-toggle {
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
@@ -656,30 +657,6 @@
} }
} }
&.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 { .page-main {
@@ -925,6 +902,13 @@ $-max-width: $-hor-spaces + ( 2 * $item-width ) - 1;
} }
} }
.mi-sharing-filter-options {
> .active button,
> * button:hover {
background-color: #3b82f6 !important;
}
}
$-hor-spaces: 2 * $side-empty-space; $-hor-spaces: 2 * $side-empty-space;
$-max-width: $-hor-spaces + ( 2 * $item-width ) - 1; $-max-width: $-hor-spaces + ( 2 * $item-width ) - 1;

View File

@@ -5,7 +5,7 @@ import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/'
import { PageStore, ProfilePageStore } from '../../utils/stores/'; import { PageStore, ProfilePageStore } from '../../utils/stores/';
import { PageActions, ProfilePageActions } from '../../utils/actions/'; import { PageActions, ProfilePageActions } from '../../utils/actions/';
import { CircleIconButton, PopupMain } from '../_shared'; import { CircleIconButton, PopupMain } from '../_shared';
import { translateString } from '../../utils/helpers/'; import { translateString, inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode } from '../../utils/helpers/';
class ProfileSearchBar extends React.PureComponent { class ProfileSearchBar extends React.PureComponent {
constructor(props) { constructor(props) {
@@ -162,12 +162,16 @@ class ProfileSearchBar extends React.PureComponent {
if (!this.state.visibleForm) { if (!this.state.visibleForm) {
return ( return (
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} onClick={this.showForm}> <span
style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }}
onClick={this.showForm}
>
<CircleIconButton buttonShadow={false}> <CircleIconButton buttonShadow={false}>
<i className="material-icons">search</i> <i className="material-icons">search</i>
</CircleIconButton> </CircleIconButton>
{hasSearchText ? ( {hasSearchText ? (
<span style={{ <span
style={{
position: 'absolute', position: 'absolute',
top: '8px', top: '8px',
right: '8px', right: '8px',
@@ -176,7 +180,8 @@ class ProfileSearchBar extends React.PureComponent {
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)', backgroundColor: 'var(--default-theme-color)',
border: '2px solid white', border: '2px solid white',
}}></span> }}
></span>
) : null} ) : null}
</span> </span>
); );
@@ -189,7 +194,8 @@ class ProfileSearchBar extends React.PureComponent {
<i className="material-icons">search</i> <i className="material-icons">search</i>
</CircleIconButton> </CircleIconButton>
{hasSearchText ? ( {hasSearchText ? (
<span style={{ <span
style={{
position: 'absolute', position: 'absolute',
top: '8px', top: '8px',
right: '8px', right: '8px',
@@ -198,7 +204,8 @@ class ProfileSearchBar extends React.PureComponent {
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)', backgroundColor: 'var(--default-theme-color)',
border: '2px solid white', border: '2px solid white',
}}></span> }}
></span>
) : null} ) : null}
</span> </span>
<span> <span>
@@ -365,30 +372,47 @@ class NavMenuInlineTabs extends React.PureComponent {
} }
render() { 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 ( return (
<nav ref="tabsNav" className="profile-nav items-list-outer list-inline list-slider"> <nav ref="tabsNav" className="profile-nav items-list-outer list-inline list-slider">
<div className="profile-nav-inner items-list-outer"> <div className="profile-nav-inner items-list-outer">
{this.state.displayPrev ? this.previousBtn : null} {this.state.displayPrev ? this.previousBtn : null}
<ul className="items-list-wrap" ref="itemsListWrap"> <ul className="items-list-wrap" ref="itemsListWrap">
{!isSelectMediaMode && !inEmbeddedApp() ? (
<InlineTab <InlineTab
id="about" id="about"
isActive={'about' === this.props.type} isActive={'about' === this.props.type}
label={translateString('About')} label={translateString('About')}
link={LinksContext._currentValue.profile.about} link={LinksContext._currentValue.profile.about}
/> />
) : null}
<InlineTab <InlineTab
id="media" id="media"
isActive={'media' === this.props.type} isActive={'media' === this.props.type}
label={translateString(this.userIsAuthor ? 'Media I own' : 'Media')} label={translateString(this.userIsAuthor ? 'Media I own' : 'Media')}
link={LinksContext._currentValue.profile.media} link={mediaLink}
/> />
{this.userIsAuthor ? ( {this.userIsAuthor ? (
<InlineTab <InlineTab
id="shared_by_me" id="shared_by_me"
isActive={'shared_by_me' === this.props.type} isActive={'shared_by_me' === this.props.type}
label={translateString('Shared by me')} label={translateString('Shared by me')}
link={LinksContext._currentValue.profile.shared_by_me} link={sharedByMeLink}
/> />
) : null} ) : null}
{this.userIsAuthor ? ( {this.userIsAuthor ? (
@@ -396,11 +420,11 @@ class NavMenuInlineTabs extends React.PureComponent {
id="shared_with_me" id="shared_with_me"
isActive={'shared_with_me' === this.props.type} isActive={'shared_with_me' === this.props.type}
label={translateString('Shared with me')} label={translateString('Shared with me')}
link={LinksContext._currentValue.profile.shared_with_me} link={sharedWithMeLink}
/> />
) : null} ) : null}
{MemberContext._currentValue.can.saveMedia ? ( {!isSelectMediaMode && MemberContext._currentValue.can.saveMedia ? (
<InlineTab <InlineTab
id="playlists" id="playlists"
isActive={'playlists' === this.props.type} isActive={'playlists' === this.props.type}
@@ -427,17 +451,32 @@ class NavMenuInlineTabs extends React.PureComponent {
{!['about', 'playlists'].includes(this.props.type) ? ( {!['about', 'playlists'].includes(this.props.type) ? (
<li className="media-search"> <li className="media-search">
<ProfileSearchBar onQueryChange={this.props.onQueryChange} toggleSearchField={this.onToggleSearchField} type={this.props.type} /> <ProfileSearchBar
onQueryChange={this.props.onQueryChange}
toggleSearchField={this.onToggleSearchField}
type={this.props.type}
/>
</li> </li>
) : null} ) : null}
{this.props.onToggleFiltersClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? ( {this.props.onToggleFiltersClick &&
['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-filters-toggle"> <li className="media-filters-toggle">
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} onClick={this.props.onToggleFiltersClick} title={translateString('Filters')}> <span
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
position: 'relative',
}}
onClick={this.props.onToggleFiltersClick}
title={translateString('Filters')}
>
<CircleIconButton buttonShadow={false}> <CircleIconButton buttonShadow={false}>
<i className="material-icons">filter_list</i> <i className="material-icons">filter_list</i>
</CircleIconButton> </CircleIconButton>
{this.props.hasActiveFilters ? ( {this.props.hasActiveFilters ? (
<span style={{ <span
style={{
position: 'absolute', position: 'absolute',
top: '8px', top: '8px',
right: '8px', right: '8px',
@@ -446,19 +485,64 @@ class NavMenuInlineTabs extends React.PureComponent {
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)', backgroundColor: 'var(--default-theme-color)',
border: '2px solid white', border: '2px solid white',
}}></span> }}
></span>
) : null} ) : null}
</span> </span>
</li> </li>
) : null} ) : null}
{this.props.onToggleTagsClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? ( {this.props.onToggleSharingClick &&
['media', 'shared_by_me'].includes(this.props.type) ? (
<li className="media-sharing-toggle">
<span
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
position: 'relative',
}}
onClick={this.props.onToggleSharingClick}
title={translateString('Shared with')}
>
<CircleIconButton buttonShadow={false}>
<i className="material-icons">people</i>
</CircleIconButton>
{this.props.hasActiveSharing ? (
<span
style={{
position: 'absolute',
top: '8px',
right: '8px',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)',
border: '2px solid white',
}}
></span>
) : null}
</span>
</li>
) : null}
{this.props.onToggleTagsClick &&
['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-tags-toggle"> <li className="media-tags-toggle">
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} onClick={this.props.onToggleTagsClick} title={translateString('Tags')}> <span
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
position: 'relative',
}}
onClick={this.props.onToggleTagsClick}
title={translateString('Tags')}
>
<CircleIconButton buttonShadow={false}> <CircleIconButton buttonShadow={false}>
<i className="material-icons">local_offer</i> <i className="material-icons">local_offer</i>
</CircleIconButton> </CircleIconButton>
{this.props.hasActiveTags ? ( {this.props.hasActiveTags ? (
<span style={{ <span
style={{
position: 'absolute', position: 'absolute',
top: '8px', top: '8px',
right: '8px', right: '8px',
@@ -467,19 +551,31 @@ class NavMenuInlineTabs extends React.PureComponent {
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)', backgroundColor: 'var(--default-theme-color)',
border: '2px solid white', border: '2px solid white',
}}></span> }}
></span>
) : null} ) : null}
</span> </span>
</li> </li>
) : null} ) : null}
{this.props.onToggleSortingClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? ( {this.props.onToggleSortingClick &&
['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-sorting-toggle"> <li className="media-sorting-toggle">
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} onClick={this.props.onToggleSortingClick} title={translateString('Sort By')}> <span
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
position: 'relative',
}}
onClick={this.props.onToggleSortingClick}
title={translateString('Sort By')}
>
<CircleIconButton buttonShadow={false}> <CircleIconButton buttonShadow={false}>
<i className="material-icons">swap_vert</i> <i className="material-icons">swap_vert</i>
</CircleIconButton> </CircleIconButton>
{this.props.hasActiveSort ? ( {this.props.hasActiveSort ? (
<span style={{ <span
style={{
position: 'absolute', position: 'absolute',
top: '8px', top: '8px',
right: '8px', right: '8px',
@@ -488,7 +584,8 @@ class NavMenuInlineTabs extends React.PureComponent {
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)', backgroundColor: 'var(--default-theme-color)',
border: '2px solid white', border: '2px solid white',
}}></span> }}
></span>
) : null} ) : null}
</span> </span>
</li> </li>
@@ -506,9 +603,11 @@ NavMenuInlineTabs.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
onQueryChange: PropTypes.func, onQueryChange: PropTypes.func,
onToggleFiltersClick: PropTypes.func, onToggleFiltersClick: PropTypes.func,
onToggleSharingClick: PropTypes.func,
onToggleTagsClick: PropTypes.func, onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func, onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool, hasActiveFilters: PropTypes.bool,
hasActiveSharing: PropTypes.bool,
hasActiveTags: PropTypes.bool, hasActiveTags: PropTypes.bool,
hasActiveSort: PropTypes.bool, hasActiveSort: PropTypes.bool,
}; };
@@ -559,12 +658,6 @@ export default function ProfilePagesHeader(props) {
const profilePageHeaderRef = useRef(null); const profilePageHeaderRef = useRef(null);
const profileNavRef = 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 userIsAdmin = !MemberContext._currentValue.is.anonymous && MemberContext._currentValue.is.admin;
const userIsAuthor = const userIsAuthor =
!MemberContext._currentValue.is.anonymous && !MemberContext._currentValue.is.anonymous &&
@@ -576,18 +669,6 @@ export default function ProfilePagesHeader(props) {
userIsAuthor || userIsAuthor ||
(!MemberContext._currentValue.is.anonymous && MemberContext._currentValue.can.deleteProfile); (!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() { function cancelProfileRemoval() {
popupContentRef.current.toggle(); popupContentRef.current.toggle();
} }
@@ -622,42 +703,27 @@ export default function ProfilePagesHeader(props) {
} }
} }
function onWindowResize() {
updateProfileNavTopPosition();
updateFixedNavPosition();
}
function onWindowScroll() {
updateFixedNavPosition();
}
useEffect(() => { useEffect(() => {
if (userCanDeleteProfile) { if (userCanDeleteProfile) {
ProfilePageStore.on('profile_delete', onProfileDelete); ProfilePageStore.on('profile_delete', onProfileDelete);
ProfilePageStore.on('profile_delete_fail', onProfileDeleteFail); 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 () => { return () => {
if (userCanDeleteProfile) { if (userCanDeleteProfile) {
ProfilePageStore.removeListener('profile_delete', onProfileDelete); ProfilePageStore.removeListener('profile_delete', onProfileDelete);
ProfilePageStore.removeListener('profile_delete_fail', onProfileDeleteFail); ProfilePageStore.removeListener('profile_delete_fail', onProfileDeleteFail);
} }
PageStore.removeListener('resize', onWindowResize);
PageStore.removeListener('changed_page_sidebar_visibility', onWindowResize);
PageStore.removeListener('window_scroll', onWindowScroll);
}; };
}, []); }, []);
return ( return (
<div ref={profilePageHeaderRef} className={'profile-page-header' + (fixedNav ? ' fixed-nav' : '')}> <div
ref={profilePageHeaderRef}
className={'profile-page-header'}
{...(isSelectMediaMode() ? { 'data-action': 'select_media' } : {})}
>
{!props.hideChannelBanner && (
<span className="profile-banner-wrap"> <span className="profile-banner-wrap">
{props.author.banner_thumbnail_url ? ( {props.author.banner_thumbnail_url ? (
<span <span
@@ -685,14 +751,22 @@ export default function ProfilePagesHeader(props) {
<PopupMain> <PopupMain>
<div className="popup-message"> <div className="popup-message">
<span className="popup-message-title">Profile removal</span> <span className="popup-message-title">Profile removal</span>
<span className="popup-message-main">You're willing to remove profile permanently?</span> <span className="popup-message-main">
You're willing to remove profile permanently?
</span>
</div> </div>
<hr /> <hr />
<span className="popup-message-bottom"> <span className="popup-message-bottom">
<button className="button-link cancel-profile-removal" onClick={cancelProfileRemoval}> <button
className="button-link cancel-profile-removal"
onClick={cancelProfileRemoval}
>
CANCEL CANCEL
</button> </button>
<button className="button-link proceed-profile-removal" onClick={proceedMediaRemoval}> <button
className="button-link proceed-profile-removal"
onClick={proceedMediaRemoval}
>
PROCEED PROCEED
</button> </button>
</span> </span>
@@ -709,17 +783,22 @@ export default function ProfilePagesHeader(props) {
) )
) : null} ) : null}
</span> </span>
)}
<div className="profile-info-nav-wrap"> <div className="profile-info-nav-wrap">
{props.author.thumbnail_url || props.author.name ? ( {!inEmbeddedApp() && (props.author.thumbnail_url || props.author.name) ? (
<div className="profile-info"> <div className="profile-info">
<div className="profile-info-inner"> <div className="profile-info-inner">
<div>{props.author.thumbnail_url ? <img src={props.author.thumbnail_url} alt="" /> : null}</div> <div>
{props.author.thumbnail_url ? <img src={props.author.thumbnail_url} alt="" /> : null}
</div>
<div> <div>
{props.author.name ? ( {props.author.name ? (
<div className="profile-name-edit-wrapper"> <div className="profile-name-edit-wrapper">
<h1>{props.author.name}</h1> <h1>{props.author.name}</h1>
{userCanEditProfile && !userIsAuthor ? <EditProfileButton link={ProfilePageStore.get('author-data').edit_url} /> : null} {userCanEditProfile && !userIsAuthor ? (
<EditProfileButton link={ProfilePageStore.get('author-data').edit_url} />
) : null}
</div> </div>
) : null} ) : null}
</div> </div>
@@ -732,9 +811,11 @@ export default function ProfilePagesHeader(props) {
type={props.type} type={props.type}
onQueryChange={props.onQueryChange} onQueryChange={props.onQueryChange}
onToggleFiltersClick={props.onToggleFiltersClick} onToggleFiltersClick={props.onToggleFiltersClick}
onToggleSharingClick={userIsAuthor ? props.onToggleSharingClick : undefined}
onToggleTagsClick={props.onToggleTagsClick} onToggleTagsClick={props.onToggleTagsClick}
onToggleSortingClick={props.onToggleSortingClick} onToggleSortingClick={props.onToggleSortingClick}
hasActiveFilters={props.hasActiveFilters} hasActiveFilters={props.hasActiveFilters}
hasActiveSharing={props.hasActiveSharing}
hasActiveTags={props.hasActiveTags} hasActiveTags={props.hasActiveTags}
hasActiveSort={props.hasActiveSort} hasActiveSort={props.hasActiveSort}
/> />
@@ -748,9 +829,11 @@ ProfilePagesHeader.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
onQueryChange: PropTypes.func, onQueryChange: PropTypes.func,
onToggleFiltersClick: PropTypes.func, onToggleFiltersClick: PropTypes.func,
onToggleSharingClick: PropTypes.func,
onToggleTagsClick: PropTypes.func, onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func, onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool, hasActiveFilters: PropTypes.bool,
hasActiveSharing: PropTypes.bool,
hasActiveTags: PropTypes.bool, hasActiveTags: PropTypes.bool,
hasActiveSort: PropTypes.bool, hasActiveSort: PropTypes.bool,
}; };

View File

@@ -0,0 +1,101 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { PageStore } from '../../utils/stores/';
import { FilterOptions } from '../_shared';
import { translateString } from '../../utils/helpers/';
import '../management-table/ManageItemList-filters.scss';
export function ProfileMediaSharing(props) {
const [isHidden, setIsHidden] = useState(props.hidden);
const containerRef = useRef(null);
const innerContainerRef = useRef(null);
function onWindowResize() {
if (!isHidden && containerRef.current && innerContainerRef.current) {
containerRef.current.style.height = 24 + innerContainerRef.current.offsetHeight + 'px';
}
}
useEffect(() => {
setIsHidden(props.hidden);
onWindowResize();
}, [props.hidden, props.sharedUsers, props.sharedGroups]);
useEffect(() => {
PageStore.on('window_resize', onWindowResize);
return () => PageStore.removeListener('window_resize', onWindowResize);
}, []);
function onUserSelect(ev) {
const username = ev.currentTarget.getAttribute('value');
const newValue = (username === 'all' || username === props.selectedSharingValue) ? null : username;
props.onSharingSelect(newValue ? 'user' : null, newValue);
}
function onGroupSelect(ev) {
const name = ev.currentTarget.getAttribute('value');
const newValue = (name === 'all' || name === props.selectedSharingValue) ? null : name;
props.onSharingSelect(newValue ? 'group' : null, newValue);
}
const hasUsers = props.sharedUsers && props.sharedUsers.length > 0;
const hasGroups = props.sharedGroups && props.sharedGroups.length > 0;
const usersOptions = [
{ id: 'all', title: translateString('All') },
...(props.sharedUsers || []).map((u) => ({ id: u.username, title: u.name })),
];
const groupsOptions = [
{ id: 'all', title: translateString('All') },
...(props.sharedGroups || []).map((g) => ({ id: g.name, title: g.name })),
];
const selectedUser = props.selectedSharingType === 'user' ? props.selectedSharingValue : 'all';
const selectedGroup = props.selectedSharingType === 'group' ? props.selectedSharingValue : 'all';
return (
<div ref={containerRef} className={'mi-filters-row' + (isHidden ? ' hidden' : '')}>
<div ref={innerContainerRef} className="mi-filters-row-inner">
{hasUsers ? (
<div className="mi-filter mi-filter-full-width">
<div className="mi-filter-title">{translateString('SHARED WITH USERS')}</div>
<div className="mi-filter-options mi-filter-options-horizontal mi-sharing-filter-options">
<FilterOptions id="shared_user" options={usersOptions} selected={selectedUser} onSelect={onUserSelect} />
</div>
</div>
) : null}
{hasGroups ? (
<div className="mi-filter mi-filter-full-width">
<div className="mi-filter-title">{translateString('SHARED WITH GROUPS')}</div>
<div className="mi-filter-options mi-filter-options-horizontal mi-sharing-filter-options">
<FilterOptions id="shared_group" options={groupsOptions} selected={selectedGroup} onSelect={onGroupSelect} />
</div>
</div>
) : null}
{!hasUsers && !hasGroups ? (
<div className="mi-filter mi-filter-full-width">
<div className="mi-filter-title">{translateString('NOT SHARED WITH ANYONE')}</div>
</div>
) : null}
</div>
</div>
);
}
ProfileMediaSharing.propTypes = {
hidden: PropTypes.bool,
sharedUsers: PropTypes.array,
sharedGroups: PropTypes.array,
onSharingSelect: PropTypes.func,
selectedSharingType: PropTypes.string,
selectedSharingValue: PropTypes.string,
};
ProfileMediaSharing.defaultProps = {
hidden: false,
sharedUsers: [],
sharedGroups: [],
selectedSharingType: null,
selectedSharingValue: null,
};

View File

@@ -3,14 +3,14 @@ import { ApiUrlConsumer } from '../utils/contexts/';
import { MediaListWrapper } from '../components/MediaListWrapper'; import { MediaListWrapper } from '../components/MediaListWrapper';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx'; import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
import { Page } from './Page'; import { Page } from './Page';
import { translateString } from '../utils/helpers/'; import { translateString, inEmbeddedApp } from '../utils/helpers/';
interface CategoriesPageProps { interface CategoriesPageProps {
id?: string; id?: string;
title?: string; title?: string;
} }
export const CategoriesPage: React.FC<CategoriesPageProps> = ({ id = 'categories', title = translateString('Categories') }) => ( export const CategoriesPage: React.FC<CategoriesPageProps> = ({ id = 'categories', title = inEmbeddedApp() ? translateString('Courses') : translateString('Categories') }) => (
<Page id={id}> <Page id={id}>
<ApiUrlConsumer> <ApiUrlConsumer>
{(apiUrl) => ( {(apiUrl) => (

View File

@@ -41,7 +41,7 @@ export const EmbedPage: React.FC = () => {
}, []); }, []);
return ( return (
<div className="embed-wrap" style={wrapperStyles}> <div className="embed-wrap media-embed-wrap" style={wrapperStyles}>
{failedMediaLoad && ( {failedMediaLoad && (
<div className="player-container player-container-error" style={containerStyles}> <div className="player-container player-container-error" style={containerStyles}>
<div className="player-container-inner" style={containerStyles}> <div className="player-container-inner" style={containerStyles}>
@@ -59,9 +59,32 @@ export const EmbedPage: React.FC = () => {
{loadedVideo && ( {loadedVideo && (
<SiteConsumer> <SiteConsumer>
{(site) => ( {(site) => {
<VideoViewer data={MediaPageStore.get('media-data')} siteUrl={site.url} containerStyles={containerStyles} /> const urlParams = new URLSearchParams(window.location.search);
)} const urlShowTitle = urlParams.get('showTitle');
const showTitle = urlShowTitle !== '0';
const urlShowRelated = urlParams.get('showRelated');
const showRelated = urlShowRelated !== '0';
const urlShowUserAvatar = urlParams.get('showUserAvatar');
const showUserAvatar = urlShowUserAvatar !== '0';
const urlLinkTitle = urlParams.get('linkTitle');
const linkTitle = urlLinkTitle !== '0';
const urlTimestamp = urlParams.get('t');
const timestamp = urlTimestamp ? parseInt(urlTimestamp, 10) : null;
return (
<VideoViewer
data={MediaPageStore.get('media-data')}
siteUrl={site.url}
containerStyles={containerStyles}
showTitle={showTitle}
showRelated={showRelated}
showUserAvatar={showUserAvatar}
linkTitle={linkTitle}
timestamp={timestamp}
/>
);
}}
</SiteConsumer> </SiteConsumer>
)} )}
</div> </div>

View File

@@ -2,13 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import UrlParse from 'url-parse'; import UrlParse from 'url-parse';
import { ApiUrlContext, MemberContext, SiteContext } from '../utils/contexts/'; import { ApiUrlContext, MemberContext, SiteContext } from '../utils/contexts/';
import { formatInnerLink, csrfToken, postRequest } from '../utils/helpers/'; import { formatInnerLink, csrfToken, postRequest, inEmbeddedApp } from '../utils/helpers/';
import { PageActions } from '../utils/actions/'; import { PageActions } from '../utils/actions/';
import { PageStore, ProfilePageStore } from '../utils/stores/'; import { PageStore, ProfilePageStore } from '../utils/stores/';
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader'; import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent'; import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
import { MediaListRow } from '../components/MediaListRow'; import { MediaListRow } from '../components/MediaListRow';
import { ProfileMediaPage } from './ProfileMediaPage'; import { ProfileMediaPageBase } from './ProfileMediaPage';
class ChannelContactForm extends React.PureComponent { class ChannelContactForm extends React.PureComponent {
constructor(props) { constructor(props) {
@@ -149,7 +149,7 @@ class ChannelContactForm extends React.PureComponent {
} }
} }
export class ProfileAboutPage extends ProfileMediaPage { export class ProfileAboutPage extends ProfileMediaPageBase {
constructor(props) { constructor(props) {
super(props, 'author-about'); super(props, 'author-about');
@@ -268,7 +268,7 @@ export class ProfileAboutPage extends ProfileMediaPage {
return [ return [
this.state.author ? ( this.state.author ? (
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="about" /> <ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="about" hideChannelBanner={inEmbeddedApp()} />
) : null, ) : null,
this.state.author ? ( this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent" enabledContactForm={this.enabledContactForm}> <ProfilePagesContent key="ProfilePagesContent" enabledContactForm={this.enabledContactForm}>

View File

@@ -2,13 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ApiUrlConsumer } from '../utils/contexts/'; import { ApiUrlConsumer } from '../utils/contexts/';
import { PageStore } from '../utils/stores/'; import { PageStore } from '../utils/stores/';
import { inEmbeddedApp } from '../utils/helpers/';
import { MediaListWrapper } from '../components/MediaListWrapper'; import { MediaListWrapper } from '../components/MediaListWrapper';
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader'; import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent'; import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx'; import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
import { ProfileMediaPage } from './ProfileMediaPage'; import { ProfileMediaPageBase } from './ProfileMediaPage';
export class ProfileHistoryPage extends ProfileMediaPage { export class ProfileHistoryPage extends ProfileMediaPageBase {
constructor(props) { constructor(props) {
super(props, 'author-history'); super(props, 'author-history');
@@ -28,7 +29,7 @@ export class ProfileHistoryPage extends ProfileMediaPage {
pageContent() { pageContent() {
return [ return [
this.state.author ? ( this.state.author ? (
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="history" /> <ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="history" hideChannelBanner={inEmbeddedApp()} />
) : null, ) : null,
this.state.author ? ( this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent"> <ProfilePagesContent key="ProfilePagesContent">

View File

@@ -2,13 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ApiUrlConsumer } from '../utils/contexts/'; import { ApiUrlConsumer } from '../utils/contexts/';
import { PageStore } from '../utils/stores/'; import { PageStore } from '../utils/stores/';
import { inEmbeddedApp } from '../utils/helpers/';
import { MediaListWrapper } from '../components/MediaListWrapper'; import { MediaListWrapper } from '../components/MediaListWrapper';
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader'; import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent'; import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync'; import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync';
import { ProfileMediaPage } from './ProfileMediaPage'; import { ProfileMediaPageBase } from './ProfileMediaPage';
export class ProfileLikedPage extends ProfileMediaPage { export class ProfileLikedPage extends ProfileMediaPageBase {
constructor(props) { constructor(props) {
super(props, 'author-liked'); super(props, 'author-liked');
@@ -28,7 +29,7 @@ export class ProfileLikedPage extends ProfileMediaPage {
pageContent() { pageContent() {
return [ return [
this.state.author ? ( this.state.author ? (
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="liked" /> <ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="liked" hideChannelBanner={inEmbeddedApp()} />
) : null, ) : null,
this.state.author ? ( this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent"> <ProfilePagesContent key="ProfilePagesContent">

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import { ApiUrlConsumer } from '../utils/contexts/'; import { ApiUrlConsumer } from '../utils/contexts/';
import { PageStore } from '../utils/stores/'; import { PageStore } from '../utils/stores/';
import { inEmbeddedApp } from '../utils/helpers/';
import { MediaListWrapper } from '../components/MediaListWrapper'; import { MediaListWrapper } from '../components/MediaListWrapper';
import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader'; import ProfilePagesHeader from '../components/profile-page/ProfilePagesHeader';
import ProfilePagesContent from '../components/profile-page/ProfilePagesContent'; import ProfilePagesContent from '../components/profile-page/ProfilePagesContent';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx'; import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
import { ProfileMediaPage } from './ProfileMediaPage'; import { ProfileMediaPageBase } from './ProfileMediaPage';
export class ProfilePlaylistsPage extends ProfileMediaPage { export class ProfilePlaylistsPage extends ProfileMediaPageBase {
constructor(props) { constructor(props) {
super(props, 'author-playlists'); super(props, 'author-playlists');
@@ -30,7 +31,7 @@ export class ProfilePlaylistsPage extends ProfileMediaPage {
pageContent() { pageContent() {
return [ return [
this.state.author ? ( this.state.author ? (
<ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="playlists" /> <ProfilePagesHeader key="ProfilePagesHeader" author={this.state.author} type="playlists" hideChannelBanner={inEmbeddedApp()} />
) : null, ) : null,
this.state.author ? ( this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent"> <ProfilePagesContent key="ProfilePagesContent">

View File

@@ -9,9 +9,10 @@ import ProfilePagesContent from '../components/profile-page/ProfilePagesContent'
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync'; import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync';
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters'; import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags'; import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
import { ProfileMediaSharing } from '../components/search-filters/ProfileMediaSharing';
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting'; import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
import { BulkActionsModals } from '../components/BulkActionsModals'; import { BulkActionsModals } from '../components/BulkActionsModals';
import { translateString } from '../utils/helpers'; import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
import { withBulkActions } from '../utils/hoc/withBulkActions'; import { withBulkActions } from '../utils/hoc/withBulkActions';
import { Page } from './_Page'; import { Page } from './_Page';
@@ -24,9 +25,7 @@ function EmptySharedByMe(props) {
{(links) => ( {(links) => (
<div className="empty-media empty-channel-media"> <div className="empty-media empty-channel-media">
<div className="welcome-title">No shared media</div> <div className="welcome-title">No shared media</div>
<div className="start-uploading"> <div className="start-uploading">Media that you have shared with others will show up here.</div>
Media that you have shared with others will show up here.
</div>
</div> </div>
)} )}
</LinksConsumer> </LinksConsumer>
@@ -49,10 +48,16 @@ class ProfileSharedByMePage extends Page {
hiddenFilters: true, hiddenFilters: true,
hiddenTags: true, hiddenTags: true,
hiddenSorting: true, hiddenSorting: true,
hiddenSharing: true,
filterArgs: '', filterArgs: '',
availableTags: [], availableTags: [],
selectedTag: 'all', selectedTag: 'all',
selectedSort: 'date_added_desc', selectedSort: 'date_added_desc',
selectedMedia: new Set(), // For select media mode
sharedUsers: [],
sharedGroups: [],
selectedSharingType: null,
selectedSharingValue: null,
}; };
this.authorDataLoad = this.authorDataLoad.bind(this); this.authorDataLoad = this.authorDataLoad.bind(this);
@@ -62,10 +67,13 @@ class ProfileSharedByMePage extends Page {
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this); this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
this.onToggleTagsClick = this.onToggleTagsClick.bind(this); this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
this.onToggleSortingClick = this.onToggleSortingClick.bind(this); this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
this.onToggleSharingClick = this.onToggleSharingClick.bind(this);
this.onFiltersUpdate = this.onFiltersUpdate.bind(this); this.onFiltersUpdate = this.onFiltersUpdate.bind(this);
this.onTagSelect = this.onTagSelect.bind(this); this.onTagSelect = this.onTagSelect.bind(this);
this.onSortSelect = this.onSortSelect.bind(this); this.onSortSelect = this.onSortSelect.bind(this);
this.onSharingSelect = this.onSharingSelect.bind(this);
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this); this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
this.handleMediaSelection = this.handleMediaSelection.bind(this);
ProfilePageStore.on('load-author-data', this.authorDataLoad); ProfilePageStore.on('load-author-data', this.authorDataLoad);
} }
@@ -81,9 +89,20 @@ class ProfileSharedByMePage extends Page {
if (author) { if (author) {
if (this.state.query) { if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_by_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
author.id +
'&show=shared_by_me&q=' +
encodeURIComponent(this.state.query) +
this.state.filterArgs;
} else { } else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_by_me' + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
author.id +
'&show=shared_by_me' +
this.state.filterArgs;
} }
} }
@@ -132,9 +151,20 @@ class ProfileSharedByMePage extends Page {
let requestUrl; let requestUrl;
if (newQuery) { if (newQuery) {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me&q=' + encodeURIComponent(newQuery) + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
this.state.author.id +
'&show=shared_by_me&q=' +
encodeURIComponent(newQuery) +
this.state.filterArgs;
} else { } else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me' + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
this.state.author.id +
'&show=shared_by_me' +
this.state.filterArgs;
} }
let title = this.state.title; let title = this.state.title;
@@ -155,6 +185,7 @@ class ProfileSharedByMePage extends Page {
hiddenFilters: !this.state.hiddenFilters, hiddenFilters: !this.state.hiddenFilters,
hiddenTags: true, hiddenTags: true,
hiddenSorting: true, hiddenSorting: true,
hiddenSharing: true,
}); });
} }
@@ -163,6 +194,7 @@ class ProfileSharedByMePage extends Page {
hiddenFilters: true, hiddenFilters: true,
hiddenTags: !this.state.hiddenTags, hiddenTags: !this.state.hiddenTags,
hiddenSorting: true, hiddenSorting: true,
hiddenSharing: true,
}); });
} }
@@ -171,6 +203,16 @@ class ProfileSharedByMePage extends Page {
hiddenFilters: true, hiddenFilters: true,
hiddenTags: true, hiddenTags: true,
hiddenSorting: !this.state.hiddenSorting, hiddenSorting: !this.state.hiddenSorting,
hiddenSharing: true,
});
}
onToggleSharingClick() {
this.setState({
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: true,
hiddenSharing: !this.state.hiddenSharing,
}); });
} }
@@ -183,6 +225,8 @@ class ProfileSharedByMePage extends Page {
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1], publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: this.state.selectedSort, sort_by: this.state.selectedSort,
tag: tag, tag: tag,
sharing_type: this.state.selectedSharingType,
sharing_value: this.state.selectedSharingValue,
}); });
}); });
} }
@@ -196,6 +240,23 @@ class ProfileSharedByMePage extends Page {
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1], publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: sortBy, sort_by: sortBy,
tag: this.state.selectedTag, tag: this.state.selectedTag,
sharing_type: this.state.selectedSharingType,
sharing_value: this.state.selectedSharingValue,
});
});
}
onSharingSelect(type, value) {
this.setState({ selectedSharingType: type, selectedSharingValue: value }, () => {
this.onFiltersUpdate({
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: this.state.selectedSort,
tag: this.state.selectedTag,
sharing_type: type,
sharing_value: value,
}); });
}); });
} }
@@ -209,6 +270,8 @@ class ProfileSharedByMePage extends Page {
sort_by: null, sort_by: null,
ordering: null, ordering: null,
t: null, t: null,
shared_user: null,
shared_group: null,
}; };
switch (updatedArgs.media_type) { switch (updatedArgs.media_type) {
@@ -270,6 +333,12 @@ class ProfileSharedByMePage extends Page {
args.t = updatedArgs.tag; args.t = updatedArgs.tag;
} }
if (updatedArgs.sharing_type === 'user' && updatedArgs.sharing_value) {
args.shared_user = updatedArgs.sharing_value;
} else if (updatedArgs.sharing_type === 'group' && updatedArgs.sharing_value) {
args.shared_group = updatedArgs.sharing_value;
}
const newArgs = []; const newArgs = [];
for (let arg in args) { for (let arg in args) {
@@ -290,9 +359,20 @@ class ProfileSharedByMePage extends Page {
let requestUrl; let requestUrl;
if (this.state.query) { if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
this.state.author.id +
'&show=shared_by_me&q=' +
encodeURIComponent(this.state.query) +
this.state.filterArgs;
} else { } else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_by_me' + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
this.state.author.id +
'&show=shared_by_me' +
this.state.filterArgs;
} }
this.setState({ this.setState({
@@ -304,23 +384,77 @@ class ProfileSharedByMePage extends Page {
onResponseDataLoaded(responseData) { onResponseDataLoaded(responseData) {
if (responseData && responseData.tags) { if (responseData && responseData.tags) {
const tags = responseData.tags.split(',').map((tag) => tag.trim()).filter((tag) => tag); const tags = responseData.tags
.split(',')
.map((tag) => tag.trim())
.filter((tag) => tag);
this.setState({ availableTags: tags }); this.setState({ availableTags: tags });
} }
if (responseData && responseData.shared_users !== undefined) {
this.setState({
sharedUsers: responseData.shared_users || [],
sharedGroups: responseData.shared_groups || [],
});
}
}
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() { pageContent() {
const authorData = ProfilePageStore.get('author-data'); const authorData = ProfilePageStore.get('author-data');
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username; const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
const isSelectMediaMode = inSelectMediaEmbedMode();
// Check if any filters are active // Check if any filters are active
const hasActiveFilters = this.state.filterArgs && ( const hasActiveFilters =
this.state.filterArgs.includes('media_type=') || this.state.filterArgs &&
(this.state.filterArgs.includes('media_type=') ||
this.state.filterArgs.includes('upload_date=') || this.state.filterArgs.includes('upload_date=') ||
this.state.filterArgs.includes('duration=') || this.state.filterArgs.includes('duration=') ||
this.state.filterArgs.includes('publish_state=') this.state.filterArgs.includes('publish_state='));
);
return [ return [
this.state.author ? ( this.state.author ? (
@@ -332,40 +466,60 @@ class ProfileSharedByMePage extends Page {
onToggleFiltersClick={this.onToggleFiltersClick} onToggleFiltersClick={this.onToggleFiltersClick}
onToggleTagsClick={this.onToggleTagsClick} onToggleTagsClick={this.onToggleTagsClick}
onToggleSortingClick={this.onToggleSortingClick} onToggleSortingClick={this.onToggleSortingClick}
onToggleSharingClick={this.onToggleSharingClick}
hasActiveFilters={hasActiveFilters} hasActiveFilters={hasActiveFilters}
hasActiveTags={this.state.selectedTag !== 'all'} hasActiveTags={this.state.selectedTag !== 'all'}
hasActiveSort={this.state.selectedSort !== 'date_added_desc'} hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
hasActiveSharing={!!this.state.selectedSharingValue}
hideChannelBanner={inEmbeddedApp()}
/> />
) : null, ) : null,
this.state.author ? ( this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent"> <ProfilePagesContent key="ProfilePagesContent">
<MediaListWrapper <MediaListWrapper
title={this.state.title} title={inEmbeddedApp() ? undefined : this.state.title}
className="items-list-ver" className="items-list-ver"
showBulkActions={isMediaAuthor} style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
selectedCount={this.props.bulkActions.selectedMedia.size} showBulkActions={!isSelectMediaMode && isMediaAuthor}
totalCount={this.props.bulkActions.availableMediaIds.length} selectedCount={isSelectMediaMode ? this.state.selectedMedia.size : this.props.bulkActions.selectedMedia.size}
totalCount={isSelectMediaMode ? 0 : this.props.bulkActions.availableMediaIds.length}
onBulkAction={this.props.bulkActions.handleBulkAction} onBulkAction={this.props.bulkActions.handleBulkAction}
onSelectAll={this.props.bulkActions.handleSelectAll} onSelectAll={this.props.bulkActions.handleSelectAll}
onDeselectAll={this.props.bulkActions.handleDeselectAll} onDeselectAll={this.props.bulkActions.handleDeselectAll}
> >
<ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} /> <ProfileMediaFilters
<ProfileMediaTags hidden={this.state.hiddenTags} tags={this.state.availableTags} onTagSelect={this.onTagSelect} /> hidden={this.state.hiddenFilters}
tags={this.state.availableTags}
onFiltersUpdate={this.onFiltersUpdate}
/>
<ProfileMediaTags
hidden={this.state.hiddenTags}
tags={this.state.availableTags}
onTagSelect={this.onTagSelect}
/>
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} /> <ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<ProfileMediaSharing
hidden={this.state.hiddenSharing}
sharedUsers={this.state.sharedUsers}
sharedGroups={this.state.sharedGroups}
onSharingSelect={this.onSharingSelect}
selectedSharingType={this.state.selectedSharingType}
selectedSharingValue={this.state.selectedSharingValue}
/>
<LazyLoadItemListAsync <LazyLoadItemListAsync
key={`${this.state.requestUrl}-${this.props.bulkActions.listKey}`} key={isSelectMediaMode ? this.state.requestUrl : `${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
requestUrl={this.state.requestUrl} requestUrl={this.state.requestUrl}
hideAuthor={true} hideAuthor={true}
itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null} itemsCountCallback={this.state.requestUrl ? this.getCountFunc : null}
hideViews={!PageStore.get('config-media-item').displayViews} hideViews={!PageStore.get('config-media-item').displayViews}
hideDate={!PageStore.get('config-media-item').displayPublishDate} hideDate={!PageStore.get('config-media-item').displayPublishDate}
canEdit={isMediaAuthor} canEdit={!isSelectMediaMode && isMediaAuthor}
onResponseDataLoaded={this.onResponseDataLoaded} onResponseDataLoaded={this.onResponseDataLoaded}
showSelection={isMediaAuthor} showSelection={isMediaAuthor || isSelectMediaMode}
hasAnySelection={this.props.bulkActions.selectedMedia.size > 0} hasAnySelection={isSelectMediaMode ? this.state.selectedMedia.size > 0 : this.props.bulkActions.selectedMedia.size > 0}
selectedMedia={this.props.bulkActions.selectedMedia} selectedMedia={isSelectMediaMode ? this.state.selectedMedia : this.props.bulkActions.selectedMedia}
onMediaSelection={this.props.bulkActions.handleMediaSelection} onMediaSelection={isSelectMediaMode ? this.handleMediaSelection : this.props.bulkActions.handleMediaSelection}
onItemsUpdate={this.props.bulkActions.handleItemsUpdate} onItemsUpdate={!isSelectMediaMode ? this.props.bulkActions.handleItemsUpdate : undefined}
/> />
{isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? ( {isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
<EmptySharedByMe name={this.state.author.name} /> <EmptySharedByMe name={this.state.author.name} />
@@ -373,7 +527,7 @@ class ProfileSharedByMePage extends Page {
</MediaListWrapper> </MediaListWrapper>
</ProfilePagesContent> </ProfilePagesContent>
) : null, ) : null,
this.state.author && isMediaAuthor ? ( this.state.author && isMediaAuthor && !isSelectMediaMode ? (
<BulkActionsModals <BulkActionsModals
key="BulkActionsModals" key="BulkActionsModals"
{...this.props.bulkActions} {...this.props.bulkActions}

View File

@@ -10,7 +10,7 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters'; import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags'; import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting'; import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
import { translateString } from '../utils/helpers'; import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
import { Page } from './_Page'; import { Page } from './_Page';
@@ -22,9 +22,7 @@ function EmptySharedWithMe(props) {
{(links) => ( {(links) => (
<div className="empty-media empty-channel-media"> <div className="empty-media empty-channel-media">
<div className="welcome-title">No shared media</div> <div className="welcome-title">No shared media</div>
<div className="start-uploading"> <div className="start-uploading">Media that others have shared with you will show up here.</div>
Media that others have shared with you will show up here.
</div>
</div> </div>
)} )}
</LinksConsumer> </LinksConsumer>
@@ -51,6 +49,7 @@ export class ProfileSharedWithMePage extends Page {
availableTags: [], availableTags: [],
selectedTag: 'all', selectedTag: 'all',
selectedSort: 'date_added_desc', selectedSort: 'date_added_desc',
selectedMedia: new Set(), // For select media mode
}; };
this.authorDataLoad = this.authorDataLoad.bind(this); this.authorDataLoad = this.authorDataLoad.bind(this);
@@ -64,6 +63,7 @@ export class ProfileSharedWithMePage extends Page {
this.onTagSelect = this.onTagSelect.bind(this); this.onTagSelect = this.onTagSelect.bind(this);
this.onSortSelect = this.onSortSelect.bind(this); this.onSortSelect = this.onSortSelect.bind(this);
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this); this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
this.handleMediaSelection = this.handleMediaSelection.bind(this);
ProfilePageStore.on('load-author-data', this.authorDataLoad); ProfilePageStore.on('load-author-data', this.authorDataLoad);
} }
@@ -79,9 +79,20 @@ export class ProfileSharedWithMePage extends Page {
if (author) { if (author) {
if (this.state.query) { if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_with_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
author.id +
'&show=shared_with_me&q=' +
encodeURIComponent(this.state.query) +
this.state.filterArgs;
} else { } else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + author.id + '&show=shared_with_me' + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
author.id +
'&show=shared_with_me' +
this.state.filterArgs;
} }
} }
@@ -130,9 +141,20 @@ export class ProfileSharedWithMePage extends Page {
let requestUrl; let requestUrl;
if (newQuery) { if (newQuery) {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me&q=' + encodeURIComponent(newQuery) + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
this.state.author.id +
'&show=shared_with_me&q=' +
encodeURIComponent(newQuery) +
this.state.filterArgs;
} else { } else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me' + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
this.state.author.id +
'&show=shared_with_me' +
this.state.filterArgs;
} }
let title = this.state.title; let title = this.state.title;
@@ -288,9 +310,20 @@ export class ProfileSharedWithMePage extends Page {
let requestUrl; let requestUrl;
if (this.state.query) { if (this.state.query) {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me&q=' + encodeURIComponent(this.state.query) + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
this.state.author.id +
'&show=shared_with_me&q=' +
encodeURIComponent(this.state.query) +
this.state.filterArgs;
} else { } else {
requestUrl = ApiUrlContext._currentValue.media + '?author=' + this.state.author.id + '&show=shared_with_me' + this.state.filterArgs; requestUrl =
ApiUrlContext._currentValue.media +
'?author=' +
this.state.author.id +
'&show=shared_with_me' +
this.state.filterArgs;
} }
this.setState({ this.setState({
@@ -302,23 +335,71 @@ export class ProfileSharedWithMePage extends Page {
onResponseDataLoaded(responseData) { onResponseDataLoaded(responseData) {
if (responseData && responseData.tags) { if (responseData && responseData.tags) {
const tags = responseData.tags.split(',').map((tag) => tag.trim()).filter((tag) => tag); const tags = responseData.tags
.split(',')
.map((tag) => tag.trim())
.filter((tag) => tag);
this.setState({ availableTags: tags }); this.setState({ availableTags: tags });
} }
} }
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() { pageContent() {
const authorData = ProfilePageStore.get('author-data'); const authorData = ProfilePageStore.get('author-data');
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username; const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
const isSelectMediaMode = inSelectMediaEmbedMode();
// Check if any filters are active // Check if any filters are active
const hasActiveFilters = this.state.filterArgs && ( const hasActiveFilters =
this.state.filterArgs.includes('media_type=') || this.state.filterArgs &&
(this.state.filterArgs.includes('media_type=') ||
this.state.filterArgs.includes('upload_date=') || this.state.filterArgs.includes('upload_date=') ||
this.state.filterArgs.includes('duration=') || this.state.filterArgs.includes('duration=') ||
this.state.filterArgs.includes('publish_state=') this.state.filterArgs.includes('publish_state='));
);
return [ return [
this.state.author ? ( this.state.author ? (
@@ -333,16 +414,26 @@ export class ProfileSharedWithMePage extends Page {
hasActiveFilters={hasActiveFilters} hasActiveFilters={hasActiveFilters}
hasActiveTags={this.state.selectedTag !== 'all'} hasActiveTags={this.state.selectedTag !== 'all'}
hasActiveSort={this.state.selectedSort !== 'date_added_desc'} hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
hideChannelBanner={inEmbeddedApp()}
/> />
) : null, ) : null,
this.state.author ? ( this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent"> <ProfilePagesContent key="ProfilePagesContent">
<MediaListWrapper <MediaListWrapper
title={this.state.title} title={inEmbeddedApp() ? undefined : this.state.title}
className="items-list-ver" className="items-list-ver"
style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
> >
<ProfileMediaFilters hidden={this.state.hiddenFilters} tags={this.state.availableTags} onFiltersUpdate={this.onFiltersUpdate} /> <ProfileMediaFilters
<ProfileMediaTags hidden={this.state.hiddenTags} tags={this.state.availableTags} onTagSelect={this.onTagSelect} /> hidden={this.state.hiddenFilters}
tags={this.state.availableTags}
onFiltersUpdate={this.onFiltersUpdate}
/>
<ProfileMediaTags
hidden={this.state.hiddenTags}
tags={this.state.availableTags}
onTagSelect={this.onTagSelect}
/>
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} /> <ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<LazyLoadItemListAsync <LazyLoadItemListAsync
key={this.state.requestUrl} key={this.state.requestUrl}
@@ -353,6 +444,10 @@ export class ProfileSharedWithMePage extends Page {
hideDate={!PageStore.get('config-media-item').displayPublishDate} hideDate={!PageStore.get('config-media-item').displayPublishDate}
canEdit={false} canEdit={false}
onResponseDataLoaded={this.onResponseDataLoaded} 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 ? ( {isMediaAuthor && 0 === this.state.channelMediaCount && !this.state.query ? (
<EmptySharedWithMe name={this.state.author.name} /> <EmptySharedWithMe name={this.state.author.name} />

View File

@@ -7,7 +7,7 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
import { SearchMediaFiltersRow } from '../components/search-filters/SearchMediaFiltersRow'; import { SearchMediaFiltersRow } from '../components/search-filters/SearchMediaFiltersRow';
import { SearchResultsFilters } from '../components/search-filters/SearchResultsFilters'; import { SearchResultsFilters } from '../components/search-filters/SearchResultsFilters';
import { Page } from './_Page'; import { Page } from './_Page';
import { translateString } from '../utils/helpers/'; import { translateString, inEmbeddedApp } from '../utils/helpers/';
export class SearchPage extends Page { export class SearchPage extends Page {
constructor(props) { constructor(props) {
@@ -115,7 +115,7 @@ export class SearchPage extends Page {
} else { } else {
if (this.state.searchCategories) { if (this.state.searchCategories) {
title = null === this.state.resultsCount || 0 === this.state.resultsCount ? 'No' : this.state.resultsCount; title = null === this.state.resultsCount || 0 === this.state.resultsCount ? 'No' : this.state.resultsCount;
title += ' ' + translateString('media in category') + ' "' + this.state.searchCategories + '"'; title += ' ' + translateString(inEmbeddedApp() ? 'media in course' : 'media in category') + ' "' + this.state.searchCategories + '"';
} else if (this.state.searchTags) { } else if (this.state.searchTags) {
title = null === this.state.resultsCount || 0 === this.state.resultsCount ? 'No' : this.state.resultsCount; title = null === this.state.resultsCount || 0 === this.state.resultsCount ? 'No' : this.state.resultsCount;
title += ' ' + translateString('media in tag') + ' "' + this.state.searchTags + '"'; title += ' ' + translateString('media in tag') + ' "' + this.state.searchTags + '"';

View File

@@ -86,22 +86,18 @@ export class _MediaPage extends Page {
{!this.state.infoAndSidebarViewType {!this.state.infoAndSidebarViewType
? [ ? [
<ViewerInfo key="viewer-info" />, <ViewerInfo key="viewer-info" />,
this.state.pagePlaylistLoaded ? (
<ViewerSidebar <ViewerSidebar
key="viewer-sidebar" key="viewer-sidebar"
mediaId={MediaPageStore.get('media-id')} mediaId={MediaPageStore.get('media-id')}
playlistData={MediaPageStore.get('playlist-data')} playlistData={MediaPageStore.get('playlist-data')}
/> />,
) : null,
] ]
: [ : [
this.state.pagePlaylistLoaded ? (
<ViewerSidebar <ViewerSidebar
key="viewer-sidebar" key="viewer-sidebar"
mediaId={MediaPageStore.get('media-id')} mediaId={MediaPageStore.get('media-id')}
playlistData={MediaPageStore.get('playlist-data')} playlistData={MediaPageStore.get('playlist-data')}
/> />,
) : null,
<ViewerInfo key="viewer-info" />, <ViewerInfo key="viewer-info" />,
]} ]}
</div> </div>

View File

@@ -54,7 +54,8 @@ export class _VideoMediaPage extends Page {
} }
onMediaLoad() { onMediaLoad() {
const isVideoMedia = 'video' === MediaPageStore.get('media-type') || 'audio' === MediaPageStore.get('media-type'); const isVideoMedia =
'video' === MediaPageStore.get('media-type') || 'audio' === MediaPageStore.get('media-type');
if (isVideoMedia) { if (isVideoMedia) {
this.onViewerModeChange = this.onViewerModeChange.bind(this); this.onViewerModeChange = this.onViewerModeChange.bind(this);

View File

@@ -1,7 +1,7 @@
import React, { createContext, useContext, useEffect, useState } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { BrowserCache } from '../classes/'; import { BrowserCache } from '../classes/';
import { PageStore } from '../stores/'; import { PageStore } from '../stores/';
import { addClassname, removeClassname } from '../helpers/'; import { addClassname, removeClassname, inEmbeddedApp } from '../helpers/';
import SiteContext from './SiteContext'; import SiteContext from './SiteContext';
let slidingSidebarTimeout; let slidingSidebarTimeout;
@@ -45,9 +45,14 @@ export const LayoutProvider = ({ children }) => {
const site = useContext(SiteContext); const site = useContext(SiteContext);
const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400); const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
const enabledSidebar = !!(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar')); const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media' || window.MediaCMS?.mediaId !== undefined, []);
const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []);
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-sidebar')); const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
const [visibleSidebar, setVisibleSidebar] = useState(
isMediaPage || isEmbeddedApp ? false : cache.get('visible-sidebar')
);
const [visibleMobileSearch, setVisibleMobileSearch] = useState(false); const [visibleMobileSearch, setVisibleMobileSearch] = useState(false);
const toggleMobileSearch = () => { const toggleMobileSearch = () => {
@@ -61,28 +66,27 @@ export const LayoutProvider = ({ children }) => {
}; };
useEffect(() => { useEffect(() => {
if (visibleSidebar) { if (!isEmbeddedApp && visibleSidebar) {
addClassname(document.body, 'visible-sidebar'); addClassname(document.body, 'visible-sidebar');
} else { } else {
removeClassname(document.body, 'visible-sidebar'); removeClassname(document.body, 'visible-sidebar');
} }
if ('media' !== PageStore.get('current-page') && 1023 < window.innerWidth) {
if (!isEmbeddedApp && !isMediaPage && 1023 < window.innerWidth) {
cache.set('visible-sidebar', visibleSidebar); cache.set('visible-sidebar', visibleSidebar);
} }
}, [visibleSidebar]); }, [isEmbeddedApp, isMediaPage, visibleSidebar]);
useEffect(() => { useEffect(() => {
PageStore.once('page_init', () => { PageStore.once('page_init', () => {
if ('media' === PageStore.get('current-page')) { if (isEmbeddedApp || isMediaPage) {
setVisibleSidebar(false); setVisibleSidebar(false);
removeClassname(document.body, 'visible-sidebar'); removeClassname(document.body, 'visible-sidebar');
} }
}); });
setVisibleSidebar( setVisibleSidebar(
'media' !== PageStore.get('current-page') && !isEmbeddedApp && !isMediaPage && 1023 < window.innerWidth && (null === visibleSidebar || visibleSidebar)
1023 < window.innerWidth &&
(null === visibleSidebar || visibleSidebar)
); );
}, []); }, []);

View File

@@ -0,0 +1,51 @@
export function inEmbeddedApp() {
try {
const params = new URL(globalThis.location.href).searchParams;
const mode = params.get('mode');
if (mode === 'lms_embed_mode') {
sessionStorage.setItem('lms_embed_mode', 'true');
return true;
}
if (mode === 'standard') {
sessionStorage.removeItem('lms_embed_mode');
return false;
}
return sessionStorage.getItem('lms_embed_mode') === 'true';
} catch (e) {
return false;
}
}
export function isSelectMediaMode() {
try {
const params = new URL(globalThis.location.href).searchParams;
const action = params.get('action');
return action === 'select_media';
} catch (e) {
return false;
}
}
export function inSelectMediaEmbedMode() {
return inEmbeddedApp() && isSelectMediaMode();
}
export function getLtiContextId(): string | null {
try {
const params = new URL(globalThis.location.href).searchParams;
const contextId = params.get('lti_context_id');
if (contextId) {
sessionStorage.setItem('lti_context_id', contextId);
return contextId;
}
return sessionStorage.getItem('lti_context_id');
} catch (e) {
return null;
}
}

View File

@@ -14,3 +14,4 @@ export * from './quickSort';
export * from './requests'; export * from './requests';
export { translateString } from './translate'; export { translateString } from './translate';
export { replaceString } from './replacementStrings'; export { replaceString } from './replacementStrings';
export { inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode } from './embeddedApp';

View File

@@ -1,5 +1,5 @@
// check templates/config/installation/translations.html for more // check templates/config/installation/translations.html for more
export function translateString(str) { export function translateString(str) {
return window.TRANSLATION?.[str] ?? str; return window.TRANSLATION?.[str] || str;
} }

View File

@@ -111,6 +111,10 @@ export function useBulkActions() {
setShowConfirmModal(true); setShowConfirmModal(true);
setPendingAction(action); setPendingAction(action);
setConfirmMessage(translateString('You are going to disable comments to') + ` ${selectedCount} ` + translateString('media, are you sure?')); setConfirmMessage(translateString('You are going to disable comments to') + ` ${selectedCount} ` + translateString('media, are you sure?'));
} else if (action === 'delete-comments') {
setShowConfirmModal(true);
setPendingAction(action);
setConfirmMessage(translateString('You are going to delete all comments from') + ` ${selectedCount} ` + translateString('media, are you sure?'));
} else if (action === 'enable-download') { } else if (action === 'enable-download') {
setShowConfirmModal(true); setShowConfirmModal(true);
setPendingAction(action); setPendingAction(action);
@@ -165,6 +169,8 @@ export function useBulkActions() {
executeEnableComments(); executeEnableComments();
} else if (action === 'disable-comments') { } else if (action === 'disable-comments') {
executeDisableComments(); executeDisableComments();
} else if (action === 'delete-comments') {
executeDeleteComments();
} else if (action === 'enable-download') { } else if (action === 'enable-download') {
executeEnableDownload(); executeEnableDownload();
} else if (action === 'disable-download') { } else if (action === 'disable-download') {
@@ -271,6 +277,37 @@ export function useBulkActions() {
}); });
}; };
// Execute delete comments
const executeDeleteComments = () => {
const selectedIds = Array.from(selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken(),
},
body: JSON.stringify({
action: 'delete_comments',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete comments');
}
return response.json();
})
.then((data) => {
showNotificationMessage(translateString('Successfully deleted comments'));
clearSelection();
})
.catch((error) => {
showNotificationMessage(translateString('Failed to delete comments.'), 'error');
clearSelection();
});
};
// Execute enable download // Execute enable download
const executeEnableDownload = () => { const executeEnableDownload = () => {
const selectedIds = Array.from(selectedMedia); const selectedIds = Array.from(selectedMedia);

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import { ThemeProvider } from './contexts/ThemeContext'; import { ThemeProvider } from './contexts/ThemeContext';
import { LayoutProvider } from './contexts/LayoutContext'; import { LayoutProvider } from './contexts/LayoutContext';
import { UserProvider } from './contexts/UserContext'; import { UserProvider } from './contexts/UserContext';
import { inEmbeddedApp } from './helpers';
const AppProviders = ({ children }) => ( const AppProviders = ({ children }) => (
<LayoutProvider> <LayoutProvider>
@@ -15,9 +16,27 @@ const AppProviders = ({ children }) => (
import { PageHeader, PageSidebar } from '../components/page-layout'; import { PageHeader, PageSidebar } from '../components/page-layout';
export function renderPage(idSelector, PageComponent) { export function renderPage(idSelector, PageComponent) {
if (inEmbeddedApp()) {
globalThis.document.body.classList.add('embedded-app');
globalThis.document.body.classList.remove('visible-sidebar');
const appContent = idSelector ? document.getElementById(idSelector) : undefined;
if (appContent && PageComponent) {
ReactDOM.render(
<AppProviders>
<PageComponent />
</AppProviders>,
appContent
);
}
return;
}
const appContent = idSelector ? document.getElementById(idSelector) : undefined;
const appHeader = document.getElementById('app-header'); const appHeader = document.getElementById('app-header');
const appSidebar = document.getElementById('app-sidebar'); const appSidebar = document.getElementById('app-sidebar');
const appContent = idSelector ? document.getElementById(idSelector) : undefined;
if (appContent && PageComponent) { if (appContent && PageComponent) {
ReactDOM.render( ReactDOM.render(

View File

@@ -195,6 +195,10 @@ class MediaPageStore extends EventEmitter {
this.emit('loaded_media_data'); this.emit('loaded_media_data');
} }
// Skip loading playlists and comments when in embed mode (to reduce API calls)
const isEmbedMode = window.location.pathname.startsWith('/embed');
if (!isEmbedMode) {
this.loadPlaylists(); this.loadPlaylists();
if (MediaCMS.features.media.actions.comment_mention === true) { if (MediaCMS.features.media.actions.comment_mention === true) {
this.loadUsers(); this.loadUsers();
@@ -204,6 +208,7 @@ class MediaPageStore extends EventEmitter {
this.loadComments(); this.loadComments();
} }
} }
}
dataErrorResponse(response) { dataErrorResponse(response) {
if (void 0 !== response.type) { if (void 0 !== response.type) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
module.exports = function(grunt) {
var path = require('path');
grunt.initConfig({
babel: {
options: {
sourceMap: true,
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-modules-amd'],
moduleIds: true,
getModuleId: function(moduleName) {
// moduleName is the absolute or relative path to the file.
// We need to convert 'tiny/mediacms/amd/src/filename' to 'tiny_mediacms/filename'
var filename = path.basename(moduleName);
return 'tiny_mediacms/' + filename;
}
},
dist: {
files: [{
expand: true,
cwd: 'tiny/mediacms/amd/src',
src: ['*.js'],
dest: 'tiny/mediacms/amd/build',
ext: '.js'
}]
}
},
uglify: {
options: {
sourceMap: true,
output: { comments: false }
},
dist: {
files: [{
expand: true,
cwd: 'tiny/mediacms/amd/build',
src: ['*.js', '!*.min.js'],
dest: 'tiny/mediacms/amd/build',
ext: '.min.js'
}]
}
}
});
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['babel', 'uglify']);
};

View File

@@ -0,0 +1,72 @@
================================================================================
MediaCMS Moodle Plugin Suite v1.0.0
Installation Guide
================================================================================
Requirements
------------
- Moodle 4.5 or later
- MediaCMS instance
- MediaCMS set as External Tool
Installation
------------------
1. Extract zip file to Moodle root public directory:
cd /var/www/moodle/public
unzip mediacms-moodle-v1.0.0.zip
This will place files in:
- filter/mediacms/
- lib/editor/tiny/plugins/mediacms/
2. Set permissions
chown -R www-data:www-data filter/mediacms
chown -R www-data:www-data lib/editor/tiny/plugins/mediacms
3. Install through Moodle
- Log in as Administrator
- Go to: Site Administration → Notifications
- Click "Upgrade Moodle database now"
- Both plugins will be installed automatically
- Set the MediaCMS tool under the LTI Tool
4. Make sure Filter is enabled
- As Administrator, visit Plugins, 'Manage Filters', find MediaCMS filter and enable it.
Then place it at the top of the filter. This is important, otherwise embeds won't load.
5. Enter 'My Media' on top navigation.
- Log in as Administrator
- Go to: Site Administration → Appearance → Advanced Theme settings -> Custom menu items:
add: My Media|/filter/mediacms/my_media.php
Notes: User needs to be enrolled on one course at least, otherwise visiting My Media won't show anything.
The plugin needs to use the ID of an existing course to the 'My Media' page and thus it is going to use it
and create a 'my media' page on the course, if it doesn't already exist. This is happening automatically the
first time a user visits the global 'my media' link, if a 'my media' page activity does not exist in the course, the
plugin will create it. If you don't want the 'my media' page to show up on the course, you can place it to a hidden part
Create a part (eg 'Hidden Links'), select the Edit options (three vertical dots) and Hide it.
Then place the 'My Media' activity there, select the three dots for the activity, Availability, and set "Make available but don't show on course page: Available to students if you provide a link."
This option won't appear if the part is not hidden, so first you want to create the part, hide it, and then edit the setting for 'my media'
To summarize, for the 'my media' global link on top, a 'my media' is also required in each course, and you can either
create it (as MediaCMS activity with any name) or let the first user that visits the global 'my media' have it created automatically.
If you don't want this activity to be visible in the course, place it under a hidden part and set it's availability as "Make available but don't show on course page", which is only possible for items inside hidden parts.
What to expect
-------
1. Create a test course
2. Add a page or label
3. Click MediaCMS button in TinyMCE editor
4. Try inserting from video library or pasting a URL
SUPPORT
-------
Issues: https://github.com/mediacms-io/mediacms/issues
Docs: https://docs.mediacms.io
================================================================================

View File

@@ -0,0 +1,110 @@
# MediaCMS for Moodle
Version: 1.0.0, tested on Moodle 5
This plugin provides complete MediaCMS integration for Moodle, consisting of two plugins that work together with unified settings:
1. **Filter Plugin (filter_mediacms):**
* Handles LTI 1.3 authentication and secure video launches
* Auto-converts MediaCMS URLs to embedded players
* **Provides core settings** (MediaCMS URL, LTI Tool ID) used by both plugins
* **Location:** Admin, Plugins, Manage filters, MediaCMS
2. **Editor Plugin (tiny_mediacms):**
* Adds MediaCMS button to TinyMCE editor
* Browse authenticated video library via LTI Deep Linking
* Configure embed options (dimensions, display, start time)
* **Reads core settings** from filter plugin
* **Location:** Admin, Plugins, TinyMCE, MediaCMS
## Installation
Upload the plugin in Moodle's public directory and unzip
# cd /var/www/moodle/public ; cp /root/mediacms-moodle-v1.0.0.zip . && unzip mediacms-moodle-v1.0.0.zip
Ensure the web server user (typically `www-data`) has ownership of the new directories:
```bash
# Example for Ubuntu/Debian systems
chown -R www-data:www-data /var/www/moodle/public/filter/mediacms
chown -R www-data:www-data /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
chmod -R 755 /var/www/moodle/public/filter/mediacms
chmod -R 755 /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
```
### 3. Install Plugins
1. Log in to Moodle as an Administrator.
2. Go to **Site administration > Notifications**.
3. Follow the prompts to upgrade the database and install the new plugins.
## Configuration
### Step 1: Core Settings (Required) - Configure Once
Go to **Site administration > Plugins > Filters > MediaCMS** (Settings)
* **MediaCMS URL:** Enter your MediaCMS instance URL (e.g., `https://lti.mediacms.io`)
* **LTI Tool:** Select the External Tool configuration for MediaCMS
* *First create an LTI 1.3 tool at: Site administration > Plugins > Activity modules > External tool > Manage tools*
> **✨ Note:** These core settings are automatically used by **both** the filter and TinyMCE editor plugin.
### Step 2: Enable Filter
1. Go to **Site administration > Plugins > Filters > Manage filters**
2. Set **MediaCMS** to "On"
### Step 3: Configure Auto-convert Defaults (Optional)
Go to **Site administration > Plugins > Text editors > TinyMCE editor > MediaCMS settings**
Configure default display options for auto-converted URLs:
* Show video title
* Link video title
* Show related videos
* Show user avatar
> **Note:** The core settings (URL, LTI Tool) are managed in the filter plugin settings.
## Usage
### For Teachers (Editor)
1. In any text editor (TinyMCE), click the **MediaCMS** icon (or "Insert MediaCMS Media" from the Insert menu).
2. You can:
* **Paste a URL:** Paste a View or Embed URL.
* **Video Library:** Click the "Video Library" tab to browse and select videos (requires LTI Deep Linking configuration).
3. The video will appear as a placeholder or iframe in the editor.
### For Students (Display)
When content is viewed, the Filter will ensure the video is loaded securely via LTI 1.3, authenticating the user with MediaCMS automatically.
## Build instructions / Developing with the plugin
two types of changes: php (no build), js (build with npx grunt amd)
needs moodle/
npx version, dependencies etc
1. make changes here in lms-plugins/mediacms-moodle
2. copy to moodle
3. run `npx grunt amd` in moodle to build the JS files
4. from moodle copy back
sudo cp -r ~/mediacms/lms-plugins/mediacms-moodle/tiny/mediacms/ -r ~/mediacms/moodle/public/lib/editor/tiny/plugins/
5. cd ~/mediacms/moodle/public/lib/editor/tiny/plugins/mediacms/
npx grunt amd
6.
cp files back...
sudo cp -r /home/user/mediacms/moodle/public/lib/editor/tiny/plugins/mediacms /home/user/mediacms/lms-plugins/mediacms-moodle/tiny/
php admin/cli/purge_caches.php after
### Troubleshooting
Admin, advanced theme settings, add `My Media|/filter/mediacms/my_media.php` in case the position is not workin

View File

@@ -0,0 +1,89 @@
#!/bin/bash
# MediaCMS Moodle Plugin Suite - Build Script
# Creates distributable ZIP package
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN}MediaCMS Moodle Plugin Suite Builder${NC}"
echo -e "${GREEN}======================================${NC}"
echo
# Configuration
VERSION="1.0.0"
BUILD_DATE=$(date +%Y%m%d)
PACKAGE_NAME="mediacms-moodle-v${VERSION}"
DIST_DIR="dist"
BUILD_DIR="${DIST_DIR}/${PACKAGE_NAME}"
# Create clean dist directory
echo -e "${YELLOW}${NC} Cleaning dist directory..."
rm -rf "${DIST_DIR}"
mkdir -p "${BUILD_DIR}"
# Copy filter plugin
echo -e "${YELLOW}${NC} Copying filter plugin..."
mkdir -p "${BUILD_DIR}/filter"
cp -r filter/mediacms "${BUILD_DIR}/filter/"
# Copy TinyMCE plugin
echo -e "${YELLOW}${NC} Copying TinyMCE plugin..."
mkdir -p "${BUILD_DIR}/lib/editor/tiny/plugins"
cp -r tiny/mediacms "${BUILD_DIR}/lib/editor/tiny/plugins/"
# Copy documentation
echo -e "${YELLOW}${NC} Copying documentation..."
cp README.md "${BUILD_DIR}/filter/mediacms/"
cp INSTALL.txt "${BUILD_DIR}/filter/mediacms/"
# Clean up development files
echo -e "${YELLOW}${NC} Removing development files..."
find "${BUILD_DIR}" -type d -name "node_modules" -exec rm -rf {} + 2>/dev/null || true
find "${BUILD_DIR}" -type f -name ".DS_Store" -delete 2>/dev/null || true
find "${BUILD_DIR}" -type f -name "*.log" -delete 2>/dev/null || true
find "${BUILD_DIR}" -type d -name ".git" -exec rm -rf {} + 2>/dev/null || true
find "${BUILD_DIR}" -type f -name ".gitignore" -delete 2>/dev/null || true
# Remove AMD source files (keep only built versions)
echo -e "${YELLOW}${NC} Cleaning AMD source files..."
find "${BUILD_DIR}/lib/editor/tiny/plugins/mediacms/amd" -type f -name "*.js" ! -name "*-lazy.js" ! -path "*/build/*" -delete 2>/dev/null || true
# Create ZIP archive
echo -e "${YELLOW}${NC} Creating ZIP archive..."
cd "${BUILD_DIR}"
zip -r "../${PACKAGE_NAME}.zip" . -q
cd ../..
# Create checksum
echo -e "${YELLOW}${NC} Generating checksum..."
cd "${DIST_DIR}"
sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256"
cd ..
# Display results
ZIP_SIZE=$(du -h "${DIST_DIR}/${PACKAGE_NAME}.zip" | cut -f1)
echo
echo -e "${GREEN}✓ Build complete!${NC}"
echo
echo "Package: ${DIST_DIR}/${PACKAGE_NAME}.zip"
echo "Size: ${ZIP_SIZE}"
echo "Checksum: ${DIST_DIR}/${PACKAGE_NAME}.zip.sha256"
echo
echo -e "${YELLOW}Contents:${NC}"
echo " - filter/mediacms/ (includes docs)"
echo " - lib/editor/tiny/plugins/mediacms/"
echo
echo -e "${GREEN}Ready for distribution!${NC}"
echo
# Show checksum
echo -e "${YELLOW}SHA256 Checksum:${NC}"
cat "${DIST_DIR}/${PACKAGE_NAME}.zip.sha256"
echo

View File

@@ -0,0 +1,57 @@
<?php
/**
* Hook callbacks for filter_mediacms.
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace filter_mediacms;
defined('MOODLE_INTERNAL') || die();
class hooks {
/**
* Appends the My Media link to the custom menus after configuration is loaded.
*
* @param \core\hook\after_config $hook
*/
public static function append_my_media_link(\core\hook\after_config $hook): void {
global $CFG;
$navposition = get_config('filter_mediacms', 'mymedia_nav_position');
// MEDIACMS_NAV_PLACEMENT_NONE = 2
if ($navposition == 2) {
return;
}
if (!get_string_manager()->string_exists('mymedia', 'filter_mediacms')) {
return;
}
$linktext = get_string('mymedia', 'filter_mediacms');
$linkurl = '/filter/mediacms/my_media.php';
$menuitem = "\n{$linktext}|{$linkurl}";
// MEDIACMS_NAV_PLACEMENT_PROFILE = 1
if ($navposition == 1) {
// Add to User Profile Menu
if (!isset($CFG->customusermenuitems)) {
$CFG->customusermenuitems = '';
}
if (strpos($CFG->customusermenuitems, $linktext) === false) {
$CFG->customusermenuitems .= $menuitem;
}
} else {
// Default to Top Navigation Menu (MEDIACMS_NAV_PLACEMENT_TOP = 0)
if (!isset($CFG->custommenuitems)) {
$CFG->custommenuitems = '';
}
if (strpos($CFG->custommenuitems, $linktext) === false) {
$CFG->custommenuitems .= $menuitem;
}
}
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace filter_mediacms\privacy;
defined('MOODLE_INTERNAL') || die();
class provider implements \core_privacy\local\metadata\null_provider {
public static function get_reason(): string {
return 'privacy:metadata';
}
}

Some files were not shown because too many files have changed in this diff Show More