Compare commits

..

242 Commits

Author SHA1 Message Date
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
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
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
Markos Gogoulos
48537515cb changes 2026-01-30 16:14:55 +02:00
Markos Gogoulos
e6db138d11 xif 2026-01-30 15:17:22 +02:00
Markos Gogoulos
2f2d32f0db fix 2026-01-30 13:36:41 +02:00
Markos Gogoulos
f4d3439246 fix 2026-01-30 13:31:47 +02:00
Markos Gogoulos
7fe9891942 wtv 2026-01-30 13:30:32 +02:00
Markos Gogoulos
9eb8a1ad62 plug 2026-01-30 13:04:22 +02:00
Markos Gogoulos
23ee0dc7cc new 2026-01-30 13:01:21 +02:00
Markos Gogoulos
e5be39f392 feat: make S3 bucket for Terraform unique 2026-01-30 11:58:57 +02:00
Markos Gogoulos
f0c084fa53 wtv 2026-01-30 11:55:15 +02:00
Markos Gogoulos
571bfcc4ce wtv 2026-01-29 16:36:57 +02:00
Markos Gogoulos
c04380af47 w 2026-01-29 16:09:01 +02:00
Markos Gogoulos
97741f780e wtv 2026-01-29 16:03:35 +02:00
Markos Gogoulos
78cce0eb10 wtv 2026-01-29 16:01:34 +02:00
Markos Gogoulos
472b3029c4 wtv 2026-01-29 15:58:07 +02:00
Markos Gogoulos
343f1e7009 wtv 2026-01-29 15:49:11 +02:00
Markos Gogoulos
8c78b67b0c wtv 2026-01-29 15:40:33 +02:00
Markos Gogoulos
29fc7fb861 wtv 2026-01-29 15:37:41 +02:00
Markos Gogoulos
b03a33d93e wtv 2026-01-29 15:31:44 +02:00
Markos Gogoulos
64472be406 wtv 2026-01-29 15:27:49 +02:00
Markos Gogoulos
cc0f4d4645 new 2026-01-29 15:18:15 +02:00
Markos Gogoulos
095e4d2cb4 latest 2026-01-29 14:58:44 +02:00
Markos Gogoulos
5c8978453e a 2026-01-29 14:52:48 +02:00
Markos Gogoulos
83189076e4 a 2026-01-29 14:42:44 +02:00
Markos Gogoulos
ca6dbf3740 rev 2026-01-29 12:57:40 +02:00
Markos Gogoulos
8646bd70dc wtv 2026-01-29 12:49:45 +02:00
Markos Gogoulos
1f493c8e15 wtv 2026-01-29 12:49:37 +02:00
Markos Gogoulos
e11cb7ea6e wtv 2026-01-29 12:45:04 +02:00
Markos Gogoulos
3131e76ef7 this 2026-01-29 12:30:57 +02:00
Markos Gogoulos
809cdccc42 remove 2026-01-29 12:30:06 +02:00
Markos Gogoulos
ed36240f45 wtv 2026-01-28 22:46:27 +02:00
Markos Gogoulos
77bafff6f6 wtv 2026-01-28 22:44:22 +02:00
Markos Gogoulos
f6252f4f77 wtv 2026-01-28 22:40:48 +02:00
Markos Gogoulos
764580287f tv 2026-01-28 22:36:47 +02:00
Markos Gogoulos
ce6c9a0a3c tv 2026-01-28 22:35:02 +02:00
Markos Gogoulos
1ced023a07 tv 2026-01-28 22:30:13 +02:00
Markos Gogoulos
981fec296c tv 2026-01-28 22:27:19 +02:00
Markos Gogoulos
40cd7916e7 this 2026-01-28 22:25:33 +02:00
Yiannis Christodoulou
bcef59c3a9 Update select_media.html 2026-01-28 17:19:15 +02:00
Markos Gogoulos
e93c8225c4 d 2026-01-25 11:37:26 +02:00
Markos Gogoulos
5c3c33ca84 this 2026-01-25 10:26:58 +02:00
Markos Gogoulos
7a954e7a3d this 2026-01-25 10:18:00 +02:00
Markos Gogoulos
8610df0c2b this 2026-01-16 13:45:06 +02:00
Markos Gogoulos
8ab9030d14 this 2026-01-16 13:44:46 +02:00
Markos Gogoulos
15c8dec041 you 2026-01-16 13:44:04 +02:00
Markos Gogoulos
9af4686bd4 static 2026-01-10 15:59:44 +02:00
Markos Gogoulos
bcc8a0858c droplist for actions 2026-01-10 15:58:16 +02:00
Markos Gogoulos
549b672d48 add friendly_token in indexing 2026-01-10 15:30:03 +02:00
Markos Gogoulos
abe950f1da w 2026-01-09 13:42:36 +02:00
Markos Gogoulos
5fecda02d6 w 2026-01-09 13:40:20 +02:00
Markos Gogoulos
3c6f8c102c same window 2026-01-09 13:37:18 +02:00
Markos Gogoulos
2d28520cd4 Merge branch 'main' into feat-lti-integration 2026-01-09 13:29:52 +02:00
Markos Gogoulos
4bd56da2d8 push 2026-01-09 13:29:18 +02:00
Markos Gogoulos
fdfa857794 fix 2026-01-08 17:44:44 +02:00
Markos Gogoulos
2c1f27c0be remove 2026-01-08 17:30:33 +02:00
Markos Gogoulos
2f0bbd2533 gunicorn 2026-01-08 17:29:41 +02:00
Markos Gogoulos
54336f6c31 fix 2026-01-06 17:19:47 +02:00
Markos Gogoulos
37e21f7ebf this 2025-12-30 19:57:27 +02:00
Markos Gogoulos
3deee80dd0 fix 2025-12-30 19:53:30 +02:00
Markos Gogoulos
2e57164831 req 2025-12-30 17:55:23 +02:00
Markos Gogoulos
de0c16729b a 2025-12-30 15:43:42 +02:00
Markos Gogoulos
2c0bba1427 a 2025-12-30 15:30:08 +02:00
Markos Gogoulos
54a8e41f6d a 2025-12-30 15:19:36 +02:00
Markos Gogoulos
78fb19b464 a 2025-12-30 14:58:01 +02:00
Markos Gogoulos
8e5e7991b7 a 2025-12-30 14:49:40 +02:00
Markos Gogoulos
5cf435eca0 f 2025-12-30 13:50:37 +02:00
Markos Gogoulos
5026ce73da wtv 2025-12-29 20:45:48 +02:00
Markos Gogoulos
8b2ebe2415 wtv 2025-12-29 20:40:48 +02:00
Markos Gogoulos
8df320e134 wtv 2025-12-29 20:40:36 +02:00
Markos Gogoulos
8c8f737460 wtv 2025-12-29 20:32:56 +02:00
Markos Gogoulos
995faedb08 wtv 2025-12-29 20:16:21 +02:00
Markos Gogoulos
bde300b4bd all 2025-12-29 20:14:32 +02:00
Markos Gogoulos
fd5c0a2908 all 2025-12-29 20:04:41 +02:00
Markos Gogoulos
9c145da2e2 all 2025-12-29 20:02:55 +02:00
Markos Gogoulos
e9e5d44c3e wtv 2025-12-29 19:47:03 +02:00
Markos Gogoulos
a624c2e5b8 wtv 2025-12-29 19:46:36 +02:00
Markos Gogoulos
748d3b39ba wtv 2025-12-29 19:42:17 +02:00
Markos Gogoulos
ddc6bf9e67 wtv 2025-12-29 19:36:53 +02:00
Markos Gogoulos
aa7dbfe534 wtv 2025-12-29 19:34:53 +02:00
Markos Gogoulos
5cc72357c6 wtv 2025-12-29 19:33:24 +02:00
Markos Gogoulos
01b061a47b wtv 2025-12-29 19:29:41 +02:00
Markos Gogoulos
fbc78e7944 wtv 2025-12-29 19:23:55 +02:00
Markos Gogoulos
9e7a8afdda wtv 2025-12-29 19:21:26 +02:00
Markos Gogoulos
5572a67019 wtv 2025-12-29 19:16:29 +02:00
Markos Gogoulos
610590972f wtv 2025-12-29 19:15:10 +02:00
Markos Gogoulos
bdf7d3c2d0 wtv 2025-12-29 19:14:50 +02:00
Markos Gogoulos
a47bf5a3f8 wtv 2025-12-29 19:11:02 +02:00
Markos Gogoulos
38caea3c7c wtv 2025-12-29 19:08:44 +02:00
Markos Gogoulos
30491bf420 wtv 2025-12-29 19:06:58 +02:00
Markos Gogoulos
d0ebe19c2a wtv 2025-12-29 19:03:31 +02:00
Markos Gogoulos
59be9f16c0 wtv 2025-12-29 19:01:47 +02:00
Markos Gogoulos
a2d898c54e wtv 2025-12-29 18:59:24 +02:00
Markos Gogoulos
9733d53c0b wtv 2025-12-29 18:57:39 +02:00
Markos Gogoulos
70e2c67f3d wtv 2025-12-29 18:55:44 +02:00
Markos Gogoulos
77721d9c0e wtv 2025-12-29 18:48:35 +02:00
Markos Gogoulos
06bc64b2c4 all 2025-12-29 18:21:44 +02:00
Markos Gogoulos
b9899476b9 this 2025-12-29 17:45:12 +02:00
Markos Gogoulos
107750406e this 2025-12-29 17:43:54 +02:00
Markos Gogoulos
ae4ae5a07e this 2025-12-29 17:41:16 +02:00
Markos Gogoulos
f346a5604c this 2025-12-29 17:34:47 +02:00
Markos Gogoulos
56026a1a96 this 2025-12-29 17:26:19 +02:00
Markos Gogoulos
a88413ce14 this 2025-12-29 17:10:38 +02:00
Markos Gogoulos
9dab3ad858 this 2025-12-29 16:53:23 +02:00
Markos Gogoulos
dfe7e8fab0 this 2025-12-29 16:46:29 +02:00
Markos Gogoulos
1181d16ab9 this 2025-12-29 16:36:53 +02:00
Markos Gogoulos
d032ee3baa this 2025-12-29 16:35:47 +02:00
Markos Gogoulos
93f66d206b this 2025-12-29 14:17:30 +02:00
Markos Gogoulos
0585513439 this 2025-12-29 14:13:45 +02:00
Markos Gogoulos
9667e6b0ad this 2025-12-29 13:57:40 +02:00
Markos Gogoulos
f56948a4a2 this 2025-12-28 16:51:03 +02:00
Markos Gogoulos
8b3e76b554 this 2025-12-28 16:44:43 +02:00
Markos Gogoulos
dc417de628 this 2025-12-28 16:43:00 +02:00
Markos Gogoulos
35cd56c85c this 2025-12-28 16:34:59 +02:00
Markos Gogoulos
f0b2451815 this 2025-12-28 16:18:32 +02:00
Markos Gogoulos
7696251394 doc 2025-12-28 16:13:37 +02:00
Markos Gogoulos
b95725660b notes 2025-12-28 16:07:42 +02:00
Markos Gogoulos
d6bf98b30e this 2025-12-28 15:47:26 +02:00
Markos Gogoulos
3baa8ef7d7 this 2025-12-28 15:41:23 +02:00
Markos Gogoulos
45246eac4f this 2025-12-28 15:41:01 +02:00
Markos Gogoulos
9685c1b5d4 this 2025-12-28 15:39:47 +02:00
Markos Gogoulos
20a1da22bb this 2025-12-28 15:37:35 +02:00
Markos Gogoulos
f9a94321ad this 2025-12-28 15:35:29 +02:00
Markos Gogoulos
f85299a600 this 2025-12-28 15:30:37 +02:00
Markos Gogoulos
29ab2a715b this 2025-12-28 15:23:51 +02:00
Markos Gogoulos
43ce685f08 this 2025-12-28 15:22:08 +02:00
Markos Gogoulos
8c682a76af this 2025-12-28 15:19:49 +02:00
Markos Gogoulos
ec6b6daa81 this 2025-12-28 15:18:55 +02:00
Markos Gogoulos
cf90169240 this 2025-12-28 15:17:52 +02:00
Markos Gogoulos
fb3f377e27 this 2025-12-28 15:16:23 +02:00
Markos Gogoulos
f5f9a7beac this 2025-12-28 15:14:14 +02:00
Markos Gogoulos
726a5b74a1 this 2025-12-28 15:12:49 +02:00
Markos Gogoulos
40c31f295a this 2025-12-28 15:11:24 +02:00
Markos Gogoulos
1d77293afc this 2025-12-28 15:10:19 +02:00
Markos Gogoulos
5c702387ca this 2025-12-28 15:09:22 +02:00
Markos Gogoulos
0001f370a9 this 2025-12-28 15:05:40 +02:00
Markos Gogoulos
af71d4c906 this 2025-12-28 15:03:58 +02:00
Markos Gogoulos
eb7503125d this 2025-12-28 15:02:57 +02:00
Markos Gogoulos
f897d0ba2b this 2025-12-28 15:00:14 +02:00
Markos Gogoulos
545cca154e this 2025-12-28 14:39:04 +02:00
Markos Gogoulos
ef4ff9cb1d this 2025-12-28 14:33:51 +02:00
Markos Gogoulos
3a40fc6d88 this 2025-12-28 14:31:19 +02:00
Markos Gogoulos
f67d2a4d78 erq 2025-12-24 17:28:27 +02:00
Markos Gogoulos
295578dae2 lti 2025-12-24 17:28:12 +02:00
260 changed files with 16957 additions and 2922 deletions

View File

@@ -1,11 +1,5 @@
# Changelog # Changelog
## [7.6.0](https://github.com/mediacms-io/mediacms/compare/v7.5.0...v7.6.0) (2026-02-07)
### Features
* Create SECURITY.md ([#1485](https://github.com/mediacms-io/mediacms/issues/1485)) ([11449c2](https://github.com/mediacms-io/mediacms/commit/11449c2187d0f450b86915d88f92595a1825e4cf))
## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06) ## [7.5.0](https://github.com/mediacms-io/mediacms/compare/v7.4.0...v7.5.0) (2026-02-06)
### Features ### Features

View File

@@ -1,23 +0,0 @@
# History
## 3.0.0
### Features
- Updates Python/Django requirements and Dockerfile to use latest 3.11 Python - https://github.com/mediacms-io/mediacms/pull/826/files. This update requires some manual steps, for existing (not new) installations. Check the update section under the [Admin docs](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#2-server-installation), either for single server or for Docker Compose installations
- Upgrade postgres on Docker Compose - https://github.com/mediacms-io/mediacms/pull/749
### Fixes
- video player options for HLS - https://github.com/mediacms-io/mediacms/pull/832
- AVI videos not correctly recognised as videos - https://github.com/mediacms-io/mediacms/pull/833
## 2.1.0
### Fixes
- Increase uwsgi buffer-size parameter. This prevents an error by uwsgi with large headers - [#5b60](https://github.com/mediacms-io/mediacms/commit/5b601698a41ad97f08c1830e14b1c18f73ab8315)
- Fix issues with comments. These were not reported on the tracker but it is certain that they would not show comments on media files (non videos but also videos). Unfortunately this reverts work done with Timestamps on comments + Mentions on comments, more on PR [#802](https://github.com/mediacms-io/mediacms/pull/802)
### Features
- Allow tags to contains other characters too, not only English alphabet ones [#801](https://github.com/mediacms-io/mediacms/pull/801)
- Add simple cookie consent code [#799](https://github.com/mediacms-io/mediacms/pull/799)
- Allow password reset & email verify pages on global login required [#790](https://github.com/mediacms-io/mediacms/pull/790)
- Add api_url field to search api [#692](https://github.com/mediacms-io/mediacms/pull/692)

View File

@@ -108,7 +108,7 @@ There are two ways to run MediaCMS, through Docker Compose and through installin
## Technology ## Technology
This software uses the following list of awesome technologies: Python, Django, Django Rest Framework, Celery, PostgreSQL, Redis, Nginx, uWSGI, React, Fine Uploader, video.js, FFMPEG, Bento4 This software uses the following list of awesome technologies: Python, Django, Django Rest Framework, Celery, PostgreSQL, Redis, Nginx, Gunicorn, React, Fine Uploader, video.js, FFMPEG, Bento4
## Who is using it ## Who is using it

View File

@@ -1,54 +0,0 @@
# Security Policy
Thank you for helping improve the security of MediaCMS.
We take security vulnerabilities seriously and appreciate responsible disclosure.
---
## Reporting a Vulnerability
If you discover a security vulnerability in MediaCMS, **please do not open a public GitHub issue**.
Instead, report it using one of the following methods:
- **GitHub Security Advisories (preferred)**
Use the "Report a vulnerability" feature in this repository.
- **Contact Form**
Submit details via the official contact page:
https://mediacms.io/contact/
Please include as much of the following information as possible:
- Affected version(s)
- Detailed description of the issue
- Steps to reproduce (PoC if available)
- Impact assessment (e.g. RCE, XSS, privilege escalation)
- Any potential mitigations you are aware of
---
## Supported Versions
Security updates are provided for the **latest stable release** of MediaCMS.
Older versions may not receive security patches.
---
## Disclosure Policy
- We aim to acknowledge reports within **7 days**
- We aim to provide a fix or mitigation within **90 days**, depending on severity
- Please allow us time to investigate before any public disclosure
We follow responsible disclosure practices and will coordinate disclosure timelines when appropriate.
---
## Recognition
At this time, MediaCMS does not operate a formal bug bounty program.
However, we are happy to acknowledge valid security reports in release notes or advisories (with your permission).
---
Thank you for helping keep MediaCMS secure.

View File

@@ -24,6 +24,7 @@ INSTALLED_APPS = [
"actions.apps.ActionsConfig", "actions.apps.ActionsConfig",
"rbac.apps.RbacConfig", "rbac.apps.RbacConfig",
"identity_providers.apps.IdentityProvidersConfig", "identity_providers.apps.IdentityProvidersConfig",
"lti.apps.LtiConfig",
"debug_toolbar", "debug_toolbar",
"mptt", "mptt",
"crispy_forms", "crispy_forms",

View File

@@ -300,6 +300,7 @@ INSTALLED_APPS = [
"actions.apps.ActionsConfig", "actions.apps.ActionsConfig",
"rbac.apps.RbacConfig", "rbac.apps.RbacConfig",
"identity_providers.apps.IdentityProvidersConfig", "identity_providers.apps.IdentityProvidersConfig",
"lti.apps.LtiConfig",
"debug_toolbar", "debug_toolbar",
"mptt", "mptt",
"crispy_forms", "crispy_forms",
@@ -555,6 +556,7 @@ DJANGO_ADMIN_URL = "admin/"
USE_SAML = False USE_SAML = False
USE_RBAC = False USE_RBAC = False
USE_IDENTITY_PROVIDERS = False USE_IDENTITY_PROVIDERS = False
USE_LTI = False # Enable LTI 1.3 integration
JAZZMIN_UI_TWEAKS = {"theme": "flatly"} JAZZMIN_UI_TWEAKS = {"theme": "flatly"}
USE_ROUNDED_CORNERS = True USE_ROUNDED_CORNERS = True
@@ -650,3 +652,18 @@ if USERS_NEEDS_TO_BE_APPROVED:
) )
auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware") auth_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
MIDDLEWARE.insert(auth_index + 1, "cms.middleware.ApprovalMiddleware") MIDDLEWARE.insert(auth_index + 1, "cms.middleware.ApprovalMiddleware")
# LTI 1.3 Integration Settings
if USE_LTI:
# Session timeout for LTI launches (seconds)
LTI_SESSION_TIMEOUT = 3600 # 1 hour
# Cookie settings required for iframe embedding from LMS
# IMPORTANT: Requires HTTPS to be enabled
SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'None'
CSRF_COOKIE_SECURE = True
# SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
# Consider using cached_db for reliability if sessions are lost between many LTI launches

View File

@@ -25,6 +25,7 @@ urlpatterns = [
re_path(r"^", include("files.urls")), re_path(r"^", include("files.urls")),
re_path(r"^", include("users.urls")), re_path(r"^", include("users.urls")),
re_path(r"^accounts/", include("allauth.urls")), re_path(r"^accounts/", include("allauth.urls")),
re_path(r"^lti/", include("lti.urls")),
re_path(r"^api-auth/", include("rest_framework.urls")), re_path(r"^api-auth/", include("rest_framework.urls")),
path(settings.DJANGO_ADMIN_URL, admin.site.urls), path(settings.DJANGO_ADMIN_URL, admin.site.urls),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),

View File

@@ -1 +1 @@
VERSION = "7.5" VERSION = "8.12"

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

@@ -1,3 +1,9 @@
# Use existing X-Forwarded-Proto from reverse proxy if present, otherwise use $scheme
map $http_x_forwarded_proto $forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
server { server {
listen 80 ; listen 80 ;
@@ -28,7 +34,10 @@ server {
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
include /etc/nginx/sites-enabled/uwsgi_params; proxy_pass http://127.0.0.1:9000;
uwsgi_pass 127.0.0.1:9000; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $forwarded_proto;
} }
} }

View File

@@ -37,7 +37,6 @@ fi
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-available/default cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-available/default
cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-enabled/default cp deploy/docker/nginx_http_only.conf /etc/nginx/sites-enabled/default
cp deploy/docker/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
cp deploy/docker/nginx.conf /etc/nginx/ cp deploy/docker/nginx.conf /etc/nginx/
#### Supervisord Configurations ##### #### Supervisord Configurations #####
@@ -45,12 +44,12 @@ cp deploy/docker/nginx.conf /etc/nginx/
cp deploy/docker/supervisord/supervisord-debian.conf /etc/supervisor/conf.d/supervisord-debian.conf cp deploy/docker/supervisord/supervisord-debian.conf /etc/supervisor/conf.d/supervisord-debian.conf
if [ X"$ENABLE_UWSGI" = X"yes" ] ; then if [ X"$ENABLE_UWSGI" = X"yes" ] ; then
echo "Enabling uwsgi app server" echo "Enabling gunicorn app server"
cp deploy/docker/supervisord/supervisord-uwsgi.conf /etc/supervisor/conf.d/supervisord-uwsgi.conf cp deploy/docker/supervisord/supervisord-gunicorn.conf /etc/supervisor/conf.d/supervisord-gunicorn.conf
fi fi
if [ X"$ENABLE_NGINX" = X"yes" ] ; then if [ X"$ENABLE_NGINX" = X"yes" ] ; then
echo "Enabling nginx as uwsgi app proxy and media server" echo "Enabling nginx as gunicorn app proxy and media server"
cp deploy/docker/supervisord/supervisord-nginx.conf /etc/supervisor/conf.d/supervisord-nginx.conf cp deploy/docker/supervisord/supervisord-nginx.conf /etc/supervisor/conf.d/supervisord-nginx.conf
fi fi

View File

@@ -11,7 +11,7 @@ else
echo "There is no script $PRE_START_PATH" echo "There is no script $PRE_START_PATH"
fi fi
# Start Supervisor, with Nginx and uWSGI # Start Supervisor, with Nginx and Gunicorn
echo "Starting server using supervisord..." echo "Starting server using supervisord..."
exec /usr/bin/supervisord exec /usr/bin/supervisord

View File

@@ -0,0 +1,9 @@
[program:gunicorn]
command=/home/mediacms.io/bin/gunicorn cms.wsgi:application --workers=2 --threads=2 --worker-class=gthread --bind=127.0.0.1:9000 --user=www-data --group=www-data --timeout=120 --keep-alive=5 --max-requests=1000 --max-requests-jitter=50 --access-logfile=- --error-logfile=- --log-level=info --chdir=/home/mediacms.io/mediacms
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
priority=100
startinorder=true
startsecs=0

View File

@@ -1,9 +0,0 @@
[program:uwsgi]
command=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/docker/uwsgi.ini
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
priority=100
startinorder=true
startsecs=0

View File

@@ -1,24 +0,0 @@
[uwsgi]
chdir = /home/mediacms.io/mediacms/
virtualenv = /home/mediacms.io
module = cms.wsgi
uid=www-data
gid=www-data
processes = 2
threads = 2
master = true
socket = 127.0.0.1:9000
workers = 2
vacuum = true
hook-master-start = unix_signal:15 gracefully_kill_them_all
need-app = true
die-on-term = true
buffer-size=32768

View File

@@ -1,16 +0,0 @@
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;

View File

@@ -1,22 +0,0 @@
[Unit]
Description=MediaCMS celery beat
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
Restart=always
RestartSec=10
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/beat%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/beat%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms beat --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
ExecStop=/bin/kill -s TERM $MAINPID
[Install]
WantedBy=multi-user.target

View File

@@ -1,29 +0,0 @@
[Unit]
Description=MediaCMS celery long queue
After=network.target
[Service]
Type=forking
User=www-data
Group=www-data
Restart=always
RestartSec=10
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERYD_NODES="long1"
Environment=CELERY_QUEUE="long_tasks"
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
Environment=CELERYD_MULTI="multi"
Environment=CELERYD_OPTS="-Ofair --prefetch-multiplier=1"
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
[Install]
WantedBy=multi-user.target

View File

@@ -1,39 +0,0 @@
[Unit]
Description=MediaCMS celery short queue
After=network.target
[Service]
Type=forking
User=www-data
Group=www-data
Restart=always
RestartSec=10
WorkingDirectory=/home/mediacms.io/mediacms
Environment=CELERYD_NODES="short1 short2"
Environment=CELERY_QUEUE="short_tasks"
# Absolute or relative path to the 'celery' command:
Environment=CELERY_BIN="/home/mediacms.io/bin/celery"
# App instance to use
# comment out this line if you don't use an app
# or fully qualified:
#CELERY_APP="proj.tasks:app"
# How to call manage.py
Environment=CELERYD_MULTI="multi"
# Extra command-line arguments to the worker
Environment=CELERYD_OPTS="--soft-time-limit=300 -c10"
# - %n will be replaced with the first part of the nodename.
# - %I will be replaced with the current child process index
# and is important when using the prefork pool to avoid race conditions.
Environment=CELERYD_PID_FILE="/home/mediacms.io/mediacms/pids/%n.pid"
Environment=CELERYD_LOG_FILE="/home/mediacms.io/mediacms/logs/%N.log"
Environment=CELERYD_LOG_LEVEL="INFO"
ExecStart=/bin/sh -c '${CELERY_BIN} -A cms multi start ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
ExecStop=/bin/sh -c '${CELERY_BIN} -A cms multi stopwait ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE}'
ExecReload=/bin/sh -c '${CELERY_BIN} -A cms multi restart ${CELERYD_NODES} --pidfile=${CELERYD_PID_FILE} --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS} -Q ${CELERY_QUEUE}'
[Install]
WantedBy=multi-user.target

View File

@@ -1,13 +0,0 @@
-----BEGIN DH PARAMETERS-----
MIICCAKCAgEAo3MMiEY/fNbu+usIM0cDi6x8G3JBApv0Lswta4kiyedWT1WN51iQ
9zhOFpmcu6517f/fR9MUdyhVKHxxSqWQTcmTEFtz4P3VLTS/W1N5VbKE2VEMLpIi
wr350aGvV1Er0ujcp5n4O4h0I1tn4/fNyDe7+pHCdwM+hxe8hJ3T0/tKtad4fnIs
WHDjl4f7m7KuFfheiK7Efb8MsT64HDDAYXn+INjtDZrbE5XPw20BqyWkrf07FcPx
8o9GW50Ox7/FYq7jVMI/skEu0BRc8u6uUD9+UOuWUQpdeHeFcvLOgW53Z03XwWuX
RXosUKzBPuGtUDAaKD/HsGW6xmGr2W9yRmu27jKpfYLUb/eWbbnRJwCw04LdzPqv
jmtq02Gioo3lf5H5wYV9IYF6M8+q/slpbttsAcKERimD1273FBRt5VhSugkXWKjr
XDhoXu6vZgj8Opei38qPa8pI1RUFoXHFlCe6WpZQmU8efL8gAMrJr9jUIY8eea1n
u20t5B9ueb9JMjrNafcq6QkKhZLi6fRDDTUyeDvc0dN9R/3Yts97SXfdi1/lX7HS
Ht4zXd5hEkvjo8GcnjsfZpAC39QfHWkDaeUGEqsl3jXjVMfkvoVY51OuokPWZzrJ
M5+wyXNpfGbH67dPk7iHgN7VJvgX0SYscDPTtms50Vk7RwEzLeGuSHMCAQI=
-----END DH PARAMETERS-----

View File

@@ -1,84 +0,0 @@
server {
listen 80 ;
server_name localhost;
gzip on;
access_log /var/log/nginx/mediacms.io.access.log;
error_log /var/log/nginx/mediacms.io.error.log warn;
# # redirect to https if logged in
# if ($http_cookie ~* "sessionid") {
# rewrite ^/(.*)$ https://localhost/$1 permanent;
# }
# # redirect basic forms to https
# location ~ (login|login_form|register|mail_password_form)$ {
# rewrite ^/(.*)$ https://localhost/$1 permanent;
# }
location /static {
alias /home/mediacms.io/mediacms/static ;
}
location /media/original {
alias /home/mediacms.io/mediacms/media_files/original;
}
location /media {
alias /home/mediacms.io/mediacms/media_files ;
}
location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
include /etc/nginx/sites-enabled/uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
}
}
server {
listen 443 ssl;
server_name localhost;
ssl_certificate_key /etc/letsencrypt/live/localhost/privkey.pem;
ssl_certificate /etc/letsencrypt/live/localhost/fullchain.pem;
ssl_dhparam /etc/nginx/dhparams/dhparams.pem;
ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_ecdh_curve secp521r1:secp384r1;
ssl_prefer_server_ciphers on;
gzip on;
access_log /var/log/nginx/mediacms.io.access.log;
error_log /var/log/nginx/mediacms.io.error.log warn;
location /static {
alias /home/mediacms.io/mediacms/static ;
}
location /media/original {
alias /home/mediacms.io/mediacms/media_files/original;
#auth_basic "auth protected area";
#auth_basic_user_file /home/mediacms.io/mediacms/deploy/local_install/.htpasswd;
}
location /media {
alias /home/mediacms.io/mediacms/media_files ;
}
location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
include /etc/nginx/sites-enabled/uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
}
}

View File

@@ -1,58 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFTjCCBDagAwIBAgISBNOUeDlerH9MkKmHLvZJeMYgMA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0yMDAzMTAxNzUxNDFaFw0y
MDA2MDgxNzUxNDFaMBYxFDASBgNVBAMTC21lZGlhY21zLmlvMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAps5Jn18nW2tq/LYFDgQ1YZGLlpF/B2AAPvvH
3yuD+AcT4skKdZouVL/a5pXrptuYL5lthO9dlcja2tuO2ltYrb7Dp01dAIFaJE8O
DKd+Sv5wr8VWQZykqzMiMBgviml7TBvUHQjvCJg8UwmnN0XSUILCttd6u4qOzS7d
lKMMsKpYzLhElBT0rzhhsWulDiy6aAZbMV95bfR74nIWsBJacy6jx3jvxAuvCtkB
OVdOoVL6BPjDE3SNEk53bAZGIb5A9ri0O5jh/zBFT6tQSjUhAUTkmv9oZP547RnV
fDj+rdvCVk/fE+Jno36mcT183Qd/Ty3fWuqFoM5g/luhnfvWEwIDAQABo4ICYDCC
AlwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTd5EZBt74zu5XxT1uXQs6oM8qOuDAf
BgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEw
LgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcw
LwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcv
MBYGA1UdEQQPMA2CC21lZGlhY21zLmlvMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcG
CysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5
cHQub3JnMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHYAXqdz+d9WwOe1Nkh90Eng
MnqRmgyEoRIShBh1loFxRVgAAAFwxcnL+AAABAMARzBFAiAb3yeBuW3j9MxcRc0T
icUBvEa/rH7Fv2eB0oQlnZ1exQIhAPf+CtTXmzxoeT/BBiivj4AmGDsq4xWhe/U6
BytYrKLeAHYAB7dcG+V9aP/xsMYdIxXHuuZXfFeUt2ruvGE6GmnTohwAAAFwxcnM
HAAABAMARzBFAiAuP5gKyyaT0LVXxwjYD9zhezvxf4Icx0P9pk75c5ao+AIhAK0+
fSJv+WTXciMT6gA1sk/tuCHuDFAuexSA/6TcRXcVMA0GCSqGSIb3DQEBCwUAA4IB
AQCPCYBU4Q/ro2MUkjDPKGmeqdxQycS4R9WvKTG/nmoahKNg30bnLaDPUcpyMU2k
sPDemdZ7uTGLZ3ZrlIva8DbrnJmrTPf9BMwaM6j+ZV/QhxvKZVIWkLkZrwiVI57X
Ba+rs5IEB4oWJ0EBaeIrzeKG5zLMkRcIdE4Hlhuwu3zGG56c+wmAPuvpIDlYoO6o
W22xRdxoTIHBvkzwonpVYUaRcaIw+48xnllxh1dHO+X69DT45wlF4tKveOUi+L50
4GWJ8Vjv7Fot/WNHEM4Mnmw0jHj9TPkIZKnPNRMdHmJ5CF/FJFDiptOeuzbfohG+
mdvuInb8JDc0XBE99Gf/S4/y
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmzkmfXydba2r8
tgUOBDVhkYuWkX8HYAA++8ffK4P4BxPiyQp1mi5Uv9rmleum25gvmW2E712VyNra
247aW1itvsOnTV0AgVokTw4Mp35K/nCvxVZBnKSrMyIwGC+KaXtMG9QdCO8ImDxT
Cac3RdJQgsK213q7io7NLt2UowywqljMuESUFPSvOGGxa6UOLLpoBlsxX3lt9Hvi
chawElpzLqPHeO/EC68K2QE5V06hUvoE+MMTdI0STndsBkYhvkD2uLQ7mOH/MEVP
q1BKNSEBROSa/2hk/njtGdV8OP6t28JWT98T4mejfqZxPXzdB39PLd9a6oWgzmD+
W6Gd+9YTAgMBAAECggEADnEJuryYQbf5GUwBAAepP3tEZJLQNqk/HDTcRxwTXuPt
+tKBD1F79WZu40vTjSyx7l0QOFQo/BDZsd0Ubx89fD1p3xA5nxOT5FTb2IifzIpe
4zjokOGo+BGDQjq10vvy6tH1+VWOrGXRwzawvX5UCRhpFz9sptQGLQmDsZy0Oo9B
LtavYVUqsbyqRWlzaclHgbythegIACWkqcalOzOtx+l6TGBRjej+c7URcwYBfr7t
XTAzbP+vnpaJovZyZT1eekr0OLzMpnjx4HvRvzL+NxauRpn6KfabsTfZlk8nrs4I
UdSjeukj1Iz8rGQilHdN/4dVJ3KzrlHVkVTBSjmMUQKBgQDaVXZnhAScfdiKeZbO
rdUAWcnwfkDghtRuAmzHaRM/FhFBEoVhdSbBuu+OUyBnIw/Ra4o2ePuEBcKIUiQO
w2tnE1CY5PPAcjw+OCSpvzy5xxjaqaRbm9BJp3FTeEYGLXERnchPpHg/NpexuF22
QOJ+FrysPyNMxuQp47ZwO9WT3QKBgQDDlSGjq/eeWxemwf7ZqMVlRyqsdJsgnCew
DkC62IGiYCBDfeEmndN+vcA/uzJHYV4iXiqS3aYJCWGaZFMhdIhIn5MgULvO1j5G
u/MxuzaaNPz22FlNCWTLBw4T1HOOvyTL+nLtZDKJ/BHxgHCmur1kiGvvZWrcCthD
afLEmseqrwKBgBuLZKCymxJTHhp6NHhmndSpfzyD8RNibzJhw+90ZiUzV4HqIEGn
Ufhm6Qn/mrroRXqaIpm0saZ6Q4yHMF1cchRS73wahlXlE4yV8KopojOd1pjfhgi4
o5JnOXjaV5s36GfcjATgLvtqm8CkDc6MaQaXP75LSNzKysYuIDoQkmVRAoGAAghF
rja2Pv4BU+lGJarcSj4gEmSvy/nza5/qSka/qhlHnIvtUAJp1TJRkhf24MkBOmgy
Fw6YkBV53ynVt05HsEGAPOC54t9VDFUdpNGmMpoEWuhKnUNQuc9b9RbLEJup3TjA
Avl8kPR+lzzXbtQX7biBLp6mKp0uPB0YubRGCN8CgYA0JMxK0x38Q2x3AQVhOmZh
YubtIa0JqVJhvpweOCFnkq3ebBpLsWYwiLTn86vuD0jupe5M3sxtefjkJmAKd8xY
aBU7QWhjh1fX4mzmggnbjcrIFbkIHsxwMeg567U/4AGxOOUsv9QUn37mqycqRKEn
YfUyYNLM6F3MmQAOs2kaHw==
-----END PRIVATE KEY-----

View File

@@ -1,13 +0,0 @@
[Unit]
Description=MediaCMS uwsgi
[Service]
ExecStart=/home/mediacms.io/bin/uwsgi --ini /home/mediacms.io/mediacms/deploy/local_install/uwsgi.ini
ExecStop=/usr/bin/killall -9 uwsgi
RestartSec=3
#ExecRestart=killall -9 uwsgi; sleep 5; /home/sss/bin/uwsgi --ini /home/sss/wordgames/uwsgi.ini
Restart=always
[Install]
WantedBy=multi-user.target

View File

@@ -1,7 +0,0 @@
/home/mediacms.io/mediacms/logs/*.log {
weekly
missingok
rotate 7
compress
notifempty
}

View File

@@ -1,38 +0,0 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 10240;
}
worker_rlimit_nofile 20000; #each connection needs a filehandle (or 2 if you are proxying)
http {
proxy_connect_timeout 75;
proxy_read_timeout 12000;
client_max_body_size 5800M;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 10;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
log_format compression '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@@ -1,34 +0,0 @@
module selinux-mediacms 1.0;
require {
type init_t;
type var_t;
type redis_port_t;
type postgresql_port_t;
type httpd_t;
type httpd_sys_content_t;
type httpd_sys_rw_content_t;
class file { append create execute execute_no_trans getattr ioctl lock open read rename setattr unlink write };
class dir { add_name remove_name rmdir };
class tcp_socket name_connect;
class lnk_file read;
}
#============= httpd_t ==============
allow httpd_t var_t:file { getattr open read };
#============= init_t ==============
allow init_t postgresql_port_t:tcp_socket name_connect;
allow init_t redis_port_t:tcp_socket name_connect;
allow init_t httpd_sys_content_t:dir rmdir;
allow init_t httpd_sys_content_t:file { append create execute execute_no_trans ioctl lock open read rename setattr unlink write };
allow init_t httpd_sys_content_t:lnk_file read;
allow init_t httpd_sys_rw_content_t:dir { add_name remove_name rmdir };
allow init_t httpd_sys_rw_content_t:file { create ioctl lock open read setattr unlink write };

View File

@@ -1,27 +0,0 @@
[uwsgi]
chdir = /home/mediacms.io/mediacms/
virtualenv = /home/mediacms.io
module = cms.wsgi
uid=www-data
gid=www-data
processes = 2
threads = 2
master = true
socket = 127.0.0.1:9000
#socket = /home/mediacms.io/mediacms/deploy/uwsgi.sock
workers = 2
vacuum = true
logto = /home/mediacms.io/mediacms/logs/errorlog.txt
disable-logging = true
buffer-size=32768

View File

@@ -1,16 +0,0 @@
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;

View File

@@ -23,7 +23,7 @@ and will start all services required for MediaCMS, as Celery/Redis for asynchron
For Django, the changes from the image produced by docker-compose.yaml are these: For Django, the changes from the image produced by docker-compose.yaml are these:
* Django runs in debug mode, with `python manage.py runserver` * Django runs in debug mode, with `python manage.py runserver`
* uwsgi and nginx are not run * gunicorn and nginx are not run
* Django runs in Debug mode, with Debug Toolbar * Django runs in Debug mode, with Debug Toolbar
* Static files (js/css) are loaded from static/ folder * Static files (js/css) are loaded from static/ folder
* corsheaders is installed and configured to allow all origins * corsheaders is installed and configured to allow all origins

View File

@@ -65,6 +65,7 @@ class CategoryAdminForm(forms.ModelForm):
class Meta: class Meta:
model = Category model = Category
# LTI fields will be shown as read-only when USE_LTI is enabled
fields = '__all__' fields = '__all__'
def clean(self): def clean(self):
@@ -135,7 +136,7 @@ class CategoryAdmin(admin.ModelAdmin):
list_display = ["title", "user", "add_date", "media_count"] list_display = ["title", "user", "add_date", "media_count"]
list_filter = [] list_filter = []
ordering = ("-add_date",) ordering = ("-add_date",)
readonly_fields = ("user", "media_count") readonly_fields = ("user", "media_count", "lti_platform", "lti_context_id")
change_form_template = 'admin/files/category/change_form.html' change_form_template = 'admin/files/category/change_form.html'
def get_list_filter(self, request): def get_list_filter(self, request):
@@ -145,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
@@ -154,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
@@ -167,6 +172,14 @@ class CategoryAdmin(admin.ModelAdmin):
), ),
] ]
additional_fieldsets = []
if getattr(settings, 'USE_LTI', False):
lti_fieldset = [
('LTI Integration', {'fields': ['lti_platform', 'lti_context_id'], 'classes': ['tab'], 'description': 'LTI/LMS integration settings (automatically managed by LTI provisioning)'}),
]
additional_fieldsets.extend(lti_fieldset)
if getattr(settings, 'USE_RBAC', False): if getattr(settings, 'USE_RBAC', False):
rbac_fieldset = [ rbac_fieldset = [
('RBAC Settings', {'fields': ['is_rbac_category'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}), ('RBAC Settings', {'fields': ['is_rbac_category'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}),
@@ -177,9 +190,9 @@ class CategoryAdmin(admin.ModelAdmin):
('RBAC Settings', {'fields': ['is_rbac_category', 'identity_provider'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}), ('RBAC Settings', {'fields': ['is_rbac_category', 'identity_provider'], 'classes': ['tab'], 'description': 'Role-Based Access Control settings'}),
('Group Access', {'fields': ['rbac_groups'], 'description': 'Select the Groups that have access to category'}), ('Group Access', {'fields': ['rbac_groups'], 'description': 'Select the Groups that have access to category'}),
] ]
return basic_fieldset + rbac_fieldset additional_fieldsets.extend(rbac_fieldset)
else:
return basic_fieldset return basic_fieldset + additional_fieldsets
class TagAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin):

View File

@@ -64,4 +64,10 @@ def stuff(request):
if request.user.is_superuser: if request.user.is_superuser:
ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL ret["DJANGO_ADMIN_URL"] = settings.DJANGO_ADMIN_URL
if getattr(settings, 'USE_LTI', False):
lti_session = request.session.get('lti_session')
if lti_session and request.user.is_authenticated:
ret['lti_session'] = lti_session
return ret return ret

View File

@@ -127,6 +127,7 @@ class MediaPublishForm(forms.ModelForm):
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 +170,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 +197,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")

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.": "فشل تعطيل التنزيل.",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "تحديد جميع الوسائط", "Select all media": "تحديد جميع الوسائط",
"Select publish state:": "اختر حالة النشر:", "Select publish state:": "اختر حالة النشر:",
"Selected": "محدد", "Selected": "محدد",
"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": "مشاركة معي",
"Sign in": "تسجيل الدخول", "Sign in": "تسجيل الدخول",
@@ -196,6 +202,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": "تم تحديث عضوية قائمة التشغيل بنجاح",
@@ -240,6 +247,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.": "ডাউনলোড নিষ্ক্রিয় করতে ব্যর্থ হয়েছে।",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "", "Select all media": "",
"Select publish state:": "", "Select publish state:": "",
"Selected": "", "Selected": "",
"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": "আমার সাথে শেয়ার করা",
"Sign in": "সাইন ইন করুন", "Sign in": "সাইন ইন করুন",
@@ -196,6 +202,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": "",
@@ -240,6 +247,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.",
@@ -178,6 +180,10 @@ 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",
"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",
"Sign in": "Log ind", "Sign in": "Log ind",
@@ -196,6 +202,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",
@@ -240,6 +247,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.",
@@ -178,6 +180,10 @@ 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",
"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",
"Sign in": "Anmelden", "Sign in": "Anmelden",
@@ -196,6 +202,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",
@@ -240,6 +247,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.": "Αποτυχία απενεργοποίησης λήψης.",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "Επιλογή όλων των αρχείων", "Select all media": "Επιλογή όλων των αρχείων",
"Select publish state:": "Επιλέξτε κατάσταση δημοσίευσης:", "Select publish state:": "Επιλέξτε κατάσταση δημοσίευσης:",
"Selected": "Επιλεγμένα", "Selected": "Επιλεγμένα",
"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": "Κοινοποιήθηκαν σε εμένα",
"Sign in": "Σύνδεση", "Sign in": "Σύνδεση",
@@ -196,6 +202,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": "Η συμμετοχή στη λίστα ενημερώθηκε με επιτυχία",
@@ -240,6 +247,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.": "",
@@ -184,6 +186,10 @@ translation_strings = {
"Selected": "", "Selected": "",
"selected": "", "selected": "",
"SHARE": "", "SHARE": "",
"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": "",
"SHOW MORE": "", "SHOW MORE": "",
@@ -201,6 +207,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": "",
@@ -252,6 +259,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.",
@@ -178,6 +180,10 @@ 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",
"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",
"Sign in": "Iniciar sesión", "Sign in": "Iniciar sesión",
@@ -196,6 +202,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",
@@ -240,6 +247,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.",
@@ -179,6 +181,10 @@ 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é",
"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",
"Sign in": "Se connecter", "Sign in": "Se connecter",
@@ -197,6 +203,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",
@@ -241,6 +248,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.": "ביטול ההורדה נכשל.",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "", "Select all media": "",
"Select publish state:": "", "Select publish state:": "",
"Selected": "", "Selected": "",
"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": "שותף איתי",
"Sign in": "התחבר", "Sign in": "התחבר",
@@ -196,6 +202,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": "",
@@ -240,6 +247,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.": "डाउनलोड अक्षम करने में विफल।",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "सभी मीडिया चुनें", "Select all media": "सभी मीडिया चुनें",
"Select publish state:": "प्रकाशन स्थिति चुनें:", "Select publish state:": "प्रकाशन स्थिति चुनें:",
"Selected": "चयनित", "Selected": "चयनित",
"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": "मेरे साथ साझा किया गया",
"Sign in": "साइन इन करें", "Sign in": "साइन इन करें",
@@ -196,6 +202,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": "प्लेलिस्ट सदस्यता सफलतापूर्वक अपडेट की गई",
@@ -240,6 +247,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.",
@@ -178,6 +180,10 @@ 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",
"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",
"Sign in": "Masuk", "Sign in": "Masuk",
@@ -196,6 +202,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",
@@ -240,6 +247,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.",
@@ -179,6 +181,10 @@ 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",
"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",
"Sign in": "Login", "Sign in": "Login",
@@ -197,6 +203,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",
@@ -241,6 +248,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.": "ダウンロードの無効化に失敗しました。",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "すべてのメディアを選択", "Select all media": "すべてのメディアを選択",
"Select publish state:": "公開状態を選択:", "Select publish state:": "公開状態を選択:",
"Selected": "選択済み", "Selected": "選択済み",
"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": "共有されたもの",
"Sign in": "サインイン", "Sign in": "サインイン",
@@ -196,6 +202,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": "プレイリストメンバーシップが正常に更新されました",
@@ -240,6 +247,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.": "다운로드 비활성화에 실패했습니다.",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "모든 미디어 선택", "Select all media": "모든 미디어 선택",
"Select publish state:": "게시 상태 선택:", "Select publish state:": "게시 상태 선택:",
"Selected": "선택됨", "Selected": "선택됨",
"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": "나와 공유됨",
"Sign in": "로그인", "Sign in": "로그인",
@@ -196,6 +202,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": "재생 목록 멤버십이 업데이트되었습니다",
@@ -240,6 +247,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.",
@@ -178,6 +180,10 @@ 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",
"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",
"Sign in": "Inloggen", "Sign in": "Inloggen",
@@ -196,6 +202,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",
@@ -240,6 +247,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.",
@@ -178,6 +180,10 @@ 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",
"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",
"Sign in": "Entrar", "Sign in": "Entrar",
@@ -196,6 +202,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",
@@ -240,6 +247,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.": "Не удалось отключить загрузку.",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "Выбрать все медиа", "Select all media": "Выбрать все медиа",
"Select publish state:": "Выберите состояние публикации:", "Select publish state:": "Выберите состояние публикации:",
"Selected": "Выбрано", "Selected": "Выбрано",
"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": "Поделено со мной",
"Sign in": "Войти", "Sign in": "Войти",
@@ -196,6 +202,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": "Членство в плейлисте успешно обновлено",
@@ -240,6 +247,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.",
@@ -178,6 +180,10 @@ 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",
"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",
"Sign in": "Prijava", "Sign in": "Prijava",
@@ -196,6 +202,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",
@@ -240,6 +247,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ı.",
@@ -178,6 +180,10 @@ 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",
"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",
"Sign in": "Giriş Yap", "Sign in": "Giriş Yap",
@@ -196,6 +202,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",
@@ -240,6 +247,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.": "ڈاؤن لوڈ غیر فعال کرنے میں ناکام۔",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "تمام میڈیا منتخب کریں", "Select all media": "تمام میڈیا منتخب کریں",
"Select publish state:": "اشاعت کی حالت منتخب کریں:", "Select publish state:": "اشاعت کی حالت منتخب کریں:",
"Selected": "منتخب شدہ", "Selected": "منتخب شدہ",
"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": "میرے ساتھ شیئر کیا گیا",
"Sign in": "سائن ان کریں", "Sign in": "سائن ان کریں",
@@ -196,6 +202,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": "پلے لسٹ ممبرشپ کامیابی سے اپ ڈیٹ ہو گئی",
@@ -240,6 +247,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.": "禁用下载失败。",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "", "Select all media": "",
"Select publish state:": "", "Select publish state:": "",
"Selected": "", "Selected": "",
"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": "分享给我的",
"Sign in": "登录", "Sign in": "登录",
@@ -196,6 +202,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": "",
@@ -240,6 +247,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.": "停用下載失敗。",
@@ -178,6 +180,10 @@ translation_strings = {
"Select all media": "", "Select all media": "",
"Select publish state:": "", "Select publish state:": "",
"Selected": "", "Selected": "",
"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": "與我分享",
"Sign in": "登入", "Sign in": "登入",
@@ -196,6 +202,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": "",
@@ -240,6 +247,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

@@ -965,3 +965,13 @@ def get_alphanumeric_only(string):
""" """
string = "".join([char for char in string if char.isalnum()]) string = "".join([char for char in string if char.isalnum()])
return string.lower() return string.lower()
def get_alphanumeric_and_spaces(string):
"""Returns a query that contains only alphanumeric characters and spaces
This include characters other than the English alphabet too
"""
string = "".join([char for char in string if char.isalnum() or char.isspace()])
# Replace multiple spaces with single space and strip
string = " ".join(string.split())
return string

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.6 on 2025-12-29 16:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0014_alter_subtitle_options_and_more'),
]
operations = [
migrations.AddField(
model_name='category',
name='is_lms_course',
field=models.BooleanField(db_index=True, default=False, help_text='Whether this category represents an LMS course'),
),
migrations.AddField(
model_name='category',
name='lti_context_id',
field=models.CharField(blank=True, db_index=True, help_text='LTI context ID from platform', max_length=255),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.2.6 on 2025-12-29 16:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('files', '0015_category_is_lms_course_category_lti_context_id'),
('lti', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='category',
name='lti_platform',
field=models.ForeignKey(
blank=True, help_text='LTI Platform if this is an LTI course', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='categories', to='lti.ltiplatform'
),
),
]

View File

@@ -47,6 +47,13 @@ class Category(models.Model):
verbose_name='IDP Config Name', verbose_name='IDP Config Name',
) )
# LTI/LMS integration fields
is_lms_course = models.BooleanField(default=False, db_index=True, help_text='Whether this category represents an LMS course')
lti_platform = models.ForeignKey('lti.LTIPlatform', blank=True, null=True, on_delete=models.SET_NULL, related_name='categories', help_text='LTI Platform if this is an LTI course')
lti_context_id = models.CharField(max_length=255, blank=True, db_index=True, help_text='LTI context ID from platform')
def __str__(self): def __str__(self):
return self.title return self.title
@@ -137,7 +144,7 @@ class Tag(models.Model):
return True return True
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.title = helpers.get_alphanumeric_only(self.title) self.title = helpers.get_alphanumeric_and_spaces(self.title)
self.title = self.title[:100] self.title = self.title[:100]
super(Tag, self).save(*args, **kwargs) super(Tag, self).save(*args, **kwargs)

View File

@@ -352,20 +352,11 @@ class Media(models.Model):
# first get anything interesting out of the media # first get anything interesting out of the media
# that needs to be search able # that needs to be search able
a_tags = b_tags = "" a_tags = ""
if self.id: if self.id:
a_tags = " ".join([tag.title for tag in self.tags.all()]) a_tags = " ".join([tag.title for tag in self.tags.all()])
b_tags = " ".join([tag.title.replace("-", " ") for tag in self.tags.all()])
items = [ items = [self.friendly_token, self.title, self.user.username, self.user.email, self.user.name, self.description, a_tags]
self.title,
self.user.username,
self.user.email,
self.user.name,
self.description,
a_tags,
b_tags,
]
for subtitle in self.subtitles.all(): for subtitle in self.subtitles.all():
items.append(subtitle.subtitle_text) items.append(subtitle.subtitle_text)
@@ -745,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

@@ -80,6 +80,7 @@ urlpatterns = [
views.trim_video, views.trim_video,
), ),
re_path(r"^api/v1/categories$", views.CategoryList.as_view()), re_path(r"^api/v1/categories$", views.CategoryList.as_view()),
re_path(r"^api/v1/categories/contributor$", views.CategoryListContributor.as_view()),
re_path(r"^api/v1/tags$", views.TagList.as_view()), re_path(r"^api/v1/tags$", views.TagList.as_view()),
re_path(r"^api/v1/comments$", views.CommentList.as_view()), re_path(r"^api/v1/comments$", views.CommentList.as_view()),
re_path( re_path(

View File

@@ -1,7 +1,7 @@
# Import all views for backward compatibility # Import all views for backward compatibility
from .auth import custom_login_view, saml_metadata # noqa: F401 from .auth import custom_login_view, saml_metadata # noqa: F401
from .categories import CategoryList, TagList # noqa: F401 from .categories import CategoryList, CategoryListContributor, TagList # noqa: F401
from .comments import CommentDetail, CommentList # noqa: F401 from .comments import CommentDetail, CommentList # noqa: F401
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401 from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
from .media import MediaActions # noqa: F401 from .media import MediaActions # noqa: F401

View File

@@ -43,6 +43,49 @@ class CategoryList(APIView):
return Response(ret) return Response(ret)
class CategoryListContributor(APIView):
"""List categories where user has contributor access"""
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
name='lms_courses_only',
type=openapi.TYPE_BOOLEAN,
in_=openapi.IN_QUERY,
description='Filter to show only LMS courses (categories with is_lms_course=True)',
),
],
tags=['Categories'],
operation_summary='Lists Categories for Contributors',
operation_description='Lists all categories where the user has contributor access',
responses={
200: openapi.Response('response description', CategorySerializer),
},
)
def get(self, request, format=None):
if not request.user.is_authenticated:
return Response([])
categories = Category.objects.none()
# Filter for LMS courses only if requested
lms_courses_only = request.GET.get('lms_courses_only', '').lower() in ['true', '1', 'yes']
if lms_courses_only:
categories = categories.filter(is_lms_course=True)
else:
categories = Category.objects.filter(is_rbac_category=False).prefetch_related("user")
# Get RBAC categories where user has contributor access
if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_contributor()
categories = categories.union(rbac_categories)
categories = categories.order_by("title")
serializer = CategorySerializer(categories, many=True, context={"request": request})
return Response(serializer.data)
class TagList(APIView): class TagList(APIView):
"""List tags""" """List tags"""

View File

@@ -35,6 +35,7 @@ from ..methods import (
) )
from ..models import ( from ..models import (
Category, Category,
Comment,
EncodeProfile, EncodeProfile,
Media, Media,
MediaPermission, MediaPermission,
@@ -164,7 +165,14 @@ 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()
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,7 +186,7 @@ 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)
@@ -295,6 +303,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 +388,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 +505,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,12 +588,31 @@ 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)
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)
categories = Category.objects.filter(uid__in=category_uids)
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
for category in categories: for category in categories:
@@ -697,12 +730,9 @@ 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 = show_related_media(media, request=request, limit=100)
related_media = [] related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
else: related_media = related_media_serializer.data
related_media = show_related_media(media, request=request, limit=100)
related_media_serializer = MediaSerializer(related_media, many=True, context={"request": request})
related_media = related_media_serializer.data
ret = serializer.data ret = serializer.data
# update rattings info with user specific ratings # update rattings info with user specific ratings

View File

@@ -24,7 +24,7 @@ from ..forms import (
WhisperSubtitlesForm, WhisperSubtitlesForm,
) )
from ..frontend_translations import translate_string from ..frontend_translations import translate_string
from ..helpers import get_alphanumeric_only from ..helpers import get_alphanumeric_and_spaces
from ..methods import ( from ..methods import (
can_transcribe_video, can_transcribe_video,
create_video_trim_request, create_video_trim_request,
@@ -310,8 +310,8 @@ def edit_media(request):
media.tags.remove(tag) media.tags.remove(tag)
if form.cleaned_data.get("new_tags"): if form.cleaned_data.get("new_tags"):
for tag in form.cleaned_data.get("new_tags").split(","): for tag in form.cleaned_data.get("new_tags").split(","):
tag = get_alphanumeric_only(tag) tag = get_alphanumeric_and_spaces(tag)
tag = tag[:99] tag = tag[:100]
if tag: if tag:
try: try:
tag = Tag.objects.get(title=tag) tag = Tag.objects.get(title=tag)
@@ -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

@@ -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,7 @@ 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> <h2>{isLmsMode ? translateString('Share with Course') : translateString('Add / Remove from Categories')}</h2>
<button className="category-modal-close" onClick={onCancel}> <button className="category-modal-close" onClick={onCancel}>
× ×
</button> </button>
@@ -192,14 +201,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 +236,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 +260,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

@@ -1,6 +1,7 @@
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;
@@ -8,14 +9,15 @@ interface BulkActionsDropdownProps {
} }
const BULK_ACTIONS = [ const BULK_ACTIONS = [
{ value: 'add-remove-coviewers', label: translateString('Add / Remove Co-Viewers'), enabled: true }, { value: 'add-remove-coviewers', label: inEmbeddedApp() ? translateString('Share with Co-Viewers') : translateString('Add / Remove Co-Viewers'), enabled: true },
{ value: 'add-remove-coeditors', label: translateString('Add / Remove Co-Editors'), enabled: true }, { value: 'add-remove-coeditors', label: inEmbeddedApp() ? translateString('Share with Co-Editors') : translateString('Add / Remove Co-Editors'), enabled: true },
{ value: 'add-remove-coowners', label: translateString('Add / Remove Co-Owners'), enabled: true }, { value: 'add-remove-coowners', label: inEmbeddedApp() ? translateString('Share with Co-Owners') : translateString('Add / Remove Co-Owners'), enabled: true },
{ 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-category', label: inEmbeddedApp() ? translateString('Share with Course Members') : 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 },
{ 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 },
{ value: 'publish-state', label: translateString('Publish State'), enabled: true }, { value: 'publish-state', label: translateString('Publish State'), enabled: true },

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

@@ -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()}
<UnderThumbWrapper title={props.title} link={props.link}> {isSelectMediaMode ? (
{titleComponent()} <UnderThumbWrapper>
{metaComponents()} {titleComponent()}
{descriptionComponent()} {metaComponents()}
</UnderThumbWrapper> {descriptionComponent()}
</UnderThumbWrapper>
) : (
<UnderThumbWrapper title={props.title} link={props.link}>
{titleComponent()}
{metaComponents()}
{descriptionComponent()}
</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()}
<UnderThumbWrapper title={props.title} link={props.link}> {isSelectMediaMode ? (
{titleComponent()} <UnderThumbWrapper>
{metaComponents()} {titleComponent()}
{descriptionComponent()} {metaComponents()}
</UnderThumbWrapper> {descriptionComponent()}
</UnderThumbWrapper>
) : (
<UnderThumbWrapper title={props.title} link={props.link}>
{titleComponent()}
{metaComponents()}
{descriptionComponent()}
</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,19 +145,27 @@ export function MediaItemVideo(props) {
</div> </div>
)} )}
{editMediaComponent()} {!isSelectMediaMode && editMediaComponent()}
{viewMediaComponent()} {!isSelectMediaMode && viewMediaComponent()}
{props.hasMediaViewer ? videoViewerComponent() : thumbnailComponent()} {props.hasMediaViewer ? videoViewerComponent() : thumbnailComponent()}
<UnderThumbWrapper title={props.title} link={props.link}> {isSelectMediaMode ? (
{titleComponent()} <UnderThumbWrapper>
{metaComponents()} {titleComponent()}
{descriptionComponent()} {metaComponents()}
</UnderThumbWrapper> {descriptionComponent()}
</div> </UnderThumbWrapper>
) : (
<UnderThumbWrapper title={props.title} link={props.link}>
{titleComponent()}
{metaComponents()}
{descriptionComponent()}
</UnderThumbWrapper>
)}
{playlistOptionsComponent()} {playlistOptionsComponent()}
</div>
</div> </div>
); );
} }

View File

@@ -79,6 +79,10 @@ function EditMediaButton(props) {
link = '/edit-media.html'; link = '/edit-media.html';
} }
if (link && inEmbeddedApp()) {
link += '&mode=lms_embed_mode';
}
return ( return (
<a href={link} rel="nofollow" title={translateString('Edit media')} className="edit-media-icon"> <a href={link} rel="nofollow" title={translateString('Edit media')} className="edit-media-icon">
<i className="material-icons">edit</i> <i className="material-icons">edit</i>
@@ -94,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');
@@ -220,9 +231,13 @@ export default function ViewerInfoContent(props) {
<MediaMetaField <MediaMetaField
value={categoriesContent} value={categoriesContent}
title={ title={
1 < categoriesContent.length inEmbeddedApp()
? translateString('Categories') ? (1 < categoriesContent.length
: translateString('Category') ? translateString('Courses')
: translateString('Course'))
: (1 < categoriesContent.length
? translateString('Categories')
: translateString('Category'))
} }
id="categories" id="categories"
/> />
@@ -274,7 +289,7 @@ export default function ViewerInfoContent(props) {
</div> </div>
</div> </div>
{!inEmbeddedApp() && <CommentsList />} <CommentsList />
</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(' +

View File

@@ -656,30 +656,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 {

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) {
@@ -372,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">
<InlineTab {!isSelectMediaMode && !inEmbeddedApp() ? (
id="about" <InlineTab
isActive={'about' === this.props.type} id="about"
label={translateString('About')} isActive={'about' === this.props.type}
link={LinksContext._currentValue.profile.about} label={translateString('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 ? (
@@ -403,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}
@@ -606,12 +623,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 &&
@@ -623,18 +634,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();
} }
@@ -669,42 +668,26 @@ 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 && ( {!props.hideChannelBanner && (
<span className="profile-banner-wrap"> <span className="profile-banner-wrap">
{props.author.banner_thumbnail_url ? ( {props.author.banner_thumbnail_url ? (
@@ -768,7 +751,7 @@ export default function ProfilePagesHeader(props) {
)} )}
<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> <div>

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

@@ -8,7 +8,7 @@ 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');

View File

@@ -7,9 +7,9 @@ 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');

View File

@@ -7,9 +7,9 @@ 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');

View File

@@ -3,27 +3,22 @@ import PropTypes from 'prop-types';
import { ApiUrlContext, LinksConsumer, MemberContext } from '../utils/contexts'; import { ApiUrlContext, LinksConsumer, MemberContext } from '../utils/contexts';
import { PageStore, ProfilePageStore } from '../utils/stores'; import { PageStore, ProfilePageStore } from '../utils/stores';
import { ProfilePageActions, PageActions } from '../utils/actions'; import { ProfilePageActions, PageActions } from '../utils/actions';
import { inEmbeddedApp, translateString } from '../utils/helpers/'; import { inEmbeddedApp, inSelectMediaEmbedMode, translateString } 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 { BulkActionConfirmModal } from '../components/BulkActionConfirmModal'; import { BulkActionsModals } from '../components/BulkActionsModals';
import { BulkActionPermissionModal } from '../components/BulkActionPermissionModal';
import { BulkActionPlaylistModal } from '../components/BulkActionPlaylistModal';
import { BulkActionChangeOwnerModal } from '../components/BulkActionChangeOwnerModal';
import { BulkActionPublishStateModal } from '../components/BulkActionPublishStateModal';
import { BulkActionCategoryModal } from '../components/BulkActionCategoryModal';
import { BulkActionTagModal } from '../components/BulkActionTagModal';
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 { withBulkActions } from '../utils/hoc/withBulkActions';
import { Page } from './_Page'; import { Page } from './_Page';
import '../components/profile-page/ProfilePage.scss'; import '../components/profile-page/ProfilePage.scss';
export class ProfileMediaPage extends Page { class ProfileMediaPage extends Page {
constructor(props, pageSlug) { constructor(props, pageSlug) {
super(props, 'string' === typeof pageSlug ? pageSlug : 'author-home'); super(props, 'string' === typeof pageSlug ? pageSlug : 'author-home');
@@ -36,15 +31,7 @@ export class ProfileMediaPage extends Page {
title: this.props.title, title: this.props.title,
query: ProfilePageStore.get('author-query'), query: ProfilePageStore.get('author-query'),
requestUrl: null, requestUrl: null,
selectedMedia: new Set(), selectedMedia: new Set(), // For select media embed mode only
availableMediaIds: [],
showConfirmModal: false,
pendingAction: null,
confirmMessage: '',
listKey: 0,
notificationMessage: '',
showNotification: false,
notificationType: 'success',
hiddenFilters: true, hiddenFilters: true,
hiddenTags: true, hiddenTags: true,
hiddenSorting: true, hiddenSorting: true,
@@ -52,13 +39,6 @@ export class ProfileMediaPage extends Page {
availableTags: [], availableTags: [],
selectedTag: 'all', selectedTag: 'all',
selectedSort: 'date_added_desc', selectedSort: 'date_added_desc',
showPermissionModal: false,
permissionType: null,
showPlaylistModal: false,
showChangeOwnerModal: false,
showPublishStateModal: false,
showCategoryModal: false,
showTagModal: false,
}; };
this.authorDataLoad = this.authorDataLoad.bind(this); this.authorDataLoad = this.authorDataLoad.bind(this);
@@ -66,20 +46,6 @@ export class ProfileMediaPage extends Page {
this.getCountFunc = this.getCountFunc.bind(this); this.getCountFunc = this.getCountFunc.bind(this);
this.changeRequestQuery = this.changeRequestQuery.bind(this); this.changeRequestQuery = this.changeRequestQuery.bind(this);
this.handleMediaSelection = this.handleMediaSelection.bind(this); this.handleMediaSelection = this.handleMediaSelection.bind(this);
this.handleBulkAction = this.handleBulkAction.bind(this);
this.handleConfirmCancel = this.handleConfirmCancel.bind(this);
this.handleConfirmProceed = this.handleConfirmProceed.bind(this);
this.clearSelectionAndRefresh = this.clearSelectionAndRefresh.bind(this);
this.clearSelection = this.clearSelection.bind(this);
this.executeEnableComments = this.executeEnableComments.bind(this);
this.executeDisableComments = this.executeDisableComments.bind(this);
this.executeEnableDownload = this.executeEnableDownload.bind(this);
this.executeDisableDownload = this.executeDisableDownload.bind(this);
this.executeCopyMedia = this.executeCopyMedia.bind(this);
this.showNotification = this.showNotification.bind(this);
this.handleSelectAll = this.handleSelectAll.bind(this);
this.handleDeselectAll = this.handleDeselectAll.bind(this);
this.handleItemsUpdate = this.handleItemsUpdate.bind(this);
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);
@@ -87,24 +53,6 @@ export class ProfileMediaPage 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.handlePermissionModalCancel = this.handlePermissionModalCancel.bind(this);
this.handlePermissionModalSuccess = this.handlePermissionModalSuccess.bind(this);
this.handlePermissionModalError = this.handlePermissionModalError.bind(this);
this.handlePlaylistModalCancel = this.handlePlaylistModalCancel.bind(this);
this.handlePlaylistModalSuccess = this.handlePlaylistModalSuccess.bind(this);
this.handlePlaylistModalError = this.handlePlaylistModalError.bind(this);
this.handleChangeOwnerModalCancel = this.handleChangeOwnerModalCancel.bind(this);
this.handleChangeOwnerModalSuccess = this.handleChangeOwnerModalSuccess.bind(this);
this.handleChangeOwnerModalError = this.handleChangeOwnerModalError.bind(this);
this.handlePublishStateModalCancel = this.handlePublishStateModalCancel.bind(this);
this.handlePublishStateModalSuccess = this.handlePublishStateModalSuccess.bind(this);
this.handlePublishStateModalError = this.handlePublishStateModalError.bind(this);
this.handleCategoryModalCancel = this.handleCategoryModalCancel.bind(this);
this.handleCategoryModalSuccess = this.handleCategoryModalSuccess.bind(this);
this.handleCategoryModalError = this.handleCategoryModalError.bind(this);
this.handleTagModalCancel = this.handleTagModalCancel.bind(this);
this.handleTagModalSuccess = this.handleTagModalSuccess.bind(this);
this.handleTagModalError = this.handleTagModalError.bind(this);
ProfilePageStore.on('load-author-data', this.authorDataLoad); ProfilePageStore.on('load-author-data', this.authorDataLoad);
} }
@@ -202,397 +150,29 @@ export class ProfileMediaPage extends Page {
} }
handleMediaSelection(mediaId, isSelected) { handleMediaSelection(mediaId, isSelected) {
// Only used in select media embed mode; normal mode is handled by bulkActions
this.setState((prevState) => { this.setState((prevState) => {
const newSelectedMedia = new Set(prevState.selectedMedia); const newSelectedMedia = new Set();
if (isSelected) { if (isSelected) {
newSelectedMedia.add(mediaId); newSelectedMedia.add(mediaId);
} else {
newSelectedMedia.delete(mediaId); if (window.parent !== window) {
const baseUrl = window.location.origin;
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
window.parent.postMessage({
type: 'videoSelected',
embedUrl: embedUrl,
videoId: mediaId
}, '*');
}
} }
return { selectedMedia: newSelectedMedia }; return { selectedMedia: newSelectedMedia };
}); });
} }
handleBulkAction(action) {
const selectedCount = this.state.selectedMedia.size;
if (selectedCount === 0) {
return;
}
if (action === 'delete-media') {
this.setState({
showConfirmModal: true,
pendingAction: action,
confirmMessage:
translateString('You are going to delete') +
` ${selectedCount} ` +
translateString('media, are you sure?'),
});
} else if (action === 'enable-comments') {
this.setState({
showConfirmModal: true,
pendingAction: action,
confirmMessage:
translateString('You are going to enable comments to') +
` ${selectedCount} ` +
translateString('media, are you sure?'),
});
} else if (action === 'disable-comments') {
this.setState({
showConfirmModal: true,
pendingAction: action,
confirmMessage:
translateString('You are going to disable comments to') +
` ${selectedCount} ` +
translateString('media, are you sure?'),
});
} else if (action === 'enable-download') {
this.setState({
showConfirmModal: true,
pendingAction: action,
confirmMessage:
translateString('You are going to enable download for') +
` ${selectedCount} ` +
translateString('media, are you sure?'),
});
} else if (action === 'disable-download') {
this.setState({
showConfirmModal: true,
pendingAction: action,
confirmMessage:
translateString('You are going to disable download for') +
` ${selectedCount} ` +
translateString('media, are you sure?'),
});
} else if (action === 'copy-media') {
this.setState({
showConfirmModal: true,
pendingAction: action,
confirmMessage:
translateString('You are going to copy') +
` ${selectedCount} ` +
translateString('media, are you sure?'),
});
} else if (action === 'add-remove-coviewers') {
this.setState({
showPermissionModal: true,
permissionType: 'viewer',
});
} else if (action === 'add-remove-coeditors') {
this.setState({
showPermissionModal: true,
permissionType: 'editor',
});
} else if (action === 'add-remove-coowners') {
this.setState({
showPermissionModal: true,
permissionType: 'owner',
});
} else if (action === 'add-remove-playlist') {
this.setState({
showPlaylistModal: true,
});
} else if (action === 'change-owner') {
this.setState({
showChangeOwnerModal: true,
});
} else if (action === 'publish-state') {
this.setState({
showPublishStateModal: true,
});
} else if (action === 'add-remove-category') {
this.setState({
showCategoryModal: true,
});
} else if (action === 'add-remove-tags') {
this.setState({
showTagModal: true,
});
} else {
// Other actions can be implemented later
}
}
handleConfirmCancel() {
this.setState({
showConfirmModal: false,
pendingAction: null,
confirmMessage: '',
});
}
handleConfirmProceed() {
const action = this.state.pendingAction;
this.setState({
showConfirmModal: false,
pendingAction: null,
confirmMessage: '',
});
if (action === 'delete-media') {
this.executeDeleteMedia();
} else if (action === 'enable-comments') {
this.executeEnableComments();
} else if (action === 'disable-comments') {
this.executeDisableComments();
} else if (action === 'enable-download') {
this.executeEnableDownload();
} else if (action === 'disable-download') {
this.executeDisableDownload();
} else if (action === 'copy-media') {
this.executeCopyMedia();
}
}
executeDeleteMedia() {
const selectedIds = Array.from(this.state.selectedMedia);
const selectedCount = selectedIds.length;
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken(),
},
body: JSON.stringify({
action: 'delete_media',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete media');
}
return response.json();
})
.then((data) => {
const message =
selectedCount === 1
? translateString('The media was deleted successfully.')
: translateString('Successfully deleted') + ` ${selectedCount} ` + translateString('media.');
this.showNotification(message);
this.clearSelectionAndRefresh();
})
.catch((error) => {
this.showNotification(translateString('Failed to delete media. Please try again.'), 'error');
this.clearSelectionAndRefresh();
});
}
getCsrfToken() {
const name = 'csrftoken';
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + '=') {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
clearSelectionAndRefresh() {
// Clear selected media and increment listKey to force re-render
this.setState((prevState) => ({
selectedMedia: new Set(),
listKey: prevState.listKey + 1,
}));
}
clearSelection() {
// Clear selected media without refreshing
this.setState({
selectedMedia: new Set(),
});
}
executeEnableComments() {
const selectedIds = Array.from(this.state.selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken(),
},
body: JSON.stringify({
action: 'enable_comments',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to enable comments');
}
return response.json();
})
.then((data) => {
this.showNotification(translateString('Successfully Enabled comments'));
this.clearSelection();
})
.catch((error) => {
this.showNotification(translateString('Failed to enable comments.'), 'error');
this.clearSelection();
});
}
executeDisableComments() {
const selectedIds = Array.from(this.state.selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken(),
},
body: JSON.stringify({
action: 'disable_comments',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to disable comments');
}
return response.json();
})
.then((data) => {
this.showNotification(translateString('Successfully Disabled comments'));
this.clearSelection();
})
.catch((error) => {
this.showNotification(translateString('Failed to disable comments.'), 'error');
this.clearSelection();
});
}
executeEnableDownload() {
const selectedIds = Array.from(this.state.selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken(),
},
body: JSON.stringify({
action: 'enable_download',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to enable download');
}
return response.json();
})
.then((data) => {
this.showNotification(translateString('Successfully Enabled Download'));
this.clearSelection();
})
.catch((error) => {
this.showNotification(translateString('Failed to enable download.'), 'error');
this.clearSelection();
});
}
executeDisableDownload() {
const selectedIds = Array.from(this.state.selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken(),
},
body: JSON.stringify({
action: 'disable_download',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to disable download');
}
return response.json();
})
.then((data) => {
this.showNotification(translateString('Successfully Disabled Download'));
this.clearSelection();
})
.catch((error) => {
this.showNotification(translateString('Failed to disable download.'), 'error');
this.clearSelection();
});
}
executeCopyMedia() {
const selectedIds = Array.from(this.state.selectedMedia);
fetch('/api/v1/media/user/bulk_actions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken(),
},
body: JSON.stringify({
action: 'copy_media',
media_ids: selectedIds,
}),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to copy media');
}
return response.json();
})
.then((data) => {
this.showNotification(translateString('Successfully Copied'));
this.clearSelectionAndRefresh();
})
.catch((error) => {
this.showNotification(translateString('Failed to copy media.'), 'error');
this.clearSelection();
});
}
showNotification(message, type = 'success') {
this.setState({
notificationMessage: message,
showNotification: true,
notificationType: type,
});
setTimeout(() => {
this.setState({ showNotification: false });
}, 5000);
}
handleItemsUpdate(items) {
// Extract media IDs from loaded items
const mediaIds = items.map((item) => item.friendly_token || item.uid || item.id);
this.setState({ availableMediaIds: mediaIds });
}
handleSelectAll() {
// Select all available media
this.setState({
selectedMedia: new Set(this.state.availableMediaIds),
});
}
handleDeselectAll() {
// Clear all selections
this.setState({
selectedMedia: new Set(),
});
}
onToggleFiltersClick() { onToggleFiltersClick() {
this.setState({ this.setState({
hiddenFilters: !this.state.hiddenFilters, hiddenFilters: !this.state.hiddenFilters,
@@ -619,7 +199,6 @@ export class ProfileMediaPage extends Page {
onTagSelect(tag) { onTagSelect(tag) {
this.setState({ selectedTag: tag }, () => { this.setState({ selectedTag: tag }, () => {
// Apply tag filter
this.onFiltersUpdate({ this.onFiltersUpdate({
media_type: this.state.filterArgs.includes('media_type') media_type: this.state.filterArgs.includes('media_type')
? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1] ? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1]
@@ -641,7 +220,6 @@ export class ProfileMediaPage extends Page {
onSortSelect(sortOption) { onSortSelect(sortOption) {
this.setState({ selectedSort: sortOption }, () => { this.setState({ selectedSort: sortOption }, () => {
// Apply sort filter
this.onFiltersUpdate({ this.onFiltersUpdate({
media_type: this.state.filterArgs.includes('media_type') media_type: this.state.filterArgs.includes('media_type')
? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1] ? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1]
@@ -690,19 +268,16 @@ export class ProfileMediaPage extends Page {
break; break;
} }
// Handle duration filter
if (updatedArgs.duration && updatedArgs.duration !== 'all') { if (updatedArgs.duration && updatedArgs.duration !== 'all') {
args.duration = updatedArgs.duration; args.duration = updatedArgs.duration;
} }
// Handle publish state filter
if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') { if (updatedArgs.publish_state && updatedArgs.publish_state !== 'all') {
args.publish_state = updatedArgs.publish_state; args.publish_state = updatedArgs.publish_state;
} }
switch (updatedArgs.sort_by) { switch (updatedArgs.sort_by) {
case 'date_added_desc': case 'date_added_desc':
// Default sorting, no need to add parameters
break; break;
case 'date_added_asc': case 'date_added_asc':
args.ordering = 'asc'; args.ordering = 'asc';
@@ -727,7 +302,6 @@ export class ProfileMediaPage extends Page {
break; break;
} }
// Handle tag filter
if (updatedArgs.tag && updatedArgs.tag !== 'all') { if (updatedArgs.tag && updatedArgs.tag !== 'all') {
args.t = updatedArgs.tag; args.t = updatedArgs.tag;
} }
@@ -743,10 +317,8 @@ export class ProfileMediaPage extends Page {
this.setState( this.setState(
{ {
filterArgs: newArgs.length ? '&' + newArgs.join('&') : '', filterArgs: newArgs.length ? '&' + newArgs.join('&') : '',
selectedMedia: new Set(), // Clear selected items when filter changes
}, },
function () { function () {
// Update the request URL with new filter args
if (!this.state.author) { if (!this.state.author) {
return; return;
} }
@@ -773,137 +345,7 @@ export class ProfileMediaPage extends Page {
); );
} }
handlePermissionModalCancel() {
this.setState({
showPermissionModal: false,
permissionType: null,
});
}
handlePermissionModalSuccess(message) {
this.showNotification(message);
this.clearSelection();
this.setState({
showPermissionModal: false,
permissionType: null,
});
}
handlePermissionModalError(message) {
this.showNotification(message, 'error');
this.setState({
showPermissionModal: false,
permissionType: null,
});
}
handlePlaylistModalCancel() {
this.setState({
showPlaylistModal: false,
});
}
handlePlaylistModalSuccess(message) {
this.showNotification(message);
this.clearSelection();
this.setState({
showPlaylistModal: false,
});
}
handlePlaylistModalError(message) {
this.showNotification(message, 'error');
this.setState({
showPlaylistModal: false,
});
}
handleChangeOwnerModalCancel() {
this.setState({
showChangeOwnerModal: false,
});
}
handleChangeOwnerModalSuccess(message) {
this.showNotification(message);
this.clearSelectionAndRefresh();
this.setState({
showChangeOwnerModal: false,
});
}
handleChangeOwnerModalError(message) {
this.showNotification(message, 'error');
this.setState({
showChangeOwnerModal: false,
});
}
handlePublishStateModalCancel() {
this.setState({
showPublishStateModal: false,
});
}
handlePublishStateModalSuccess(message) {
this.showNotification(message);
this.clearSelectionAndRefresh();
this.setState({
showPublishStateModal: false,
});
}
handlePublishStateModalError(message) {
this.showNotification(message, 'error');
this.setState({
showPublishStateModal: false,
});
}
handleCategoryModalCancel() {
this.setState({
showCategoryModal: false,
});
}
handleCategoryModalSuccess(message) {
this.showNotification(message);
this.clearSelection();
this.setState({
showCategoryModal: false,
});
}
handleCategoryModalError(message) {
this.showNotification(message, 'error');
this.setState({
showCategoryModal: false,
});
}
handleTagModalCancel() {
this.setState({
showTagModal: false,
});
}
handleTagModalSuccess(message) {
this.showNotification(message);
this.clearSelection();
this.setState({
showTagModal: false,
});
}
handleTagModalError(message) {
this.showNotification(message, 'error');
this.setState({
showTagModal: false,
});
}
onResponseDataLoaded(responseData) { onResponseDataLoaded(responseData) {
// Extract tags from response
if (responseData && responseData.tags) { if (responseData && responseData.tags) {
const tags = responseData.tags const tags = responseData.tags
.split(',') .split(',')
@@ -917,8 +359,8 @@ export class ProfileMediaPage extends Page {
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 (excluding default sort and tags)
const hasActiveFilters = const hasActiveFilters =
this.state.filterArgs && this.state.filterArgs &&
(this.state.filterArgs.includes('media_type=') || (this.state.filterArgs.includes('media_type=') ||
@@ -948,15 +390,16 @@ export class ProfileMediaPage extends Page {
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.state.selectedMedia.size} showBulkActions={!isSelectMediaMode && isMediaAuthor}
totalCount={this.state.availableMediaIds.length} selectedCount={isSelectMediaMode ? this.state.selectedMedia.size : this.props.bulkActions.selectedMedia.size}
onBulkAction={this.handleBulkAction} totalCount={isSelectMediaMode ? 0 : this.props.bulkActions.availableMediaIds.length}
onSelectAll={this.handleSelectAll} onBulkAction={this.props.bulkActions.handleBulkAction}
onDeselectAll={this.handleDeselectAll} onSelectAll={this.props.bulkActions.handleSelectAll}
showAddMediaButton={isMediaAuthor} onDeselectAll={this.props.bulkActions.handleDeselectAll}
showAddMediaButton={!isSelectMediaMode && isMediaAuthor}
> >
<ProfileMediaFilters <ProfileMediaFilters
hidden={this.state.hiddenFilters} hidden={this.state.hiddenFilters}
@@ -972,105 +415,51 @@ export class ProfileMediaPage extends Page {
/> />
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} /> <ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<LazyLoadItemListAsync <LazyLoadItemListAsync
key={`${this.state.requestUrl}-${this.state.listKey}`} key={`${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={isMediaAuthor}
showSelection={isMediaAuthor} showSelection={isMediaAuthor || isSelectMediaMode}
hasAnySelection={this.state.selectedMedia.size > 0} hasAnySelection={isSelectMediaMode ? this.state.selectedMedia.size > 0 : this.props.bulkActions.selectedMedia.size > 0}
selectedMedia={this.state.selectedMedia} selectedMedia={isSelectMediaMode ? this.state.selectedMedia : this.props.bulkActions.selectedMedia}
onMediaSelection={this.handleMediaSelection} onMediaSelection={isSelectMediaMode ? this.handleMediaSelection : this.props.bulkActions.handleMediaSelection}
onItemsUpdate={this.handleItemsUpdate} onItemsUpdate={!isSelectMediaMode ? this.props.bulkActions.handleItemsUpdate : undefined}
onResponseDataLoaded={this.onResponseDataLoaded} onResponseDataLoaded={this.onResponseDataLoaded}
/> />
</MediaListWrapper> </MediaListWrapper>
</ProfilePagesContent> </ProfilePagesContent>
) : null, ) : null,
<BulkActionConfirmModal this.state.author && isMediaAuthor && !isSelectMediaMode ? (
key="BulkActionConfirmModal" <BulkActionsModals
isOpen={this.state.showConfirmModal} key="BulkActionsModals"
message={this.state.confirmMessage} {...this.props.bulkActions}
onCancel={this.handleConfirmCancel} selectedMediaIds={Array.from(this.props.bulkActions.selectedMedia)}
onProceed={this.handleConfirmProceed} csrfToken={this.props.bulkActions.getCsrfToken()}
/>, username={this.state.author.username}
<BulkActionPermissionModal onConfirmCancel={this.props.bulkActions.handleConfirmCancel}
key="BulkActionPermissionModal" onConfirmProceed={this.props.bulkActions.handleConfirmProceed}
isOpen={this.state.showPermissionModal} onPermissionModalCancel={this.props.bulkActions.handlePermissionModalCancel}
permissionType={this.state.permissionType} onPermissionModalSuccess={this.props.bulkActions.handlePermissionModalSuccess}
selectedMediaIds={Array.from(this.state.selectedMedia)} onPermissionModalError={this.props.bulkActions.handlePermissionModalError}
onCancel={this.handlePermissionModalCancel} onPlaylistModalCancel={this.props.bulkActions.handlePlaylistModalCancel}
onSuccess={this.handlePermissionModalSuccess} onPlaylistModalSuccess={this.props.bulkActions.handlePlaylistModalSuccess}
onError={this.handlePermissionModalError} onPlaylistModalError={this.props.bulkActions.handlePlaylistModalError}
csrfToken={this.getCsrfToken()} onChangeOwnerModalCancel={this.props.bulkActions.handleChangeOwnerModalCancel}
/>, onChangeOwnerModalSuccess={this.props.bulkActions.handleChangeOwnerModalSuccess}
<BulkActionPlaylistModal onChangeOwnerModalError={this.props.bulkActions.handleChangeOwnerModalError}
key="BulkActionPlaylistModal" onPublishStateModalCancel={this.props.bulkActions.handlePublishStateModalCancel}
isOpen={this.state.showPlaylistModal} onPublishStateModalSuccess={this.props.bulkActions.handlePublishStateModalSuccess}
selectedMediaIds={Array.from(this.state.selectedMedia)} onPublishStateModalError={this.props.bulkActions.handlePublishStateModalError}
onCancel={this.handlePlaylistModalCancel} onCategoryModalCancel={this.props.bulkActions.handleCategoryModalCancel}
onSuccess={this.handlePlaylistModalSuccess} onCategoryModalSuccess={this.props.bulkActions.handleCategoryModalSuccess}
onError={this.handlePlaylistModalError} onCategoryModalError={this.props.bulkActions.handleCategoryModalError}
csrfToken={this.getCsrfToken()} onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
username={this.state.author ? this.state.author.username : ''} onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
/>, onTagModalError={this.props.bulkActions.handleTagModalError}
<BulkActionChangeOwnerModal />
key="BulkActionChangeOwnerModal"
isOpen={this.state.showChangeOwnerModal}
selectedMediaIds={Array.from(this.state.selectedMedia)}
onCancel={this.handleChangeOwnerModalCancel}
onSuccess={this.handleChangeOwnerModalSuccess}
onError={this.handleChangeOwnerModalError}
csrfToken={this.getCsrfToken()}
/>,
<BulkActionPublishStateModal
key="BulkActionPublishStateModal"
isOpen={this.state.showPublishStateModal}
selectedMediaIds={Array.from(this.state.selectedMedia)}
onCancel={this.handlePublishStateModalCancel}
onSuccess={this.handlePublishStateModalSuccess}
onError={this.handlePublishStateModalError}
csrfToken={this.getCsrfToken()}
/>,
<BulkActionCategoryModal
key="BulkActionCategoryModal"
isOpen={this.state.showCategoryModal}
selectedMediaIds={Array.from(this.state.selectedMedia)}
onCancel={this.handleCategoryModalCancel}
onSuccess={this.handleCategoryModalSuccess}
onError={this.handleCategoryModalError}
csrfToken={this.getCsrfToken()}
/>,
<BulkActionTagModal
key="BulkActionTagModal"
isOpen={this.state.showTagModal}
selectedMediaIds={Array.from(this.state.selectedMedia)}
onCancel={this.handleTagModalCancel}
onSuccess={this.handleTagModalSuccess}
onError={this.handleTagModalError}
csrfToken={this.getCsrfToken()}
/>,
this.state.showNotification ? (
<div
key="SimpleNotification"
style={{
position: 'fixed',
bottom: '20px',
left: '260px',
backgroundColor: this.state.notificationType === 'error' ? '#f44336' : '#4CAF50',
color: 'white',
padding: '16px 24px',
borderRadius: '4px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
zIndex: 1000,
fontSize: '14px',
fontWeight: '500',
}}
>
{this.state.notificationMessage}
</div>
) : null, ) : null,
]; ];
} }
@@ -1078,8 +467,17 @@ export class ProfileMediaPage extends Page {
ProfileMediaPage.propTypes = { ProfileMediaPage.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
bulkActions: PropTypes.object.isRequired,
}; };
ProfileMediaPage.defaultProps = { ProfileMediaPage.defaultProps = {
title: 'Uploads', title: 'Uploads',
}; };
// Export the raw class for subclasses to extend
export { ProfileMediaPage as ProfileMediaPageBase };
// Export the HOC-wrapped version as the renderable component
const WrappedProfileMediaPage = withBulkActions(ProfileMediaPage);
export { WrappedProfileMediaPage as ProfileMediaPage };
export default WrappedProfileMediaPage;

View File

@@ -6,9 +6,9 @@ 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');

View File

@@ -11,7 +11,7 @@ import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFi
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 { BulkActionsModals } from '../components/BulkActionsModals'; import { BulkActionsModals } from '../components/BulkActionsModals';
import { inEmbeddedApp, 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';
@@ -51,6 +51,7 @@ class ProfileSharedByMePage 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 +65,7 @@ class ProfileSharedByMePage 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);
} }
@@ -343,10 +345,55 @@ class ProfileSharedByMePage extends Page {
} }
} }
handleMediaSelection(mediaId, isSelected) {
const isSelectMediaMode = inSelectMediaEmbedMode();
this.setState((prevState) => {
const newSelectedMedia = new Set();
// In select media mode, only allow single selection
if (isSelectMediaMode) {
if (isSelected) {
newSelectedMedia.add(mediaId);
console.log('Selected media item:', mediaId);
// Send postMessage to parent window (Moodle TinyMCE plugin)
if (window.parent !== window) {
// Construct the embed URL
const baseUrl = window.location.origin;
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
// Send message in the format expected by the Moodle plugin
window.parent.postMessage({
type: 'videoSelected',
embedUrl: embedUrl,
videoId: mediaId
}, '*');
console.log('Sent postMessage to parent:', { embedUrl, videoId: mediaId });
}
}
} else {
// Normal mode: should not reach here as bulk actions handle this
newSelectedMedia.clear();
prevState.selectedMedia.forEach((id) => newSelectedMedia.add(id));
if (isSelected) {
newSelectedMedia.add(mediaId);
} else {
newSelectedMedia.delete(mediaId);
}
}
return { selectedMedia: newSelectedMedia };
});
}
pageContent() { 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 = const hasActiveFilters =
@@ -375,11 +422,12 @@ class ProfileSharedByMePage extends Page {
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}
@@ -396,19 +444,19 @@ class ProfileSharedByMePage extends Page {
/> />
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} /> <ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<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} />
@@ -416,7 +464,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 { inEmbeddedApp, translateString } from '../utils/helpers'; import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
import { Page } from './_Page'; import { Page } from './_Page';
@@ -49,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);
@@ -62,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);
} }
@@ -341,10 +343,55 @@ export class ProfileSharedWithMePage extends Page {
} }
} }
handleMediaSelection(mediaId, isSelected) {
const isSelectMediaMode = inSelectMediaEmbedMode();
this.setState((prevState) => {
const newSelectedMedia = new Set();
// In select media mode, only allow single selection
if (isSelectMediaMode) {
if (isSelected) {
newSelectedMedia.add(mediaId);
console.log('Selected media item:', mediaId);
// Send postMessage to parent window (Moodle TinyMCE plugin)
if (window.parent !== window) {
// Construct the embed URL
const baseUrl = window.location.origin;
const embedUrl = `${baseUrl}/embed?m=${mediaId}`;
// Send message in the format expected by the Moodle plugin
window.parent.postMessage({
type: 'videoSelected',
embedUrl: embedUrl,
videoId: mediaId
}, '*');
console.log('Sent postMessage to parent:', { embedUrl, videoId: mediaId });
}
}
} else {
// Normal mode: no selection UI in this page normally
newSelectedMedia.clear();
prevState.selectedMedia.forEach((id) => newSelectedMedia.add(id));
if (isSelected) {
newSelectedMedia.add(mediaId);
} else {
newSelectedMedia.delete(mediaId);
}
}
return { selectedMedia: newSelectedMedia };
});
}
pageContent() { 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 = const hasActiveFilters =
@@ -372,7 +419,11 @@ export class ProfileSharedWithMePage extends Page {
) : null, ) : null,
this.state.author ? ( this.state.author ? (
<ProfilePagesContent key="ProfilePagesContent"> <ProfilePagesContent key="ProfilePagesContent">
<MediaListWrapper title={this.state.title} className="items-list-ver"> <MediaListWrapper
title={inEmbeddedApp() ? undefined : this.state.title}
className="items-list-ver"
style={inEmbeddedApp() ? { marginTop: '24px' } : undefined}
>
<ProfileMediaFilters <ProfileMediaFilters
hidden={this.state.hiddenFilters} hidden={this.state.hiddenFilters}
tags={this.state.availableTags} tags={this.state.availableTags}
@@ -393,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

@@ -45,12 +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 isMediaPage = useMemo(() => PageStore.get('current-page') === 'media', []); const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media' || window.MediaCMS?.mediaId !== undefined, []);
const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []); const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []);
const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar')); const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-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 = () => {

View File

@@ -3,18 +3,49 @@ export function inEmbeddedApp() {
const params = new URL(globalThis.location.href).searchParams; const params = new URL(globalThis.location.href).searchParams;
const mode = params.get('mode'); const mode = params.get('mode');
if (mode === 'embed_mode') { if (mode === 'lms_embed_mode') {
sessionStorage.setItem('media_cms_embed_mode', 'true'); sessionStorage.setItem('lms_embed_mode', 'true');
return true; return true;
} }
if (mode === 'standard') { if (mode === 'standard') {
sessionStorage.removeItem('media_cms_embed_mode'); sessionStorage.removeItem('lms_embed_mode');
return false; return false;
} }
return sessionStorage.getItem('media_cms_embed_mode') === 'true'; return sessionStorage.getItem('lms_embed_mode') === 'true';
} catch (e) { } catch (e) {
return false; 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,4 +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 * from './embeddedApp'; export { inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode } from './embeddedApp';

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

@@ -195,13 +195,18 @@ class MediaPageStore extends EventEmitter {
this.emit('loaded_media_data'); this.emit('loaded_media_data');
} }
this.loadPlaylists(); // Skip loading playlists and comments when in embed mode (to reduce API calls)
if (MediaCMS.features.media.actions.comment_mention === true) { const isEmbedMode = window.location.pathname.startsWith('/embed');
this.loadUsers();
}
if (this.mediacms_config.member.can.readComment) { if (!isEmbedMode) {
this.loadComments(); this.loadPlaylists();
if (MediaCMS.features.media.actions.comment_mention === true) {
this.loadUsers();
}
if (this.mediacms_config.member.can.readComment) {
this.loadComments();
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,302 +0,0 @@
#!/bin/bash
# should be run as root on a rhel8-like system
function update_permissions
{
# fix permissions of /srv/mediacms directory
chown -R nginx:root $1
}
echo "Welcome to the MediacMS installation!";
if [ `id -u` -ne 0 ]; then
echo "Please run as root user"
exit
fi
while true; do
read -p "
This script will attempt to perform a system update, install required dependencies, and configure PostgreSQL, NGINX, Redis and a few other utilities.
It is expected to run on a new system **with no running instances of any these services**. Make sure you check the script before you continue. Then enter y or n
" yn
case $yn in
[Yy]* ) echo "OK!"; break;;
[Nn]* ) echo "Have a great day"; exit;;
* ) echo "Please answer y or n.";;
esac
done
# update configuration files
sed -i 's/\/home\/mediacms\.io\/mediacms\/Bento4-SDK-1-6-0-637\.x86_64-unknown-linux\/bin\/mp4hls/\/srv\/mediacms\/bento4\/bin\/mp4hls/g' cms/settings.py
sed -i 's/www-data/nginx/g;s/\/home\/mediacms\.io\/mediacms\/logs/\/var\/log\/mediacms/g;s/\/home\/mediacms\.io\/mediacms/\/srv\/mediacms/g;s/\/home\/mediacms\.io\/bin/\/srv\/mediacms\/virtualenv\/bin/g' deploy/local_install/celery_*.service
sed -i 's/\/home\/mediacms\.io\/mediacms/\/srv\/mediacms/g' deploy/local_install/mediacms.io
sed -i 's/\/home\/mediacms\.io\/bin/\/srv\/mediacms\/virtualenv\/bin/g;s/\/home\/mediacms\.io\/mediacms/\/srv\/mediacms/g' deploy/local_install/mediacms.service
sed -i 's/\/home\/mediacms\.io\/mediacms/\/var\/log\/mediacms/g' deploy/local_install/mediacms_logrorate
sed -i 's/www-data/nginx/g' deploy/local_install/nginx.conf
sed -i 's/www-data/nginx/g;s/\/home\/mediacms\.io\/mediacms\/logs/\/var\/log\/mediacms/g;s/\/home\/mediacms\.io\/mediacms/\/srv\/mediacms/g;s/\/home\/mediacms\.io/\/srv\/mediacms\/virtualenv/g' deploy/local_install/uwsgi.ini
osVersion=
if [[ -f /etc/os-release ]]; then
osVersion=$(grep ^ID /etc/os-release)
fi
if [[ $osVersion == *"fedora"* ]] || [[ $osVersion == *"rhel"* ]] || [[ $osVersion == *"centos"* ]] || [[ *"rocky"* ]]; then
dnf install -y epel-release https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm yum-utils
yum-config-manager --enable powertools
dnf install -y python3-virtualenv python39-devel redis postgresql postgresql-server nginx git gcc vim unzip ImageMagick python3-certbot-nginx certbot wget xz ffmpeg policycoreutils-devel cmake gcc gcc-c++ wget git bsdtar
else
echo "unsupported or unknown os"
exit -1
fi
# fix permissions of /srv/mediacms directory
update_permissions /srv/mediacms/
read -p "Enter portal URL, or press enter for localhost : " FRONTEND_HOST
read -p "Enter portal name, or press enter for 'MediaCMS : " PORTAL_NAME
[ -z "$PORTAL_NAME" ] && PORTAL_NAME='MediaCMS'
[ -z "$FRONTEND_HOST" ] && FRONTEND_HOST='localhost'
echo "Configuring postgres"
if [ ! command -v postgresql-setup > /dev/null 2>&1 ]; then
echo "Something went wrong, the command 'postgresql-setup' was not found in the system path."
exit -1
fi
postgresql-setup --initdb
# set authentication method for mediacms user to scram-sha-256
sed -i 's/.*password_encryption.*/password_encryption = scram-sha-256/' /var/lib/pgsql/data/postgresql.conf
sed -i '/# IPv4 local connections:/a host\tmediacms\tmediacms\t127.0.0.1/32\tscram-sha-256' /var/lib/pgsql/data/pg_hba.conf
systemctl enable postgresql.service --now
su -c "psql -c \"CREATE DATABASE mediacms\"" postgres
su -c "psql -c \"CREATE USER mediacms WITH ENCRYPTED PASSWORD 'mediacms'\"" postgres
su -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE mediacms TO mediacms\"" postgres
echo 'Creating python virtualenv on /srv/mediacms/virtualenv/'
mkdir /srv/mediacms/virtualenv/
cd /srv/mediacms/virtualenv/
virtualenv . --python=python3
source /srv/mediacms/virtualenv/bin/activate
cd /srv/mediacms/
pip install -r requirements.txt
systemctl enable redis.service --now
SECRET_KEY=`python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'`
# remove http or https prefix
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/http:\/\///g'`
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/https:\/\///g'`
FRONTEND_HOST_HTTP_PREFIX='http://'$FRONTEND_HOST
echo 'FRONTEND_HOST='\'"$FRONTEND_HOST_HTTP_PREFIX"\' >> cms/local_settings.py
echo 'PORTAL_NAME='\'"$PORTAL_NAME"\' >> cms/local_settings.py
echo "SSL_FRONTEND_HOST = FRONTEND_HOST.replace('http', 'https')" >> cms/local_settings.py
echo 'SECRET_KEY='\'"$SECRET_KEY"\' >> cms/local_settings.py
echo "LOCAL_INSTALL = True" >> cms/local_settings.py
mkdir /var/log/mediacms/
mkdir pids
update_permissions /var/log/mediacms/
python manage.py migrate
python manage.py loaddata fixtures/encoding_profiles.json
python manage.py loaddata fixtures/categories.json
python manage.py collectstatic --noinput
ADMIN_PASS=`python -c "import secrets;chars = 'abcdefghijklmnopqrstuvwxyz0123456789';print(''.join(secrets.choice(chars) for i in range(10)))"`
echo "from users.models import User; User.objects.create_superuser('admin', 'admin@example.com', '$ADMIN_PASS')" | python manage.py shell
echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell
update_permissions /srv/mediacms/
cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service
cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service
cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service
cp deploy/local_install/mediacms.service /etc/systemd/system/mediacms.service
mkdir -p /etc/letsencrypt/live/$FRONTEND_HOST
mkdir -p /etc/nginx/sites-enabled
mkdir -p /etc/nginx/sites-available
mkdir -p /etc/nginx/dhparams/
rm -rf /etc/nginx/conf.d/default.conf
rm -rf /etc/nginx/sites-enabled/default
cp deploy/local_install/mediacms.io_fullchain.pem /etc/letsencrypt/live/$FRONTEND_HOST/fullchain.pem
cp deploy/local_install/mediacms.io_privkey.pem /etc/letsencrypt/live/$FRONTEND_HOST/privkey.pem
cp deploy/local_install/mediacms.io /etc/nginx/sites-available/mediacms.io
ln -s /etc/nginx/sites-available/mediacms.io /etc/nginx/sites-enabled/mediacms.io
cp deploy/local_install/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
cp deploy/local_install/nginx.conf /etc/nginx/
# attempt to get a valid certificate for specified domain
while true ; do
echo "Would you like to run [c]ertbot, or [s]kip?"
read -p " : " certbotConfig
case $certbotConfig in
[cC*] )
if [ "$FRONTEND_HOST" != "localhost" ]; then
systemctl start
echo 'attempt to get a valid certificate for specified url $FRONTEND_HOST'
certbot --nginx -n --agree-tos --register-unsafely-without-email -d $FRONTEND_HOST
certbot --nginx -n --agree-tos --register-unsafely-without-email -d $FRONTEND_HOST
# unfortunately for some reason it needs to be run two times in order to create the entries
# and directory structure!!!
systemctl stop nginx
# Generate individual DH params
openssl dhparam -out /etc/nginx/dhparams/dhparams.pem 4096
fi
break
;;
[sS*] )
echo "will not call certbot utility to update ssl certificate for url 'localhost', using default ssl certificate"
cp deploy/local_install/dhparams.pem /etc/nginx/dhparams/dhparams.pem
break
;;
* )
echo "Unknown option: $certbotConfig"
;;
esac
done
# configure bento4 utility installation, for HLS
while true ; do
echo "Configuring Bento4"
echo "Would you like to [d]ownload a pre-compiled bento4 binary, or [b]uild it now?"
read -p "b/d : " bentoConfig
case $bentoConfig in
[bB*] )
echo "Building bento4 from source"
git clone -b v1.6.0-640 https://github.com/axiomatic-systems/Bento4 /srv/mediacms/bento4
cd /srv/mediacms/bento4/
mkdir bin
cd /srv/mediacms/bento4/bin/
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
chmod +x ../Source/Python/utils/mp4-hls.py
echo -e '#!/bin/bash' >> mp4hls
echo -e 'BASEDIR=$(pwd)' >> mp4hls
echo -e 'exec python3 "$BASEDIR/../Source/Python/utils/mp4-hls.py"' >> mp4hls
chmod +x mp4hls
break
;;
[dD*] )
cd /srv/mediacms/
wget http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
bsdtar -xf Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -s '/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/bento4/'
break
;;
* )
echo "Unknown option: $bentoConfig"
;;
esac
done
mkdir /srv/mediacms/media_files/hls
# update permissions
update_permissions /srv/mediacms/
# configure selinux
while true ; do
echo "Configuring SELinux"
echo "Would you like to [d]isable SELinux until next reboot, [c]onfigure our SELinux module, or [s]kip and not do any SELinux confgiguration?"
read -p "d/c/s : " seConfig
case $seConfig in
[Dd]* )
echo "Disabling SELinux until next reboot"
break
;;
[Cc]* )
echo "Configuring custom mediacms selinux module"
semanage fcontext -a -t bin_t /srv/mediacms/virtualenv/bin/
semanage fcontext -a -t httpd_sys_content_t "/srv/mediacms(/.*)?"
restorecon -FRv /srv/mediacms/
sebools=(httpd_can_network_connect httpd_graceful_shutdown httpd_can_network_relay nis_enabled httpd_setrlimit domain_can_mmap_files)
for bool in "${sebools[@]}"
do
setsebool -P $bool 1
done
cd /srv/mediacms/deploy/local_install/
make -f /usr/share/selinux/devel/Makefile selinux-mediacms.pp
semodule -i selinux-mediacms.pp
break
;;
[Ss]* )
echo "Skipping SELinux configuration"
break
;;
* )
echo "Unknown option: $seConfig"
;;
esac
done
# configure firewall
if command -v firewall-cmd > /dev/null 2>&1 ; then
while true ; do
echo "Configuring firewall"
echo "Would you like to configure http, https, or skip and not do any firewall configuration?"
read -p "http/https/skip : " fwConfig
case $fwConfig in
http )
echo "Opening port 80 until next reboot"
firewall-cmd --add-port=80/tcp
break
;;
https )
echo "Opening port 443 permanently"
firewall-cmd --add-port=443/tcp --permanent
firewall-cmd --reload
break
;;
skip )
echo "Skipping firewall configuration"
break
;;
* )
echo "Unknown option: $fwConfig"
;;
esac
done
fi
systemctl daemon-reload
systemctl start celery_long.service
systemctl start celery_short.service
systemctl start celery_beat.service
systemctl start mediacms.service
systemctl start nginx.service
echo 'MediaCMS installation completed, open browser on http://'"$FRONTEND_HOST"' and login with user admin and password '"$ADMIN_PASS"''

View File

@@ -1,140 +0,0 @@
#!/bin/bash
# should be run as root and only on Ubuntu 20/22, Debian 10/11 (Buster/Bullseye) versions!
echo "Welcome to the MediacMS installation!";
if [ `id -u` -ne 0 ]
then echo "Please run as root"
exit
fi
while true; do
read -p "
This script will attempt to perform a system update and install services including PostgreSQL, nginx and Django.
It is expected to run on a new system **with no running instances of any these services**.
This has been tested only in Ubuntu Linux 22 and 24. Make sure you check the script before you continue. Then enter yes or no
" yn
case $yn in
[Yy]* ) echo "OK!"; break;;
[Nn]* ) echo "Have a great day"; exit;;
* ) echo "Please answer yes or no.";;
esac
done
apt-get update && apt-get -y upgrade && apt-get install pkg-config python3-venv python3-dev virtualenv redis-server postgresql nginx git gcc vim unzip imagemagick procps libxml2-dev libxmlsec1-dev libxmlsec1-openssl python3-certbot-nginx certbot wget xz-utils -y
# install ffmpeg
echo "Downloading and installing ffmpeg"
wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
mkdir -p tmp
tar -xf ffmpeg-release-amd64-static.tar.xz --strip-components 1 -C tmp
cp -v tmp/{ffmpeg,ffprobe,qt-faststart} /usr/local/bin
rm -rf tmp ffmpeg-release-amd64-static.tar.xz
echo "ffmpeg installed to /usr/local/bin"
read -p "Enter portal URL, or press enter for localhost : " FRONTEND_HOST
read -p "Enter portal name, or press enter for 'MediaCMS : " PORTAL_NAME
[ -z "$PORTAL_NAME" ] && PORTAL_NAME='MediaCMS'
[ -z "$FRONTEND_HOST" ] && FRONTEND_HOST='localhost'
echo 'Creating database to be used in MediaCMS'
su -c "psql -c \"CREATE DATABASE mediacms\"" postgres
su -c "psql -c \"CREATE USER mediacms WITH ENCRYPTED PASSWORD 'mediacms'\"" postgres
su -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE mediacms TO mediacms\"" postgres
su -c "psql -d mediacms -c \"GRANT CREATE, USAGE ON SCHEMA public TO mediacms\"" postgres
echo 'Creating python virtualenv on /home/mediacms.io'
cd /home/mediacms.io
virtualenv . --python=python3
source /home/mediacms.io/bin/activate
cd mediacms
pip install --no-binary lxml,xmlsec -r requirements.txt
SECRET_KEY=`python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'`
# remove http or https prefix
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/http:\/\///g'`
FRONTEND_HOST=`echo "$FRONTEND_HOST" | sed -r 's/https:\/\///g'`
sed -i s/localhost/$FRONTEND_HOST/g deploy/local_install/mediacms.io
FRONTEND_HOST_HTTP_PREFIX='http://'$FRONTEND_HOST
echo 'FRONTEND_HOST='\'"$FRONTEND_HOST_HTTP_PREFIX"\' >> cms/local_settings.py
echo 'PORTAL_NAME='\'"$PORTAL_NAME"\' >> cms/local_settings.py
echo "SSL_FRONTEND_HOST = FRONTEND_HOST.replace('http', 'https')" >> cms/local_settings.py
echo 'SECRET_KEY='\'"$SECRET_KEY"\' >> cms/local_settings.py
echo "LOCAL_INSTALL = True" >> cms/local_settings.py
mkdir logs
mkdir pids
python manage.py migrate
python manage.py loaddata fixtures/encoding_profiles.json
python manage.py loaddata fixtures/categories.json
python manage.py collectstatic --noinput
ADMIN_PASS=`python -c "import secrets;chars = 'abcdefghijklmnopqrstuvwxyz0123456789';print(''.join(secrets.choice(chars) for i in range(10)))"`
echo "from users.models import User; User.objects.create_superuser('admin', 'admin@example.com', '$ADMIN_PASS')" | python manage.py shell
echo "from django.contrib.sites.models import Site; Site.objects.update(name='$FRONTEND_HOST', domain='$FRONTEND_HOST')" | python manage.py shell
chown -R www-data. /home/mediacms.io/
cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service && systemctl enable celery_long && systemctl start celery_long
cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service && systemctl enable celery_short && systemctl start celery_short
cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service && systemctl enable celery_beat &&systemctl start celery_beat
cp deploy/local_install/mediacms.service /etc/systemd/system/mediacms.service && systemctl enable mediacms.service && systemctl start mediacms.service
mkdir -p /etc/letsencrypt/live/mediacms.io/
mkdir -p /etc/letsencrypt/live/$FRONTEND_HOST
mkdir -p /etc/nginx/sites-enabled
mkdir -p /etc/nginx/sites-available
mkdir -p /etc/nginx/dhparams/
rm -rf /etc/nginx/conf.d/default.conf
rm -rf /etc/nginx/sites-enabled/default
cp deploy/local_install/mediacms.io_fullchain.pem /etc/letsencrypt/live/$FRONTEND_HOST/fullchain.pem
cp deploy/local_install/mediacms.io_privkey.pem /etc/letsencrypt/live/$FRONTEND_HOST/privkey.pem
cp deploy/local_install/dhparams.pem /etc/nginx/dhparams/dhparams.pem
cp deploy/local_install/mediacms.io /etc/nginx/sites-available/mediacms.io
ln -s /etc/nginx/sites-available/mediacms.io /etc/nginx/sites-enabled/mediacms.io
cp deploy/local_install/uwsgi_params /etc/nginx/sites-enabled/uwsgi_params
cp deploy/local_install/nginx.conf /etc/nginx/
systemctl stop nginx
systemctl start nginx
# attempt to get a valid certificate for specified domain
if [ "$FRONTEND_HOST" != "localhost" ]; then
echo 'attempt to get a valid certificate for specified url $FRONTEND_HOST'
certbot --nginx -n --agree-tos --register-unsafely-without-email -d $FRONTEND_HOST
certbot --nginx -n --agree-tos --register-unsafely-without-email -d $FRONTEND_HOST
# unfortunately for some reason it needs to be run two times in order to create the entries
# and directory structure!!!
systemctl restart nginx
else
echo "will not call certbot utility to update ssl certificate for url 'localhost', using default ssl certificate"
fi
# Generate individual DH params
if [ "$FRONTEND_HOST" != "localhost" ]; then
# Only generate new DH params when using "real" certificates.
openssl dhparam -out /etc/nginx/dhparams/dhparams.pem 4096
systemctl restart nginx
else
echo "will not generate new DH params for url 'localhost', using default DH params"
fi
# Bento4 utility installation, for HLS
cd /home/mediacms.io/mediacms
wget http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip
mkdir /home/mediacms.io/mediacms/media_files/hls
# last, set default owner
chown -R www-data. /home/mediacms.io/
echo 'MediaCMS installation completed, open browser on http://'"$FRONTEND_HOST"' and login with user admin and password '"$ADMIN_PASS"''

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,90 @@
# MediaCMS for Moodle
**Version:** 1.0.0 | **Release Date:** 2026-02-12 | **Moodle:** 4.5+
This package 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:** `filter/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:** `lib/editor/tiny/plugins/mediacms`
## Installation
This package is distributed as a single repository but contains two distinct Moodle plugins that must be installed in their respective directories.
### 1. Copy Files
Copy the directories into your Moodle installation as follows (example assuming Moodle is at `/var/www/moodle/public`):
* Copy `filter/mediacms` to `/var/www/moodle/public/filter/mediacms`.
* Copy `tiny/mediacms` to `/var/www/moodle/public/lib/editor/tiny/plugins/mediacms`.
### 2. Set Permissions
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.

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