mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 01:39:37 +02:00
Compare commits
312 commits
3fd439839d
...
c922886f8a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c922886f8a | ||
![]() |
2bd5f564a3 | ||
![]() |
f4c969fd00 | ||
![]() |
c3daefa6e9 | ||
![]() |
4c5813463f | ||
![]() |
6f318071db | ||
![]() |
b3da438a00 | ||
![]() |
945604ea79 | ||
![]() |
f914e1b7bf | ||
![]() |
4cb99c3fd5 | ||
![]() |
715719238a | ||
![]() |
7b38c21631 | ||
![]() |
958aab240d | ||
![]() |
4719cf26f4 | ||
![]() |
a6266dc4bf | ||
![]() |
12c9825658 | ||
![]() |
448bc823ef | ||
![]() |
78eb54464c | ||
![]() |
75fbdbf70e | ||
![]() |
326bf8d85f | ||
![]() |
38e04969df | ||
![]() |
a1bf55c7ae | ||
![]() |
707e1e9b98 | ||
![]() |
7f9f1feed5 | ||
![]() |
52a94815c0 | ||
![]() |
6594ac1262 | ||
![]() |
6e34fadcc2 | ||
![]() |
a16955136d | ||
![]() |
d50c038e07 | ||
![]() |
b59bded648 | ||
![]() |
3d825c56bf | ||
![]() |
756b5646f6 | ||
![]() |
640be407cf | ||
![]() |
d7948ad0bc | ||
![]() |
b02cbfce7f | ||
![]() |
59d5f28ed6 | ||
![]() |
24a0d7fd00 | ||
![]() |
91afa1004e | ||
![]() |
0882d96624 | ||
![]() |
3ea32ba891 | ||
![]() |
12b4893239 | ||
![]() |
aea6983cc4 | ||
![]() |
65f5bd1c37 | ||
![]() |
c5ec42f587 | ||
![]() |
741c8f62e0 | ||
![]() |
5b1ac25794 | ||
![]() |
eedcca7879 | ||
![]() |
7f70d7f3f2 | ||
![]() |
8447769447 | ||
![]() |
4d45ca6f09 | ||
![]() |
77c3a279f7 | ||
![]() |
fde057171d | ||
![]() |
6c3d7501e2 | ||
![]() |
1b283c8694 | ||
![]() |
401e5c0b07 | ||
![]() |
7e1701d9d1 | ||
![]() |
06e5f534ba | ||
![]() |
3ee605b433 | ||
![]() |
bc0132239e | ||
![]() |
796229ac34 | ||
![]() |
3d75a7288f | ||
![]() |
42bf34c29a | ||
![]() |
d0e810d29a | ||
![]() |
b7aa685009 | ||
![]() |
cfe49b37ec | ||
![]() |
f383fe101a | ||
![]() |
7db2817877 | ||
![]() |
24dbbbad64 | ||
![]() |
5894c362d6 | ||
![]() |
57667734c6 | ||
![]() |
8f852e3153 | ||
![]() |
aaae2910e7 | ||
![]() |
507cedca1c | ||
![]() |
a94128a9ce | ||
![]() |
d477aa0df6 | ||
![]() |
198192aeb4 | ||
![]() |
d1b9a88297 | ||
![]() |
2ebd9a74f5 | ||
![]() |
c2fc1cbbd9 | ||
![]() |
423633bc2b | ||
![]() |
045409fa35 | ||
![]() |
74bb0e58c0 | ||
![]() |
bf1c1379a2 | ||
![]() |
c9eb2e2289 | ||
![]() |
d090795d0c | ||
![]() |
6822bcfcb3 | ||
![]() |
9ae1a0177c | ||
![]() |
c26c2c6007 | ||
![]() |
2955824366 | ||
![]() |
fac0f0bc1d | ||
![]() |
33aaa7f1d1 | ||
![]() |
2453a82856 | ||
![]() |
0967ee953c | ||
![]() |
a1a6515524 | ||
![]() |
0dd0198693 | ||
![]() |
6dcdc680e0 | ||
![]() |
8f0389ab8c | ||
![]() |
035846578f | ||
![]() |
c858dd9982 | ||
![]() |
789696d22f | ||
![]() |
0bb20d7e19 | ||
![]() |
e3f0beb6cb | ||
![]() |
a383baa812 | ||
![]() |
65f89db861 | ||
![]() |
b591f42914 | ||
![]() |
3ca2fec672 | ||
![]() |
263cd2e3d1 | ||
![]() |
d7ae2daed1 | ||
![]() |
667dc856ce | ||
![]() |
00b70b84ab | ||
![]() |
748b940dc4 | ||
![]() |
6126aed19e | ||
![]() |
82b92b497f | ||
![]() |
67f495bdac | ||
![]() |
51ecd4c2b0 | ||
![]() |
3561f25806 | ||
![]() |
05c4e1c43d | ||
![]() |
0c5f88a541 | ||
![]() |
e631b1d32e | ||
![]() |
f6c77d1383 | ||
![]() |
3656df036f | ||
![]() |
bc2b2a6c19 | ||
![]() |
54f572fbd0 | ||
![]() |
98ccaec295 | ||
![]() |
ce64ee9c5c | ||
![]() |
4222dd6686 | ||
![]() |
d7ffde7299 | ||
![]() |
bcb012977c | ||
![]() |
18d05d3a40 | ||
![]() |
5a87e008e5 | ||
![]() |
c5f854164c | ||
![]() |
d301a9f3ae | ||
![]() |
fd0fc9a5f9 | ||
![]() |
264558e2ca | ||
![]() |
92a4123f25 | ||
![]() |
5926787861 | ||
![]() |
8d262d01b2 | ||
![]() |
ae9a802403 | ||
![]() |
4fd2cdb390 | ||
![]() |
3edb4b75ee | ||
![]() |
59adaaad69 | ||
![]() |
82815624b4 | ||
![]() |
1496961c53 | ||
![]() |
18b22257a8 | ||
![]() |
bc1a7d0873 | ||
![]() |
bd6eb388f8 | ||
![]() |
71c832f6b2 | ||
![]() |
f3f77f596e | ||
![]() |
ef690bc792 | ||
![]() |
ad41b6c06e | ||
![]() |
e10ed4f5ed | ||
![]() |
15867684f5 | ||
![]() |
57a8e18022 | ||
![]() |
19ec133abb | ||
![]() |
f5d6097980 | ||
![]() |
93f5a7d789 | ||
![]() |
afc1f0e6b0 | ||
![]() |
adfa6b43ad | ||
![]() |
06fd09b93a | ||
![]() |
9d9607feff | ||
![]() |
d1a35e8421 | ||
![]() |
da23ad1d09 | ||
![]() |
b4df49b87f | ||
![]() |
6fce7c808c | ||
![]() |
89360a4ef0 | ||
![]() |
acaabaace1 | ||
![]() |
e763ad3036 | ||
![]() |
b44dfef3f9 | ||
![]() |
9619c2ea7d | ||
![]() |
83f74169da | ||
![]() |
fc986076c9 | ||
![]() |
8c9b4abe45 | ||
![]() |
a5c087d3d4 | ||
![]() |
04245f9dc1 | ||
![]() |
9af56c26bc | ||
![]() |
1967546cab | ||
![]() |
bd337df442 | ||
![]() |
57caf25611 | ||
![]() |
a53ed039b8 | ||
![]() |
1289d645d8 | ||
![]() |
f19954414f | ||
![]() |
1c5101a22b | ||
![]() |
94802f3175 | ||
![]() |
3e1cdb9fa2 | ||
![]() |
309068ae1d | ||
![]() |
17247f205f | ||
![]() |
cdb861a26a | ||
![]() |
37e13bbcd2 | ||
![]() |
c9905ecd3a | ||
![]() |
37da276f9c | ||
![]() |
e0eebb1c7e | ||
![]() |
06d9c7a13d | ||
![]() |
121d9029b9 | ||
![]() |
13bceb5f40 | ||
![]() |
29a88c0dde | ||
![]() |
445866967f | ||
![]() |
2612112c63 | ||
![]() |
d46ca9b53a | ||
![]() |
924b2cb614 | ||
![]() |
3f3bb74f99 | ||
![]() |
8cb0062bb7 | ||
![]() |
54ef9b308f | ||
![]() |
29506af46b | ||
![]() |
dcf3eb95a5 | ||
![]() |
7a7f7b5dab | ||
![]() |
1638b6c22d | ||
![]() |
aa94ea8dd9 | ||
![]() |
06b9252b64 | ||
![]() |
d82b737133 | ||
![]() |
07e3ce416a | ||
![]() |
7586210cb8 | ||
![]() |
cb736664b8 | ||
![]() |
459e40762a | ||
![]() |
78bc2482fd | ||
![]() |
57ebcba34c | ||
![]() |
382ecf4d73 | ||
![]() |
84968a3c02 | ||
![]() |
165d956fb5 | ||
![]() |
09596e3b56 | ||
![]() |
89eed6ca11 | ||
![]() |
222e0fc635 | ||
![]() |
4cf831573a | ||
![]() |
c2365a945e | ||
![]() |
cebbf9173d | ||
![]() |
8fc5a1aba5 | ||
![]() |
fe3dbf5ec9 | ||
![]() |
3744ca473f | ||
![]() |
d6e4dac032 | ||
![]() |
b45fbf4337 | ||
![]() |
eadbf4e001 | ||
![]() |
e9af88b332 | ||
![]() |
32fbe20b13 | ||
![]() |
2790ec5aaa | ||
![]() |
8f4ba03550 | ||
![]() |
a02704a7a4 | ||
![]() |
cd8573d79e | ||
![]() |
e399515941 | ||
![]() |
a60eeeff60 | ||
![]() |
4233b20451 | ||
![]() |
c52f4b4d48 | ||
![]() |
db7c5d8a0e | ||
![]() |
76aa084fd5 | ||
![]() |
51d5a523ea | ||
![]() |
17eb4dc74f | ||
![]() |
622babd39e | ||
![]() |
b6cd911bc2 | ||
![]() |
e8a1d94ac2 | ||
![]() |
4df35a65e2 | ||
![]() |
0689fc7d87 | ||
![]() |
ce73fefa84 | ||
![]() |
d462d354f9 | ||
![]() |
5c73deed31 | ||
![]() |
fe413000f4 | ||
![]() |
d1b15d4f3c | ||
![]() |
eeaec6d9a2 | ||
![]() |
4a44c774d8 | ||
![]() |
10f2497c82 | ||
![]() |
3f032d24af | ||
![]() |
015a324fad | ||
![]() |
45dbdb13c8 | ||
![]() |
d8de2f3ff9 | ||
![]() |
ee96cf3a19 | ||
![]() |
f2556d80e3 | ||
![]() |
1a9ef4ceaa | ||
![]() |
d3863d3a9a | ||
![]() |
037197a7e9 | ||
![]() |
b0d5a6776b | ||
![]() |
49da883f7e | ||
![]() |
e1b543bfa0 | ||
![]() |
65ae21436a | ||
![]() |
81f04543e5 | ||
![]() |
1dfc3a3b74 | ||
![]() |
242e9ad983 | ||
![]() |
754b60cfe1 | ||
![]() |
175f065811 | ||
![]() |
62d58a86fa | ||
![]() |
50a66c6843 | ||
![]() |
81b8706524 | ||
![]() |
4739e367a6 | ||
![]() |
fe1448fa19 | ||
![]() |
11eea4731e | ||
![]() |
d2a5d4d25d | ||
![]() |
7fcdae56ac | ||
![]() |
66bcbaf21c | ||
![]() |
57472ed255 | ||
![]() |
30656ae18f | ||
![]() |
0adafa0fc0 | ||
![]() |
546bd42240 | ||
![]() |
532020e2af | ||
![]() |
70010fac73 | ||
![]() |
ed8f96354f | ||
![]() |
4323ffbb4e | ||
![]() |
ff400224c2 | ||
![]() |
1bdd0a8299 | ||
![]() |
94046baaf0 | ||
![]() |
6c85bbf852 | ||
![]() |
53472daa07 | ||
![]() |
de884c6721 | ||
![]() |
3fbaae8ac2 | ||
![]() |
c0f4de6077 | ||
![]() |
f5fd593976 | ||
![]() |
031b61c466 | ||
![]() |
ce28c64750 | ||
![]() |
614d906ca6 | ||
![]() |
0c7a89a70a | ||
![]() |
24b59a2560 | ||
![]() |
e9bb222b6c | ||
![]() |
eb11e5793f | ||
![]() |
a6b89bde2b | ||
![]() |
03425e10d3 | ||
![]() |
069f5d019b | ||
![]() |
cbce8580d2 |
1043 changed files with 329005 additions and 184602 deletions
15
.github/CONTRIBUTING.md
vendored
15
.github/CONTRIBUTING.md
vendored
|
@ -223,8 +223,19 @@ Instance configurations are in `config/test-{1,2,3}.yaml`.
|
|||
|
||||
To test emails with PeerTube:
|
||||
|
||||
* Run [mailslurper](http://mailslurper.com/)
|
||||
* Run PeerTube using mailslurper SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=dev node dist/server`
|
||||
* Run [MailDev](https://github.com/maildev/maildev) using Docker
|
||||
* Run PeerTube using MailDev SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=dev node dist/server`
|
||||
|
||||
To test all emails without having to run actions manually on the web interface, you can run notification unit tests with environment variables to relay emails to your MailDev instance. For example:
|
||||
|
||||
```sh
|
||||
MAILDEV_RELAY_HOST=localhost MAILDEV_RELAY_PORT=2500 mocha --exit --bail packages/tests/src/api/notifications/comments-notifications.ts
|
||||
```
|
||||
|
||||
You can then go to the MailDev web interface and see how emails look like.
|
||||
|
||||
The admin web interface also have a button to send some email templates to a specific email address.
|
||||
|
||||
|
||||
### Environment variables
|
||||
|
||||
|
|
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
|
@ -38,6 +38,14 @@ jobs:
|
|||
ports:
|
||||
- 9444:9000
|
||||
|
||||
keycloak:
|
||||
image: chocobozzz/peertube-tests-keycloak
|
||||
ports:
|
||||
- 8082:8080
|
||||
env:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
69
CHANGELOG.md
69
CHANGELOG.md
|
@ -1,5 +1,74 @@
|
|||
# Changelog
|
||||
|
||||
## v7.3.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* Minimum supported NodeJS version is `20.17`
|
||||
|
||||
### NGINX
|
||||
|
||||
* Disable request buffering on upload endpoints to fix HTTP request timeouts: https://github.com/Chocobozzz/PeerTube/commit/d1a35e8421195088e2754b787c4af1e765b9eaa9
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
* Add server API (https://docs.joinpeertube.org/api/plugins):
|
||||
* Support `externalRedirectUri` for `registerExternalAuth` so PeerTube redirects users on another URL set by the plugin
|
||||
* If your plugin uses `filter:email.template-path.result` server hook: emails now use Handlebars template engine instead of Pug template engine
|
||||
|
||||
### Features
|
||||
|
||||
* :tada: Emails can now be translated :tada: Check the [translation documentation](https://docs.joinpeertube.org/support/doc/translation) to help us translate emails in your language!
|
||||
* :tada: Introduce a web configuration wizard to help administrators to configure their instance automatically :tada:
|
||||
* The wizard appears once the administrators have logged in following the installation of the PeerTube instance
|
||||
* Admins can also run the wizard via a button in the web admin config
|
||||
* The main instance information (e.g. name, short description, logo, primary colour) can be entered using the wizard.
|
||||
* It also helps the admin to apply a configuration depending on the instance type (community-based, institutional, private)
|
||||
* :tada: Redesign the admin config to use a lateral menu for navigating between subsections :tada:
|
||||
* Add a new *Customization* page to easily change the main colors and shape of the client interface
|
||||
* Add a new *Logo* page where admins can upload logos/favicon and social media images for their instances
|
||||
* Add an option to set the default licence, privacy and comments policy when publishing videos
|
||||
* The email prefix and body can now be changed in the web admin config. These configurations also support the `{{instanceName}}` template variable, which is replaced by the instance name
|
||||
* Improve admin federation control:
|
||||
* Add the ability for admins to completely disable remote subscriptions to local channels
|
||||
* Admins can also set up automatic rejection of video comments from remote instances
|
||||
* Add 2FA column information in admin users overview table
|
||||
* Display remote runner version in admin
|
||||
* Add ability for users to set the planned date of a live. These lives are displayed when browsing videos [#7144](https://github.com/Chocobozzz/PeerTube/pull/7144)
|
||||
* Improve data tables UX/UI
|
||||
* Improve account/channel playlists management:
|
||||
* Use a data table to manage account and channel playlists
|
||||
* Allow to manually set the order of the public playlists displayed in a channel
|
||||
* Improve sensitive content warning in embed player
|
||||
* Improve audio transcoding quality, especially with FLAC input
|
||||
* Support Creole French languages in video language metadata
|
||||
* Add ability for users to list and revoke token sessions
|
||||
* Support *Free of known copyright restrictions* and *Copyrighted - All Rights Reserved* video licence metadata
|
||||
* Play/pause the video player using `k` key
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix ActivityPub audience for unlisted videos
|
||||
* Use an array of URL in `attributedTo` ActivityPub field
|
||||
* Prefer `og:image` instead of `og:image:url`
|
||||
* Better thumbnail blur for sensitive content [#7105](https://github.com/Chocobozzz/PeerTube/pull/7105)
|
||||
* Prefer `allow="fullscreen"` for video embed `iframe` [#7043](https://github.com/Chocobozzz/PeerTube/pull/7043)
|
||||
* Respect the sensitive content policy, even for videos owned by the user
|
||||
* Fix the issue of the scroll position not being restored when pages load slowly [#7143](https://github.com/Chocobozzz/PeerTube/pull/7143)
|
||||
* Fix remote actor follow counter after a local subscription
|
||||
* Fix reloading videos in *Browser videos* when the link only changes query parameters
|
||||
* Add stall job check for remote studio and transcription runner jobs
|
||||
* Prevent metric warning for redundancy gauge
|
||||
* Fix disabling *Wait transcoding* checkbox
|
||||
* Correctly import new elements of a playlist in channel synchronization
|
||||
* Fix overflow in discover page
|
||||
* Fix restoring scroll position when going back in the web browser on the homepage set by the admin
|
||||
* Fill video support on channel sync
|
||||
* Respect instance default privacy setting when publishing imports and lives
|
||||
* Remove useless help for live transcoding
|
||||
* Fix RTL margins on some components
|
||||
|
||||
|
||||
## v7.2.3
|
||||
|
||||
### SECURITY
|
||||
|
|
144
CREDITS.md
144
CREDITS.md
|
@ -3,20 +3,23 @@
|
|||
* Chocobozzz
|
||||
* Rigel Kent
|
||||
* DignifiedSilence
|
||||
* Александр
|
||||
* T.S
|
||||
* josé m
|
||||
* Александр
|
||||
* Hồ Nhất Duy
|
||||
* Jeff Huang
|
||||
* josé m
|
||||
* Milo Ivir
|
||||
* Ihor Hordiichuk
|
||||
* Filip Bengtsson
|
||||
* fran secs
|
||||
* kontrollanten
|
||||
* Payman Moghadam
|
||||
* Berto Te
|
||||
* kontrollanten
|
||||
* Milo Ivir
|
||||
* Simon Brosdetzko
|
||||
* Jiri Podhorecky
|
||||
* Phongpanot
|
||||
* Sveinn í Felli
|
||||
* Hannes Ylä-Jääski
|
||||
* GunChleoc
|
||||
* hecko
|
||||
* Laurent Ettouati
|
||||
|
@ -24,65 +27,76 @@
|
|||
* Zet
|
||||
* Ewout van Mansom
|
||||
* Aitor Salaberria
|
||||
* Sveinn í Felli
|
||||
* Leif-Jöran Olsson
|
||||
* Clemens Schielicke
|
||||
* Luca Calcaterra
|
||||
* Racida S
|
||||
* Marcin Mikołajczak
|
||||
* Eivind Ødegård
|
||||
* Balázs Meskó
|
||||
* Tirifto
|
||||
* Marcin Mikołajczak
|
||||
* Wicklow
|
||||
* Blood Axe
|
||||
* Eivind Ødegård
|
||||
* John Livingston
|
||||
* Hannes Ylä-Jääski
|
||||
* Kim
|
||||
* Tirifto
|
||||
* Besnik Bleta
|
||||
* Kim
|
||||
* Vodoyo Kamal
|
||||
* Jiří Podhorecký
|
||||
* Armin
|
||||
* Fontan 030
|
||||
* ButterflyOfFire
|
||||
* Mohamad Reza
|
||||
* Quentin PAGÈS
|
||||
* Kimsible
|
||||
* Felix Ableitner
|
||||
* Frank Sträter
|
||||
* Free coss
|
||||
* Ettore Atalan
|
||||
* Andrea Gavioli
|
||||
* Mürteza MERT
|
||||
* Gérald Niel
|
||||
* ButterflyOfFire
|
||||
* Duy
|
||||
* Eric Guichaoua
|
||||
* Renne Rocha
|
||||
* Slimane Selyan AMIRI
|
||||
* Dingzhong Chen
|
||||
* Eric Guichaoua
|
||||
* Filip Hanes
|
||||
* Julien Maulny
|
||||
* Mark Van den Borre
|
||||
* x
|
||||
* Booteille
|
||||
* Manuel Viens
|
||||
* Jorropo
|
||||
* Josh Morel
|
||||
* Renne Rocha
|
||||
* dxuser514
|
||||
* BO41
|
||||
* Ettore Atalan
|
||||
* Marc Strange
|
||||
* vachan
|
||||
* AP
|
||||
* Elegant Codes
|
||||
* Florian CUNY
|
||||
* Francesc
|
||||
* alex gabilondo
|
||||
* mando laress
|
||||
* Ľubomír Šima
|
||||
* Артём Котлубай
|
||||
* Fjuro
|
||||
* Ricardo Biloti
|
||||
* 0que
|
||||
* Blood Axe
|
||||
* Cedric F
|
||||
* Florent
|
||||
* Marc Strange
|
||||
* Ricardo Simões
|
||||
* lutangar
|
||||
* Ch
|
||||
* J. Lavoie
|
||||
* Luc Didry
|
||||
* YILDIRIM YAPRAK
|
||||
* alex gabilondo
|
||||
* barzofarev2
|
||||
* jan Seli
|
||||
* 李奕寯
|
||||
* Erik Guldberg
|
||||
* Kempelen
|
||||
* Kerim Demirkaynak
|
||||
* Martin Hoefler
|
||||
* Porrumentzio
|
||||
* Poslovitch
|
||||
|
@ -91,67 +105,75 @@
|
|||
* Alexander Ivanov
|
||||
* Balázs Úr
|
||||
* Echo Kilo
|
||||
* Erik Guldberg
|
||||
* Jan Keromnes
|
||||
* Jiří Podhorecký
|
||||
* Luc Didry
|
||||
* Siourdakis Thanos
|
||||
* Thomas Citharel
|
||||
* knuxify
|
||||
* tray
|
||||
* Adrià Martín
|
||||
* Agron Selimaj
|
||||
* Attila F
|
||||
* Caroline Chuong
|
||||
* David Soh
|
||||
* Diazepan Medina
|
||||
* Jason Zhou
|
||||
* Kerim Demirkaynak
|
||||
* Loukas Stamellos
|
||||
* Ms Kimsible
|
||||
* NorbiPeti
|
||||
* Sergey Zigachev
|
||||
* Thomas Citharel
|
||||
* Txopi
|
||||
* Benjamin Bouvier
|
||||
* Filip Hanes
|
||||
* Cavernosa
|
||||
* Ghost of Sparta
|
||||
* Joe Bill
|
||||
* Julien
|
||||
* Jure Repinc
|
||||
* Kemal Oktay Aktoğan
|
||||
* Lucas Declercq
|
||||
* Ryan He
|
||||
* Sirxy
|
||||
* Viorel-Cătălin Răpițeanu
|
||||
* matograine
|
||||
* Adrià Martín
|
||||
* 偶尔来巡山
|
||||
* Ahmed ABERWAG
|
||||
* Daniel Santos
|
||||
* David Libeau
|
||||
* Ewald Arnold
|
||||
* Florent F
|
||||
* Florent Poinsaut
|
||||
* Ignacio Carrera González
|
||||
* Jayme Soares Almeida Cruz
|
||||
* Lety Does Stuff
|
||||
* Nassim Bounouas
|
||||
* Rafael Fontenelle
|
||||
* Thomas Kuntz
|
||||
* Tzafrir Cohen
|
||||
* Viorel-Cătălin Răpițeanu
|
||||
* Vri
|
||||
* miro
|
||||
* nexi
|
||||
* owiox8+1viroxeaziaxw@sharklasers.com
|
||||
* spf
|
||||
* wazakovsky
|
||||
* yns bag
|
||||
* Anne-Gaelle Moulun
|
||||
* Arman
|
||||
* Asier Iturralde Sarasola
|
||||
* BRAINS YUM
|
||||
* Belkacem Mohammed
|
||||
* Bob Oob
|
||||
* Côme 744
|
||||
* Dimitri Gilbert
|
||||
* Flavio F. M
|
||||
* Florent Poinsaut
|
||||
* Frank Chang
|
||||
* Green-Star
|
||||
* I_Automne
|
||||
* Ilia
|
||||
* Marek Ľach
|
||||
* Micah Elizabeth Scott
|
||||
* Pierre-Jean
|
||||
* Ret Samys
|
||||
* SVNET Libre
|
||||
* StarAtt
|
||||
* Tomasz
|
||||
* Tony Simoes
|
||||
* William Lahti
|
||||
|
@ -160,18 +182,18 @@
|
|||
* boris joeson
|
||||
* frankstrater
|
||||
* mater
|
||||
* spf
|
||||
* test2a
|
||||
* think4web
|
||||
* 路过是好事
|
||||
* Ajeje Brazorf
|
||||
* Andreas Grupp
|
||||
* Andrey
|
||||
* Angristan
|
||||
* Benjamin Seitz
|
||||
* Bob Oob
|
||||
* Booteille
|
||||
* Cirnos
|
||||
* Cokelat8
|
||||
* DontUseGithub
|
||||
* Eder Etxebarria
|
||||
* Farooq Karimi Zadeh
|
||||
* Frederic Bezies
|
||||
* Iñigo
|
||||
|
@ -180,24 +202,30 @@
|
|||
* José M
|
||||
* Kristoffer Grundström
|
||||
* LecygneNoir
|
||||
* Liu Zhiyu
|
||||
* Lukas
|
||||
* MahdiTurki
|
||||
* Martijn Dekker
|
||||
* Mats Blomdahl
|
||||
* Maxime Louet
|
||||
* Mildred
|
||||
* Murat Hasdemir
|
||||
* Murat Özalp
|
||||
* Nikolay
|
||||
* Okhin
|
||||
* Osama
|
||||
* Pierre-Alain TORET
|
||||
* Serge Victor
|
||||
* Théo Le Calvar
|
||||
* Ugaitz
|
||||
* Vaclovas Intas
|
||||
* Vincent Finance
|
||||
* aschaap
|
||||
* clementbrizard
|
||||
* gohoso9454
|
||||
* helabasa
|
||||
* kaiyou
|
||||
* max
|
||||
* roberto marcolin
|
||||
* Ahsan Haris Ahmed
|
||||
* Alberto Teira
|
||||
|
@ -212,11 +240,12 @@
|
|||
* Asr128
|
||||
* Aurélien Bertron
|
||||
* Axel Viala
|
||||
* Casper Ruttten
|
||||
* Charles-Edouard Gervais
|
||||
* Danail Emandiev
|
||||
* Daniele Garau
|
||||
* Dep Pranata
|
||||
* Dirk Kelly
|
||||
* Eder Etxebarria
|
||||
* Ehsan Gholami
|
||||
* Elga Ahmad Prayoga
|
||||
* Girish Ramakrishnan
|
||||
|
@ -237,13 +266,14 @@
|
|||
* Lukas Winkler
|
||||
* M Z
|
||||
* Manuela Silva
|
||||
* Marian
|
||||
* Morpheus Tao
|
||||
* Mélanie Chauvel
|
||||
* Natsuki Tsukishiro
|
||||
* Paolo Mauri
|
||||
* Pedro
|
||||
* Petr Balíček
|
||||
* Piotr Sikora
|
||||
* Ryan He
|
||||
* Stardream
|
||||
* Stefan Keks
|
||||
* Tom Wellington
|
||||
|
@ -259,8 +289,10 @@
|
|||
* h3n3
|
||||
* iapellaniz
|
||||
* jonathanraes
|
||||
* legiorange
|
||||
* numéro6
|
||||
* saleh oukiki
|
||||
* Àngel Pérez Beroy
|
||||
* Ömer Faruk Çakmak
|
||||
* AQR_Rastiq
|
||||
* Al-Hassan Abdel-Raouf
|
||||
|
@ -277,13 +309,14 @@
|
|||
* Average Dude
|
||||
* BGR2
|
||||
* BitTube
|
||||
* Boo
|
||||
* Boo Teille
|
||||
* Branislav Pavelka
|
||||
* Casper Ruttten
|
||||
* Dashie
|
||||
* David Luís Pereira Pires
|
||||
* David Marzal
|
||||
* Doug Luce
|
||||
* Emv
|
||||
* EndoGai
|
||||
* Fatih Özsoy
|
||||
* FediverseTV
|
||||
|
@ -304,6 +337,8 @@
|
|||
* Jan Hartig
|
||||
* Jan Marsalek
|
||||
* Jerguš Fonfer
|
||||
* Jeroen de Wijn
|
||||
* José Daniel Angulo Plata
|
||||
* Joël Galeran
|
||||
* Julien Lemaire
|
||||
* Julien Rabier
|
||||
|
@ -314,11 +349,12 @@
|
|||
* Mondo Xíbaro
|
||||
* Moritz Warning
|
||||
* Mostafa Ahangarha
|
||||
* Murat Özalp
|
||||
* Neko Nekowazarashi
|
||||
* Nicolai Larsen
|
||||
* Nojus
|
||||
* Olivier Bouillet
|
||||
* Pedro hates github.com
|
||||
* Pep
|
||||
* Pierre Jaury
|
||||
* Piotr Strębski
|
||||
* Puryx
|
||||
|
@ -328,19 +364,24 @@
|
|||
* SerCom_KC
|
||||
* Skid
|
||||
* Stakovicz
|
||||
* Suthep
|
||||
* Takeshi Umeda
|
||||
* Thai Localization
|
||||
* The Cashew Trader
|
||||
* Thijs Kinkhorst
|
||||
* Timur Seber
|
||||
* Toso Malero
|
||||
* Tsuki
|
||||
* Túlio Simões Martins Padilha
|
||||
* Valvin
|
||||
* XblateX
|
||||
* Yaron Shahrabani
|
||||
* YiDai
|
||||
* Yogesh K S
|
||||
* ahmadsharifian
|
||||
* bopol
|
||||
* brucekomike
|
||||
* darek
|
||||
* dingycle
|
||||
* framail
|
||||
* imgradeone Yan
|
||||
|
@ -348,13 +389,13 @@
|
|||
* les
|
||||
* libertas
|
||||
* merty
|
||||
* ou jian bo
|
||||
* plr20
|
||||
* q_h
|
||||
* qwerty
|
||||
* taziden
|
||||
* vancha march
|
||||
* victor héry
|
||||
* Àngel Pérez Beroy
|
||||
* 3risian
|
||||
* A.D.R.S
|
||||
* Acid Chicken (硫酸鶏)
|
||||
|
@ -368,6 +409,7 @@
|
|||
* Alberto Mardegan
|
||||
* Alejandro Criado-Pérez
|
||||
* Aleksandr Sokolov
|
||||
* Alessandro Molina
|
||||
* Alexander F. Rødseth
|
||||
* Ali Alim
|
||||
* Alperen Abak
|
||||
|
@ -388,7 +430,7 @@
|
|||
* Ben Lubar
|
||||
* Benjamin EWFT
|
||||
* Benoît Piédallu
|
||||
* Boo
|
||||
* Bojidar Marinov
|
||||
* Brad Johnson
|
||||
* Cadence Ember
|
||||
* Cale
|
||||
|
@ -397,15 +439,16 @@
|
|||
* Charlie Lambda
|
||||
* Christoph Geschwind
|
||||
* Chronos
|
||||
* Cirnos
|
||||
* Claude
|
||||
* Clifford Garwood II
|
||||
* Clément Brizard
|
||||
* Cédric Bahirwe
|
||||
* DLP
|
||||
* Daniel Dutra
|
||||
* David Baumgold
|
||||
* David Dobryakov
|
||||
* DeeJayBro
|
||||
* Denis Dupont
|
||||
* Deval
|
||||
* Dimitri DI GUSTO
|
||||
* Dimitrios Glentadakis
|
||||
|
@ -418,6 +461,7 @@
|
|||
* Erwan Croze
|
||||
* Esmail_Hazem
|
||||
* Ethan Corgatelli
|
||||
* FB
|
||||
* Fabio Agreles Bezerra
|
||||
* FediThing
|
||||
* Fernandez, ReK2
|
||||
|
@ -433,6 +477,7 @@
|
|||
* Henri BAUDESSON
|
||||
* HesioZ
|
||||
* Hozan Şahin
|
||||
* Hydrolien
|
||||
* ICabaleiro
|
||||
* Iker Garaialde
|
||||
* Ismaël Bouya
|
||||
|
@ -440,6 +485,7 @@
|
|||
* Iván Cabaleiro
|
||||
* J Webb
|
||||
* Jacen
|
||||
* Jackson
|
||||
* Jackson Chen
|
||||
* Jacob
|
||||
* Jacques Foucry
|
||||
|
@ -453,6 +499,7 @@
|
|||
* Jeston Tan
|
||||
* Jinn Koriech
|
||||
* Jlll1
|
||||
* Johan van Dongen
|
||||
* Johnny Jazeix
|
||||
* Jonas Sulzer
|
||||
* Jonatan Nyberg
|
||||
|
@ -465,24 +512,27 @@
|
|||
* Kent Anderson
|
||||
* Kevin Cope
|
||||
* Kevin Pliester
|
||||
* Khyvodul
|
||||
* Knackie
|
||||
* Kody
|
||||
* Konstantinos Agiannis
|
||||
* Kyâne Pichou
|
||||
* Leo Mouyna
|
||||
* Lesterpig
|
||||
* Lety Does Stuff
|
||||
* Levi Bard
|
||||
* LiPeK
|
||||
* Lint
|
||||
* LoveIsGrief
|
||||
* Luca B
|
||||
* Lucian I. Last
|
||||
* Lucien A
|
||||
* Lupinard
|
||||
* Léane GRASSER
|
||||
* Léo Andrès
|
||||
* ManMade-cube42
|
||||
* Marcel Fuhrmann
|
||||
* Marco Zehe
|
||||
* Marcus Schwarz
|
||||
* Marian Steinbach
|
||||
* Mario Pepe
|
||||
* Markus Richter
|
||||
|
@ -491,11 +541,14 @@
|
|||
* Mateusz Piotrowski
|
||||
* Mathieu Agopian
|
||||
* Mathieu Brunot
|
||||
* Matthias Frey
|
||||
* Matthieu De Beule
|
||||
* Max Rosenfors
|
||||
* Michael Koppmann
|
||||
* Michael Williams
|
||||
* Midgard
|
||||
* Miguel Mayol Tur
|
||||
* Miguel P.L
|
||||
* Mike
|
||||
* Mikel Gartzia Santamaria
|
||||
* Milo van der Linden
|
||||
|
@ -510,10 +563,11 @@
|
|||
* Novel Martin Harianto
|
||||
* Nuño Sempere
|
||||
* Olivier Jolly
|
||||
* Oliwier Jaszczyszyn
|
||||
* Pablo Joubert
|
||||
* Paul FLORENCE
|
||||
* Paul V
|
||||
* Pedro hates github.com
|
||||
* Pavel 7 Tomsk
|
||||
* PhieF
|
||||
* Philip Durbin
|
||||
* Philipp Fischbeck
|
||||
|
@ -522,15 +576,18 @@
|
|||
* Quantic Axe
|
||||
* Quentin Dupont
|
||||
* Quentí
|
||||
* RF9A5V
|
||||
* ROPEDE
|
||||
* Ramazan Geven
|
||||
* Ramiellll
|
||||
* Rangel Prodanov
|
||||
* Raphael
|
||||
* Raphaël Droz
|
||||
* Ray
|
||||
* Rebecca
|
||||
* Rech
|
||||
* Rep Dolsay
|
||||
* RiQuY
|
||||
* Robert Riemann
|
||||
* Roberto Resoli
|
||||
* Robin
|
||||
|
@ -542,6 +599,7 @@
|
|||
* Scott Starkey
|
||||
* Sebastian Paweł Wolski
|
||||
* Seth Falco
|
||||
* Shalabh Agarwal
|
||||
* Showfom
|
||||
* Shun Sakai
|
||||
* Simon Gilliot
|
||||
|
@ -549,8 +607,10 @@
|
|||
* Stefan Schüller
|
||||
* Steffen
|
||||
* Steffen Möller
|
||||
* Subh B
|
||||
* Sumit Khanna
|
||||
* SupC
|
||||
* Sébastien NOBILI
|
||||
* TA
|
||||
* Tanguy BERNARD
|
||||
* Thavarasa Prasanth
|
||||
|
@ -562,7 +622,6 @@
|
|||
* Tomás Sebastián Romero
|
||||
* TrashMacNugget
|
||||
* Treacle
|
||||
* Tsuki
|
||||
* Unetelle Inconnue
|
||||
* Vagelis F
|
||||
* Varik Valefor
|
||||
|
@ -577,10 +636,12 @@
|
|||
* Yehuda Deutsch
|
||||
* Yorwba
|
||||
* Yun
|
||||
* Zack Birkenbuel
|
||||
* Zekovski
|
||||
* Zig-03
|
||||
* [ Bie ] Watcharapong Suriyawan
|
||||
* adam iter
|
||||
* allmiha2
|
||||
* anmol26s
|
||||
* april
|
||||
* ar9708
|
||||
|
@ -600,7 +661,6 @@
|
|||
* jomo
|
||||
* kukhariev
|
||||
* lambdacastix
|
||||
* legiorange
|
||||
* libertysoft3
|
||||
* lost_geographer
|
||||
* lsde
|
||||
|
@ -624,16 +684,19 @@
|
|||
* philippe lhardy
|
||||
* pitchum
|
||||
* potedeo
|
||||
* q0ntinuum
|
||||
* rdxuan
|
||||
* retiolus
|
||||
* ruvilonix
|
||||
* sanchis
|
||||
* skyone-wzw
|
||||
* slendermon
|
||||
* smilekison
|
||||
* sn0wygecko
|
||||
* soonsouth
|
||||
* thecashewtrader
|
||||
* tilllt
|
||||
* tmpod
|
||||
* tomamplius
|
||||
* toobad
|
||||
* treac1e
|
||||
|
@ -646,6 +709,7 @@
|
|||
* Артур Кирпо
|
||||
* Дмитрий Кузнецов
|
||||
* noisawe
|
||||
* 姚霁恒
|
||||
* abdhessuk
|
||||
* abidin24
|
||||
* aditoo
|
||||
|
|
5
apps/peertube-cli/CHANGELOG.md
Normal file
5
apps/peertube-cli/CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## v1.0.3
|
||||
|
||||
* Fix `util.isArray` deprecation warning
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@peertube/peertube-cli",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"type": "module",
|
||||
"main": "dist/peertube.js",
|
||||
"bin": "dist/peertube.js",
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## v0.2.0
|
||||
|
||||
* Add runner version in request and register payloads
|
||||
* Update dependencies to fix vulnerabilities
|
||||
|
||||
## v0.1.3
|
||||
|
||||
* Disable log coloring when TTY does not support it
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@peertube/peertube-runner",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"main": "dist/peertube-runner.js",
|
||||
"bin": "dist/peertube-runner.js",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { pick, shuffle, wait } from '@peertube/peertube-core-utils'
|
||||
import { PeerTubeProblemDocument, RunnerJobType, ServerErrorCode } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer as PeerTubeServerCommand } from '@peertube/peertube-server-commands'
|
||||
import { ensureDir, remove } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { pick, shuffle, wait } from '@peertube/peertube-core-utils'
|
||||
import { PeerTubeProblemDocument, RunnerJobType, ServerErrorCode } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer as PeerTubeServerCommand } from '@peertube/peertube-server-commands'
|
||||
import { ConfigManager } from '../shared/index.js'
|
||||
import { IPCServer } from '../shared/ipc/index.js'
|
||||
import { logger } from '../shared/logger.js'
|
||||
|
@ -95,7 +95,12 @@ export class RunnerServer {
|
|||
logger.info(`Registering runner ${runnerName} on ${url}...`)
|
||||
|
||||
const serverCommand = new PeerTubeServerCommand({ url })
|
||||
const { runnerToken } = await serverCommand.runners.register({ name: runnerName, description: runnerDescription, registrationToken })
|
||||
const { runnerToken } = await serverCommand.runners.register({
|
||||
name: runnerName,
|
||||
description: runnerDescription,
|
||||
registrationToken,
|
||||
version: process.env.PACKAGE_VERSION
|
||||
})
|
||||
|
||||
const server: PeerTubeServer = Object.assign(serverCommand, {
|
||||
runnerToken,
|
||||
|
@ -268,7 +273,9 @@ export class RunnerServer {
|
|||
|
||||
jobTypes: this.enabledJobsArray.length !== getSupportedJobsList().length
|
||||
? this.enabledJobsArray
|
||||
: undefined
|
||||
: undefined,
|
||||
|
||||
version: process.env.PACKAGE_VERSION
|
||||
})
|
||||
|
||||
// FIXME: remove in PeerTube v8: jobTypes has been introduced in PeerTube v7, so do the filter here too
|
||||
|
@ -358,6 +365,8 @@ export class RunnerServer {
|
|||
|
||||
try {
|
||||
for (const { server, job } of this.processingJobs) {
|
||||
logger.info(`Aborting job ${job.uuid} on ${server.url} as the runner is stopping`)
|
||||
|
||||
await server.runnerJobs.abort({
|
||||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
|
|
|
@ -137,7 +137,7 @@ export class ConfigManager {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Thanks: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
|
||||
private deepFreeze <T extends object> (object: T) {
|
||||
private deepFreeze<T extends object> (object: T) {
|
||||
const propNames = Reflect.ownKeys(object)
|
||||
|
||||
// Freeze properties before freezing self
|
||||
|
|
|
@ -53,11 +53,11 @@
|
|||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "22.14.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.1.tgz#53b54585cec81c21eee3697521e31312d6ca1e6f"
|
||||
integrity sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==
|
||||
version "24.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.1.0.tgz#0993f7dc31ab5cc402d112315b463e383d68a49c"
|
||||
integrity sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==
|
||||
dependencies:
|
||||
undici-types "~6.21.0"
|
||||
undici-types "~7.8.0"
|
||||
|
||||
atomic-sleep@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
@ -75,14 +75,14 @@ dateformat@^4.6.3:
|
|||
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
|
||||
|
||||
detect-libc@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
|
||||
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8"
|
||||
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c"
|
||||
integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
|
@ -146,9 +146,9 @@ msgpackr-extract@^3.0.2:
|
|||
"@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3"
|
||||
|
||||
msgpackr@^1.3.2:
|
||||
version "1.11.2"
|
||||
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.2.tgz#4463b7f7d68f2e24865c395664973562ad24473d"
|
||||
integrity sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==
|
||||
version "1.11.5"
|
||||
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.5.tgz#edf0b9d9cb7d8ed6897dd0e42cfb865a2f4b602e"
|
||||
integrity sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==
|
||||
optionalDependencies:
|
||||
msgpackr-extract "^3.0.2"
|
||||
|
||||
|
@ -203,31 +203,31 @@ pino-std-serializers@^7.0.0:
|
|||
integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
|
||||
|
||||
pino@^9.6.0:
|
||||
version "9.6.0"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-9.6.0.tgz#6bc628159ba0cc81806d286718903b7fc6b13169"
|
||||
integrity sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==
|
||||
version "9.7.0"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-9.7.0.tgz#ff7cd86eb3103ee620204dbd5ca6ffda8b53f645"
|
||||
integrity sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
fast-redact "^3.1.1"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^2.0.0"
|
||||
pino-std-serializers "^7.0.0"
|
||||
process-warning "^4.0.0"
|
||||
process-warning "^5.0.0"
|
||||
quick-format-unescaped "^4.0.3"
|
||||
real-require "^0.2.0"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
sonic-boom "^4.0.1"
|
||||
thread-stream "^3.0.0"
|
||||
|
||||
process-warning@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb"
|
||||
integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==
|
||||
process-warning@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7"
|
||||
integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
|
||||
integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d"
|
||||
integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
@ -276,10 +276,10 @@ thread-stream@^3.0.0:
|
|||
dependencies:
|
||||
real-require "^0.2.0"
|
||||
|
||||
undici-types@~6.21.0:
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
|
||||
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
|
||||
undici-types@~7.8.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294"
|
||||
integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
|
|
|
@ -189,8 +189,7 @@
|
|||
}
|
||||
},
|
||||
"assets": [
|
||||
"src/assets/images",
|
||||
"src/manifest.webmanifest"
|
||||
"src/assets/images"
|
||||
],
|
||||
"styles": [
|
||||
"src/sass/application.scss"
|
||||
|
@ -214,7 +213,6 @@
|
|||
"escape-string-regexp",
|
||||
"is-plain-object",
|
||||
"parse-srcset",
|
||||
"deepmerge",
|
||||
"core-js/features/reflect",
|
||||
"hammerjs",
|
||||
"jschannel"
|
||||
|
|
|
@ -2,25 +2,19 @@ import { NSFWPolicyType } from '@peertube/peertube-models'
|
|||
import { browserSleep, go, setCheckboxEnabled } from '../utils'
|
||||
|
||||
export class AdminConfigPage {
|
||||
async navigateTo (tab: 'instance-homepage' | 'basic-configuration' | 'instance-information' | 'live') {
|
||||
const waitTitles = {
|
||||
'instance-homepage': 'INSTANCE HOMEPAGE',
|
||||
'basic-configuration': 'APPEARANCE',
|
||||
'instance-information': 'INSTANCE',
|
||||
'live': 'LIVE'
|
||||
async navigateTo (page: 'information' | 'live' | 'general' | 'homepage') {
|
||||
const url = '/admin/settings/config/' + page
|
||||
|
||||
const currentUrl = await browser.getUrl()
|
||||
if (!currentUrl.endsWith(url)) {
|
||||
await go(url)
|
||||
}
|
||||
|
||||
const url = '/admin/settings/config/edit-custom#' + tab
|
||||
|
||||
if (await browser.getUrl() !== url) {
|
||||
await go('/admin/settings/config/edit-custom#' + tab)
|
||||
}
|
||||
|
||||
await $('h2=' + waitTitles[tab]).waitForDisplayed()
|
||||
await $('a.active[href="' + url + '"]').waitForDisplayed()
|
||||
}
|
||||
|
||||
async updateNSFWSetting (newValue: NSFWPolicyType) {
|
||||
await this.navigateTo('instance-information')
|
||||
await this.navigateTo('information')
|
||||
|
||||
const elem = $(`#instanceDefaultNSFWPolicy-${newValue} + label`)
|
||||
|
||||
|
@ -32,25 +26,25 @@ export class AdminConfigPage {
|
|||
}
|
||||
|
||||
async updateHomepage (newValue: string) {
|
||||
await this.navigateTo('instance-homepage')
|
||||
await this.navigateTo('homepage')
|
||||
|
||||
return $('#instanceCustomHomepageContent').setValue(newValue)
|
||||
return $('#homepageContent').setValue(newValue)
|
||||
}
|
||||
|
||||
async toggleSignup (enabled: boolean) {
|
||||
await this.navigateTo('basic-configuration')
|
||||
await this.navigateTo('general')
|
||||
|
||||
return setCheckboxEnabled('signupEnabled', enabled)
|
||||
}
|
||||
|
||||
async toggleSignupApproval (required: boolean) {
|
||||
await this.navigateTo('basic-configuration')
|
||||
await this.navigateTo('general')
|
||||
|
||||
return setCheckboxEnabled('signupRequiresApproval', required)
|
||||
}
|
||||
|
||||
async toggleSignupEmailVerification (required: boolean) {
|
||||
await this.navigateTo('basic-configuration')
|
||||
await this.navigateTo('general')
|
||||
|
||||
return setCheckboxEnabled('signupRequiresEmailVerification', required)
|
||||
}
|
||||
|
@ -62,11 +56,18 @@ export class AdminConfigPage {
|
|||
}
|
||||
|
||||
async save () {
|
||||
const button = $('input[type=submit]')
|
||||
const button = $('my-admin-save-bar .save-button')
|
||||
|
||||
try {
|
||||
await button.waitForClickable()
|
||||
await button.click()
|
||||
} catch {
|
||||
// The config may have not been changed
|
||||
return
|
||||
} finally {
|
||||
await browserSleep(1000) // Wait for the button to be clickable
|
||||
}
|
||||
|
||||
await browserSleep(1000)
|
||||
await button.click()
|
||||
await button.waitForClickable({ reverse: true })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ export class LoginPage {
|
|||
}
|
||||
|
||||
async logout () {
|
||||
const loggedInDropdown = $('.logged-in-container .logged-in-info')
|
||||
const loggedInDropdown = $('.logged-in-container .dropdown-toggle')
|
||||
|
||||
await loggedInDropdown.waitForClickable()
|
||||
await loggedInDropdown.click()
|
||||
|
|
|
@ -185,7 +185,7 @@ export class MyAccountPage {
|
|||
const playlist = () => {
|
||||
return $$('my-video-playlist-miniature')
|
||||
.filter(async e => {
|
||||
const t = await e.$('.miniature-name').getText()
|
||||
const t = await e.$('img').getAttribute('aria-label')
|
||||
|
||||
return t.includes(name)
|
||||
})
|
||||
|
|
|
@ -72,15 +72,15 @@ export class PlayerPage {
|
|||
}
|
||||
|
||||
getNSFWContentText () {
|
||||
return $('.video-js .nsfw-content').getText()
|
||||
return $('.video-js .nsfw-info').getText()
|
||||
}
|
||||
|
||||
getNSFWMoreContent () {
|
||||
return $('.video-js .nsfw-more-content')
|
||||
getNSFWDetailsContent () {
|
||||
return $('.video-js .nsfw-details-content')
|
||||
}
|
||||
|
||||
getMoreNSFWInfoButton () {
|
||||
return $('.video-js .nsfw-container button')
|
||||
return $('.video-js .nsfw-info button')
|
||||
}
|
||||
|
||||
async hasPoster () {
|
||||
|
|
|
@ -86,7 +86,7 @@ export class VideoListPage {
|
|||
async expectVideoNSFWTooltip (name: string, summary?: string) {
|
||||
const miniature = await this.getVideoMiniature(name)
|
||||
|
||||
const warning = await miniature.$('.nsfw-warning')
|
||||
const warning = miniature.$('.nsfw-warning')
|
||||
await warning.waitForDisplayed()
|
||||
|
||||
expect(await warning.getAttribute('aria-label')).toEqual(summary)
|
||||
|
|
|
@ -76,7 +76,7 @@ export abstract class VideoManage {
|
|||
await input.waitForClickable()
|
||||
await input.click()
|
||||
|
||||
const nextMonth = $('.p-datepicker-next')
|
||||
const nextMonth = $('.p-datepicker-next-button')
|
||||
await nextMonth.click()
|
||||
|
||||
await $('.p-datepicker-calendar td[aria-label="1"] > span').click()
|
||||
|
@ -135,7 +135,13 @@ export abstract class VideoManage {
|
|||
}
|
||||
|
||||
protected async goOnPage (page: 'Main information' | 'Moderation' | 'Live settings') {
|
||||
const el = $('my-video-manage-container .menu').$('*=' + page)
|
||||
const urls = {
|
||||
'Main information': '',
|
||||
'Moderation': 'moderation',
|
||||
'Live settings': 'live'
|
||||
}
|
||||
|
||||
const el = $(`my-video-manage-container .menu a[href*="/${urls[page]}"]`)
|
||||
await el.waitForClickable()
|
||||
await el.click()
|
||||
}
|
||||
|
|
|
@ -167,6 +167,7 @@ export class VideoWatchPage {
|
|||
|
||||
async clickOnMoreDropdownIcon () {
|
||||
const dropdown = $('my-video-actions-dropdown .action-button')
|
||||
await dropdown.scrollIntoView({ block: 'center' })
|
||||
await dropdown.click()
|
||||
|
||||
await $('.dropdown-menu.show .dropdown-item').waitForDisplayed()
|
||||
|
@ -176,8 +177,12 @@ export class VideoWatchPage {
|
|||
// Playlists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
clickOnSave () {
|
||||
return $('.action-button-save').click()
|
||||
async clickOnSave () {
|
||||
const button = $('.action-button-save')
|
||||
|
||||
await button.scrollIntoView({ block: 'center' })
|
||||
|
||||
return button.click()
|
||||
}
|
||||
|
||||
async createPlaylist (name: string) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari } from '../utils'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari, prepareWebBrowser } from '../utils'
|
||||
|
||||
describe('Live all workflow', () => {
|
||||
let videoWatchPage: VideoWatchPage
|
||||
|
@ -10,9 +10,7 @@ describe('Live all workflow', () => {
|
|||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
playerPage = new PlayerPage()
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should go to the live page', async () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { PlayerPage } from '../po/player.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari } from '../utils'
|
||||
import { FIXTURE_URLS, go, isMobileDevice, isSafari, prepareWebBrowser } from '../utils'
|
||||
|
||||
async function checkCorrectlyPlay (playerPage: PlayerPage) {
|
||||
await playerPage.playAndPauseVideo(false, 2)
|
||||
|
@ -22,9 +22,7 @@ describe('Private videos all workflow', () => {
|
|||
loginPage = new LoginPage(isMobileDevice())
|
||||
playerPage = new PlayerPage()
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should log in', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { VideoListPage } from '../po/video-list.po'
|
|||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoUpdatePage } from '../po/video-update.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { FIXTURE_URLS, go, isIOS, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
function isUploadUnsupported () {
|
||||
if (isMobileDevice() || isSafari()) {
|
||||
|
@ -53,9 +53,7 @@ describe('Videos all workflow', () => {
|
|||
playerPage = new PlayerPage()
|
||||
videoListPage = new VideoListPage(isMobileDevice(), isSafari())
|
||||
|
||||
if (!isMobileDevice()) {
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should log in', async () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Custom server defaults', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -15,7 +15,7 @@ describe('Custom server defaults', () => {
|
|||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Publish default values', function () {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { VideoListPage } from '../po/video-list.po'
|
|||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoSearchPage } from '../po/video-search.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('NSFW', () => {
|
||||
let videoListPage: VideoListPage
|
||||
|
@ -102,6 +102,8 @@ describe('NSFW', () => {
|
|||
|
||||
for (const video of videos) {
|
||||
await videoSearchPage.search(video)
|
||||
|
||||
await browser.saveScreenshot(getScreenshotPath('before-test.png'))
|
||||
await checkVideo({ policy, videoName: video, nsfwTooltip })
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +155,7 @@ describe('NSFW', () => {
|
|||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
anonymousSettingsPage = new AnonymousSettingsPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Preparation', function () {
|
||||
|
@ -265,10 +267,10 @@ describe('NSFW', () => {
|
|||
expect(await moreButton.isDisplayed()).toBeTruthy()
|
||||
|
||||
await moreButton.click()
|
||||
await playerPage.getNSFWMoreContent().waitForDisplayed()
|
||||
await playerPage.getNSFWDetailsContent().waitForDisplayed()
|
||||
|
||||
const moreContent = await playerPage.getNSFWMoreContent().getText()
|
||||
expect(moreContent).toContain('Violence')
|
||||
const moreContent = await playerPage.getNSFWDetailsContent().getText()
|
||||
expect(moreContent).toContain('Potentially violent content')
|
||||
expect(moreContent).toContain('bibi is violent')
|
||||
}
|
||||
|
||||
|
|
119
client/e2e/src/suites-local/page-crash.e2e-spec.ts
Normal file
119
client/e2e/src/suites-local/page-crash.e2e-spec.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { AdminConfigPage } from '../po/admin-config.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, selectCustomSelect, waitServerUp } from '../utils'
|
||||
|
||||
// These tests help to notice crash with invalid translated strings
|
||||
describe('Page crash', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
let loginPage: LoginPage
|
||||
let videoWatchPage: VideoWatchPage
|
||||
let adminConfigPage: AdminConfigPage
|
||||
|
||||
const languages = [
|
||||
'العربية',
|
||||
'Català',
|
||||
'Čeština',
|
||||
'Deutsch',
|
||||
'ελληνικά',
|
||||
'Esperanto',
|
||||
'Español',
|
||||
'Euskara',
|
||||
'فارسی',
|
||||
'Suomi',
|
||||
'Français',
|
||||
'Gàidhlig',
|
||||
'Galego',
|
||||
'Hrvatski',
|
||||
'Magyar',
|
||||
'Íslenska',
|
||||
'Italiano',
|
||||
'日本語',
|
||||
'Taqbaylit',
|
||||
'Norsk bokmål',
|
||||
'Nederlands',
|
||||
'Norsk nynorsk',
|
||||
'Occitan',
|
||||
'Polski',
|
||||
'Português (Brasil)',
|
||||
'Português (Portugal)',
|
||||
'Pусский',
|
||||
'Slovenčina',
|
||||
'Shqip',
|
||||
'Svenska',
|
||||
'ไทย',
|
||||
'Toki Pona',
|
||||
'Türkçe',
|
||||
'украї́нська мо́ва',
|
||||
'Tiếng Việt',
|
||||
'简体中文(中国)',
|
||||
'繁體中文(台灣)'
|
||||
]
|
||||
|
||||
before(async () => {
|
||||
await waitServerUp()
|
||||
|
||||
adminConfigPage = new AdminConfigPage()
|
||||
loginPage = new LoginPage(isMobileDevice())
|
||||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
||||
for (const language of languages) {
|
||||
describe('For language: ' + language, () => {
|
||||
it('Should change the language', async function () {
|
||||
await go('/')
|
||||
|
||||
await $('.settings-button').waitForClickable()
|
||||
await $('.settings-button').click()
|
||||
|
||||
await selectCustomSelect('language', language)
|
||||
|
||||
await $('my-user-interface-settings .primary-button').waitForClickable()
|
||||
await $('my-user-interface-settings .primary-button').click()
|
||||
})
|
||||
|
||||
it('Should upload and watch a video', async function () {
|
||||
await videoPublishPage.navigateTo()
|
||||
await videoPublishPage.uploadVideo('video3.mp4')
|
||||
await videoPublishPage.validSecondStep('video')
|
||||
|
||||
await videoPublishPage.clickOnWatch()
|
||||
await videoWatchPage.waitWatchVideoName('video')
|
||||
})
|
||||
|
||||
it('Should set a homepage', async function () {
|
||||
await adminConfigPage.updateHomepage('My custom homepage content')
|
||||
await adminConfigPage.save()
|
||||
|
||||
// All tests
|
||||
await go('/home')
|
||||
|
||||
await $('*=My custom homepage content').waitForDisplayed()
|
||||
})
|
||||
|
||||
it('Should go on client pages and not crash', async function () {
|
||||
await $('a[href="/videos/overview"]').waitForClickable()
|
||||
await $('a[href="/videos/overview"]').click()
|
||||
|
||||
await $('my-video-overview').waitForExist()
|
||||
})
|
||||
|
||||
it('Should go on videos from subscriptions pages', async function () {
|
||||
await $('a[href="/videos/subscriptions"]').waitForClickable()
|
||||
await $('a[href="/videos/subscriptions"]').click()
|
||||
|
||||
await $('my-videos-user-subscriptions').waitForExist()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.saveScreenshot(getScreenshotPath(`after-page-crash-test-${language}.png`))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
|
@ -3,7 +3,7 @@ import { LoginPage } from '../po/login.po'
|
|||
import { MyAccountPage } from '../po/my-account.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Player settings', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -21,7 +21,7 @@ describe('Player settings', () => {
|
|||
myAccountPage = new MyAccountPage()
|
||||
anonymousSettingsPage = new AnonymousSettingsPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('P2P', function () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AdminPluginPage } from '../po/admin-plugin.po'
|
||||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { getCheckbox, getScreenshotPath, isMobileDevice, waitServerUp } from '../utils'
|
||||
import { getCheckbox, getScreenshotPath, isMobileDevice, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Plugins', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -28,7 +28,7 @@ describe('Plugins', () => {
|
|||
videoPublishPage = new VideoPublishPage()
|
||||
adminPluginPage = new AdminPluginPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
it('Should install hello world plugin', async () => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { AdminConfigPage } from '../po/admin-config.po'
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Publish live', function () {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -18,7 +18,7 @@ describe('Publish live', function () {
|
|||
adminConfigPage = new AdminConfigPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { LoginPage } from '../po/login.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Publish video', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -15,7 +15,7 @@ describe('Publish video', () => {
|
|||
videoPublishPage = new VideoPublishPage()
|
||||
videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari())
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
|
||||
await loginPage.loginAsRootUser()
|
||||
})
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
go,
|
||||
isMobileDevice,
|
||||
MockSMTPServer,
|
||||
prepareWebBrowser,
|
||||
waitServerUp
|
||||
} from '../utils'
|
||||
|
||||
|
@ -76,6 +77,7 @@ describe('Signup', () => {
|
|||
}) {
|
||||
await loginPage.loginAsRootUser()
|
||||
|
||||
// Ensure we change the state of the form to "dirty" so we can save the form
|
||||
await adminConfigPage.toggleSignup(options.enabled)
|
||||
|
||||
if (options.enabled) {
|
||||
|
@ -104,7 +106,7 @@ describe('Signup', () => {
|
|||
signupPage = new SignupPage()
|
||||
adminRegistrationPage = new AdminRegistrationPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Signup disabled', function () {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
go,
|
||||
isMobileDevice,
|
||||
MockSMTPServer,
|
||||
prepareWebBrowser,
|
||||
waitServerUp
|
||||
} from '../utils'
|
||||
|
||||
|
@ -29,7 +30,7 @@ describe('User settings', () => {
|
|||
|
||||
await MockSMTPServer.Instance.collectEmails(await getEmailPort(), emails)
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Email', function () {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { PlayerPage } from '../po/player.po'
|
|||
import { SignupPage } from '../po/signup.po'
|
||||
import { VideoPublishPage } from '../po/video-publish.po'
|
||||
import { VideoWatchPage } from '../po/video-watch.po'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, waitServerUp } from '../utils'
|
||||
import { getScreenshotPath, go, isMobileDevice, isSafari, prepareWebBrowser, waitServerUp } from '../utils'
|
||||
|
||||
describe('Password protected videos', () => {
|
||||
let videoPublishPage: VideoPublishPage
|
||||
|
@ -50,7 +50,7 @@ describe('Password protected videos', () => {
|
|||
playerPage = new PlayerPage()
|
||||
myAccountPage = new MyAccountPage()
|
||||
|
||||
await browser.maximizeWindow()
|
||||
await prepareWebBrowser()
|
||||
})
|
||||
|
||||
describe('Owner', function () {
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
async function browserSleep (amount: number) {
|
||||
export async function browserSleep (amount: number) {
|
||||
await browser.pause(amount)
|
||||
}
|
||||
|
||||
function isMobileDevice () {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function isMobileDevice () {
|
||||
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
|
||||
|
||||
return platformName === 'android' || platformName === 'ios'
|
||||
}
|
||||
|
||||
function isAndroid () {
|
||||
export function isAndroid () {
|
||||
const platformName = (browser.capabilities['platformName'] || '').toLowerCase()
|
||||
|
||||
return platformName === 'android'
|
||||
}
|
||||
|
||||
function isSafari () {
|
||||
export function isSafari () {
|
||||
return browser.capabilities['browserName'] &&
|
||||
browser.capabilities['browserName'].toLowerCase() === 'safari'
|
||||
}
|
||||
|
||||
function isIOS () {
|
||||
export function isIOS () {
|
||||
return isMobileDevice() && isSafari()
|
||||
}
|
||||
|
||||
async function go (url: string) {
|
||||
export async function go (url: string) {
|
||||
await browser.url(url)
|
||||
|
||||
await browser.execute(() => {
|
||||
|
@ -33,7 +35,20 @@ async function go (url: string) {
|
|||
})
|
||||
}
|
||||
|
||||
async function waitServerUp () {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function prepareWebBrowser () {
|
||||
if (isMobileDevice()) return
|
||||
|
||||
// Window size on chromium doesn't seem to work in "new" headless mode
|
||||
if (process.env.MOZ_HEADLESS_WIDTH) {
|
||||
await browser.setWindowSize(+process.env.MOZ_HEADLESS_WIDTH, +process.env.MOZ_HEADLESS_HEIGHT)
|
||||
}
|
||||
|
||||
await browser.maximizeWindow()
|
||||
}
|
||||
|
||||
export async function waitServerUp () {
|
||||
await browser.waitUntil(async () => {
|
||||
await go('/')
|
||||
await browserSleep(500)
|
||||
|
@ -41,13 +56,3 @@ async function waitServerUp () {
|
|||
return $('<my-app>').isDisplayed()
|
||||
}, { timeout: 20 * 1000 })
|
||||
}
|
||||
|
||||
export {
|
||||
isMobileDevice,
|
||||
isSafari,
|
||||
isIOS,
|
||||
isAndroid,
|
||||
waitServerUp,
|
||||
go,
|
||||
browserSleep
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ export async function clickOnRadio (name: string) {
|
|||
export async function selectCustomSelect (id: string, valueLabel: string) {
|
||||
const wrapper = $(`[formcontrolname=${id}] span[role=combobox]`)
|
||||
|
||||
await wrapper.waitForExist()
|
||||
await wrapper.scrollIntoView({ block: 'center' })
|
||||
await wrapper.waitForClickable()
|
||||
await wrapper.click()
|
||||
|
||||
|
@ -65,9 +67,9 @@ export async function selectCustomSelect (id: string, valueLabel: string) {
|
|||
|
||||
export async function findParentElement (
|
||||
el: ChainablePromiseElement,
|
||||
finder: (el: WebdriverIO.Element) => Promise<boolean>
|
||||
finder: (el: ChainablePromiseElement) => Promise<boolean>
|
||||
) {
|
||||
if (await finder(el) === true) return el
|
||||
|
||||
return findParentElement(await el.parentElement(), finder)
|
||||
return findParentElement(el.parentElement(), finder)
|
||||
}
|
||||
|
|
|
@ -95,18 +95,18 @@ module.exports = {
|
|||
{
|
||||
browserName: 'Chrome',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Latest Chrome Android', deviceName: 'Samsung Galaxy S8', osVersion: '7.0' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Latest Chrome Android', deviceName: 'Samsung Galaxy S10', osVersion: '9.0' })
|
||||
},
|
||||
{
|
||||
browserName: 'Safari',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 11', osVersion: '14' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 12', osVersion: '14' })
|
||||
},
|
||||
|
||||
{
|
||||
browserName: 'Safari',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad Pro 11 2020', osVersion: '14' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad Pro 12.9 2021', osVersion: '14' })
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -121,7 +121,8 @@ module.exports = {
|
|||
|
||||
services: [
|
||||
[
|
||||
'browserstack', { browserstackLocal: true }
|
||||
'browserstack',
|
||||
{ browserstackLocal: true }
|
||||
]
|
||||
],
|
||||
|
||||
|
@ -174,6 +175,5 @@ module.exports = {
|
|||
|
||||
onPrepare: onBrowserStackPrepare,
|
||||
onComplete: onBrowserStackComplete
|
||||
|
||||
} as WebdriverIO.Config
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ module.exports = {
|
|||
'browserName': 'chrome',
|
||||
'acceptInsecureCerts': true,
|
||||
'goog:chromeOptions': {
|
||||
args: [ '--disable-gpu', windowSizeArg ],
|
||||
args: [ '--headless', '--disable-gpu', windowSizeArg ],
|
||||
prefs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,6 +150,7 @@ export default defineConfig([
|
|||
'no-return-assign': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'import/no-named-default': 'off',
|
||||
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
|
||||
|
||||
"@typescript-eslint/no-deprecated": [ 'error', {
|
||||
allow: [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "7.2.3",
|
||||
"version": "7.3.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
@ -56,6 +56,7 @@
|
|||
"@peertube/xliffmerge": "^2.0.3",
|
||||
"@plussub/srt-vtt-parser": "^2.0.5",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@primeng/themes": "^19.1.2",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/jschannel": "^1.0.0",
|
||||
|
@ -94,7 +95,7 @@
|
|||
"ngx-uploadx": "^7.0.0",
|
||||
"p2p-media-loader-core": "^2.2.1",
|
||||
"p2p-media-loader-hlsjs": "^2.2.1",
|
||||
"primeng": "^17",
|
||||
"primeng": "^19.1.2",
|
||||
"rxjs": "^7.3.0",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"sha.js": "^2.4.11",
|
||||
|
@ -111,7 +112,7 @@
|
|||
"video.js": "^7.19.2",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-checker": "^0.9.3",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -51,15 +51,14 @@ export class AccountVideosComponent implements OnInit, OnDestroy, DisableForReus
|
|||
}
|
||||
|
||||
getVideosObservable (pagination: ComponentPaginationLight, filters: VideoFilters) {
|
||||
const options = {
|
||||
return this.videoService.listAccountVideos({
|
||||
...filters.toVideosAPIObject(),
|
||||
|
||||
videoPagination: pagination,
|
||||
account: this.account,
|
||||
skipCount: true
|
||||
}
|
||||
|
||||
return this.videoService.listAccountVideos(options)
|
||||
skipCount: true,
|
||||
includeScheduledLive: true
|
||||
})
|
||||
}
|
||||
|
||||
getSyndicationItems () {
|
||||
|
|
9
client/src/app/+admin/config/admin-config.component.html
Normal file
9
client/src/app/+admin/config/admin-config.component.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div class="root">
|
||||
<div>
|
||||
<my-lateral-menu [config]="menuConfig"></my-lateral-menu>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
13
client/src/app/+admin/config/admin-config.component.scss
Normal file
13
client/src/app/+admin/config/admin-config.component.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $medium-view) {
|
||||
.root {
|
||||
margin-bottom: 150px;
|
||||
}
|
||||
}
|
72
client/src/app/+admin/config/admin-config.component.ts
Normal file
72
client/src/app/+admin/config/admin-config.component.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { LateralMenuComponent, LateralMenuConfig } from '../../shared/shared-main/menu/lateral-menu.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config',
|
||||
styleUrls: [ './admin-config.component.scss' ],
|
||||
templateUrl: './admin-config.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
LateralMenuComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigComponent implements OnInit {
|
||||
menuConfig: LateralMenuConfig
|
||||
|
||||
ngOnInit (): void {
|
||||
this.menuConfig = {
|
||||
title: $localize`Configuration`,
|
||||
entries: [
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Information`,
|
||||
routerLink: 'information'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Logo`,
|
||||
routerLink: 'logo'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`General`,
|
||||
routerLink: 'general'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Homepage`,
|
||||
routerLink: 'homepage'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Customization`,
|
||||
routerLink: 'customization'
|
||||
},
|
||||
|
||||
{ type: 'separator' },
|
||||
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`VOD`,
|
||||
routerLink: 'vod'
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Live`,
|
||||
routerLink: 'live'
|
||||
},
|
||||
|
||||
{ type: 'separator' },
|
||||
|
||||
{
|
||||
type: 'link',
|
||||
label: $localize`Advanced`,
|
||||
routerLink: 'advanced'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,64 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config'
|
||||
import { UserRightGuard } from '@app/core'
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
import { inject } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, Routes } from '@angular/router'
|
||||
import { CanDeactivateGuard, ServerService, UserRightGuard } from '@app/core'
|
||||
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
||||
import { CustomConfig, UserRight, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { map } from 'rxjs'
|
||||
import { AdminConfigComponent } from './admin-config.component'
|
||||
import {
|
||||
AdminConfigAdvancedComponent,
|
||||
AdminConfigGeneralComponent,
|
||||
AdminConfigHomepageComponent,
|
||||
AdminConfigInformationComponent,
|
||||
AdminConfigLiveComponent,
|
||||
AdminConfigVODComponent
|
||||
} from './pages'
|
||||
import { AdminConfigCustomizationComponent } from './pages/admin-config-customization.component'
|
||||
import { AdminConfigService } from '../../shared/shared-admin/admin-config.service'
|
||||
import { AdminConfigLogoComponent } from './pages/admin-config-logo.component'
|
||||
import { InstanceLogoService } from '../../shared/shared-instance/instance-logo.service'
|
||||
|
||||
export const customConfigResolver: ResolveFn<CustomConfig> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(AdminConfigService).getCustomConfig()
|
||||
}
|
||||
|
||||
export const homepageResolver: ResolveFn<string> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(CustomPageService).getInstanceHomepage()
|
||||
.pipe(map(({ content }) => content))
|
||||
}
|
||||
|
||||
export const categoriesResolver: ResolveFn<VideoConstant<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoCategories()
|
||||
}
|
||||
|
||||
export const languagesResolver: ResolveFn<VideoConstant<string>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoLanguages()
|
||||
}
|
||||
|
||||
export const licencesResolver: ResolveFn<VideoConstant<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoLicences()
|
||||
}
|
||||
|
||||
export const privaciesResolver: ResolveFn<VideoConstant<VideoPrivacyType>[]> = (
|
||||
_route: ActivatedRouteSnapshot,
|
||||
_state: RouterStateSnapshot
|
||||
) => {
|
||||
return inject(ServerService).getVideoPrivacies()
|
||||
}
|
||||
|
||||
export const commentPoliciesResolver: ResolveFn<VideoConstant<VideoCommentPolicyType>[]> = (
|
||||
_route: ActivatedRouteSnapshot,
|
||||
_state: RouterStateSnapshot
|
||||
) => {
|
||||
return inject(ServerService).getCommentPolicies()
|
||||
}
|
||||
|
||||
export const logosResolver: ResolveFn<ReturnType<InstanceLogoService['getAllLogos']>> = (
|
||||
_route: ActivatedRouteSnapshot,
|
||||
_state: RouterStateSnapshot
|
||||
) => {
|
||||
return inject(InstanceLogoService).getAllLogos()
|
||||
}
|
||||
|
||||
export const configRoutes: Routes = [
|
||||
{
|
||||
|
@ -10,18 +67,117 @@ export const configRoutes: Routes = [
|
|||
data: {
|
||||
userRight: UserRight.MANAGE_CONFIGURATION
|
||||
},
|
||||
resolve: {
|
||||
customConfig: customConfigResolver
|
||||
},
|
||||
providers: [
|
||||
InstanceLogoService
|
||||
],
|
||||
component: AdminConfigComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'edit-custom',
|
||||
// Old path with PeerTube < 7.3
|
||||
path: 'edit-custom',
|
||||
redirectTo: 'information',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'edit-custom',
|
||||
component: EditCustomConfigComponent,
|
||||
path: '',
|
||||
redirectTo: 'information',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'homepage',
|
||||
component: AdminConfigHomepageComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
homepageContent: homepageResolver
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Edit custom configuration`
|
||||
title: $localize`Edit your platform homepage`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'customization',
|
||||
component: AdminConfigCustomizationComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Platform customization`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'information',
|
||||
component: AdminConfigInformationComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
categories: categoriesResolver,
|
||||
languages: languagesResolver
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Platform information`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'logo',
|
||||
component: AdminConfigLogoComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
logos: logosResolver
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Platform logos`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'general',
|
||||
component: AdminConfigGeneralComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
privacies: privaciesResolver,
|
||||
licences: licencesResolver,
|
||||
commentPolicies: commentPoliciesResolver
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`General configuration`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'vod',
|
||||
component: AdminConfigVODComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`VOD configuration`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'live',
|
||||
component: AdminConfigLiveComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Live configuration`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'advanced',
|
||||
component: AdminConfigAdvancedComponent,
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Advanced configuration`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
<ng-container [formGroup]="form()">
|
||||
|
||||
<div class="pt-two-cols mt-5"> <!-- cache grid -->
|
||||
|
||||
<div class="title-col">
|
||||
<h2 i18n>CACHE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Some files are not federated, and fetched when necessary. Define their caching policies.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="cache">
|
||||
<div class="form-group" formGroupName="previews">
|
||||
<label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cachePreviewsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.previews.size'] }"
|
||||
>
|
||||
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().cache.previews.size" class="form-error" role="alert">{{ formErrors().cache.previews.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="captions">
|
||||
<label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheCaptionsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.captions.size'] }"
|
||||
>
|
||||
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().cache.captions.size" class="form-error" role="alert">{{ formErrors().cache.captions.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrents">
|
||||
<label i18n for="cacheTorrentsSize">Number of video torrents to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheTorrentsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.torrents.size'] }"
|
||||
>
|
||||
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().cache.torrents.size" class="form-error" role="alert">{{ formErrors().cache.torrents.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrents">
|
||||
<label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheStoryboardsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors()['cache.storyboards.size'] }"
|
||||
>
|
||||
<span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().cache.storyboards.size" class="form-error" role="alert">{{ formErrors().cache.storyboards.size }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- cache grid -->
|
||||
<div class="title-col">
|
||||
<div class="anchor" id="customizations"></div> <!-- customizations anchor -->
|
||||
<h2 i18n>CUSTOMIZATIONS</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Slight modifications to your PeerTube instance for when creating a plugin or theme is overkill.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="instance">
|
||||
<ng-container formGroupName="customizations">
|
||||
<div class="form-group">
|
||||
<label i18n for="customizationJavascript">JavaScript</label>
|
||||
<my-help>
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">Write JavaScript code directly. Example:</p>
|
||||
<pre>console.log('my instance is amazing');</pre>
|
||||
</ng-container>
|
||||
</my-help>
|
||||
|
||||
<textarea
|
||||
id="customizationJavascript" formControlName="javascript" class="form-control" dir="ltr"
|
||||
[ngClass]="{ 'input-error': formErrors()['instance.customizations.javascript'] }"
|
||||
></textarea>
|
||||
|
||||
<div *ngIf="formErrors().instance.customizations.javascript" class="form-error" role="alert">{{ formErrors().instance.customizations.javascript }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="customizationCSS">CSS</label>
|
||||
|
||||
<my-help>
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">Write CSS code directly. Example:</p>
|
||||
<pre>
|
||||
#custom-css {{ '{' }}
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
<p class="mb-2">Prepend with <em>#custom-css</em> to override styles. Example:</p>
|
||||
<pre>
|
||||
#custom-css .logged-in-email {{ '{' }}
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
</ng-container>
|
||||
</my-help>
|
||||
|
||||
<textarea
|
||||
id="customizationCSS" formControlName="css" class="form-control" dir="ltr"
|
||||
[ngClass]="{ 'input-error': formErrors()['instance.customizations.css'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors().instance.customizations.css" class="form-error" role="alert">{{ formErrors().instance.customizations.css }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
|
@ -1,19 +0,0 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { Component, input } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-advanced-configuration',
|
||||
templateUrl: './edit-advanced-configuration.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [ FormsModule, ReactiveFormsModule, NgClass, NgIf, HelpComponent ]
|
||||
})
|
||||
export class EditAdvancedConfigurationComponent {
|
||||
readonly form = input<FormGroup>(undefined)
|
||||
readonly formErrors = input<any>(undefined)
|
||||
|
||||
getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') {
|
||||
return this.form().value['cache'][type]['size']
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { Component, OnChanges, OnInit, SimpleChanges, inject, input } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { ThemeService } from '@app/core'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-basic-configuration',
|
||||
templateUrl: './edit-basic-configuration.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
SelectCustomValueComponent,
|
||||
NgIf,
|
||||
PeertubeCheckboxComponent,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
NgClass,
|
||||
UserRealQuotaInfoComponent,
|
||||
SelectOptionsComponent,
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
||||
private configService = inject(ConfigService)
|
||||
private themeService = inject(ThemeService)
|
||||
|
||||
readonly form = input<FormGroup>(undefined)
|
||||
readonly formErrors = input<any>(undefined)
|
||||
|
||||
readonly serverConfig = input<HTMLServerConfig>(undefined)
|
||||
|
||||
signupAlertMessage: string
|
||||
defaultLandingPageOptions: SelectOptionsItem[] = []
|
||||
availableThemes: SelectOptionsItem[]
|
||||
|
||||
exportExpirationOptions: SelectOptionsItem[] = []
|
||||
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
||||
|
||||
ngOnInit () {
|
||||
this.buildLandingPageOptions()
|
||||
this.checkSignupField()
|
||||
this.checkImportSyncField()
|
||||
|
||||
this.availableThemes = [
|
||||
this.themeService.getDefaultThemeItem(),
|
||||
|
||||
...this.themeService.buildAvailableThemes()
|
||||
]
|
||||
|
||||
this.exportExpirationOptions = [
|
||||
{ id: 1000 * 3600 * 24, label: $localize`1 day` },
|
||||
{ id: 1000 * 3600 * 24 * 2, label: $localize`2 days` },
|
||||
{ id: 1000 * 3600 * 24 * 7, label: $localize`7 days` },
|
||||
{ id: 1000 * 3600 * 24 * 30, label: $localize`30 days` }
|
||||
]
|
||||
|
||||
this.exportMaxUserVideoQuotaOptions = this.configService.videoQuotaOptions.filter(o => (o.id as number) >= 1)
|
||||
}
|
||||
|
||||
ngOnChanges (changes: SimpleChanges) {
|
||||
if (changes['serverConfig']) {
|
||||
this.buildLandingPageOptions()
|
||||
}
|
||||
}
|
||||
|
||||
countExternalAuth () {
|
||||
return this.serverConfig().plugin.registeredExternalAuths.length
|
||||
}
|
||||
|
||||
getVideoQuotaOptions () {
|
||||
return this.configService.videoQuotaOptions
|
||||
}
|
||||
|
||||
getVideoQuotaDailyOptions () {
|
||||
return this.configService.videoQuotaDailyOptions
|
||||
}
|
||||
|
||||
doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) {
|
||||
const enabled = this.form().value['trending']['videos']['algorithms']['enabled']
|
||||
if (!Array.isArray(enabled)) return false
|
||||
|
||||
return !!enabled.find((e: string) => e === algorithm)
|
||||
}
|
||||
|
||||
getUserVideoQuota () {
|
||||
return this.form().value['user']['videoQuota']
|
||||
}
|
||||
|
||||
isExportUsersEnabled () {
|
||||
return this.form().value['export']['users']['enabled'] === true
|
||||
}
|
||||
|
||||
getDisabledExportUsersClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isExportUsersEnabled() }
|
||||
}
|
||||
|
||||
isSignupEnabled () {
|
||||
return this.form().value['signup']['enabled'] === true
|
||||
}
|
||||
|
||||
getDisabledSignupClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isSignupEnabled() }
|
||||
}
|
||||
|
||||
isImportVideosHttpEnabled (): boolean {
|
||||
return this.form().value['import']['videos']['http']['enabled'] === true
|
||||
}
|
||||
|
||||
importSynchronizationChecked () {
|
||||
return this.isImportVideosHttpEnabled() && this.form().value['import']['videoChannelSynchronization']['enabled']
|
||||
}
|
||||
|
||||
hasUnlimitedSignup () {
|
||||
return this.form().value['signup']['limit'] === -1
|
||||
}
|
||||
|
||||
isSearchIndexEnabled () {
|
||||
return this.form().value['search']['searchIndex']['enabled'] === true
|
||||
}
|
||||
|
||||
getDisabledSearchIndexClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isSearchIndexEnabled() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isTranscriptionEnabled () {
|
||||
return this.form().value['videoTranscription']['enabled'] === true
|
||||
}
|
||||
|
||||
getTranscriptionRunnerDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isTranscriptionEnabled() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isAutoFollowIndexEnabled () {
|
||||
return this.form().value['followings']['instance']['autoFollowIndex']['enabled'] === true
|
||||
}
|
||||
|
||||
buildLandingPageOptions () {
|
||||
let links: { label: string, path: string }[] = []
|
||||
|
||||
if (this.serverConfig().homepage.enabled) {
|
||||
links.push({ label: $localize`Home`, path: '/home' })
|
||||
}
|
||||
|
||||
links = links.concat([
|
||||
{ label: $localize`Discover`, path: '/videos/overview' },
|
||||
{ label: $localize`Browse all videos`, path: '/videos/browse' },
|
||||
{ label: $localize`Browse local videos`, path: '/videos/browse?scope=local' }
|
||||
])
|
||||
|
||||
this.defaultLandingPageOptions = links.map(o => ({
|
||||
id: o.path,
|
||||
label: o.label,
|
||||
description: o.path
|
||||
}))
|
||||
}
|
||||
|
||||
private checkImportSyncField () {
|
||||
const importSyncControl = this.form().get('import.videoChannelSynchronization.enabled')
|
||||
const importVideosHttpControl = this.form().get('import.videos.http.enabled')
|
||||
|
||||
importVideosHttpControl.valueChanges
|
||||
.subscribe(httpImportEnabled => {
|
||||
importSyncControl.setValue(httpImportEnabled && importSyncControl.value)
|
||||
if (httpImportEnabled) {
|
||||
importSyncControl.enable()
|
||||
} else {
|
||||
importSyncControl.disable()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private checkSignupField () {
|
||||
const signupControl = this.form().get('signup.enabled')
|
||||
|
||||
signupControl.valueChanges
|
||||
.pipe(pairwise())
|
||||
.subscribe(([ oldValue, newValue ]) => {
|
||||
if (oldValue === false && newValue === true) {
|
||||
this.signupAlertMessage =
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
|
||||
|
||||
this.form().patchValue({
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
signupControl.updateValueAndValidity()
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
import { Injectable } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { formatICU } from '@app/helpers'
|
||||
|
||||
export type ResolutionOption = {
|
||||
id: string
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EditConfigurationService {
|
||||
|
||||
getTranscodingResolutions () {
|
||||
return [
|
||||
{
|
||||
id: '0p',
|
||||
label: $localize`Audio-only`,
|
||||
// eslint-disable-next-line max-len
|
||||
description: $localize`"Split audio and video" must be enabled for the PeerTube player to propose an "Audio only" resolution to users`
|
||||
},
|
||||
{
|
||||
id: '144p',
|
||||
label: $localize`144p`
|
||||
},
|
||||
{
|
||||
id: '240p',
|
||||
label: $localize`240p`
|
||||
},
|
||||
{
|
||||
id: '360p',
|
||||
label: $localize`360p`
|
||||
},
|
||||
{
|
||||
id: '480p',
|
||||
label: $localize`480p`
|
||||
},
|
||||
{
|
||||
id: '720p',
|
||||
label: $localize`720p`
|
||||
},
|
||||
{
|
||||
id: '1080p',
|
||||
label: $localize`1080p`
|
||||
},
|
||||
{
|
||||
id: '1440p',
|
||||
label: $localize`1440p`
|
||||
},
|
||||
{
|
||||
id: '2160p',
|
||||
label: $localize`2160p`
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
isTranscodingEnabled (form: FormGroup) {
|
||||
return form.value['transcoding']['enabled'] === true
|
||||
}
|
||||
|
||||
isHLSEnabled (form: FormGroup) {
|
||||
return form.value['transcoding']['hls']['enabled'] === true
|
||||
}
|
||||
|
||||
isRemoteRunnerVODEnabled (form: FormGroup) {
|
||||
return form.value['transcoding']['remoteRunners']['enabled'] === true
|
||||
}
|
||||
|
||||
isRemoteRunnerLiveEnabled (form: FormGroup) {
|
||||
return form.value['live']['transcoding']['remoteRunners']['enabled'] === true
|
||||
}
|
||||
|
||||
isStudioEnabled (form: FormGroup) {
|
||||
return form.value['videoStudio']['enabled'] === true
|
||||
}
|
||||
|
||||
isLiveEnabled (form: FormGroup) {
|
||||
return form.value['live']['enabled'] === true
|
||||
}
|
||||
|
||||
isLiveTranscodingEnabled (form: FormGroup) {
|
||||
return form.value['live']['transcoding']['enabled'] === true
|
||||
}
|
||||
|
||||
getTotalTranscodingThreads (form: FormGroup) {
|
||||
const transcodingEnabled = form.value['transcoding']['enabled']
|
||||
const transcodingThreads = form.value['transcoding']['threads']
|
||||
const liveTranscodingEnabled = form.value['live']['transcoding']['enabled']
|
||||
const liveTranscodingThreads = form.value['live']['transcoding']['threads']
|
||||
|
||||
// checks whether all enabled method are on fixed values and not on auto (= 0)
|
||||
let noneOnAuto = !transcodingEnabled || +transcodingThreads > 0
|
||||
noneOnAuto &&= !liveTranscodingEnabled || +liveTranscodingThreads > 0
|
||||
|
||||
// count total of fixed value, repalcing auto by a single thread (knowing it will display "at least")
|
||||
let value = 0
|
||||
if (transcodingEnabled) value += +transcodingThreads || 1
|
||||
if (liveTranscodingEnabled) value += +liveTranscodingThreads || 1
|
||||
|
||||
return {
|
||||
value,
|
||||
atMost: noneOnAuto, // auto switches everything to a least estimation since ffmpeg will take as many threads as possible
|
||||
unit: formatICU($localize`{value, plural, =1 {thread} other {threads}}`, { value })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
<h1 class="visually-hidden" i18n>Configuration</h1>
|
||||
|
||||
<my-alert type="warning" *ngIf="!isUpdateAllowed()" i18n>
|
||||
Updating instance configuration from the web interface is disabled by the system administrator.
|
||||
</my-alert>
|
||||
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs">
|
||||
|
||||
<ng-container ngbNavItem="instance-homepage">
|
||||
<a ngbNavLink i18n>Homepage</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-homepage [form]="form" [formErrors]="formErrors"></my-edit-homepage>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="instance-information">
|
||||
<a ngbNavLink i18n>Information</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
|
||||
</my-edit-instance-information>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="basic-configuration">
|
||||
<a ngbNavLink i18n>Basic</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
||||
</my-edit-basic-configuration>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="transcoding">
|
||||
<a ngbNavLink i18n>VOD Transcoding</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-vod-transcoding [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
||||
</my-edit-vod-transcoding>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="live">
|
||||
<a ngbNavLink i18n>Live streaming</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-live-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
||||
</my-edit-live-configuration>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="advanced-configuration">
|
||||
<a ngbNavLink i18n>Advanced</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
|
||||
</my-edit-advanced-configuration>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
|
||||
<div class="row mt-4"> <!-- submit placement block -->
|
||||
<div class="col-md-7 col-xl-5"></div>
|
||||
<div class="col-md-5 col-xl-5">
|
||||
|
||||
<div role="alert" class="form-error submit-error" i18n *ngIf="!form.valid && isUpdateAllowed()">
|
||||
There are errors in the form:
|
||||
|
||||
<ul>
|
||||
<li *ngFor="let error of grabAllErrors()">
|
||||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<span role="alert" class="form-error submit-error" i18n *ngIf="!hasLiveAllowReplayConsistentOptions()">
|
||||
You cannot allow live replay if you don't enable transcoding.
|
||||
</span>
|
||||
|
||||
<span i18n *ngIf="!isUpdateAllowed()">
|
||||
You cannot change the server configuration because it's managed externally.
|
||||
</span>
|
||||
|
||||
<input
|
||||
class="peertube-button primary-button"
|
||||
(click)="formValidated()" type="submit" i18n-value value="Update configuration"
|
||||
[disabled]="!form.valid || !hasConsistentOptions() || !isUpdateAllowed()"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -1,144 +0,0 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
|
||||
$form-base-input-width: 340px;
|
||||
$form-max-width: 500px;
|
||||
|
||||
form {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
my-markdown-textarea {
|
||||
display: block;
|
||||
max-width: $form-max-width;
|
||||
}
|
||||
|
||||
.homepage my-markdown-textarea {
|
||||
display: block;
|
||||
max-width: 90%;
|
||||
|
||||
::ng-deep textarea {
|
||||
height: 300px !important;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
input[type=number] {
|
||||
@include peertube-input-text($form-base-input-width);
|
||||
}
|
||||
|
||||
.number-with-unit {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
|
||||
input[type=number] + span {
|
||||
position: absolute;
|
||||
top: 0.4em;
|
||||
right: 3em;
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
@include peertube-checkbox;
|
||||
}
|
||||
|
||||
.peertube-select-container {
|
||||
@include peertube-select-container($form-base-input-width);
|
||||
}
|
||||
|
||||
my-select-checkbox,
|
||||
my-select-options,
|
||||
my-select-custom-value {
|
||||
display: block;
|
||||
|
||||
@include responsive-width($form-base-input-width);
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
display: flex;
|
||||
|
||||
@include margin-left(auto);
|
||||
|
||||
+ .form-error {
|
||||
display: inline;
|
||||
|
||||
@include margin-left(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.inner-form-description {
|
||||
font-size: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
|
||||
@include peertube-textarea(500px, 150px);
|
||||
|
||||
&.small {
|
||||
height: 75px;
|
||||
}
|
||||
}
|
||||
|
||||
.label-small-info {
|
||||
font-style: italic;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.disabled-checkbox-extra {
|
||||
&,
|
||||
::ng-deep label {
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
ngb-tabset:not(.previews) ::ng-deep {
|
||||
.nav-link {
|
||||
font-size: 105%;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-error {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-signup {
|
||||
width: fit-content;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.callout-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: $form-max-width;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: $font-bold;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
|
@ -1,479 +0,0 @@
|
|||
import { NgFor, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
||||
import { Notifier } from '@app/core'
|
||||
import { ServerService } from '@app/core/server/server.service'
|
||||
import { URL_VALIDATOR } from '@app/shared/form-validators/common-validators'
|
||||
import {
|
||||
ADMIN_EMAIL_VALIDATOR,
|
||||
CACHE_SIZE_VALIDATOR,
|
||||
CONCURRENCY_VALIDATOR,
|
||||
EXPORT_EXPIRATION_VALIDATOR,
|
||||
EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
||||
INSTANCE_NAME_VALIDATOR,
|
||||
INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
||||
MAX_INSTANCE_LIVES_VALIDATOR,
|
||||
MAX_LIVE_DURATION_VALIDATOR,
|
||||
MAX_SYNC_PER_USER,
|
||||
MAX_USER_LIVES_VALIDATOR,
|
||||
MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR,
|
||||
SERVICES_TWITTER_USERNAME_VALIDATOR,
|
||||
SIGNUP_LIMIT_VALIDATOR,
|
||||
SIGNUP_MINIMUM_AGE_VALIDATOR,
|
||||
TRANSCODING_MAX_FPS_VALIDATOR,
|
||||
TRANSCODING_THREADS_VALIDATOR
|
||||
} from '@app/shared/form-validators/custom-config-validators'
|
||||
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
||||
import { NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavLinkBase, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { CustomConfig, CustomPage, HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import merge from 'lodash-es/merge'
|
||||
import omit from 'lodash-es/omit'
|
||||
import { forkJoin } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { EditAdvancedConfigurationComponent } from './edit-advanced-configuration.component'
|
||||
import { EditBasicConfigurationComponent } from './edit-basic-configuration.component'
|
||||
import { EditConfigurationService } from './edit-configuration.service'
|
||||
import { EditHomepageComponent } from './edit-homepage.component'
|
||||
import { EditInstanceInformationComponent } from './edit-instance-information.component'
|
||||
import { EditLiveConfigurationComponent } from './edit-live-configuration.component'
|
||||
import { EditVODTranscodingComponent } from './edit-vod-transcoding.component'
|
||||
|
||||
type ComponentCustomConfig = CustomConfig & {
|
||||
instanceCustomHomepage: CustomPage
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-custom-config',
|
||||
templateUrl: './edit-custom-config.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [
|
||||
NgIf,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbNav,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavLinkBase,
|
||||
NgbNavContent,
|
||||
EditHomepageComponent,
|
||||
EditInstanceInformationComponent,
|
||||
EditBasicConfigurationComponent,
|
||||
EditVODTranscodingComponent,
|
||||
EditLiveConfigurationComponent,
|
||||
EditAdvancedConfigurationComponent,
|
||||
NgbNavOutlet,
|
||||
NgFor,
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
private router = inject(Router)
|
||||
private route = inject(ActivatedRoute)
|
||||
private notifier = inject(Notifier)
|
||||
private configService = inject(ConfigService)
|
||||
private customPage = inject(CustomPageService)
|
||||
private serverService = inject(ServerService)
|
||||
private editConfigurationService = inject(EditConfigurationService)
|
||||
|
||||
activeNav: string
|
||||
|
||||
customConfig: ComponentCustomConfig
|
||||
serverConfig: HTMLServerConfig
|
||||
|
||||
homepage: CustomPage
|
||||
|
||||
languageItems: SelectOptionsItem[] = []
|
||||
categoryItems: SelectOptionsItem[] = []
|
||||
|
||||
ngOnInit () {
|
||||
this.serverConfig = this.serverService.getHTMLConfig()
|
||||
|
||||
const formGroupData: { [key in keyof ComponentCustomConfig]: any } = {
|
||||
instance: {
|
||||
name: INSTANCE_NAME_VALIDATOR,
|
||||
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
||||
description: null,
|
||||
|
||||
isNSFW: false,
|
||||
defaultNSFWPolicy: null,
|
||||
|
||||
terms: null,
|
||||
codeOfConduct: null,
|
||||
|
||||
creationReason: null,
|
||||
moderationInformation: null,
|
||||
administrator: null,
|
||||
maintenanceLifetime: null,
|
||||
businessModel: null,
|
||||
|
||||
hardwareInformation: null,
|
||||
|
||||
categories: null,
|
||||
languages: null,
|
||||
|
||||
serverCountry: null,
|
||||
support: {
|
||||
text: null
|
||||
},
|
||||
social: {
|
||||
externalLink: URL_VALIDATOR,
|
||||
mastodonLink: URL_VALIDATOR,
|
||||
blueskyLink: URL_VALIDATOR
|
||||
},
|
||||
|
||||
defaultClientRoute: null,
|
||||
|
||||
customizations: {
|
||||
javascript: null,
|
||||
css: null
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
default: null
|
||||
},
|
||||
services: {
|
||||
twitter: {
|
||||
username: SERVICES_TWITTER_USERNAME_VALIDATOR
|
||||
}
|
||||
},
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: null
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: null
|
||||
}
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
},
|
||||
captions: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
},
|
||||
torrents: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
},
|
||||
storyboards: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
}
|
||||
},
|
||||
signup: {
|
||||
enabled: null,
|
||||
limit: SIGNUP_LIMIT_VALIDATOR,
|
||||
requiresApproval: null,
|
||||
requiresEmailVerification: null,
|
||||
minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
|
||||
},
|
||||
import: {
|
||||
videos: {
|
||||
concurrency: CONCURRENCY_VALIDATOR,
|
||||
http: {
|
||||
enabled: null
|
||||
},
|
||||
torrent: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
videoChannelSynchronization: {
|
||||
enabled: null,
|
||||
maxPerUser: MAX_SYNC_PER_USER
|
||||
},
|
||||
users: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
export: {
|
||||
users: {
|
||||
enabled: null,
|
||||
maxUserVideoQuota: EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
||||
exportExpiration: EXPORT_EXPIRATION_VALIDATOR
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
videos: {
|
||||
algorithms: {
|
||||
enabled: null,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
},
|
||||
admin: {
|
||||
email: ADMIN_EMAIL_VALIDATOR
|
||||
},
|
||||
contactForm: {
|
||||
enabled: null
|
||||
},
|
||||
user: {
|
||||
history: {
|
||||
videos: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
|
||||
videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR
|
||||
},
|
||||
videoChannels: {
|
||||
maxPerUser: MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR
|
||||
},
|
||||
transcoding: {
|
||||
enabled: null,
|
||||
threads: TRANSCODING_THREADS_VALIDATOR,
|
||||
allowAdditionalExtensions: null,
|
||||
allowAudioFiles: null,
|
||||
profile: null,
|
||||
concurrency: CONCURRENCY_VALIDATOR,
|
||||
resolutions: {},
|
||||
alwaysTranscodeOriginalResolution: null,
|
||||
originalFile: {
|
||||
keep: null
|
||||
},
|
||||
hls: {
|
||||
enabled: null,
|
||||
splitAudioAndVideo: null
|
||||
},
|
||||
webVideos: {
|
||||
enabled: null
|
||||
},
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
},
|
||||
fps: {
|
||||
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||
}
|
||||
},
|
||||
live: {
|
||||
enabled: null,
|
||||
|
||||
maxDuration: MAX_LIVE_DURATION_VALIDATOR,
|
||||
maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR,
|
||||
maxUserLives: MAX_USER_LIVES_VALIDATOR,
|
||||
allowReplay: null,
|
||||
latencySetting: {
|
||||
enabled: null
|
||||
},
|
||||
|
||||
transcoding: {
|
||||
enabled: null,
|
||||
threads: TRANSCODING_THREADS_VALIDATOR,
|
||||
profile: null,
|
||||
resolutions: {},
|
||||
alwaysTranscodeOriginalResolution: null,
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
},
|
||||
fps: {
|
||||
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||
}
|
||||
}
|
||||
},
|
||||
videoStudio: {
|
||||
enabled: null,
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
videoTranscription: {
|
||||
enabled: null,
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
videoFile: {
|
||||
update: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: null
|
||||
}
|
||||
}
|
||||
},
|
||||
followers: {
|
||||
instance: {
|
||||
enabled: null,
|
||||
manualApproval: null
|
||||
}
|
||||
},
|
||||
followings: {
|
||||
instance: {
|
||||
autoFollowBack: {
|
||||
enabled: null
|
||||
},
|
||||
autoFollowIndex: {
|
||||
enabled: null,
|
||||
indexUrl: URL_VALIDATOR
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: null,
|
||||
level: null,
|
||||
dismissable: null,
|
||||
message: null
|
||||
},
|
||||
search: {
|
||||
remoteUri: {
|
||||
users: null,
|
||||
anonymous: null
|
||||
},
|
||||
searchIndex: {
|
||||
enabled: null,
|
||||
url: URL_VALIDATOR,
|
||||
disableLocalSearch: null,
|
||||
isDefaultSearch: null
|
||||
}
|
||||
},
|
||||
|
||||
instanceCustomHomepage: {
|
||||
content: null
|
||||
},
|
||||
|
||||
storyboards: {
|
||||
enabled: null
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
transcoding: {
|
||||
resolutions: {} as { [id: string]: string }
|
||||
},
|
||||
live: {
|
||||
transcoding: {
|
||||
resolutions: {} as { [id: string]: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const resolution of this.editConfigurationService.getTranscodingResolutions()) {
|
||||
defaultValues.transcoding.resolutions[resolution.id] = 'false'
|
||||
formGroupData.transcoding.resolutions[resolution.id] = null
|
||||
|
||||
defaultValues.live.transcoding.resolutions[resolution.id] = 'false'
|
||||
formGroupData.live.transcoding.resolutions[resolution.id] = null
|
||||
}
|
||||
|
||||
this.buildForm(formGroupData)
|
||||
|
||||
if (this.route.snapshot.fragment) {
|
||||
this.onNavChange(this.route.snapshot.fragment)
|
||||
}
|
||||
|
||||
this.loadConfigAndUpdateForm()
|
||||
this.loadCategoriesAndLanguages()
|
||||
|
||||
if (!this.isUpdateAllowed()) {
|
||||
this.form.disable()
|
||||
}
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
this.forceCheck()
|
||||
if (!this.form.valid) return
|
||||
|
||||
const value: ComponentCustomConfig = merge(this.customConfig, this.form.getRawValue())
|
||||
|
||||
forkJoin([
|
||||
this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')),
|
||||
this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content)
|
||||
])
|
||||
.subscribe({
|
||||
next: ([ resConfig ]) => {
|
||||
const instanceCustomHomepage = { content: value.instanceCustomHomepage.content }
|
||||
|
||||
this.customConfig = { ...resConfig, instanceCustomHomepage }
|
||||
|
||||
// Reload general configuration
|
||||
this.serverService.resetConfig()
|
||||
.subscribe(config => {
|
||||
this.serverConfig = config
|
||||
})
|
||||
|
||||
this.updateForm()
|
||||
|
||||
this.notifier.success($localize`Configuration updated.`)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
isUpdateAllowed () {
|
||||
return this.serverConfig.webadmin.configuration.edition.allowed === true
|
||||
}
|
||||
|
||||
hasConsistentOptions () {
|
||||
if (this.hasLiveAllowReplayConsistentOptions()) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
hasLiveAllowReplayConsistentOptions () {
|
||||
if (
|
||||
this.editConfigurationService.isTranscodingEnabled(this.form) === false &&
|
||||
this.editConfigurationService.isLiveEnabled(this.form) &&
|
||||
this.form.value['live']['allowReplay'] === true
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
onNavChange (newActiveNav: string) {
|
||||
this.activeNav = newActiveNav
|
||||
|
||||
this.router.navigate([], { fragment: this.activeNav })
|
||||
}
|
||||
|
||||
grabAllErrors () {
|
||||
return this.formReactiveService.grabAllErrors(this.formErrors)
|
||||
}
|
||||
|
||||
private updateForm () {
|
||||
this.form.patchValue(this.customConfig)
|
||||
}
|
||||
|
||||
private loadConfigAndUpdateForm () {
|
||||
forkJoin([
|
||||
this.configService.getCustomConfig(),
|
||||
this.customPage.getInstanceHomepage()
|
||||
]).subscribe({
|
||||
next: ([ config, homepage ]) => {
|
||||
this.customConfig = { ...config, instanceCustomHomepage: homepage }
|
||||
|
||||
this.updateForm()
|
||||
this.markAllAsDirty()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private loadCategoriesAndLanguages () {
|
||||
forkJoin([
|
||||
this.serverService.getVideoLanguages(),
|
||||
this.serverService.getVideoCategories()
|
||||
]).subscribe({
|
||||
next: ([ languages, categories ]) => {
|
||||
this.languageItems = languages.map(l => ({ label: l.label, id: l.id }))
|
||||
this.categoryItems = categories.map(l => ({ label: l.label, id: l.id }))
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<ng-container [formGroup]="form()">
|
||||
|
||||
<ng-container formGroupName="instanceCustomHomepage">
|
||||
|
||||
<div class="homepage pt-two-cols mt-5"> <!-- homepage grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>INSTANCE HOMEPAGE</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceCustomHomepageContent">Homepage</label>
|
||||
<div class="label-small-info">
|
||||
<my-custom-markup-help></my-custom-markup-help>
|
||||
</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceCustomHomepageContent" formControlName="content"
|
||||
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
||||
[formError]="formErrors()['instanceCustomHomepage.content']"
|
||||
dir="ltr" monospace="true"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors().instanceCustomHomepage.content" class="form-error" role="alert">{{ formErrors().instanceCustomHomepage.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
|
@ -1,25 +0,0 @@
|
|||
import { Component, inject, input } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgIf } from '@angular/common'
|
||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-homepage',
|
||||
templateUrl: './edit-homepage.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [ FormsModule, ReactiveFormsModule, CustomMarkupHelpComponent, MarkdownTextareaComponent, NgIf ]
|
||||
})
|
||||
export class EditHomepageComponent {
|
||||
private customMarkup = inject(CustomMarkupService)
|
||||
|
||||
readonly form = input<FormGroup>(undefined)
|
||||
readonly formErrors = input<any>(undefined)
|
||||
|
||||
customMarkdownRenderer: (text: string) => Promise<HTMLElement>
|
||||
|
||||
getCustomMarkdownRenderer () {
|
||||
return this.customMarkup.getCustomMarkdownRenderer()
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { Component, OnInit, inject, input } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { genericUploadErrorHandler } from '@app/helpers'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||
import { SelectRadioComponent } from '@app/shared/shared-forms/select/select-radio.component'
|
||||
import { InstanceService } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { maxBy } from '@peertube/peertube-core-utils'
|
||||
import { ActorImage, HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { ActorAvatarEditComponent } from '../../../shared/shared-actor-image-edit/actor-avatar-edit.component'
|
||||
import { ActorBannerEditComponent } from '../../../shared/shared-actor-image-edit/actor-banner-edit.component'
|
||||
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-instance-information',
|
||||
templateUrl: './edit-instance-information.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ActorAvatarEditComponent,
|
||||
ActorBannerEditComponent,
|
||||
SelectRadioComponent,
|
||||
CommonModule,
|
||||
CustomMarkupHelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
SelectCheckboxComponent,
|
||||
RouterLink,
|
||||
PeertubeCheckboxComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
HelpComponent
|
||||
]
|
||||
})
|
||||
export class EditInstanceInformationComponent implements OnInit {
|
||||
private customMarkup = inject(CustomMarkupService)
|
||||
private notifier = inject(Notifier)
|
||||
private instanceService = inject(InstanceService)
|
||||
private server = inject(ServerService)
|
||||
|
||||
readonly form = input<FormGroup>(undefined)
|
||||
readonly formErrors = input<any>(undefined)
|
||||
|
||||
readonly languageItems = input<SelectOptionsItem[]>([])
|
||||
readonly categoryItems = input<SelectOptionsItem[]>([])
|
||||
|
||||
instanceBannerUrl: string
|
||||
instanceAvatars: ActorImage[] = []
|
||||
|
||||
nsfwItems: SelectOptionsItem[] = [
|
||||
{
|
||||
id: 'do_not_list',
|
||||
label: $localize`Hide`
|
||||
},
|
||||
{
|
||||
id: 'warn',
|
||||
label: $localize`Warn`
|
||||
},
|
||||
{
|
||||
id: 'blur',
|
||||
label: $localize`Blur`
|
||||
},
|
||||
{
|
||||
id: 'display',
|
||||
label: $localize`Display`
|
||||
}
|
||||
]
|
||||
|
||||
private serverConfig: HTMLServerConfig
|
||||
|
||||
get instanceName () {
|
||||
return this.server.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.serverConfig = this.server.getHTMLConfig()
|
||||
|
||||
this.updateActorImages()
|
||||
}
|
||||
|
||||
getCustomMarkdownRenderer () {
|
||||
return this.customMarkup.getCustomMarkdownRenderer()
|
||||
}
|
||||
|
||||
onBannerChange (formData: FormData) {
|
||||
this.instanceService.updateInstanceBanner(formData)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Banner changed.`)
|
||||
|
||||
this.resetActorImages()
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`banner`, notifier: this.notifier })
|
||||
})
|
||||
}
|
||||
|
||||
onBannerDelete () {
|
||||
this.instanceService.deleteInstanceBanner()
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Banner deleted.`)
|
||||
|
||||
this.resetActorImages()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
onAvatarChange (formData: FormData) {
|
||||
this.instanceService.updateInstanceAvatar(formData)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Avatar changed.`)
|
||||
|
||||
this.resetActorImages()
|
||||
},
|
||||
|
||||
error: (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`avatar`, notifier: this.notifier })
|
||||
})
|
||||
}
|
||||
|
||||
onAvatarDelete () {
|
||||
this.instanceService.deleteInstanceAvatar()
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Avatar deleted.`)
|
||||
|
||||
this.resetActorImages()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private updateActorImages () {
|
||||
this.instanceBannerUrl = maxBy(this.serverConfig.instance.banners, 'width')?.path
|
||||
this.instanceAvatars = this.serverConfig.instance.avatars
|
||||
}
|
||||
|
||||
private resetActorImages () {
|
||||
this.server.resetConfig()
|
||||
.subscribe(config => {
|
||||
this.serverConfig = config
|
||||
|
||||
this.updateActorImages()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { Component, OnChanges, OnInit, SimpleChanges, inject, input } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { NgClass, NgIf, NgFor } from '@angular/common'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-live-configuration',
|
||||
templateUrl: './edit-live-configuration.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PeertubeCheckboxComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
NgClass,
|
||||
NgIf,
|
||||
SelectOptionsComponent,
|
||||
NgFor,
|
||||
RouterLink,
|
||||
SelectCustomValueComponent
|
||||
]
|
||||
})
|
||||
export class EditLiveConfigurationComponent implements OnInit, OnChanges {
|
||||
private configService = inject(ConfigService)
|
||||
private editConfigurationService = inject(EditConfigurationService)
|
||||
|
||||
readonly form = input<FormGroup>(undefined)
|
||||
readonly formErrors = input<any>(undefined)
|
||||
readonly serverConfig = input<HTMLServerConfig>(undefined)
|
||||
|
||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||
transcodingProfiles: SelectOptionsItem[] = []
|
||||
|
||||
liveMaxDurationOptions: SelectOptionsItem[] = []
|
||||
liveResolutions: ResolutionOption[] = []
|
||||
|
||||
ngOnInit () {
|
||||
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
||||
|
||||
this.liveMaxDurationOptions = [
|
||||
{ id: -1, label: $localize`No limit` },
|
||||
{ id: 1000 * 3600, label: $localize`1 hour` },
|
||||
{ id: 1000 * 3600 * 3, label: $localize`3 hours` },
|
||||
{ id: 1000 * 3600 * 5, label: $localize`5 hours` },
|
||||
{ id: 1000 * 3600 * 10, label: $localize`10 hours` }
|
||||
]
|
||||
|
||||
this.liveResolutions = this.editConfigurationService.getTranscodingResolutions()
|
||||
}
|
||||
|
||||
ngOnChanges (changes: SimpleChanges) {
|
||||
if (changes['serverConfig']) {
|
||||
this.transcodingProfiles = this.buildAvailableTranscodingProfile()
|
||||
}
|
||||
}
|
||||
|
||||
buildAvailableTranscodingProfile () {
|
||||
const profiles = this.serverConfig().live.transcoding.availableProfiles
|
||||
|
||||
return profiles.map(p => {
|
||||
if (p === 'default') {
|
||||
return { id: p, label: $localize`Default`, description: $localize`x264, targeting maximum device compatibility` }
|
||||
}
|
||||
|
||||
return { id: p, label: p }
|
||||
})
|
||||
}
|
||||
|
||||
getResolutionKey (resolution: string) {
|
||||
return 'live.transcoding.resolutions.' + resolution
|
||||
}
|
||||
|
||||
getLiveRTMPPort () {
|
||||
return this.serverConfig().live.rtmp.port
|
||||
}
|
||||
|
||||
isLiveEnabled () {
|
||||
return this.editConfigurationService.isLiveEnabled(this.form())
|
||||
}
|
||||
|
||||
isRemoteRunnerLiveEnabled () {
|
||||
return this.editConfigurationService.isRemoteRunnerLiveEnabled(this.form())
|
||||
}
|
||||
|
||||
getDisabledLiveClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() }
|
||||
}
|
||||
|
||||
getDisabledLiveTranscodingClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() || !this.isLiveTranscodingEnabled() }
|
||||
}
|
||||
|
||||
getDisabledLiveLocalTranscodingClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() || !this.isLiveTranscodingEnabled() || this.isRemoteRunnerLiveEnabled() }
|
||||
}
|
||||
|
||||
isLiveTranscodingEnabled () {
|
||||
return this.editConfigurationService.isLiveTranscodingEnabled(this.form())
|
||||
}
|
||||
|
||||
getTotalTranscodingThreads () {
|
||||
return this.editConfigurationService.getTotalTranscodingThreads(this.form())
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
import { NgClass, NgFor, NgIf } from '@angular/common'
|
||||
import { Component, OnChanges, OnInit, SimpleChanges, inject, input } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { Notifier } from '@app/core'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
import { EditConfigurationService, ResolutionOption } from './edit-configuration.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-vod-transcoding',
|
||||
templateUrl: './edit-vod-transcoding.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PeertubeCheckboxComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
NgClass,
|
||||
NgFor,
|
||||
NgIf,
|
||||
RouterLink,
|
||||
SelectCustomValueComponent,
|
||||
SelectOptionsComponent
|
||||
]
|
||||
})
|
||||
export class EditVODTranscodingComponent implements OnInit, OnChanges {
|
||||
private configService = inject(ConfigService)
|
||||
private editConfigurationService = inject(EditConfigurationService)
|
||||
private notifier = inject(Notifier)
|
||||
|
||||
readonly form = input<FormGroup>(undefined)
|
||||
readonly formErrors = input<any>(undefined)
|
||||
readonly serverConfig = input<HTMLServerConfig>(undefined)
|
||||
|
||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||
transcodingProfiles: SelectOptionsItem[] = []
|
||||
resolutions: ResolutionOption[] = []
|
||||
|
||||
additionalVideoExtensions = ''
|
||||
|
||||
ngOnInit () {
|
||||
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
||||
this.resolutions = this.editConfigurationService.getTranscodingResolutions()
|
||||
|
||||
this.checkTranscodingFields()
|
||||
}
|
||||
|
||||
ngOnChanges (changes: SimpleChanges) {
|
||||
if (changes['serverConfig']) {
|
||||
this.transcodingProfiles = this.buildAvailableTranscodingProfile()
|
||||
|
||||
this.additionalVideoExtensions = this.serverConfig().video.file.extensions.join(' ')
|
||||
}
|
||||
}
|
||||
|
||||
buildAvailableTranscodingProfile () {
|
||||
const profiles = this.serverConfig().transcoding.availableProfiles
|
||||
|
||||
return profiles.map(p => {
|
||||
if (p === 'default') {
|
||||
return { id: p, label: $localize`Default`, description: $localize`x264, targeting maximum device compatibility` }
|
||||
}
|
||||
|
||||
return { id: p, label: p }
|
||||
})
|
||||
}
|
||||
|
||||
getResolutionKey (resolution: string) {
|
||||
return 'transcoding.resolutions.' + resolution
|
||||
}
|
||||
|
||||
isRemoteRunnerVODEnabled () {
|
||||
return this.editConfigurationService.isRemoteRunnerVODEnabled(this.form())
|
||||
}
|
||||
|
||||
isTranscodingEnabled () {
|
||||
return this.editConfigurationService.isTranscodingEnabled(this.form())
|
||||
}
|
||||
|
||||
isHLSEnabled () {
|
||||
return this.editConfigurationService.isHLSEnabled(this.form())
|
||||
}
|
||||
|
||||
isStudioEnabled () {
|
||||
return this.editConfigurationService.isStudioEnabled(this.form())
|
||||
}
|
||||
|
||||
getTranscodingDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() }
|
||||
}
|
||||
|
||||
getHLSDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isHLSEnabled() }
|
||||
}
|
||||
|
||||
getLocalTranscodingDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() || this.isRemoteRunnerVODEnabled() }
|
||||
}
|
||||
|
||||
getStudioDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isStudioEnabled() }
|
||||
}
|
||||
|
||||
getTotalTranscodingThreads () {
|
||||
return this.editConfigurationService.getTotalTranscodingThreads(this.form())
|
||||
}
|
||||
|
||||
private checkTranscodingFields () {
|
||||
const transcodingControl = this.form().get('transcoding.enabled')
|
||||
const videoStudioControl = this.form().get('videoStudio.enabled')
|
||||
const hlsControl = this.form().get('transcoding.hls.enabled')
|
||||
const webVideosControl = this.form().get('transcoding.webVideos.enabled')
|
||||
|
||||
webVideosControl.valueChanges
|
||||
.subscribe(newValue => {
|
||||
if (newValue === false && hlsControl.value === false) {
|
||||
hlsControl.setValue(true)
|
||||
|
||||
this.notifier.info(
|
||||
$localize`Automatically enable HLS transcoding because at least 1 output format must be enabled when transcoding is enabled`,
|
||||
'',
|
||||
10000
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
hlsControl.valueChanges
|
||||
.subscribe(newValue => {
|
||||
if (newValue === false && webVideosControl.value === false) {
|
||||
webVideosControl.setValue(true)
|
||||
|
||||
this.notifier.info(
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`Automatically enable Web Videos transcoding because at least 1 output format must be enabled when transcoding is enabled`,
|
||||
'',
|
||||
10000
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
transcodingControl.valueChanges
|
||||
.subscribe(newValue => {
|
||||
if (newValue === false) {
|
||||
videoStudioControl.setValue(false)
|
||||
}
|
||||
})
|
||||
|
||||
transcodingControl.updateValueAndValidity()
|
||||
webVideosControl.updateValueAndValidity()
|
||||
videoStudioControl.updateValueAndValidity()
|
||||
hlsControl.updateValueAndValidity()
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export * from './edit-advanced-configuration.component'
|
||||
export * from './edit-basic-configuration.component'
|
||||
export * from './edit-configuration.service'
|
||||
export * from './edit-custom-config.component'
|
||||
export * from './edit-homepage.component'
|
||||
export * from './edit-instance-information.component'
|
||||
export * from './edit-live-configuration.component'
|
||||
export * from './edit-vod-transcoding.component'
|
|
@ -1,2 +1,2 @@
|
|||
export * from './edit-custom-config'
|
||||
export * from './pages'
|
||||
export * from './config.routes'
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
<my-admin-save-bar i18n-title title="Advanced configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
|
||||
<div class="title-col">
|
||||
<h2 i18n>CACHE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Some files are not federated, and fetched when necessary. Define their caching policies.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="cache">
|
||||
<div class="form-group" formGroupName="previews">
|
||||
<label i18n for="cachePreviewsSize">Number of previews to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cachePreviewsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.previews.size }"
|
||||
>
|
||||
<span i18n>{getCacheSize('previews'), plural, =1 {cached image} other {cached images}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.cache.previews.size" class="form-error" role="alert">{{ formErrors.cache.previews.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="captions">
|
||||
<label i18n for="cacheCaptionsSize">Number of video captions to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheCaptionsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.captions.size }"
|
||||
>
|
||||
<span i18n>{getCacheSize('captions'), plural, =1 {cached caption} other {cached captions}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.cache.captions.size" class="form-error" role="alert">{{ formErrors.cache.captions.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrents">
|
||||
<label i18n for="cacheTorrentsSize">Number of video torrents to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheTorrentsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.torrents.size }"
|
||||
>
|
||||
<span i18n>{getCacheSize('torrents'), plural, =1 {cached torrent} other {cached torrents}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.cache.torrents.size" class="form-error" role="alert">{{ formErrors.cache.torrents.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="storyboards">
|
||||
<label i18n for="cacheTorrentsSize">Number of video storyboard images to keep in cache</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="0" id="cacheStoryboardsSize" class="form-control"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors.cache.storyboards.size }"
|
||||
>
|
||||
<span i18n>{getCacheSize('storyboards'), plural, =1 {cached storyboard} other {cached storyboards}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.cache.storyboards.size" class="form-error" role="alert">{{ formErrors.cache.storyboards.size }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>TWITTER/X</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Extra configuration required by Twitter/X. All other social media (Facebook, Mastodon, etc.) are supported out of the box.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="services">
|
||||
<ng-container formGroupName="twitter">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="servicesTwitterUsername" i18n>Your Twitter/X username</label>
|
||||
|
||||
<div class="form-group-description">
|
||||
<p i18n class="mb-0">Indicates the Twitter/X account for the website or platform where the content was published.</p>
|
||||
|
||||
<p i18n>This is just an extra information injected in PeerTube HTML that is required by Twitter/X. If you don't have a Twitter/X account, just leave the default value.</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text" id="servicesTwitterUsername" class="form-control"
|
||||
formControlName="username" [ngClass]="{ 'input-error': formErrors.services.twitter.username }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.services.twitter.username" class="form-error" role="alert">{{ formErrors.services.twitter.username }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,129 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CanComponentDeactivate } from '@app/core'
|
||||
import { CACHE_SIZE_VALIDATOR, SERVICES_TWITTER_USERNAME_VALIDATOR } from '@app/shared/form-validators/custom-config-validators'
|
||||
import {
|
||||
BuildFormArgumentTyped,
|
||||
FormDefaultTyped,
|
||||
FormReactiveErrorsTyped,
|
||||
FormReactiveMessagesTyped
|
||||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
type Form = {
|
||||
services: FormGroup<{
|
||||
twitter: FormGroup<{
|
||||
username: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
|
||||
cache: FormGroup<{
|
||||
previews: FormGroup<{
|
||||
size: FormControl<number>
|
||||
}>
|
||||
captions: FormGroup<{
|
||||
size: FormControl<number>
|
||||
}>
|
||||
torrents: FormGroup<{
|
||||
size: FormControl<number>
|
||||
}>
|
||||
storyboards: FormGroup<{
|
||||
size: FormControl<number>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config-advanced',
|
||||
templateUrl: './admin-config-advanced.component.html',
|
||||
styleUrls: [ './admin-config-common.scss' ],
|
||||
imports: [ CommonModule, FormsModule, ReactiveFormsModule, AdminSaveBarComponent ]
|
||||
})
|
||||
export class AdminConfigAdvancedComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private route = inject(ActivatedRoute)
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
this.buildForm()
|
||||
|
||||
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||
.subscribe(customConfig => {
|
||||
this.customConfig = customConfig
|
||||
|
||||
this.form.patchValue(this.customConfig)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return { canDeactivate: !this.form.dirty }
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
services: {
|
||||
twitter: {
|
||||
username: SERVICES_TWITTER_USERNAME_VALIDATOR
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
},
|
||||
captions: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
},
|
||||
torrents: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
},
|
||||
storyboards: {
|
||||
size: CACHE_SIZE_VALIDATOR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
getCacheSize (type: 'captions' | 'previews' | 'torrents' | 'storyboards') {
|
||||
return this.form.value.cache[type].size
|
||||
}
|
||||
|
||||
save () {
|
||||
this.adminConfigService.saveAndUpdateCurrent({
|
||||
currentConfig: this.customConfig,
|
||||
form: this.form,
|
||||
formConfig: this.form.value,
|
||||
success: $localize`Advanced configuration updated.`
|
||||
})
|
||||
}
|
||||
}
|
80
client/src/app/+admin/config/pages/admin-config-common.scss
Normal file
80
client/src/app/+admin/config/pages/admin-config-common.scss
Normal file
|
@ -0,0 +1,80 @@
|
|||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
$form-base-input-width: 340px;
|
||||
$form-max-width: 500px;
|
||||
|
||||
form {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
my-markdown-textarea {
|
||||
display: block;
|
||||
max-width: $form-max-width;
|
||||
}
|
||||
|
||||
.homepage my-markdown-textarea {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
|
||||
::ng-deep textarea {
|
||||
height: 300px !important;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
@include peertube-input-text($form-base-input-width);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
@include peertube-checkbox;
|
||||
}
|
||||
|
||||
.peertube-select-container {
|
||||
@include peertube-select-container($form-base-input-width);
|
||||
}
|
||||
|
||||
my-select-checkbox,
|
||||
my-select-options,
|
||||
my-select-custom-value {
|
||||
display: block;
|
||||
|
||||
@include responsive-width($form-base-input-width);
|
||||
}
|
||||
|
||||
.inner-form-description {
|
||||
font-size: 14px;
|
||||
margin-bottom: 1rem;
|
||||
color: pvar(--fg-300);
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
|
||||
@include peertube-textarea(500px, 150px);
|
||||
|
||||
&.small {
|
||||
height: 75px;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled-checkbox-extra {
|
||||
&,
|
||||
::ng-deep label {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
my-actor-banner-edit {
|
||||
max-width: $form-max-width;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: $font-bold;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
<my-admin-save-bar i18n-title title="Platform customization" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h2 i18n>APPEARANCE</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="theme">
|
||||
<div class="form-group">
|
||||
<label i18n for="themeDefault">Theme</label>
|
||||
|
||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="client">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="miniature">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientVideosMiniaturePreferAuthorDisplayName"
|
||||
formControlName="preferAuthorDisplayName"
|
||||
i18n-labelText
|
||||
labelText="Prefer author display name in video miniature"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>CUSTOMIZATION</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Use <a class="link-primary" routerLink="/admin/settings/plugins">plugins & themes</a> for more involved changes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-template #alertIntro>
|
||||
<div i18n>UI customization only applies if the user is using the default platform theme.</div>
|
||||
</ng-template>
|
||||
|
||||
@if (getCurrentThemeName() !== getDefaultThemeName()) {
|
||||
<my-alert type="warning">
|
||||
<ng-template *ngTemplateOutlet="alertIntro"></ng-template>
|
||||
|
||||
<div i18n>You can't preview the changes because you aren't using your platform's default theme.</div>
|
||||
<div i18n>Current theme: <strong>{{ getCurrentThemeLabel() }}</strong></div>
|
||||
<div i18n>Platform theme: <strong>{{ getDefaultThemeLabel() }}</strong>.</div>
|
||||
</my-alert>
|
||||
} @else {
|
||||
<my-alert type="info">
|
||||
<ng-template *ngTemplateOutlet="alertIntro"></ng-template>
|
||||
|
||||
<div i18n>You can preview your UI customization but <strong>don't forget to save your changes</strong> once you are happy with the results.</div>
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div class="form-group" formGroupName="theme">
|
||||
<ng-container formGroupName="customization">
|
||||
@for (field of customizationFormFields; track field.name) {
|
||||
<div class="form-group">
|
||||
<label [for]="field.inputId">{{ field.label }}</label>
|
||||
|
||||
<button
|
||||
*ngIf="!hasDefaultCustomizationValue(field.name)"
|
||||
type="button"
|
||||
i18n
|
||||
class="reset-button reset-button-small"
|
||||
(click)="resetCustomizationField(field.name)"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
<div *ngIf="field.description" class="form-group-description">{{ field.description }}</div>
|
||||
|
||||
@if (field.type === 'color') {
|
||||
<p-colorpicker class="d-block" [inputId]="field.inputId" [formControlName]="field.name" />
|
||||
} @else if (field.type === 'radius') {
|
||||
<my-select-custom-value
|
||||
[labelId]="field.inputId"
|
||||
[inputId]="field.inputId"
|
||||
[items]="field.items"
|
||||
[formControlName]="field.name"
|
||||
inputType="text"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
} @else {
|
||||
<input
|
||||
type="text"
|
||||
[id]="field.inputId"
|
||||
[name]="field.inputId"
|
||||
class="form-control"
|
||||
[formControlName]="field.name"
|
||||
[ngClass]="{ 'input-error': formErrors.theme.customization[field.name]}"
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h2 i18n>EMAIL</h2>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="email">
|
||||
<div class="content-col">
|
||||
<div class="form-group" formGroupName="subject">
|
||||
<label i18n for="emailSubjectPrefix">Subject prefix</label>
|
||||
<div class="form-group-description" i18n>Support <pre class="d-inline">{{ '{{instanceName}}' }}</pre> template variable</div>
|
||||
|
||||
<input
|
||||
type="text" id="emailSubjectPrefix" class="form-control"
|
||||
formControlName="prefix" [ngClass]="{ 'input-error': formErrors.email.subject.prefix }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.email.subject.prefix" class="form-error" role="alert">{{ formErrors.email.subject.prefix }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="body">
|
||||
<label i18n for="emailBodySignature">Body signature</label>
|
||||
<div class="form-group-description" i18n>Support <pre class="d-inline">{{ '{{instanceName}}' }}</pre> template variable</div>
|
||||
|
||||
<input
|
||||
type="text" id="emailBodySignature" class="form-control"
|
||||
formControlName="signature" [ngClass]="{ 'input-error': formErrors.email.body.signature }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.email.body.signature" class="form-error" role="alert">{{ formErrors.email.body.signature }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<div class="anchor" id="customizations"></div>
|
||||
<h2 i18n>ADVANCED</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Advanced modifications to your PeerTube platform if creating a plugin or a theme is overkill.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="instance">
|
||||
<ng-container formGroupName="customizations">
|
||||
<div class="form-group">
|
||||
<label i18n for="customizationJavascript">JavaScript</label>
|
||||
<my-help>
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">Write JavaScript code directly. Example:</p>
|
||||
<pre>console.log('my instance is amazing');</pre>
|
||||
</ng-container>
|
||||
</my-help>
|
||||
|
||||
<textarea
|
||||
id="customizationJavascript"
|
||||
formControlName="javascript"
|
||||
class="form-control"
|
||||
dir="ltr"
|
||||
[ngClass]="{ 'input-error': formErrors.instance.customizations.javascript }"
|
||||
></textarea>
|
||||
|
||||
<div *ngIf="formErrors.instance.customizations.javascript" class="form-error" role="alert">{{ formErrors.instance.customizations.javascript }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="customizationCSS">CSS</label>
|
||||
|
||||
<my-help>
|
||||
<ng-container i18n>
|
||||
<p class="mb-2">Write CSS code directly. Example:</p>
|
||||
<pre>
|
||||
#custom-css {{ '{' }}
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
<p class="mb-2">Prepend with <em>#custom-css</em> to override styles. Example:</p>
|
||||
<pre>
|
||||
#custom-css .logged-in-email {{ '{' }}
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
</ng-container>
|
||||
</my-help>
|
||||
|
||||
<textarea
|
||||
id="customizationCSS"
|
||||
formControlName="css"
|
||||
class="form-control"
|
||||
dir="ltr"
|
||||
[ngClass]="{ 'input-error': formErrors.instance.customizations.css }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors.instance.customizations.css" class="form-error" role="alert">{{ formErrors.instance.customizations.css }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,385 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, ValueChangeEvent } from '@angular/forms'
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router'
|
||||
import { CanComponentDeactivate, ServerService, ThemeService } from '@app/core'
|
||||
import { BuildFormArgumentTyped, FormDefaultTyped, FormReactiveMessagesTyped } from '@app/shared/form-validators/form-validator.model'
|
||||
import { FormReactiveErrorsTyped, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { PeertubeCheckboxComponent } from '@app/shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '@app/shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { objectKeysTyped } from '@peertube/peertube-core-utils'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { capitalizeFirstLetter } from '@root-helpers/string'
|
||||
import { ColorPaletteThemeConfig, ThemeCustomizationKey } from '@root-helpers/theme-manager'
|
||||
import debug from 'debug'
|
||||
import { ColorPickerModule } from 'primeng/colorpicker'
|
||||
import { debounceTime, Subscription } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types'
|
||||
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
const debugLogger = debug('peertube:config')
|
||||
|
||||
type Form = {
|
||||
instance: FormGroup<{
|
||||
customizations: FormGroup<{
|
||||
css: FormControl<string>
|
||||
javascript: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
|
||||
client: FormGroup<{
|
||||
videos: FormGroup<{
|
||||
miniature: FormGroup<{
|
||||
preferAuthorDisplayName: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
}>
|
||||
|
||||
email: FormGroup<{
|
||||
subject: FormGroup<{
|
||||
prefix: FormControl<string>
|
||||
}>
|
||||
|
||||
body: FormGroup<{
|
||||
signature: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
|
||||
theme: FormGroup<{
|
||||
default: FormControl<string>
|
||||
|
||||
customization: FormGroup<{
|
||||
primaryColor: FormControl<string>
|
||||
foregroundColor: FormControl<string>
|
||||
backgroundColor: FormControl<string>
|
||||
backgroundSecondaryColor: FormControl<string>
|
||||
menuForegroundColor: FormControl<string>
|
||||
menuBackgroundColor: FormControl<string>
|
||||
menuBorderRadius: FormControl<string>
|
||||
headerForegroundColor: FormControl<string>
|
||||
headerBackgroundColor: FormControl<string>
|
||||
inputBorderRadius: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
type FieldType = 'color' | 'radius'
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config-customization',
|
||||
templateUrl: './admin-config-customization.component.html',
|
||||
styleUrls: [ './admin-config-common.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterModule,
|
||||
ReactiveFormsModule,
|
||||
AdminSaveBarComponent,
|
||||
ColorPickerModule,
|
||||
AlertComponent,
|
||||
SelectOptionsComponent,
|
||||
HelpComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
SelectCustomValueComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigCustomizationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
private serverService = inject(ServerService)
|
||||
private themeService = inject(ThemeService)
|
||||
private route = inject(ActivatedRoute)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
customizationFormFields: {
|
||||
label: string
|
||||
inputId: string
|
||||
name: ThemeCustomizationKey
|
||||
description?: string
|
||||
type: FieldType
|
||||
items?: SelectOptionsItem[]
|
||||
}[] = []
|
||||
|
||||
availableThemes: SelectOptionsItem[]
|
||||
|
||||
private customizationResetFields = new Set<ThemeCustomizationKey>()
|
||||
private customConfig: CustomConfig
|
||||
|
||||
private customConfigSub: Subscription
|
||||
|
||||
private readonly formFieldsObject: Record<
|
||||
ThemeCustomizationKey,
|
||||
{ label: string, description?: string, type: FieldType, items?: SelectOptionsItem[] }
|
||||
> = {
|
||||
primaryColor: { label: $localize`Primary color`, type: 'color' },
|
||||
foregroundColor: { label: $localize`Foreground color`, type: 'color' },
|
||||
backgroundColor: { label: $localize`Background color`, type: 'color' },
|
||||
backgroundSecondaryColor: {
|
||||
label: $localize`Secondary background color`,
|
||||
description: $localize`Used as a background for inputs, overlays...`,
|
||||
type: 'color'
|
||||
},
|
||||
menuForegroundColor: { label: $localize`Menu foreground color`, type: 'color' },
|
||||
menuBackgroundColor: { label: $localize`Menu background color`, type: 'color' },
|
||||
|
||||
menuBorderRadius: {
|
||||
label: $localize`Menu rounding`,
|
||||
type: 'radius',
|
||||
items: [
|
||||
{ id: '0', label: $localize`Not rounded` },
|
||||
{ id: '6px', label: $localize`Slightly rounded` },
|
||||
{ id: '14px', label: $localize`Moderately rounded (default)` },
|
||||
{ id: '60px', label: $localize`Rounded` }
|
||||
]
|
||||
},
|
||||
|
||||
headerForegroundColor: { label: $localize`Header foreground color`, type: 'color' },
|
||||
headerBackgroundColor: { label: $localize`Header background color`, type: 'color' },
|
||||
|
||||
inputBorderRadius: {
|
||||
label: $localize`Input rounding`,
|
||||
type: 'radius',
|
||||
items: [
|
||||
{ id: '0', label: $localize`Not rounded` },
|
||||
{ id: '4px', label: $localize`Slightly rounded (default)` },
|
||||
{ id: '10px', label: $localize`Moderately rounded` },
|
||||
{ id: '20px', label: $localize`Rounded` }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
this.availableThemes = [
|
||||
this.themeService.getDefaultThemeItem(),
|
||||
|
||||
...this.themeService.buildAvailableThemes()
|
||||
]
|
||||
|
||||
this.buildForm()
|
||||
this.subscribeToCustomizationChanges()
|
||||
|
||||
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||
.subscribe(customConfig => {
|
||||
this.customConfig = customConfig
|
||||
|
||||
this.form.patchValue(this.getDefaultFormValues(), { emitEvent: false })
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return { canDeactivate: !this.form.dirty }
|
||||
}
|
||||
|
||||
private subscribeToCustomizationChanges () {
|
||||
let currentAnimationFrame: number
|
||||
|
||||
this.form.get('theme.customization').valueChanges.pipe(debounceTime(250)).subscribe(formValues => {
|
||||
if (currentAnimationFrame) {
|
||||
cancelAnimationFrame(currentAnimationFrame)
|
||||
currentAnimationFrame = null
|
||||
}
|
||||
|
||||
currentAnimationFrame = requestAnimationFrame(() => {
|
||||
this.themeService.updateColorPalette({
|
||||
...this.customConfig.theme,
|
||||
|
||||
customization: this.buildNewCustomization(formValues)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
for (const [ key, control ] of Object.entries((this.form.get('theme.customization') as FormGroup).controls)) {
|
||||
control.events.subscribe(event => {
|
||||
if (event instanceof ValueChangeEvent) {
|
||||
debugLogger(`Deleting "${key}" from reset fields`)
|
||||
|
||||
this.customizationResetFields.delete(key as ThemeCustomizationKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
for (const [ untypedName, info ] of Object.entries(this.formFieldsObject)) {
|
||||
const name = untypedName as ThemeCustomizationKey
|
||||
|
||||
this.customizationFormFields.push({
|
||||
label: info.label,
|
||||
type: info.type,
|
||||
inputId: `themeCustomization${capitalizeFirstLetter(name)}`,
|
||||
name,
|
||||
items: info.items
|
||||
})
|
||||
|
||||
if (!this.customConfig.theme.customization[name]) {
|
||||
this.customizationResetFields.add(name)
|
||||
}
|
||||
}
|
||||
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: null
|
||||
}
|
||||
}
|
||||
},
|
||||
email: {
|
||||
subject: {
|
||||
prefix: null
|
||||
},
|
||||
body: {
|
||||
signature: null
|
||||
}
|
||||
},
|
||||
instance: {
|
||||
customizations: {
|
||||
css: null,
|
||||
javascript: null
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
default: null,
|
||||
customization: {
|
||||
primaryColor: null,
|
||||
foregroundColor: null,
|
||||
backgroundColor: null,
|
||||
backgroundSecondaryColor: null,
|
||||
menuForegroundColor: null,
|
||||
menuBackgroundColor: null,
|
||||
menuBorderRadius: null,
|
||||
headerForegroundColor: null,
|
||||
headerBackgroundColor: null,
|
||||
inputBorderRadius: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, this.getDefaultFormValues())
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
getCurrentThemeName () {
|
||||
return this.themeService.getCurrentThemeName()
|
||||
}
|
||||
|
||||
getCurrentThemeLabel () {
|
||||
return this.availableThemes.find(t => t.id === this.themeService.getCurrentThemeName())?.label
|
||||
}
|
||||
|
||||
getDefaultThemeName () {
|
||||
return this.serverService.getHTMLConfig().theme.default
|
||||
}
|
||||
|
||||
getDefaultThemeLabel () {
|
||||
return this.availableThemes.find(t => t.id === this.getDefaultThemeName())?.label
|
||||
}
|
||||
|
||||
hasDefaultCustomizationValue (field: ThemeCustomizationKey) {
|
||||
return this.customizationResetFields.has(field)
|
||||
}
|
||||
|
||||
resetCustomizationField (field: ThemeCustomizationKey) {
|
||||
this.customizationResetFields.add(field)
|
||||
|
||||
this.themeService.updateColorPalette({
|
||||
...this.customConfig.theme,
|
||||
|
||||
customization: this.buildNewCustomization(this.form.get('theme.customization').value)
|
||||
})
|
||||
|
||||
const value = this.formatCustomizationFieldForForm(field, this.themeService.getCSSConfigValue(field))
|
||||
const control = this.getCustomizationControl(field)
|
||||
|
||||
control.patchValue(value, { emitEvent: false })
|
||||
control.markAsDirty()
|
||||
}
|
||||
|
||||
save () {
|
||||
const formValues = this.form.value
|
||||
formValues.theme.customization = this.buildNewCustomization(formValues.theme.customization)
|
||||
|
||||
this.adminConfigService.saveAndUpdateCurrent({
|
||||
currentConfig: this.customConfig,
|
||||
form: this.form,
|
||||
formConfig: this.form.value,
|
||||
success: $localize`Platform customization updated.`
|
||||
})
|
||||
}
|
||||
|
||||
private getCustomizationControl (field: ThemeCustomizationKey) {
|
||||
return this.form.get('theme.customization').get(field)
|
||||
}
|
||||
|
||||
private getDefaultFormValues (): FormDefaultTyped<Form> {
|
||||
return {
|
||||
...this.customConfig,
|
||||
|
||||
theme: {
|
||||
default: this.customConfig.theme.default,
|
||||
customization: this.getDefaultCustomization()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultCustomization () {
|
||||
const config = this.customConfig.theme.customization
|
||||
|
||||
return objectKeysTyped(this.formFieldsObject).reduce((acc, field) => {
|
||||
acc[field] = config[field]
|
||||
? this.formatCustomizationFieldForForm(field, config[field])
|
||||
: this.formatCustomizationFieldForForm(field, this.themeService.getCSSConfigValue(field))
|
||||
|
||||
return acc
|
||||
}, {} as Record<ThemeCustomizationKey, string>)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private formatCustomizationFieldForForm (field: ThemeCustomizationKey, value: string) {
|
||||
if (this.formFieldsObject[field].type === 'color') {
|
||||
return this.themeService.formatColorForForm(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private buildNewCustomization (formValues: any) {
|
||||
return objectKeysTyped(this.customConfig.theme.customization).reduce(
|
||||
(acc: ColorPaletteThemeConfig['customization'], field) => {
|
||||
acc[field] = this.formatCustomizationFieldForTheme(field, formValues[field])
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as ColorPaletteThemeConfig['customization']
|
||||
)
|
||||
}
|
||||
|
||||
private formatCustomizationFieldForTheme (field: ThemeCustomizationKey, value: string) {
|
||||
if (this.customizationResetFields.has(field)) return null
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
|
@ -1,23 +1,13 @@
|
|||
<ng-container [formGroup]="form()">
|
||||
<div class="pt-two-cols mt-5"> <!-- appearance grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>APPEARANCE</h2>
|
||||
<my-admin-save-bar i18n-title title="General configuration" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Use <a class="link-primary" routerLink="/admin/settings/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-primary" routerLink="/admin/settings/config/edit-custom" fragment="advanced-configuration">customizations</a>.
|
||||
</div>
|
||||
<form [formGroup]="form">
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h2 i18n>BEHAVIOR</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="theme">
|
||||
<div class="form-group">
|
||||
<label i18n for="themeDefault">Theme</label>
|
||||
|
||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="form-group" formGroupName="instance">
|
||||
<label i18n id="instanceDefaultClientRouteLabel" for="instanceDefaultClientRoute">Landing page</label>
|
||||
|
||||
|
@ -30,7 +20,7 @@
|
|||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors().instance.defaultClientRoute" class="form-error" role="alert">{{ formErrors().instance.defaultClientRoute }}</div>
|
||||
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error" role="alert">{{ formErrors.instance.defaultClientRoute }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="trending">
|
||||
|
@ -47,24 +37,13 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().trending.videos.algorithms.default" class="form-error" role="alert">{{ formErrors().trending.videos.algorithms.default }}</div>
|
||||
<div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error" role="alert">{{ formErrors.trending.videos.algorithms.default }}</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="client">
|
||||
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="miniature">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientVideosMiniaturePreferAuthorDisplayName" formControlName="preferAuthorDisplayName"
|
||||
i18n-labelText labelText="Prefer author display name in video miniature"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="menu">
|
||||
<ng-container formGroupName="login">
|
||||
<div class="form-group">
|
||||
|
@ -73,8 +52,11 @@
|
|||
i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span *ngIf="countExternalAuth() === 0" i18n>⚠️ You don't have any external auth plugin enabled.</span>
|
||||
<span *ngIf="countExternalAuth() > 1" i18n>⚠️ You have multiple external auth plugins enabled.</span>
|
||||
@if (countExternalAuth() === 0) {
|
||||
<span i18n>⚠️ You don't have any external auth plugin enabled</span>
|
||||
} @else if (countExternalAuth() > 1) {
|
||||
<span i18n>⚠️ You have multiple external auth plugins enabled</span>
|
||||
}
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
@ -85,11 +67,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- broadcast grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>BROADCAST MESSAGE</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Display a message on your instance
|
||||
Display a message on your platform
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -122,7 +104,7 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().broadcastMessage.level" class="form-error" role="alert">{{ formErrors().broadcastMessage.level }}</div>
|
||||
<div *ngIf="formErrors.broadcastMessage.level" class="form-error" role="alert">{{ formErrors.broadcastMessage.level }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -130,10 +112,10 @@
|
|||
|
||||
<my-markdown-textarea
|
||||
inputId="broadcastMessageMessage" formControlName="message"
|
||||
[formError]="formErrors()['broadcastMessage.message']" markdownType="to-unsafe-html"
|
||||
[formError]="formErrors.broadcastMessage.message" markdownType="to-unsafe-html"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors().broadcastMessage.message" class="form-error" role="alert">{{ formErrors().broadcastMessage.message }}</div>
|
||||
<div *ngIf="formErrors.broadcastMessage.message" class="form-error" role="alert">{{ formErrors.broadcastMessage.message }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@ -144,9 +126,6 @@
|
|||
<div class="pt-two-cols mt-4"> <!-- new users grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>NEW USERS</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-primary" routerLink="/admin/overview/users">users</a> to set their quota individually.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
@ -158,9 +137,9 @@
|
|||
i18n-labelText labelText="Enable Signup"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation</span>
|
||||
|
||||
<my-alert type="primary" class="alert-signup" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</my-alert>
|
||||
<my-alert type="primary" class="d-block mt-2" *ngIf="signupAlertMessage">{{ signupAlertMessage }}</my-alert>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
|
@ -180,19 +159,19 @@
|
|||
|
||||
<div [ngClass]="getDisabledSignupClass()">
|
||||
<label i18n for="signupLimit">Signup limit</label>
|
||||
<span i18n class="small muted ms-1">When the total number of users in your instance reaches this limit, registrations are disabled. -1 == unlimited</span>
|
||||
<span i18n class="small muted ms-1">When the total number of users in your platform reaches this limit, registrations are disabled. -1 = unlimited</span>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="-1" id="signupLimit" class="form-control"
|
||||
formControlName="limit" [ngClass]="{ 'input-error': formErrors()['signup.limit'] }"
|
||||
formControlName="limit" [ngClass]="{ 'input-error': formErrors.signup.limit }"
|
||||
>
|
||||
<span i18n>{form().value['signup']['limit'], plural, =1 {user} other {users}}</span>
|
||||
<span i18n>{form.value.signup.limit, plural, =1 {user} other {users}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().signup.limit" class="form-error" role="alert">{{ formErrors().signup.limit }}</div>
|
||||
<div *ngIf="formErrors.signup.limit" class="form-error" role="alert">{{ formErrors.signup.limit }}</div>
|
||||
|
||||
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users.</small>
|
||||
<small i18n *ngIf="hasUnlimitedSignup()" class="muted small">Signup won't be limited to a fixed number of users</small>
|
||||
</div>
|
||||
|
||||
<div [ngClass]="getDisabledSignupClass()" class="mt-3">
|
||||
|
@ -201,12 +180,12 @@
|
|||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="signupMinimumAge" class="form-control"
|
||||
formControlName="minimumAge" [ngClass]="{ 'input-error': formErrors()['signup.minimumAge'] }"
|
||||
formControlName="minimumAge" [ngClass]="{ 'input-error': formErrors.signup.minimumAge }"
|
||||
>
|
||||
<span i18n>{form().value['signup']['minimumAge'], plural, =1 {year old} other {years old}}</span>
|
||||
<span i18n>{form.value.signup.minimumAge, plural, =1 {year old} other {years old}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().signup.minimumAge" class="form-error" role="alert">{{ formErrors().signup.minimumAge }}</div>
|
||||
<div *ngIf="formErrors.signup.minimumAge" class="form-error" role="alert">{{ formErrors.signup.minimumAge }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
|
@ -215,7 +194,7 @@
|
|||
|
||||
<ng-container formGroupName="user">
|
||||
<div class="form-group">
|
||||
<label i18n id="userVideoQuotaLabel" for="userVideoQuota">Default video quota per user</label>
|
||||
<label i18n id="userVideoQuotaLabel" for="userVideoQuota">Default video quota for a new user</label>
|
||||
|
||||
<my-select-custom-value
|
||||
labelId="userVideoQuotaLabel"
|
||||
|
@ -228,11 +207,11 @@
|
|||
|
||||
<my-user-real-quota-info class="mt-2 d-block small muted" [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
|
||||
|
||||
<div *ngIf="formErrors().user.videoQuota" class="form-error" role="alert">{{ formErrors().user.videoQuota }}</div>
|
||||
<div *ngIf="formErrors.user.videoQuota" class="form-error" role="alert">{{ formErrors.user.videoQuota }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n id="userVideoQuotaDaily" for="userVideoQuotaDaily">Default daily upload limit per user</label>
|
||||
<label i18n id="userVideoQuotaDaily" for="userVideoQuotaDaily">Default daily upload limit for a new user</label>
|
||||
|
||||
<my-select-custom-value
|
||||
labelId="userVideoQuotaDailyLabel"
|
||||
|
@ -243,14 +222,14 @@
|
|||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors().user.videoQuotaDaily" class="form-error" role="alert">{{ formErrors().user.videoQuotaDaily }}</div>
|
||||
<div *ngIf="formErrors.user.videoQuotaDaily" class="form-error" role="alert">{{ formErrors.user.videoQuotaDaily }}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<ng-container formGroupName="history">
|
||||
<ng-container formGroupName="videos">
|
||||
<my-peertube-checkbox
|
||||
inputName="videosHistoryEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically enable video history for new users"
|
||||
i18n-labelText labelText="Automatically enable video history for a new user"
|
||||
>
|
||||
</my-peertube-checkbox>
|
||||
</ng-container>
|
||||
|
@ -261,9 +240,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- videos grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>VIDEOS</h2>
|
||||
<h2 i18n>VIDEO IMPORTS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
@ -274,14 +253,14 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label i18n for="importConcurrency">Import jobs concurrency</label>
|
||||
<span i18n class="small muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart.</span>
|
||||
<span i18n class="small muted ms-1">allows to import multiple videos in parallel. ⚠️ Requires a PeerTube restart</span>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input type="number" id="importConcurrency" formControlName="concurrency" />
|
||||
<span i18n>jobs in parallel</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().import.concurrency" class="form-error" role="alert">{{ formErrors().import.concurrency }}</div>
|
||||
<div *ngIf="formErrors.import.videos.concurrency" class="form-error" role="alert">{{ formErrors.import.videos.concurrency }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="http">
|
||||
|
@ -328,16 +307,25 @@
|
|||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelSynchronizationMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors()['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||
>
|
||||
<span i18n>{form().value['import']['videoChannelSynchronization']['maxPerUser'], plural, =1 {sync} other {syncs}}</span>
|
||||
<span i18n>{form.value.import.videoChannelSynchronization.maxPerUser, plural, =1 {sync} other {syncs}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">{{ formErrors().import.videoChannelSynchronization.maxPerUser }}</div>
|
||||
<div *ngIf="formErrors.import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">{{ formErrors.import.videoChannelSynchronization.maxPerUser }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>VIDEOS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="autoBlacklist">
|
||||
<ng-container formGroupName="videos">
|
||||
|
@ -411,10 +399,39 @@
|
|||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="defaults">
|
||||
<ng-container formGroupName="publish">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="defaultsPublishPrivacy">Default video privacy</label>
|
||||
|
||||
<my-select-options inputId="defaultsPublishPrivacy" [items]="privacyOptions" formControlName="privacy"></my-select-options>
|
||||
|
||||
<div *ngIf="formErrors.defaults.publish.privacy" class="form-error" role="alert">{{ formErrors.defaults.publish.privacy }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="defaultsPublishLicence">Default video licence</label>
|
||||
|
||||
<my-select-options inputId="defaultsPublishLicence" [items]="licenceOptions" formControlName="licence" clearable="true"></my-select-options>
|
||||
|
||||
<div *ngIf="formErrors.defaults.publish.licence" class="form-error" role="alert">{{ formErrors.defaults.publish.licence }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="defaultsPublishCommentsPolicy">Default comment policy</label>
|
||||
|
||||
<my-select-options inputId="defaultsPublishCommentsPolicy" [items]="commentPoliciesOptions" formControlName="commentsPolicy"></my-select-options>
|
||||
|
||||
<div *ngIf="formErrors.defaults.publish.commentsPolicy" class="form-error" role="alert">{{ formErrors.defaults.publish.commentsPolicy }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- video channels grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>VIDEO CHANNELS</h2>
|
||||
</div>
|
||||
|
@ -426,17 +443,161 @@
|
|||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelsMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors()['videoChannels.maxPerUser'] }"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors.videoChannels.maxPerUser }"
|
||||
>
|
||||
<span i18n>{form().value['videoChannels']['maxPerUser'], plural, =1 {channel} other {channels}}</span>
|
||||
<span i18n>{form.value.videoChannels.maxPerUser, plural, =1 {channel} other {channels}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().videoChannels.maxPerUser" class="form-error" role="alert">{{ formErrors().videoChannels.maxPerUser }}</div>
|
||||
<div *ngIf="formErrors.videoChannels.maxPerUser" class="form-error" role="alert">{{ formErrors.videoChannels.maxPerUser }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- search grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>FEDERATION</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-primary" routerLink="/admin/settings/follows">relations</a> with other platforms.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="videoComments">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoCommentsAcceptRemoteComments" formControlName="acceptRemoteComments"
|
||||
i18n-labelText labelText="Accept comments made on remote platforms"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current comments from remote platforms will not be deleted</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="followers">
|
||||
<ng-container formGroupName="channels">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersChannelsEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Remote actors can follow channels of your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current followers of channels of your platform will not be affected</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Remote actors can follow your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>This setting is not retroactive: current followers of your platform will not be affected</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceManualApproval" formControlName="manualApproval"
|
||||
i18n-labelText labelText="Manually approve new followers that follow your platform"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="followings">
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<ng-container formGroupName="autoFollowBack">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow back followers that follow your platform"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="autoFollowIndex">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow platforms of a public index"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
|
||||
|
||||
<span i18n>
|
||||
See <a class="link-primary" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
|
||||
<label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
|
||||
<input
|
||||
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
||||
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors.followings.instance.autoFollowIndex.indexUrl }"
|
||||
>
|
||||
<div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>PLAYER</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<ng-container formGroupName="defaults">
|
||||
|
||||
<div class="form-group" formGroupName="player">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsPlayerAutoplay" formControlName="autoPlay"
|
||||
i18n-labelText labelText="Automatically play videos in the player"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="p2p">
|
||||
|
||||
<div class="form-group" formGroupName="webapp">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsP2PWebappEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable P2P streaming by default on your platform"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="embed">
|
||||
<my-peertube-checkbox
|
||||
inputName="defaultsP2PEmbedEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable P2P streaming by default for videos embedded on external websites"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>SEARCH</h2>
|
||||
</div>
|
||||
|
@ -452,7 +613,7 @@
|
|||
i18n-labelText labelText="Allow users to do remote URI/handle search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your instance</span>
|
||||
<span i18n>Allow <strong>your users</strong> to look up remote videos/actors that may not be federated with your platform</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
@ -463,7 +624,7 @@
|
|||
i18n-labelText labelText="Allow anonymous to do remote URI/handle search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your instance</span>
|
||||
<span i18n>Allow <strong>anonymous users</strong> to look up remote videos/actors that may not be federated with your platform</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
@ -477,23 +638,23 @@
|
|||
i18n-labelText labelText="Enable global search"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</div>
|
||||
<div i18n>⚠️ This functionality depends heavily on the moderation of platforms followed by the search index you select</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div [ngClass]="getDisabledSearchIndexClass()">
|
||||
<label i18n for="searchIndexUrl">Search index URL</label>
|
||||
|
||||
<div i18n class="label-small-info">
|
||||
Use your <a class="link-primary" target="_blank" href="https://framagit.org/framasoft/peertube/search-index">your own search index</a> or choose the official one, <a class="link-primary" target="_blank" href="https://sepiasearch.org">https://sepiasearch.org</a>, that is not moderated.
|
||||
<div i18n class="form-group-description">
|
||||
Use <a class="link-primary" target="_blank" href="https://framagit.org/framasoft/peertube/search-index">your own search index</a> or choose the official one, <a class="link-primary" target="_blank" href="https://sepiasearch.org">https://sepiasearch.org</a>, that is not moderated.
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text" id="searchIndexUrl" class="form-control"
|
||||
formControlName="url" [ngClass]="{ 'input-error': formErrors()['search.searchIndex.url'] }"
|
||||
formControlName="url" [ngClass]="{ 'input-error': formErrors.search.searchIndex.url }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().search.searchIndex.url" class="form-error" role="alert">{{ formErrors().search.searchIndex.url }}</div>
|
||||
<div *ngIf="formErrors.search.searchIndex.url" class="form-error" role="alert">{{ formErrors.search.searchIndex.url }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
|
@ -509,7 +670,7 @@
|
|||
i18n-labelText labelText="Search bar uses the global search index by default"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Otherwise the local search stays used by default</span>
|
||||
<span i18n>Otherwise, the local search will be used by default</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
@ -525,7 +686,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- import/export grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>USER IMPORT/EXPORT</h2>
|
||||
</div>
|
||||
|
@ -577,7 +738,7 @@
|
|||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors().export.users.maxUserVideoQuota" class="form-error" role="alert">{{ formErrors().export.users.maxUserVideoQuota }}</div>
|
||||
<div *ngIf="formErrors.export.users.maxUserVideoQuota" class="form-error" role="alert">{{ formErrors.export.users.maxUserVideoQuota }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
|
||||
|
@ -585,9 +746,9 @@
|
|||
|
||||
<my-select-options inputId="exportUsersExportExpiration" [items]="exportExpirationOptions" formControlName="exportExpiration"></my-select-options>
|
||||
|
||||
<div i18n class="mt-1 small muted">The archive file is deleted after this period.</div>
|
||||
<div i18n class="mt-1 small muted">The archive file is deleted after this period</div>
|
||||
|
||||
<div *ngIf="formErrors().export.users.exportExpiration" class="form-error" role="alert">{{ formErrors().export.users.exportExpiration }}</div>
|
||||
<div *ngIf="formErrors.export.users.exportExpiration" class="form-error" role="alert">{{ formErrors.export.users.exportExpiration }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@ -599,147 +760,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- federation grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>FEDERATION</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-primary" routerLink="/admin/settings/follows">relations</a> with other instances.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="followers">
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Other instances can follow yours"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followersInstanceManualApproval" formControlName="manualApproval"
|
||||
i18n-labelText labelText="Manually approve new instance followers"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="followings">
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<ng-container formGroupName="autoFollowBack">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow back instances"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="autoFollowIndex">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Automatically follow instances of a public index"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
|
||||
|
||||
<span i18n>
|
||||
See <a class="link-primary" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div [ngClass]="{ 'disabled-checkbox-extra': !isAutoFollowIndexEnabled() }">
|
||||
<label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
|
||||
<input
|
||||
type="text" id="followingsInstanceAutoFollowIndexUrl" class="form-control"
|
||||
formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors()['followings.instance.autoFollowIndex.indexUrl'] }"
|
||||
>
|
||||
<div *ngIf="formErrors().followings.instance.autoFollowIndex.indexUrl" class="form-error" role="alert">{{ formErrors().followings.instance.autoFollowIndex.indexUrl }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- administrators grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>ADMINISTRATORS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<div class="form-group" formGroupName="admin">
|
||||
<label i18n for="adminEmail">Admin email</label>
|
||||
|
||||
<input
|
||||
type="text" id="adminEmail" class="form-control"
|
||||
formControlName="email" [ngClass]="{ 'input-error': formErrors()['admin.email'] }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().admin.email" class="form-error" role="alert">{{ formErrors().admin.email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="contactForm">
|
||||
<my-peertube-checkbox
|
||||
inputName="enableContactForm" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable contact form"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- Twitter grid -->
|
||||
<div class="title-col">
|
||||
<h2 i18n>TWITTER/X</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Extra configuration required by Twitter/X. All other social media (Facebook, Mastodon, etc.) are supported out of the box.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="services">
|
||||
<ng-container formGroupName="twitter">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="servicesTwitterUsername" i18n>Your Twitter/X username</label>
|
||||
|
||||
<div class="label-small-info">
|
||||
<p i18n class="mb-0">Indicates the Twitter/X account for the website or platform where the content was published.</p>
|
||||
|
||||
<p i18n>This is just an extra information injected in PeerTube HTML that is required by Twitter/X. If you don't have a Twitter/X account, just leave the default value.</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text" id="servicesTwitterUsername" class="form-control"
|
||||
formControlName="username" [ngClass]="{ 'input-error': formErrors()['services.twitter.username'] }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().services.twitter.username" class="form-error" role="alert">{{ formErrors().services.twitter.username }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
|
@ -0,0 +1,599 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { FormArray, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
import { getVideoQuotaDailyOptions, getVideoQuotaOptions } from '@app/+admin/shared/user-quota-options'
|
||||
import { CanComponentDeactivate, ServerService } from '@app/core'
|
||||
import { URL_VALIDATOR } from '@app/shared/form-validators/common-validators'
|
||||
import {
|
||||
CONCURRENCY_VALIDATOR,
|
||||
EXPORT_EXPIRATION_VALIDATOR,
|
||||
EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
||||
MAX_SYNC_PER_USER,
|
||||
MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR,
|
||||
SIGNUP_LIMIT_VALIDATOR,
|
||||
SIGNUP_MINIMUM_AGE_VALIDATOR
|
||||
} from '@app/shared/form-validators/custom-config-validators'
|
||||
import {
|
||||
BuildFormArgumentTyped,
|
||||
FormDefaultTyped,
|
||||
FormReactiveErrorsTyped,
|
||||
FormReactiveMessagesTyped
|
||||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { BroadcastMessageLevel, CustomConfig, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
type Form = {
|
||||
instance: FormGroup<{
|
||||
defaultClientRoute: FormControl<string>
|
||||
}>
|
||||
|
||||
client: FormGroup<{
|
||||
menu: FormGroup<{
|
||||
login: FormGroup<{
|
||||
redirectOnSingleExternalAuth: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
}>
|
||||
|
||||
signup: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
limit: FormControl<number>
|
||||
requiresApproval: FormControl<boolean>
|
||||
requiresEmailVerification: FormControl<boolean>
|
||||
minimumAge: FormControl<number>
|
||||
}>
|
||||
|
||||
import: FormGroup<{
|
||||
videos: FormGroup<{
|
||||
concurrency: FormControl<number>
|
||||
|
||||
http: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
|
||||
torrent: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
videoChannelSynchronization: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
maxPerUser: FormControl<number>
|
||||
}>
|
||||
users: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
|
||||
export: FormGroup<{
|
||||
users: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
maxUserVideoQuota: FormControl<number>
|
||||
exportExpiration: FormControl<number>
|
||||
}>
|
||||
}>
|
||||
|
||||
trending: FormGroup<{
|
||||
videos: FormGroup<{
|
||||
algorithms: FormGroup<{
|
||||
enabled: FormArray<FormControl<string>>
|
||||
default: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
}>
|
||||
|
||||
user: FormGroup<{
|
||||
history: FormGroup<{
|
||||
videos: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
videoQuota: FormControl<number>
|
||||
videoQuotaDaily: FormControl<number>
|
||||
}>
|
||||
|
||||
videoChannels: FormGroup<{
|
||||
maxPerUser: FormControl<number>
|
||||
}>
|
||||
|
||||
videoTranscription: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
remoteRunners: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
|
||||
videoFile: FormGroup<{
|
||||
update: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
|
||||
autoBlacklist: FormGroup<{
|
||||
videos: FormGroup<{
|
||||
ofUsers: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
}>
|
||||
|
||||
followers: FormGroup<{
|
||||
instance: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
manualApproval: FormControl<boolean>
|
||||
}>
|
||||
channels: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
|
||||
followings: FormGroup<{
|
||||
instance: FormGroup<{
|
||||
autoFollowBack: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
autoFollowIndex: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
indexUrl: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
}>
|
||||
|
||||
broadcastMessage: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
level: FormControl<BroadcastMessageLevel>
|
||||
dismissable: FormControl<boolean>
|
||||
message: FormControl<string>
|
||||
}>
|
||||
|
||||
search: FormGroup<{
|
||||
remoteUri: FormGroup<{
|
||||
users: FormControl<boolean>
|
||||
anonymous: FormControl<boolean>
|
||||
}>
|
||||
searchIndex: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
url: FormControl<string>
|
||||
disableLocalSearch: FormControl<boolean>
|
||||
isDefaultSearch: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
|
||||
storyboards: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
|
||||
defaults: FormGroup<{
|
||||
publish: FormGroup<{
|
||||
commentsPolicy: FormControl<VideoCommentPolicyType>
|
||||
privacy: FormControl<VideoPrivacyType>
|
||||
licence: FormControl<number>
|
||||
}>
|
||||
|
||||
p2p: FormGroup<{
|
||||
webapp: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
|
||||
embed: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
|
||||
player: FormGroup<{
|
||||
autoPlay: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
|
||||
videoComments: FormGroup<{
|
||||
acceptRemoteComments: FormControl<boolean>
|
||||
}>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config-general',
|
||||
templateUrl: './admin-config-general.component.html',
|
||||
styleUrls: [ './admin-config-common.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
SelectCustomValueComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
HelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
UserRealQuotaInfoComponent,
|
||||
SelectOptionsComponent,
|
||||
AlertComponent,
|
||||
AdminSaveBarComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private server = inject(ServerService)
|
||||
private route = inject(ActivatedRoute)
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
private videoService = inject(VideoService)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
signupAlertMessage: string
|
||||
defaultLandingPageOptions: SelectOptionsItem[] = []
|
||||
|
||||
exportExpirationOptions: SelectOptionsItem[] = []
|
||||
exportMaxUserVideoQuotaOptions: SelectOptionsItem[] = []
|
||||
|
||||
privacyOptions: SelectOptionsItem[] = []
|
||||
commentPoliciesOptions: SelectOptionsItem[] = []
|
||||
licenceOptions: SelectOptionsItem[] = []
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
const data = this.route.snapshot.data as {
|
||||
licences: VideoConstant<number>[]
|
||||
privacies: VideoConstant<VideoPrivacyType>[]
|
||||
commentPolicies: VideoConstant<VideoCommentPolicyType>[]
|
||||
}
|
||||
|
||||
this.privacyOptions = this.videoService.explainedPrivacyLabels(data.privacies).videoPrivacies
|
||||
this.licenceOptions = data.licences
|
||||
|
||||
this.commentPoliciesOptions = data.commentPolicies
|
||||
|
||||
this.buildLandingPageOptions()
|
||||
|
||||
this.exportExpirationOptions = [
|
||||
{ id: 1000 * 3600 * 24, label: $localize`1 day` },
|
||||
{ id: 1000 * 3600 * 24 * 2, label: $localize`2 days` },
|
||||
{ id: 1000 * 3600 * 24 * 7, label: $localize`7 days` },
|
||||
{ id: 1000 * 3600 * 24 * 30, label: $localize`30 days` }
|
||||
]
|
||||
|
||||
this.exportMaxUserVideoQuotaOptions = this.getVideoQuotaOptions().filter(o => o.id >= 1)
|
||||
|
||||
this.buildForm()
|
||||
|
||||
this.subscribeToSignupChanges()
|
||||
this.subscribeToImportSyncChanges()
|
||||
|
||||
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||
.subscribe(customConfig => {
|
||||
this.customConfig = customConfig
|
||||
|
||||
this.form.patchValue(this.customConfig)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
instance: {
|
||||
defaultClientRoute: null
|
||||
},
|
||||
client: {
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: null
|
||||
}
|
||||
}
|
||||
},
|
||||
signup: {
|
||||
enabled: null,
|
||||
limit: SIGNUP_LIMIT_VALIDATOR,
|
||||
requiresApproval: null,
|
||||
requiresEmailVerification: null,
|
||||
minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
|
||||
},
|
||||
import: {
|
||||
videos: {
|
||||
concurrency: CONCURRENCY_VALIDATOR,
|
||||
http: {
|
||||
enabled: null
|
||||
},
|
||||
torrent: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
videoChannelSynchronization: {
|
||||
enabled: null,
|
||||
maxPerUser: MAX_SYNC_PER_USER
|
||||
},
|
||||
users: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
export: {
|
||||
users: {
|
||||
enabled: null,
|
||||
maxUserVideoQuota: EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
|
||||
exportExpiration: EXPORT_EXPIRATION_VALIDATOR
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
videos: {
|
||||
algorithms: {
|
||||
enabled: null,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
history: {
|
||||
videos: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
videoQuota: USER_VIDEO_QUOTA_VALIDATOR,
|
||||
videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR
|
||||
},
|
||||
videoChannels: {
|
||||
maxPerUser: MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR
|
||||
},
|
||||
videoTranscription: {
|
||||
enabled: null,
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
videoFile: {
|
||||
update: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: null
|
||||
}
|
||||
}
|
||||
},
|
||||
followers: {
|
||||
instance: {
|
||||
enabled: null,
|
||||
manualApproval: null
|
||||
},
|
||||
channels: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
followings: {
|
||||
instance: {
|
||||
autoFollowBack: {
|
||||
enabled: null
|
||||
},
|
||||
autoFollowIndex: {
|
||||
enabled: null,
|
||||
indexUrl: URL_VALIDATOR
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: null,
|
||||
level: null,
|
||||
dismissable: null,
|
||||
message: null
|
||||
},
|
||||
search: {
|
||||
remoteUri: {
|
||||
users: null,
|
||||
anonymous: null
|
||||
},
|
||||
searchIndex: {
|
||||
enabled: null,
|
||||
url: URL_VALIDATOR,
|
||||
disableLocalSearch: null,
|
||||
isDefaultSearch: null
|
||||
}
|
||||
},
|
||||
storyboards: {
|
||||
enabled: null
|
||||
},
|
||||
defaults: {
|
||||
publish: {
|
||||
commentsPolicy: null,
|
||||
privacy: null,
|
||||
licence: null
|
||||
},
|
||||
p2p: {
|
||||
webapp: {
|
||||
enabled: null
|
||||
},
|
||||
embed: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
player: {
|
||||
autoPlay: null
|
||||
}
|
||||
},
|
||||
videoComments: {
|
||||
acceptRemoteComments: null
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return { canDeactivate: !this.form.dirty }
|
||||
}
|
||||
|
||||
countExternalAuth () {
|
||||
return this.server.getHTMLConfig().plugin.registeredExternalAuths.length
|
||||
}
|
||||
|
||||
getVideoQuotaOptions () {
|
||||
return getVideoQuotaOptions()
|
||||
}
|
||||
|
||||
getVideoQuotaDailyOptions () {
|
||||
return getVideoQuotaDailyOptions()
|
||||
}
|
||||
|
||||
doesTrendingVideosAlgorithmsEnabledInclude (algorithm: string) {
|
||||
const enabled = this.form.value.trending.videos.algorithms.enabled
|
||||
if (!Array.isArray(enabled)) return false
|
||||
|
||||
return !!enabled.find((e: string) => e === algorithm)
|
||||
}
|
||||
|
||||
getUserVideoQuota () {
|
||||
return this.form.value.user.videoQuota
|
||||
}
|
||||
|
||||
isExportUsersEnabled () {
|
||||
return this.form.value.export.users.enabled === true
|
||||
}
|
||||
|
||||
getDisabledExportUsersClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isExportUsersEnabled() }
|
||||
}
|
||||
|
||||
isSignupEnabled () {
|
||||
return this.form.value.signup.enabled === true
|
||||
}
|
||||
|
||||
getDisabledSignupClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isSignupEnabled() }
|
||||
}
|
||||
|
||||
isImportVideosHttpEnabled (): boolean {
|
||||
return this.form.value.import.videos.http.enabled === true
|
||||
}
|
||||
|
||||
importSynchronizationChecked () {
|
||||
return this.isImportVideosHttpEnabled() && this.form.value.import.videoChannelSynchronization.enabled
|
||||
}
|
||||
|
||||
hasUnlimitedSignup () {
|
||||
return this.form.value.signup.limit === -1
|
||||
}
|
||||
|
||||
isSearchIndexEnabled () {
|
||||
return this.form.value.search.searchIndex.enabled === true
|
||||
}
|
||||
|
||||
getDisabledSearchIndexClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isSearchIndexEnabled() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isTranscriptionEnabled () {
|
||||
return this.form.value.videoTranscription.enabled === true
|
||||
}
|
||||
|
||||
getTranscriptionRunnerDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isTranscriptionEnabled() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isAutoFollowIndexEnabled () {
|
||||
return this.form.value.followings.instance.autoFollowIndex.enabled === true
|
||||
}
|
||||
|
||||
buildLandingPageOptions () {
|
||||
let links: { label: string, path: string }[] = []
|
||||
|
||||
if (this.server.getHTMLConfig().homepage.enabled) {
|
||||
links.push({ label: $localize`Home`, path: '/home' })
|
||||
}
|
||||
|
||||
links = links.concat([
|
||||
{ label: $localize`Discover`, path: '/videos/overview' },
|
||||
{ label: $localize`Browse all videos`, path: '/videos/browse' },
|
||||
{ label: $localize`Browse local videos`, path: '/videos/browse?scope=local' }
|
||||
])
|
||||
|
||||
this.defaultLandingPageOptions = links.map(o => ({
|
||||
id: o.path,
|
||||
label: o.label,
|
||||
description: o.path
|
||||
}))
|
||||
}
|
||||
|
||||
private subscribeToImportSyncChanges () {
|
||||
const controls = this.form.controls
|
||||
|
||||
const importSyncControl = controls.import.controls.videoChannelSynchronization.controls.enabled
|
||||
const importVideosHttpControl = controls.import.controls.videos.controls.http.controls.enabled
|
||||
|
||||
importVideosHttpControl.valueChanges
|
||||
.subscribe(httpImportEnabled => {
|
||||
importSyncControl.setValue(httpImportEnabled && importSyncControl.value)
|
||||
|
||||
if (httpImportEnabled) importSyncControl.enable()
|
||||
else importSyncControl.disable()
|
||||
})
|
||||
}
|
||||
|
||||
private subscribeToSignupChanges () {
|
||||
const signupControl = this.form.controls.signup.controls.enabled
|
||||
|
||||
signupControl.valueChanges
|
||||
.pipe(pairwise())
|
||||
.subscribe(([ oldValue, newValue ]) => {
|
||||
if (oldValue === false && newValue === true) {
|
||||
this.signupAlertMessage =
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`You enabled signup: we automatically enabled the "Block new videos automatically" checkbox of the "Videos" section just below.`
|
||||
|
||||
this.form.patchValue({
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
signupControl.updateValueAndValidity()
|
||||
}
|
||||
|
||||
save () {
|
||||
this.adminConfigService.saveAndUpdateCurrent({
|
||||
currentConfig: this.customConfig,
|
||||
form: this.form,
|
||||
formConfig: this.form.value,
|
||||
success: $localize`General configuration updated.`
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<my-admin-save-bar i18n-title title="Edit your homepage" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form class="homepage pt-two-cols" [formGroup]="form">
|
||||
<div class="title-col">
|
||||
<h2 i18n>HOMEPAGE</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<div class="form-group">
|
||||
<label i18n for="homepageContent">Homepage content</label>
|
||||
<div class="form-group-description">
|
||||
<my-custom-markup-help></my-custom-markup-help>
|
||||
</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="homepageContent"
|
||||
formControlName="homepageContent"
|
||||
[customMarkdownRenderer]="getCustomMarkdownRenderer()"
|
||||
[debounceTime]="500"
|
||||
[formError]="formErrors['homepageContent']"
|
||||
dir="ltr"
|
||||
monospace="true"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors.homepageContent" class="form-error" role="alert">{{ formErrors.homepageContent }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,83 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CanComponentDeactivate, Notifier } from '@app/core'
|
||||
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||
import { FormReactiveErrors, FormReactiveMessages, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
||||
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
type Form = {
|
||||
homepageContent: FormControl<string>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config-homepage',
|
||||
templateUrl: './admin-config-homepage.component.html',
|
||||
styleUrls: [ './admin-config-common.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CustomMarkupHelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
AdminSaveBarComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigHomepageComponent implements OnInit, CanComponentDeactivate {
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private notifier = inject(Notifier)
|
||||
|
||||
private route = inject(ActivatedRoute)
|
||||
private customMarkup = inject(CustomMarkupService)
|
||||
private customPage = inject(CustomPageService)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrors = {}
|
||||
validationMessages: FormReactiveMessages = {}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm()
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return { canDeactivate: !this.form.dirty }
|
||||
}
|
||||
|
||||
getCustomMarkdownRenderer () {
|
||||
return this.customMarkup.getCustomMarkdownRenderer()
|
||||
}
|
||||
|
||||
save () {
|
||||
this.customPage.updateInstanceHomepage(this.form.value.homepageContent)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.form.markAsPristine()
|
||||
|
||||
this.notifier.success($localize`Homepage updated.`)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgument = {
|
||||
homepageContent: null
|
||||
}
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, { homepageContent: this.route.snapshot.data['homepageContent'] })
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
}
|
|
@ -1,50 +1,52 @@
|
|||
<ng-container [formGroup]="form()">
|
||||
<my-admin-save-bar i18n-title title="Platform information" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<ng-container formGroupName="instance">
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols mt-5"> <!-- instance grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>INSTANCE</h2>
|
||||
<h2 i18n>ADMINISTRATORS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<div class="form-group">
|
||||
<label i18n for="avatarfile">Square icon</label>
|
||||
|
||||
<div class="label-small-info">
|
||||
<p i18n class="mb-0">Square icon can be used on your custom homepage.</p>
|
||||
<div class="form-group" formGroupName="admin">
|
||||
<label i18n for="adminEmail">Admin email</label>
|
||||
|
||||
<input
|
||||
type="text" id="adminEmail" class="form-control"
|
||||
formControlName="email" [ngClass]="{ 'input-error': formErrors.admin.email }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.admin.email" class="form-error" role="alert">{{ formErrors.admin.email }}</div>
|
||||
</div>
|
||||
|
||||
<my-actor-avatar-edit
|
||||
class="d-block mb-4"
|
||||
actorType="account" previewImage="false" [username]="instanceName" displayUsername="false"
|
||||
[avatars]="instanceAvatars" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||
></my-actor-avatar-edit>
|
||||
<div class="form-group" formGroupName="contactForm">
|
||||
<my-peertube-checkbox
|
||||
inputName="enableContactForm" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable contact form"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="bannerfile">Banner</label>
|
||||
|
||||
<div class="label-small-info">
|
||||
<p i18n class="mb-0">Banner is displayed in the about, login and registration pages and be used on your custom homepage.</p>
|
||||
<p i18n>It can also be displayed on external websites to promote your instance, such as <a target="_blank" href="https://joinpeertube.org/instances">JoinPeerTube.org</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-actor-banner-edit
|
||||
[previewImage]="false" class="d-block mb-4"
|
||||
[bannerUrl]="instanceBannerUrl" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
|
||||
></my-actor-banner-edit>
|
||||
<ng-container formGroupName="instance">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h2 i18n>PLATFORM</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceName">Name</label>
|
||||
|
||||
<input
|
||||
type="text" id="instanceName" class="form-control"
|
||||
formControlName="name" [ngClass]="{ 'input-error': formErrors().instance.name }"
|
||||
formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().instance.name" class="form-error" role="alert">{{ formErrors().instance.name }}</div>
|
||||
<div *ngIf="formErrors.instance.name" class="form-error" role="alert">{{ formErrors.instance.name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -52,32 +54,44 @@
|
|||
|
||||
<textarea
|
||||
id="instanceShortDescription" formControlName="shortDescription" class="form-control small"
|
||||
[ngClass]="{ 'input-error': formErrors()['instance.shortDescription'] }"
|
||||
[ngClass]="{ 'input-error': formErrors.instance.shortDescription }"
|
||||
></textarea>
|
||||
|
||||
<div *ngIf="formErrors().instance.shortDescription" class="form-error" role="alert">{{ formErrors().instance.shortDescription }}</div>
|
||||
<div *ngIf="formErrors.instance.shortDescription" class="form-error" role="alert">{{ formErrors.instance.shortDescription }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDescription">Description</label>
|
||||
<div class="label-small-info">
|
||||
<div class="form-group-description">
|
||||
<my-custom-markup-help supportRelMe="true"></my-custom-markup-help>
|
||||
</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceDescription" formControlName="description"
|
||||
[customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
|
||||
[formError]="formErrors()['instance.description']"
|
||||
[formError]="formErrors.instance.description"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDefaultLanguage">Default language</label>
|
||||
<div class="form-group-description">
|
||||
<div i18n>Default language used for users, in emails for example.</div>
|
||||
<div i18n>The web interface still uses the web browser preferred language if not overridden by user preference</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<my-select-options inputId="instanceDefaultLanguage" formControlName="defaultLanguage" [items]="defaultLanguageItems"></my-select-options>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceCategories">Main instance categories</label>
|
||||
|
||||
<div>
|
||||
<my-select-checkbox
|
||||
inputId="instanceCategories"
|
||||
formControlName="categories" [availableItems]="categoryItems()"
|
||||
formControlName="categories" [availableItems]="categoryItems"
|
||||
[selectableGroup]="false"
|
||||
i18n-placeholder placeholder="Add a new category"
|
||||
>
|
||||
|
@ -91,7 +105,7 @@
|
|||
<div>
|
||||
<my-select-checkbox
|
||||
inputId="instanceLanguages"
|
||||
formControlName="languages" [availableItems]="languageItems()"
|
||||
formControlName="languages" [availableItems]="languageItems"
|
||||
[selectableGroup]="false"
|
||||
i18n-placeholder placeholder="Add a new language"
|
||||
>
|
||||
|
@ -101,20 +115,20 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceServerCountry">Server country</label>
|
||||
<div i18n class="label-small-info">PeerTube uses this setting to explain to your users which law they must follow in the "About" pages</div>
|
||||
<div i18n class="form-group-description">PeerTube uses this setting to explain to your users which law they must follow in the "About" pages</div>
|
||||
|
||||
<input
|
||||
type="text" id="instanceServerCountry" class="form-control"
|
||||
formControlName="serverCountry" [ngClass]="{ 'input-error': formErrors().instance.serverCountry }"
|
||||
formControlName="serverCountry" [ngClass]="{ 'input-error': formErrors.instance.serverCountry }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().instance.serverCountry" class="form-error" role="alert">{{ formErrors().instance.serverCountry }}</div>
|
||||
<div *ngIf="formErrors.instance.serverCountry" class="form-error" role="alert">{{ formErrors.instance.serverCountry }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- social grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>SOCIAL</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
|
@ -126,25 +140,25 @@
|
|||
|
||||
<div class="form-group" formGroupName="support">
|
||||
<label i18n for="instanceSupportText">Support text</label><my-help helpType="markdownText"></my-help>
|
||||
<div i18n class="label-small-info">Explain to your users how to support your platform. If set, PeerTube will display a "Support" button in "About" instance pages</div>
|
||||
<div i18n class="form-group-description">Explain to your users how to support your platform. If set, PeerTube will display a "Support" button in "About" instance pages</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceSupportText" formControlName="text" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.support.text']"
|
||||
[formError]="formErrors.instance.support.text"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="social">
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceSocialExternalLink">External link</label>
|
||||
<div i18n class="label-small-info">Link to your main website</div>
|
||||
<div i18n class="form-group-description">Link to your main website</div>
|
||||
|
||||
<input
|
||||
type="text" id="instanceSocialExternalLink" class="form-control"
|
||||
formControlName="externalLink" [ngClass]="{ 'input-error': formErrors().instance.social.externalLink }"
|
||||
formControlName="externalLink" [ngClass]="{ 'input-error': formErrors.instance.social.externalLink }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().instance.social.externalLink" class="form-error" role="alert">{{ formErrors().instance.social.externalLink }}</div>
|
||||
<div *ngIf="formErrors.instance.social.externalLink" class="form-error" role="alert">{{ formErrors.instance.social.externalLink }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -152,10 +166,10 @@
|
|||
|
||||
<input
|
||||
type="text" id="instanceSocialMastodonLink" class="form-control"
|
||||
formControlName="mastodonLink" [ngClass]="{ 'input-error': formErrors().instance.social.mastodonLink }"
|
||||
formControlName="mastodonLink" [ngClass]="{ 'input-error': formErrors.instance.social.mastodonLink }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().instance.social.mastodonLink" class="form-error" role="alert">{{ formErrors().instance.social.mastodonLink }}</div>
|
||||
<div *ngIf="formErrors.instance.social.mastodonLink" class="form-error" role="alert">{{ formErrors.instance.social.mastodonLink }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -163,10 +177,10 @@
|
|||
|
||||
<input
|
||||
type="text" id="instanceSocialBlueskyLink" class="form-control"
|
||||
formControlName="blueskyLink" [ngClass]="{ 'input-error': formErrors().instance.social.blueskyLink }"
|
||||
formControlName="blueskyLink" [ngClass]="{ 'input-error': formErrors.instance.social.blueskyLink }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors().instance.social.blueskyLink" class="form-error" role="alert">{{ formErrors().instance.social.blueskyLink }}</div>
|
||||
<div *ngIf="formErrors.instance.social.blueskyLink" class="form-error" role="alert">{{ formErrors.instance.social.blueskyLink }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
@ -174,7 +188,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- moderation grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>MODERATION & SENSITIVE CONTENT</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
|
@ -205,7 +219,7 @@
|
|||
formControlName="defaultNSFWPolicy"
|
||||
></my-select-radio>
|
||||
|
||||
<div *ngIf="formErrors().instance.defaultNSFWPolicy" class="form-error" role="alert">{{ formErrors().instance.defaultNSFWPolicy }}</div>
|
||||
<div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error" role="alert">{{ formErrors.instance.defaultNSFWPolicy }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
@ -213,7 +227,7 @@
|
|||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceTerms" formControlName="terms" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.terms']"
|
||||
[formError]="formErrors.instance.terms"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
|
@ -222,74 +236,74 @@
|
|||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceCodeOfConduct" formControlName="codeOfConduct" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.codeOfConduct']"
|
||||
[formError]="formErrors.instance.codeOfConduct"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
|
||||
<div i18n class="label-small-info">Who moderates the instance? What is the policy regarding sensitive content? Political videos? etc</div>
|
||||
<div i18n class="form-group-description">Who moderates the instance? What is the policy regarding sensitive content? Political videos? etc</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceModerationInformation" formControlName="moderationInformation" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.moderationInformation']"
|
||||
[formError]="formErrors.instance.moderationInformation"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- you and your instance grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>YOU AND YOUR INSTANCE</h2>
|
||||
<h2 i18n>YOU AND YOUR PLATFORM</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceAdministrator">Who is behind the instance?</label><my-help helpType="markdownText"></my-help>
|
||||
<div i18n class="label-small-info">A single person? A non-profit? A company?</div>
|
||||
<div i18n class="form-group-description">A single person? A non-profit? A company?</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceAdministrator" formControlName="administrator" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.administrator']"
|
||||
[formError]="formErrors.instance.administrator"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceCreationReason">Why did you create this instance?</label><my-help helpType="markdownText"></my-help>
|
||||
<div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
|
||||
<div i18n class="form-group-description">To share your personal videos? To open registrations and allow people to upload what they want?</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceCreationReason" formControlName="creationReason" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.creationReason']"
|
||||
[formError]="formErrors.instance.creationReason"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label><my-help helpType="markdownText"></my-help>
|
||||
<div i18n class="label-small-info">It's important to know for users who want to register on your instance</div>
|
||||
<div i18n class="form-group-description">It's important to know for users who want to register on your instance</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.maintenanceLifetime']"
|
||||
[formError]="formErrors.instance.maintenanceLifetime"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label><my-help helpType="markdownText"></my-help>
|
||||
<div i18n class="label-small-info">With your own funds? With user donations? Advertising?</div>
|
||||
<div i18n class="form-group-description">With your own funds? With user donations? Advertising?</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceBusinessModel" formControlName="businessModel" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.businessModel']"
|
||||
[formError]="formErrors.instance.businessModel"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4"> <!-- other information grid -->
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>OTHER INFORMATION</h2>
|
||||
</div>
|
||||
|
@ -298,11 +312,11 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
|
||||
<div i18n class="label-small-info">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
|
||||
<div i18n class="form-group-description">i.e. 2vCore 2GB RAM, a direct the link to the server you rent, etc.</div>
|
||||
|
||||
<my-markdown-textarea
|
||||
inputId="instanceHardwareInformation" formControlName="hardwareInformation" markdownType="enhanced"
|
||||
[formError]="formErrors()['instance.hardwareInformation']"
|
||||
[formError]="formErrors.instance.hardwareInformation"
|
||||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
|
@ -311,4 +325,4 @@
|
|||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</form>
|
|
@ -0,0 +1,249 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
import { CanComponentDeactivate, ServerService } from '@app/core'
|
||||
import { URL_VALIDATOR } from '@app/shared/form-validators/common-validators'
|
||||
import {
|
||||
ADMIN_EMAIL_VALIDATOR,
|
||||
INSTANCE_NAME_VALIDATOR,
|
||||
INSTANCE_SHORT_DESCRIPTION_VALIDATOR
|
||||
} from '@app/shared/form-validators/custom-config-validators'
|
||||
import {
|
||||
BuildFormArgumentTyped,
|
||||
FormDefaultTyped,
|
||||
FormReactiveErrorsTyped,
|
||||
FormReactiveMessagesTyped
|
||||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { SelectRadioComponent } from '@app/shared/shared-forms/select/select-radio.component'
|
||||
import { getCompleteLocale, I18N_LOCALES } from '@peertube/peertube-core-utils'
|
||||
import { ActorImage, CustomConfig, NSFWPolicyType, VideoConstant } from '@peertube/peertube-models'
|
||||
import merge from 'lodash-es/merge'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { CustomMarkupHelpComponent } from '../../../shared/shared-custom-markup/custom-markup-help.component'
|
||||
import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown-textarea.component'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCheckboxComponent } from '../../../shared/shared-forms/select/select-checkbox.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
type Form = {
|
||||
admin: FormGroup<{
|
||||
email: FormControl<string>
|
||||
}>
|
||||
|
||||
contactForm: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
|
||||
instance: FormGroup<{
|
||||
name: FormControl<string>
|
||||
shortDescription: FormControl<string>
|
||||
description: FormControl<string>
|
||||
categories: FormControl<number[]>
|
||||
defaultLanguage: FormControl<string>
|
||||
languages: FormControl<string[]>
|
||||
serverCountry: FormControl<string>
|
||||
|
||||
support: FormGroup<{
|
||||
text: FormControl<string>
|
||||
}>
|
||||
|
||||
social: FormGroup<{
|
||||
externalLink: FormControl<string>
|
||||
mastodonLink: FormControl<string>
|
||||
blueskyLink: FormControl<string>
|
||||
}>
|
||||
|
||||
isNSFW: FormControl<boolean>
|
||||
defaultNSFWPolicy: FormControl<NSFWPolicyType>
|
||||
|
||||
terms: FormControl<string>
|
||||
codeOfConduct: FormControl<string>
|
||||
moderationInformation: FormControl<string>
|
||||
administrator: FormControl<string>
|
||||
creationReason: FormControl<string>
|
||||
maintenanceLifetime: FormControl<string>
|
||||
businessModel: FormControl<string>
|
||||
hardwareInformation: FormControl<string>
|
||||
}>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config-information',
|
||||
templateUrl: './admin-config-information.component.html',
|
||||
styleUrls: [ './admin-config-common.scss' ],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
SelectRadioComponent,
|
||||
CommonModule,
|
||||
CustomMarkupHelpComponent,
|
||||
MarkdownTextareaComponent,
|
||||
SelectCheckboxComponent,
|
||||
RouterLink,
|
||||
PeertubeCheckboxComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
HelpComponent,
|
||||
AdminSaveBarComponent,
|
||||
SelectOptionsComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private customMarkup = inject(CustomMarkupService)
|
||||
private server = inject(ServerService)
|
||||
private route = inject(ActivatedRoute)
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
languageItems: SelectOptionsItem[] = []
|
||||
categoryItems: SelectOptionsItem[] = []
|
||||
|
||||
instanceBannerUrl: string
|
||||
instanceAvatars: ActorImage[] = []
|
||||
|
||||
nsfwItems: SelectOptionsItem[] = [
|
||||
{
|
||||
id: 'do_not_list',
|
||||
label: $localize`Hide`
|
||||
},
|
||||
{
|
||||
id: 'warn',
|
||||
label: $localize`Warn`
|
||||
},
|
||||
{
|
||||
id: 'blur',
|
||||
label: $localize`Blur`
|
||||
},
|
||||
{
|
||||
id: 'display',
|
||||
label: $localize`Display`
|
||||
}
|
||||
]
|
||||
|
||||
defaultLanguageItems: SelectOptionsItem[] = []
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
get instanceName () {
|
||||
return this.server.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
const data = this.route.snapshot.data as {
|
||||
languages: VideoConstant<string>[]
|
||||
categories: VideoConstant<number>[]
|
||||
}
|
||||
|
||||
this.languageItems = data.languages.map(l => ({ label: l.label, id: l.id }))
|
||||
this.categoryItems = data.categories.map(l => ({ label: l.label, id: l.id }))
|
||||
this.defaultLanguageItems = Object.entries(I18N_LOCALES).map(([ id, label ]) => ({ label, id }))
|
||||
|
||||
this.buildForm()
|
||||
|
||||
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||
.subscribe(customConfig => {
|
||||
this.customConfig = customConfig
|
||||
|
||||
this.form.patchValue(this.customConfig)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
admin: {
|
||||
email: ADMIN_EMAIL_VALIDATOR
|
||||
},
|
||||
contactForm: {
|
||||
enabled: null
|
||||
},
|
||||
instance: {
|
||||
name: INSTANCE_NAME_VALIDATOR,
|
||||
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
||||
description: null,
|
||||
|
||||
isNSFW: null,
|
||||
defaultNSFWPolicy: null,
|
||||
|
||||
terms: null,
|
||||
codeOfConduct: null,
|
||||
|
||||
creationReason: null,
|
||||
moderationInformation: null,
|
||||
administrator: null,
|
||||
maintenanceLifetime: null,
|
||||
businessModel: null,
|
||||
|
||||
hardwareInformation: null,
|
||||
|
||||
defaultLanguage: null,
|
||||
|
||||
categories: null,
|
||||
languages: null,
|
||||
|
||||
serverCountry: null,
|
||||
support: {
|
||||
text: null
|
||||
},
|
||||
social: {
|
||||
externalLink: URL_VALIDATOR,
|
||||
mastodonLink: URL_VALIDATOR,
|
||||
blueskyLink: URL_VALIDATOR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValues: FormDefaultTyped<Form> = merge(
|
||||
this.customConfig,
|
||||
{
|
||||
instance: {
|
||||
defaultLanguage: getCompleteLocale(this.customConfig.instance.defaultLanguage)
|
||||
}
|
||||
} satisfies FormDefaultTyped<Form>
|
||||
)
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return { canDeactivate: !this.form.dirty }
|
||||
}
|
||||
|
||||
getCustomMarkdownRenderer () {
|
||||
return this.customMarkup.getCustomMarkdownRenderer()
|
||||
}
|
||||
|
||||
save () {
|
||||
this.adminConfigService.saveAndUpdateCurrent({
|
||||
currentConfig: this.customConfig,
|
||||
form: this.form,
|
||||
formConfig: this.form.value,
|
||||
success: $localize`Information updated.`
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
<ng-container [formGroup]="form()">
|
||||
<my-admin-save-bar i18n-title title="Live configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
||||
|
||||
<div class="pt-two-cols mt-5">
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h2 i18n>LIVE</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Enable users of your instance to stream live.
|
||||
Enable users of your platform to stream a live.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -46,16 +48,16 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||
<label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your instance</label>
|
||||
<label i18n for="liveMaxInstanceLives">Max simultaneous lives created on your platform</label>
|
||||
|
||||
<span i18n class="ms-2 small muted">(-1 for "unlimited")</span>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input type="number" id="liveMaxInstanceLives" formControlName="maxInstanceLives" />
|
||||
<span i18n>{form().value['live']['maxInstanceLives'], plural, =1 {live} other {lives}}</span>
|
||||
<span i18n>{form.value.live.maxInstanceLives, plural, =1 {live} other {lives}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().live.maxInstanceLives" class="form-error" role="alert">{{ formErrors().live.maxInstanceLives }}</div>
|
||||
<div *ngIf="formErrors.live.maxInstanceLives" class="form-error" role="alert">{{ formErrors.live.maxInstanceLives }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||
|
@ -64,10 +66,10 @@
|
|||
|
||||
<div class="number-with-unit">
|
||||
<input type="number" id="liveMaxUserLives" formControlName="maxUserLives" />
|
||||
<span i18n>{form().value['live']['maxUserLives'], plural, =1 {live} other {lives}}</span>
|
||||
<span i18n>{form.value.live.maxUserLives, plural, =1 {live} other {lives}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().live.maxUserLives" class="form-error" role="alert">{{ formErrors().live.maxUserLives }}</div>
|
||||
<div *ngIf="formErrors.live.maxUserLives" class="form-error" role="alert">{{ formErrors.live.maxUserLives }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getDisabledLiveClass()">
|
||||
|
@ -75,7 +77,7 @@
|
|||
|
||||
<my-select-options inputId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"></my-select-options>
|
||||
|
||||
<div *ngIf="formErrors().live.maxDuration" class="form-error" role="alert">{{ formErrors().live.maxDuration }}</div>
|
||||
<div *ngIf="formErrors.live.maxDuration" class="form-error" role="alert">{{ formErrors.live.maxDuration }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@ -123,7 +125,7 @@
|
|||
<span>FPS</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().live.transcoding.fps.max" class="form-error" role="alert">{{ formErrors().live.transcoding.fps.max }}</div>
|
||||
<div *ngIf="formErrors.live.transcoding.fps.max" class="form-error" role="alert">{{ formErrors.live.transcoding.fps.max }}</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-2 mt-3">
|
||||
|
@ -193,7 +195,7 @@
|
|||
formControlName="threads"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
<div *ngIf="formErrors().live.transcoding.threads" class="form-error" role="alert">{{ formErrors().live.transcoding.threads }}</div>
|
||||
<div *ngIf="formErrors.live.transcoding.threads" class="form-error" role="alert">{{ formErrors.live.transcoding.threads }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-4" [ngClass]="getDisabledLiveLocalTranscodingClass()">
|
||||
|
@ -202,7 +204,7 @@
|
|||
|
||||
<my-select-options inputId="liveTranscodingProfile" formControlName="profile" [items]="transcodingProfiles"></my-select-options>
|
||||
|
||||
<div *ngIf="formErrors().live.transcoding.profile" class="form-error" role="alert">{{ formErrors().live.transcoding.profile }}</div>
|
||||
<div *ngIf="formErrors.live.transcoding.profile" class="form-error" role="alert">{{ formErrors.live.transcoding.profile }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@ -210,4 +212,4 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
|
@ -0,0 +1,237 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, OnDestroy, inject } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
import { CanComponentDeactivate, ServerService } from '@app/core'
|
||||
import {
|
||||
MAX_INSTANCE_LIVES_VALIDATOR,
|
||||
MAX_LIVE_DURATION_VALIDATOR,
|
||||
MAX_USER_LIVES_VALIDATOR,
|
||||
TRANSCODING_MAX_FPS_VALIDATOR,
|
||||
TRANSCODING_THREADS_VALIDATOR
|
||||
} from '@app/shared/form-validators/custom-config-validators'
|
||||
import {
|
||||
BuildFormArgumentTyped,
|
||||
FormDefaultTyped,
|
||||
FormReactiveErrorsTyped,
|
||||
FormReactiveMessagesTyped
|
||||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { AdminConfigService, FormResolutions, ResolutionOption } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
type Form = {
|
||||
live: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
allowReplay: FormControl<boolean>
|
||||
latencySetting: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
maxInstanceLives: FormControl<number>
|
||||
maxUserLives: FormControl<number>
|
||||
maxDuration: FormControl<number>
|
||||
|
||||
transcoding: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
|
||||
fps: FormGroup<{
|
||||
max: FormControl<number>
|
||||
}>
|
||||
|
||||
resolutions: FormGroup<FormResolutions>
|
||||
alwaysTranscodeOriginalResolution: FormControl<boolean>
|
||||
|
||||
remoteRunners: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
|
||||
threads: FormControl<number>
|
||||
profile: FormControl<string>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config-live',
|
||||
templateUrl: './admin-config-live.component.html',
|
||||
styleUrls: [ './admin-config-common.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PeertubeCheckboxComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
SelectOptionsComponent,
|
||||
RouterLink,
|
||||
SelectCustomValueComponent,
|
||||
AdminSaveBarComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigLiveComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private configService = inject(AdminConfigService)
|
||||
private server = inject(ServerService)
|
||||
private route = inject(ActivatedRoute)
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||
transcodingProfiles: SelectOptionsItem[] = []
|
||||
|
||||
liveMaxDurationOptions: SelectOptionsItem[] = []
|
||||
liveResolutions: ResolutionOption[] = []
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
||||
|
||||
this.liveMaxDurationOptions = [
|
||||
{ id: -1, label: $localize`No limit` },
|
||||
{ id: 1000 * 3600, label: $localize`1 hour` },
|
||||
{ id: 1000 * 3600 * 3, label: $localize`3 hours` },
|
||||
{ id: 1000 * 3600 * 5, label: $localize`5 hours` },
|
||||
{ id: 1000 * 3600 * 10, label: $localize`10 hours` }
|
||||
]
|
||||
|
||||
this.liveResolutions = this.adminConfigService.getTranscodingOptions('live')
|
||||
this.transcodingProfiles = this.adminConfigService.buildTranscodingProfiles(
|
||||
this.server.getHTMLConfig().live.transcoding.availableProfiles
|
||||
)
|
||||
|
||||
this.buildForm()
|
||||
|
||||
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||
.subscribe(customConfig => {
|
||||
this.customConfig = customConfig
|
||||
|
||||
this.form.patchValue(this.customConfig)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
live: {
|
||||
enabled: null,
|
||||
allowReplay: null,
|
||||
|
||||
maxDuration: MAX_LIVE_DURATION_VALIDATOR,
|
||||
maxInstanceLives: MAX_INSTANCE_LIVES_VALIDATOR,
|
||||
maxUserLives: MAX_USER_LIVES_VALIDATOR,
|
||||
latencySetting: {
|
||||
enabled: null
|
||||
},
|
||||
|
||||
transcoding: {
|
||||
enabled: null,
|
||||
threads: TRANSCODING_THREADS_VALIDATOR,
|
||||
profile: null,
|
||||
resolutions: this.adminConfigService.buildFormResolutions('live'),
|
||||
alwaysTranscodeOriginalResolution: null,
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
},
|
||||
fps: {
|
||||
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return { canDeactivate: !this.form.dirty }
|
||||
}
|
||||
|
||||
getResolutionKey (resolution: string) {
|
||||
return 'live.transcoding.resolutions.' + resolution
|
||||
}
|
||||
|
||||
getLiveRTMPPort () {
|
||||
return this.server.getHTMLConfig().live.rtmp.port
|
||||
}
|
||||
|
||||
isLiveEnabled () {
|
||||
return this.form.value.live.enabled === true
|
||||
}
|
||||
|
||||
isRemoteRunnerLiveEnabled () {
|
||||
return this.form.value.live.transcoding.remoteRunners.enabled === true
|
||||
}
|
||||
|
||||
getDisabledLiveClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() }
|
||||
}
|
||||
|
||||
getDisabledLiveTranscodingClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() || !this.isLiveTranscodingEnabled() }
|
||||
}
|
||||
|
||||
getDisabledLiveLocalTranscodingClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isLiveEnabled() || !this.isLiveTranscodingEnabled() || this.isRemoteRunnerLiveEnabled() }
|
||||
}
|
||||
|
||||
isLiveTranscodingEnabled () {
|
||||
return this.form.value.live.transcoding.enabled === true
|
||||
}
|
||||
|
||||
getTotalTranscodingThreads () {
|
||||
return this.adminConfigService.getTotalTranscodingThreads({
|
||||
transcoding: this.customConfig.transcoding,
|
||||
live: {
|
||||
transcoding: {
|
||||
enabled: this.form.value.live.transcoding.enabled,
|
||||
threads: this.form.value.live.transcoding.threads
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
save () {
|
||||
this.adminConfigService.saveAndUpdateCurrent({
|
||||
currentConfig: this.customConfig,
|
||||
form: this.form,
|
||||
formConfig: this.form.value,
|
||||
success: $localize`Live configuration updated.`
|
||||
})
|
||||
}
|
||||
|
||||
checkTranscodingConsistentOptions () {
|
||||
return this.adminConfigService.checkTranscodingConsistentOptions({
|
||||
transcoding: this.customConfig.transcoding,
|
||||
live: {
|
||||
enabled: this.form.value.live.enabled,
|
||||
allowReplay: this.form.value.live.allowReplay
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<my-admin-save-bar i18n-title title="Upload logos" (save)="save()" [form]="form" [formErrors]="formErrors"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>LOGO</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<div class="form-group">
|
||||
<label i18n for="avatarfile">Square icon</label>
|
||||
|
||||
<div class="form-group-description">
|
||||
<div class="mb-0">
|
||||
<div i18n>Square icon is used in the mobile application and can be used on your custom homepage.</div>
|
||||
<div i18n>It's also used as a fallback for the favicon, header icon and social media logo.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-preview-upload class="avatar-preview" formControlName="avatar" inputName="avatar" displayDelete="true"></my-preview-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="bannerfile">Banner</label>
|
||||
|
||||
<div class="form-group-description">
|
||||
<p i18n class="mb-0">Banner is displayed in the about, login and registration pages and be used on your custom homepage.</p>
|
||||
<p i18n>
|
||||
It can also be displayed on external websites to promote your instance, such as <a target="_blank" href="https://joinpeertube.org/instances"
|
||||
>JoinPeerTube.org</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<my-preview-upload class="banner-preview" formControlName="banner" inputName="banner" displayDelete="true"></my-preview-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="favicon">Favicon</label>
|
||||
|
||||
<div class="form-group-description">
|
||||
<p class="mb-0">
|
||||
<ng-container i18n>Favicon is the icon displayed in web browser tab. </ng-container>
|
||||
<ng-container i18n>If not set, the <strong>Square icon</strong> will be used.</ng-container>
|
||||
</p>
|
||||
|
||||
<p i18n>It will be resized to 32x32 pixels and converted to .png format.</p>
|
||||
</div>
|
||||
|
||||
<my-preview-upload
|
||||
class="favicon-preview"
|
||||
formControlName="favicon"
|
||||
inputName="favicon"
|
||||
displayDelete="true"
|
||||
buttonsAside="true"
|
||||
[previewSize]="{ height: '32px', width: '32px' }"
|
||||
></my-preview-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="header-wide">Desktop header logo</label>
|
||||
|
||||
<div class="form-group-description">
|
||||
<p class="mb-0">
|
||||
<ng-container i18n>Logo displayed in the header on large screens such as desktop computers.</ng-container>
|
||||
<ng-container i18n>If not set, the <strong>Square icon</strong> will be used.</ng-container>
|
||||
</p>
|
||||
|
||||
<p i18n>Its height will be reduced to 48 pixels and the width will be calculated based on the original file ratio.</p>
|
||||
</div>
|
||||
|
||||
<my-preview-upload
|
||||
class="header-wide-preview"
|
||||
formControlName="header-wide"
|
||||
inputName="header-wide"
|
||||
displayDelete="true"
|
||||
buttonsAside="true"
|
||||
[previewSize]="{ height: '48px', width: 'auto' }"
|
||||
></my-preview-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="hideInstanceName"
|
||||
formControlName="hideInstanceName"
|
||||
i18n-labelText
|
||||
labelText="Hide the name of your platform in the header on desktop (wide screens)"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<div i18n>Useful for example if your "Desktop header logo" already includes your platform name</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="header-square">Mobile header logo</label>
|
||||
|
||||
<div class="form-group-description">
|
||||
<p class="mb-0">
|
||||
<ng-container i18n>Logo displayed in the header on small screens such as mobile devices. </ng-container>
|
||||
<ng-container i18n>If not set, the <strong>Square icon</strong> will be used.</ng-container>
|
||||
</p>
|
||||
|
||||
<p i18n>It will be resized to 48x48 pixels.</p>
|
||||
</div>
|
||||
|
||||
<my-preview-upload
|
||||
class="header-square-preview"
|
||||
formControlName="header-square"
|
||||
inputName="header-square"
|
||||
displayDelete="true"
|
||||
buttonsAside="true"
|
||||
[previewSize]="{ height: '48px', width: '48px' }"
|
||||
></my-preview-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="opengraph">Social media logo</label>
|
||||
|
||||
<div class="form-group-description">
|
||||
<p class="mb-0">
|
||||
<ng-container i18n>Default logo displayed on social media. </ng-container>
|
||||
<ng-container i18n>If not set, the <strong>Square icon</strong> will be used.</ng-container>
|
||||
</p>
|
||||
|
||||
<p i18n>It will be resized to 1200x650 pixels.</p>
|
||||
</div>
|
||||
|
||||
<my-preview-upload class="opengraph-preview" formControlName="opengraph" inputName="opengraph" displayDelete="true"></my-preview-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,30 @@
|
|||
@use "sass:math";
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
.banner-preview {
|
||||
max-width: 500px;
|
||||
aspect-ratio: math.div(1, $banner-inverted-ratio);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.avatar-preview,
|
||||
.header-square-preview {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.header-wide-preview {
|
||||
width: auto;
|
||||
height: 128px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.opengraph-preview {
|
||||
max-width: 500px;
|
||||
aspect-ratio: math.div(1200, 630);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||
import {
|
||||
BuildFormArgumentTyped,
|
||||
FormReactiveErrorsTyped,
|
||||
FormReactiveMessagesTyped
|
||||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { PeertubeCheckboxComponent } from '@app/shared/shared-forms/peertube-checkbox.component'
|
||||
import { CustomConfig, LogoType } from '@peertube/peertube-models'
|
||||
import { of, Subscription, switchMap, tap } from 'rxjs'
|
||||
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { PreviewUploadComponent } from '../../../shared/shared-forms/preview-upload.component'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
import { InstanceLogoService } from '../../../shared/shared-instance/instance-logo.service'
|
||||
|
||||
type Form = {
|
||||
hideInstanceName: FormControl<boolean>
|
||||
|
||||
avatar: FormControl<Blob>
|
||||
banner: FormControl<Blob>
|
||||
favicon: FormControl<Blob>
|
||||
'header-square': FormControl<Blob>
|
||||
'header-wide': FormControl<Blob>
|
||||
opengraph: FormControl<Blob>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config-logo',
|
||||
templateUrl: './admin-config-logo.component.html',
|
||||
styleUrls: [ './admin-config-logo.component.scss', './admin-config-common.scss' ],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CommonModule,
|
||||
AdminSaveBarComponent,
|
||||
PreviewUploadComponent,
|
||||
PeertubeCheckboxComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigLogoComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private notifier = inject(Notifier)
|
||||
private logoService = inject(InstanceLogoService)
|
||||
private server = inject(ServerService)
|
||||
private route = inject(ActivatedRoute)
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private serverService = inject(ServerService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
get instanceName () {
|
||||
return this.server.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
this.buildForm()
|
||||
|
||||
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||
.subscribe(customConfig => {
|
||||
this.customConfig = customConfig
|
||||
|
||||
this.form.patchValue({ hideInstanceName: customConfig.client.header.hideInstanceName })
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
'hideInstanceName': null,
|
||||
|
||||
'avatar': null,
|
||||
'banner': null,
|
||||
'favicon': null,
|
||||
'header-square': null,
|
||||
'header-wide': null,
|
||||
'opengraph': null
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
hideInstanceName: this.customConfig.client.header.hideInstanceName,
|
||||
|
||||
...this.route.snapshot.data.logos
|
||||
}
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return { canDeactivate: !this.form.dirty }
|
||||
}
|
||||
|
||||
save () {
|
||||
this.adminConfigService.updateCustomConfig({
|
||||
client: {
|
||||
header: {
|
||||
hideInstanceName: this.form.value.hideInstanceName
|
||||
}
|
||||
}
|
||||
}).pipe(
|
||||
switchMap(() => this.serverService.resetConfig()),
|
||||
tap(newConfig => Object.assign(this.customConfig, newConfig)),
|
||||
switchMap(() => this.buildSaveAvatar()),
|
||||
switchMap(() => this.saveBanner()),
|
||||
switchMap(() => this.saveLogo('favicon')),
|
||||
switchMap(() => this.saveLogo('header-square')),
|
||||
switchMap(() => this.saveLogo('header-wide')),
|
||||
switchMap(() => this.saveLogo('opengraph')),
|
||||
switchMap(() => this.serverService.resetConfig()),
|
||||
switchMap(() => this.logoService.getAllLogos())
|
||||
).subscribe({
|
||||
next: logos => {
|
||||
this.notifier.success($localize`Logos updated`)
|
||||
|
||||
this.form.patchValue(logos)
|
||||
this.form.markAsPristine()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
private buildSaveAvatar () {
|
||||
if (this.form.controls.avatar.pristine) return of(true)
|
||||
|
||||
const avatar = this.form.value.avatar
|
||||
|
||||
return avatar
|
||||
? this.logoService.updateAvatar(avatar)
|
||||
: this.logoService.deleteAvatar()
|
||||
}
|
||||
|
||||
private saveBanner () {
|
||||
if (this.form.controls.banner.pristine) return of(true)
|
||||
|
||||
const banner = this.form.value.banner
|
||||
|
||||
return banner
|
||||
? this.logoService.updateBanner(banner)
|
||||
: this.logoService.deleteBanner()
|
||||
}
|
||||
|
||||
private saveLogo (type: LogoType) {
|
||||
const control = this.form.get(type)
|
||||
if (control.pristine) return of(true)
|
||||
|
||||
const logo = control.value
|
||||
|
||||
return logo
|
||||
? this.logoService.updateLogo(logo, type)
|
||||
: this.logoService.deleteLogo(type)
|
||||
}
|
||||
}
|
|
@ -1,10 +1,18 @@
|
|||
<ng-container [formGroup]="form()">
|
||||
<my-admin-save-bar i18n-title title="VOD configuration" (save)="save()" [form]="form" [formErrors]="formErrors" [inconsistentOptions]="checkTranscodingConsistentOptions()"></my-admin-save-bar>
|
||||
|
||||
<form [formGroup]="form">
|
||||
|
||||
<div class="pt-two-cols">
|
||||
<div class="title-col">
|
||||
<h2 i18n>TRANSCODING</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Process uploaded videos so that they are streamable on any device. Although this is costly in terms of resources, it is a critical part of PeerTube, so proceed with caution.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col"></div>
|
||||
<div class="content-col">
|
||||
|
||||
<div class="callout callout-primary">
|
||||
<div class="callout callout-primary mb-4">
|
||||
<span i18n>
|
||||
Estimating a server's capacity to transcode and stream videos isn't easy and we can't tune PeerTube automatically.
|
||||
</span>
|
||||
|
@ -15,20 +23,6 @@
|
|||
However, you may want to read <a class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/admin/configuration#vod-transcoding">our guidelines</a> before tweaking the following values.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-two-cols mt-4">
|
||||
<div class="title-col">
|
||||
<h2 i18n>TRANSCODING</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Process uploaded videos so that they are in a streamable form that any device can play. Though costly in
|
||||
resources, this is a critical part of PeerTube, so tread carefully.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<ng-container formGroupName="transcoding">
|
||||
|
||||
|
@ -151,7 +145,7 @@
|
|||
<span>FPS</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().transcoding.fps.max" class="form-error" role="alert">{{ formErrors().transcoding.fps.max }}</div>
|
||||
<div *ngIf="formErrors.transcoding.fps.max" class="form-error" role="alert">{{ formErrors.transcoding.fps.max }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getTranscodingDisabledClass()">
|
||||
|
@ -220,7 +214,7 @@
|
|||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors().transcoding.threads" class="form-error" role="alert">{{ formErrors().transcoding.threads }}</div>
|
||||
<div *ngIf="formErrors.transcoding.threads" class="form-error" role="alert">{{ formErrors.transcoding.threads }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
||||
|
@ -232,7 +226,7 @@
|
|||
<span i18n>jobs in parallel</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors().transcoding.concurrency" class="form-error" role="alert">{{ formErrors().transcoding.concurrency }}</div>
|
||||
<div *ngIf="formErrors.transcoding.concurrency" class="form-error" role="alert">{{ formErrors.transcoding.concurrency }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" [ngClass]="getLocalTranscodingDisabledClass()">
|
||||
|
@ -241,7 +235,7 @@
|
|||
|
||||
<my-select-options inputId="transcodingProfile" formControlName="profile" [items]="transcodingProfiles"></my-select-options>
|
||||
|
||||
<div *ngIf="formErrors().transcoding.profile" class="form-error" role="alert">{{ formErrors().transcoding.profile }}</div>
|
||||
<div *ngIf="formErrors.transcoding.profile" class="form-error" role="alert">{{ formErrors.transcoding.profile }}</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@ -287,4 +281,4 @@
|
|||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
308
client/src/app/+admin/config/pages/admin-config-vod.component.ts
Normal file
308
client/src/app/+admin/config/pages/admin-config-vod.component.ts
Normal file
|
@ -0,0 +1,308 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, OnDestroy, inject } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
import { CanComponentDeactivate, Notifier, ServerService } from '@app/core'
|
||||
import {
|
||||
CONCURRENCY_VALIDATOR,
|
||||
TRANSCODING_MAX_FPS_VALIDATOR,
|
||||
TRANSCODING_THREADS_VALIDATOR
|
||||
} from '@app/shared/form-validators/custom-config-validators'
|
||||
import {
|
||||
BuildFormArgumentTyped,
|
||||
FormDefaultTyped,
|
||||
FormReactiveErrorsTyped,
|
||||
FormReactiveMessagesTyped
|
||||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { AdminConfigService, FormResolutions, ResolutionOption } from '../../../shared/shared-admin/admin-config.service'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||
|
||||
type Form = {
|
||||
transcoding: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
allowAdditionalExtensions: FormControl<boolean>
|
||||
allowAudioFiles: FormControl<boolean>
|
||||
|
||||
originalFile: FormGroup<{
|
||||
keep: FormControl<boolean>
|
||||
}>
|
||||
|
||||
webVideos: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
|
||||
hls: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
splitAudioAndVideo: FormControl<boolean>
|
||||
}>
|
||||
|
||||
fps: FormGroup<{
|
||||
max: FormControl<number>
|
||||
}>
|
||||
|
||||
resolutions: FormGroup<FormResolutions>
|
||||
alwaysTranscodeOriginalResolution: FormControl<boolean>
|
||||
|
||||
remoteRunners: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
|
||||
threads: FormControl<number>
|
||||
|
||||
profile: FormControl<string>
|
||||
concurrency: FormControl<number>
|
||||
}>
|
||||
|
||||
videoStudio: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
remoteRunners: FormGroup<{
|
||||
enabled: FormControl<boolean>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-config-vod',
|
||||
templateUrl: './admin-config-vod.component.html',
|
||||
styleUrls: [ './admin-config-common.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PeertubeCheckboxComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
RouterLink,
|
||||
SelectCustomValueComponent,
|
||||
SelectOptionsComponent,
|
||||
AdminSaveBarComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigVODComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
private configService = inject(AdminConfigService)
|
||||
private notifier = inject(Notifier)
|
||||
private server = inject(ServerService)
|
||||
private route = inject(ActivatedRoute)
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private adminConfigService = inject(AdminConfigService)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||
transcodingProfiles: SelectOptionsItem[] = []
|
||||
resolutions: ResolutionOption[] = []
|
||||
|
||||
additionalVideoExtensions = ''
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
const serverConfig = this.server.getHTMLConfig()
|
||||
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
this.transcodingThreadOptions = this.configService.transcodingThreadOptions
|
||||
this.resolutions = this.adminConfigService.getTranscodingOptions('vod')
|
||||
this.additionalVideoExtensions = serverConfig.video.file.extensions.join(' ')
|
||||
this.transcodingProfiles = this.adminConfigService.buildTranscodingProfiles(serverConfig.transcoding.availableProfiles)
|
||||
|
||||
this.buildForm()
|
||||
|
||||
this.subscribeToTranscodingChanges()
|
||||
|
||||
this.customConfigSub = this.adminConfigService.getCustomConfigReloadedObs()
|
||||
.subscribe(customConfig => {
|
||||
this.customConfig = customConfig
|
||||
|
||||
this.form.patchValue(this.customConfig)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.customConfigSub) this.customConfigSub.unsubscribe()
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
transcoding: {
|
||||
enabled: null,
|
||||
allowAdditionalExtensions: null,
|
||||
allowAudioFiles: null,
|
||||
|
||||
originalFile: {
|
||||
keep: null
|
||||
},
|
||||
|
||||
webVideos: {
|
||||
enabled: null
|
||||
},
|
||||
|
||||
hls: {
|
||||
enabled: null,
|
||||
splitAudioAndVideo: null
|
||||
},
|
||||
|
||||
fps: {
|
||||
max: TRANSCODING_MAX_FPS_VALIDATOR
|
||||
},
|
||||
|
||||
resolutions: this.adminConfigService.buildFormResolutions('vod'),
|
||||
alwaysTranscodeOriginalResolution: null,
|
||||
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
},
|
||||
|
||||
threads: TRANSCODING_THREADS_VALIDATOR,
|
||||
|
||||
profile: null,
|
||||
concurrency: CONCURRENCY_VALIDATOR
|
||||
},
|
||||
|
||||
videoStudio: {
|
||||
enabled: null,
|
||||
remoteRunners: {
|
||||
enabled: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj, defaultValues)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
return { canDeactivate: !this.form.dirty }
|
||||
}
|
||||
|
||||
getResolutionKey (resolution: string) {
|
||||
return 'transcoding.resolutions.' + resolution
|
||||
}
|
||||
|
||||
isRemoteRunnerVODEnabled () {
|
||||
return this.form.value.transcoding.remoteRunners.enabled === true
|
||||
}
|
||||
|
||||
isTranscodingEnabled () {
|
||||
return this.form.value.transcoding.enabled === true
|
||||
}
|
||||
|
||||
isHLSEnabled () {
|
||||
return this.form.value.transcoding.hls.enabled === true
|
||||
}
|
||||
|
||||
isStudioEnabled () {
|
||||
return this.form.value.videoStudio.enabled === true
|
||||
}
|
||||
|
||||
getTranscodingDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() }
|
||||
}
|
||||
|
||||
getHLSDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isHLSEnabled() }
|
||||
}
|
||||
|
||||
getLocalTranscodingDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isTranscodingEnabled() || this.isRemoteRunnerVODEnabled() }
|
||||
}
|
||||
|
||||
getStudioDisabledClass () {
|
||||
return { 'disabled-checkbox-extra': !this.isStudioEnabled() }
|
||||
}
|
||||
|
||||
getTotalTranscodingThreads () {
|
||||
return this.adminConfigService.getTotalTranscodingThreads({
|
||||
live: this.customConfig.live,
|
||||
transcoding: {
|
||||
enabled: this.form.value.transcoding.enabled,
|
||||
threads: this.form.value.transcoding.threads
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private subscribeToTranscodingChanges () {
|
||||
const controls = this.form.controls
|
||||
|
||||
const transcodingControl = controls.transcoding.controls.enabled
|
||||
const videoStudioControl = controls.videoStudio.controls.enabled
|
||||
const hlsControl = controls.transcoding.controls.hls.controls.enabled
|
||||
const webVideosControl = controls.transcoding.controls.webVideos.controls.enabled
|
||||
|
||||
webVideosControl.valueChanges
|
||||
.subscribe(newValue => {
|
||||
if (newValue === false && hlsControl.value === false) {
|
||||
hlsControl.setValue(true)
|
||||
|
||||
this.notifier.info(
|
||||
$localize`Automatically enable HLS transcoding because at least 1 output format must be enabled when transcoding is enabled`,
|
||||
'',
|
||||
10000
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
hlsControl.valueChanges
|
||||
.subscribe(newValue => {
|
||||
if (newValue === false && webVideosControl.value === false) {
|
||||
webVideosControl.setValue(true)
|
||||
|
||||
this.notifier.info(
|
||||
// eslint-disable-next-line max-len
|
||||
$localize`Automatically enable Web Videos transcoding because at least 1 output format must be enabled when transcoding is enabled`,
|
||||
'',
|
||||
10000
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
transcodingControl.valueChanges
|
||||
.subscribe(newValue => {
|
||||
if (newValue === false) {
|
||||
videoStudioControl.setValue(false)
|
||||
}
|
||||
})
|
||||
|
||||
transcodingControl.updateValueAndValidity()
|
||||
webVideosControl.updateValueAndValidity()
|
||||
videoStudioControl.updateValueAndValidity()
|
||||
hlsControl.updateValueAndValidity()
|
||||
}
|
||||
|
||||
save () {
|
||||
this.adminConfigService.saveAndUpdateCurrent({
|
||||
currentConfig: this.customConfig,
|
||||
form: this.form,
|
||||
formConfig: this.form.value,
|
||||
success: $localize`VOD configuration updated.`
|
||||
})
|
||||
}
|
||||
|
||||
checkTranscodingConsistentOptions () {
|
||||
return this.adminConfigService.checkTranscodingConsistentOptions({
|
||||
transcoding: {
|
||||
enabled: this.form.value.transcoding.enabled
|
||||
},
|
||||
live: this.customConfig.live
|
||||
})
|
||||
}
|
||||
}
|
6
client/src/app/+admin/config/pages/index.ts
Normal file
6
client/src/app/+admin/config/pages/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export * from './admin-config-advanced.component'
|
||||
export * from './admin-config-general.component'
|
||||
export * from './admin-config-homepage.component'
|
||||
export * from './admin-config-information.component'
|
||||
export * from './admin-config-live.component'
|
||||
export * from './admin-config-vod.component'
|
|
@ -0,0 +1,29 @@
|
|||
<div class="root">
|
||||
<div class="root-bar">
|
||||
<h2>{{ title() }}</h2>
|
||||
|
||||
<div class="buttons">
|
||||
<my-button theme="secondary" class="pre-config" (click)="openConfigWizard()" i18n>Open config wizard</my-button>
|
||||
|
||||
<my-button theme="primary" class="save-button" icon="circle-tick" [disabled]="!canUpdate()" (click)="onSave($event)" i18n>Save</my-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!isUpdateAllowed()) {
|
||||
<my-alert type="primary" i18n class="d-block mt-3">
|
||||
Updating platform configuration from the web interface is disabled by the system administrator.
|
||||
</my-alert>
|
||||
} @else if (displayFormErrors && !form().valid) {
|
||||
<my-alert type="danger" class="d-block mt-3">
|
||||
<ng-container i18n>There are errors in the configuration:</ng-container>
|
||||
|
||||
<ul class="mb-0">
|
||||
<li *ngFor="let error of grabAllErrors()">{{ error }}</li>
|
||||
</ul>
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
@if (inconsistentOptions()) {
|
||||
<my-alert type="danger" class="d-block mt-3">{{ inconsistentOptions() }}</my-alert>
|
||||
}
|
||||
</div>
|
|
@ -0,0 +1,60 @@
|
|||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
.root {
|
||||
position: sticky;
|
||||
top: pvar(--header-height);
|
||||
z-index: 11;
|
||||
background-color: pvar(--bg);
|
||||
|
||||
@include rfs(3rem, margin-bottom);
|
||||
}
|
||||
|
||||
.root-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 14px;
|
||||
background-color: pvar(--bg-secondary-350);
|
||||
|
||||
@include rfs(1.5rem, padding);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@include margin-left(auto);
|
||||
}
|
||||
|
||||
.pre-config {
|
||||
display: inline-block;
|
||||
|
||||
@include margin-right(0.5rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
flex-shrink: 1;
|
||||
color: pvar(--fg-350);
|
||||
font-weight: $font-bold;
|
||||
margin-bottom: 0;
|
||||
line-height: normal;
|
||||
|
||||
@include margin-left(auto);
|
||||
@include font-size(1.5rem);
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
.root-bar {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.buttons,
|
||||
h2 {
|
||||
@include margin-left(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, input, OnDestroy, OnInit, output } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { ScreenService, ServerService } from '@app/core'
|
||||
import { HeaderService } from '@app/header/header.service'
|
||||
import { FormReactiveErrors, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
|
||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||
import { AlertComponent } from '../../../shared/shared-main/common/alert.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-admin-save-bar',
|
||||
styleUrls: [ './admin-save-bar.component.scss' ],
|
||||
templateUrl: './admin-save-bar.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
ButtonComponent,
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
export class AdminSaveBarComponent implements OnInit, OnDestroy {
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private server = inject(ServerService)
|
||||
private headerService = inject(HeaderService)
|
||||
private screenService = inject(ScreenService)
|
||||
private peertubeModal = inject(PeertubeModalService)
|
||||
|
||||
readonly title = input.required<string>()
|
||||
readonly form = input.required<FormGroup>()
|
||||
readonly formErrors = input.required<FormReactiveErrors>()
|
||||
readonly inconsistentOptions = input<string>()
|
||||
|
||||
readonly save = output()
|
||||
|
||||
displayFormErrors = false
|
||||
|
||||
ngOnInit () {
|
||||
if (this.screenService.isInMobileView()) {
|
||||
this.headerService.setSearchHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.headerService.setSearchHidden(false)
|
||||
}
|
||||
|
||||
isUpdateAllowed () {
|
||||
return this.server.getHTMLConfig().webadmin.configuration.edition.allowed === true
|
||||
}
|
||||
|
||||
canUpdate () {
|
||||
if (!this.isUpdateAllowed()) return false
|
||||
if (this.inconsistentOptions()) return false
|
||||
|
||||
return this.form().dirty
|
||||
}
|
||||
|
||||
grabAllErrors () {
|
||||
return this.formReactiveService.grabAllErrors(this.formErrors())
|
||||
}
|
||||
|
||||
openConfigWizard () {
|
||||
this.peertubeModal.openAdminConfigWizardSubject.next({ showWelcome: false })
|
||||
}
|
||||
|
||||
onSave (event: Event) {
|
||||
this.displayFormErrors = false
|
||||
|
||||
if (this.form().valid) {
|
||||
this.save.emit()
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
this.displayFormErrors = true
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import { catchError } from 'rxjs/operators'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { RestExtractor } from '@app/core'
|
||||
import { CustomConfig } from '@peertube/peertube-models'
|
||||
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
||||
import { environment } from '../../../../environments/environment'
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
private authHttp = inject(HttpClient)
|
||||
private restExtractor = inject(RestExtractor)
|
||||
|
||||
private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/config'
|
||||
|
||||
videoQuotaOptions: SelectOptionsItem[] = []
|
||||
videoQuotaDailyOptions: SelectOptionsItem[] = []
|
||||
transcodingThreadOptions: SelectOptionsItem[] = []
|
||||
|
||||
constructor () {
|
||||
this.videoQuotaOptions = [
|
||||
{ id: -1, label: $localize`Unlimited` },
|
||||
{ id: 0, label: $localize`None - no upload possible` },
|
||||
{ id: 100 * 1024 * 1024, label: $localize`100MB` },
|
||||
{ id: 500 * 1024 * 1024, label: $localize`500MB` },
|
||||
{ id: 1024 * 1024 * 1024, label: $localize`1GB` },
|
||||
{ id: 5 * 1024 * 1024 * 1024, label: $localize`5GB` },
|
||||
{ id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` },
|
||||
{ id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` },
|
||||
{ id: 100 * 1024 * 1024 * 1024, label: $localize`100GB` },
|
||||
{ id: 200 * 1024 * 1024 * 1024, label: $localize`200GB` },
|
||||
{ id: 500 * 1024 * 1024 * 1024, label: $localize`500GB` }
|
||||
]
|
||||
|
||||
this.videoQuotaDailyOptions = [
|
||||
{ id: -1, label: $localize`Unlimited` },
|
||||
{ id: 0, label: $localize`None - no upload possible` },
|
||||
{ id: 10 * 1024 * 1024, label: $localize`10MB` },
|
||||
{ id: 50 * 1024 * 1024, label: $localize`50MB` },
|
||||
{ id: 100 * 1024 * 1024, label: $localize`100MB` },
|
||||
{ id: 500 * 1024 * 1024, label: $localize`500MB` },
|
||||
{ id: 2 * 1024 * 1024 * 1024, label: $localize`2GB` },
|
||||
{ id: 5 * 1024 * 1024 * 1024, label: $localize`5GB` },
|
||||
{ id: 10 * 1024 * 1024 * 1024, label: $localize`10GB` },
|
||||
{ id: 20 * 1024 * 1024 * 1024, label: $localize`20GB` },
|
||||
{ id: 50 * 1024 * 1024 * 1024, label: $localize`50GB` }
|
||||
]
|
||||
|
||||
this.transcodingThreadOptions = [
|
||||
{ id: 0, label: $localize`Auto (via ffmpeg)` },
|
||||
{ id: 1, label: '1' },
|
||||
{ id: 2, label: '2' },
|
||||
{ id: 4, label: '4' },
|
||||
{ id: 8, label: '8' },
|
||||
{ id: 12, label: '12' },
|
||||
{ id: 16, label: '16' },
|
||||
{ id: 32, label: '32' }
|
||||
]
|
||||
}
|
||||
|
||||
getCustomConfig () {
|
||||
return this.authHttp.get<CustomConfig>(ConfigService.BASE_APPLICATION_URL + '/custom')
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
updateCustomConfig (data: CustomConfig) {
|
||||
return this.authHttp.put<CustomConfig>(ConfigService.BASE_APPLICATION_URL + '/custom', data)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
}
|
|
@ -1,51 +1,22 @@
|
|||
<p-table
|
||||
[value]="followers" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||
[showCurrentPageReport]="true" [currentPageReportTemplate]="getPaginationTemplate()"
|
||||
[(selection)]="selectedRows"
|
||||
<my-table
|
||||
#table
|
||||
key="FollowersListComponent"
|
||||
[defaultColumns]="columns"
|
||||
i18n-paginatorText
|
||||
paginatorText="followers per page"
|
||||
[dataLoader]="dataLoader"
|
||||
columnConfig="false"
|
||||
[bulkActions]="bulkActions"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="left-buttons">
|
||||
<my-action-dropdown
|
||||
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
|
||||
[actions]="bulkActions" [entry]="selectedRows"
|
||||
>
|
||||
</my-action-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #totalTitle let-totalRecords>
|
||||
<ng-container i18n>{ totalRecords, plural, =0 {No follower} =1 {1 follower} other {{{ totalRecords | myNumberFormatter }} followers}}</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px">
|
||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
<th scope="col" style="width: 150px;" i18n>Actions</th>
|
||||
<th scope="col" i18n>Follower</th>
|
||||
<th scope="col" style="width: 100px;" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||
<th scope="col" style="width: 150px;" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="score">Link health <p-sortIcon field="score"></p-sortIcon></th>
|
||||
<th scope="col" style="width: 150px;" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
</tr>
|
||||
<ng-template #captionRight>
|
||||
<my-advanced-input-filter [filters]="searchFilters" (search)="table.onSearch($event)"></my-advanced-input-filter>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-follow>
|
||||
<tr>
|
||||
<td class="checkbox-cell">
|
||||
<p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
|
||||
</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-button *ngIf="follow.state !== 'accepted'" i18n-title title="Accept" icon="tick" (click)="acceptFollower([ follow ])"></my-button>
|
||||
<my-button *ngIf="follow.state !== 'rejected'" i18n-title title="Reject" icon="cross" (click)="rejectFollower([ follow ])"></my-button>
|
||||
|
||||
<my-delete-button *ngIf="follow.state === 'rejected'" (click)="deleteFollowers([ follow ])"></my-delete-button>
|
||||
</td>
|
||||
<ng-template #tableCells let-follow>
|
||||
<td>
|
||||
<a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer">
|
||||
{{ buildFollowerName(follow) }}
|
||||
|
@ -54,24 +25,32 @@
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<span *ngIf="follow.state === 'accepted'" class="pt-badge badge-green" i18n>Accepted</span>
|
||||
<span *ngIf="follow.state === 'pending'" class="pt-badge badge-yellow" i18n>Pending</span>
|
||||
<span *ngIf="follow.state === 'rejected'" class="pt-badge badge-red" i18n>Rejected</span>
|
||||
@if (follow.state === 'accepted') {
|
||||
<span class="pt-badge badge-green" i18n>Accepted</span>
|
||||
} @else if (follow.state === 'pending') {
|
||||
<span class="pt-badge badge-yellow" i18n>Pending</span>
|
||||
} @else if (follow.state === 'rejected') {
|
||||
<span class="pt-badge badge-red" i18n>Rejected</span>
|
||||
}
|
||||
</td>
|
||||
|
||||
<td>{{ follow.score / 10000 * 100 }}</td>
|
||||
<td>{{ follow.score / 10000 * 100 }}%</td>
|
||||
<td>{{ follow.createdAt | ptDate: 'short' }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td myAutoColspan>
|
||||
<div class="no-results">
|
||||
<ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container>
|
||||
<ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #actionCell let-follow>
|
||||
<my-button *ngIf="follow.state !== 'accepted'" i18n-title title="Accept" icon="tick" (click)="acceptFollower([ follow ])"></my-button>
|
||||
<my-button *ngIf="follow.state !== 'rejected'" i18n-title title="Reject" icon="cross" (click)="rejectFollower([ follow ])"></my-button>
|
||||
|
||||
<my-delete-button *ngIf="follow.state === 'rejected'" (click)="deleteFollowers([ follow ])"></my-delete-button>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<ng-template #noResults let-search>
|
||||
@if (search) {
|
||||
<ng-container i18n>No follower found matching current filters.</ng-container>
|
||||
} @else {
|
||||
<ng-container i18n>Your instance doesn't have any follower.</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</my-table>
|
||||
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.action-cell {
|
||||
my-button:first-child {
|
||||
@include margin-right(10px);
|
||||
}
|
||||
}
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
my-delete-button {
|
||||
max-width: 130px;
|
||||
|
|
|
@ -1,55 +1,58 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { ConfirmService, Notifier } from '@app/core'
|
||||
import { formatICU } from '@app/helpers'
|
||||
import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DataLoaderOptions, TableColumnInfo, TableComponent } from '@app/shared/shared-tables/table.component'
|
||||
import { ActorFollow } from '@peertube/peertube-models'
|
||||
import { SharedModule, SortMeta } from 'primeng/api'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { ActionDropdownComponent, DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
||||
import { DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||
import { DeleteButtonComponent } from '../../../shared/shared-main/buttons/delete-button.component'
|
||||
import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-colspan.directive'
|
||||
import { NumberFormatterPipe } from '../../../shared/shared-main/common/number-formatter.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'my-followers-list',
|
||||
templateUrl: './followers-list.component.html',
|
||||
styleUrls: [ './followers-list.component.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GlobalIconComponent,
|
||||
TableModule,
|
||||
SharedModule,
|
||||
NgIf,
|
||||
ActionDropdownComponent,
|
||||
AdvancedInputFilterComponent,
|
||||
NgbTooltip,
|
||||
ButtonComponent,
|
||||
DeleteButtonComponent,
|
||||
AutoColspanDirective,
|
||||
PTDatePipe
|
||||
PTDatePipe,
|
||||
NumberFormatterPipe,
|
||||
TableComponent
|
||||
]
|
||||
})
|
||||
export class FollowersListComponent extends RestTable<ActorFollow> implements OnInit {
|
||||
export class FollowersListComponent implements OnInit {
|
||||
private confirmService = inject(ConfirmService)
|
||||
private notifier = inject(Notifier)
|
||||
private followService = inject(InstanceFollowService)
|
||||
|
||||
followers: ActorFollow[] = []
|
||||
totalRecords = 0
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
readonly table = viewChild<TableComponent<ActorFollow>>('table')
|
||||
|
||||
searchFilters: AdvancedInputFilter[] = []
|
||||
|
||||
bulkActions: DropdownAction<ActorFollow[]>[] = []
|
||||
|
||||
ngOnInit () {
|
||||
this.initialize()
|
||||
columns: TableColumnInfo<string>[] = [
|
||||
{ id: 'follower', label: $localize`Follower`, sortable: false },
|
||||
{ id: 'state', label: $localize`State`, sortable: true },
|
||||
{ id: 'score', label: $localize`Reliability`, sortable: true },
|
||||
{ id: 'createdAt', label: $localize`Created`, sortable: true }
|
||||
]
|
||||
|
||||
dataLoader: typeof this._dataLoader
|
||||
|
||||
constructor () {
|
||||
this.dataLoader = this._dataLoader.bind(this)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.searchFilters = this.followService.buildFollowsListFilters()
|
||||
|
||||
this.bulkActions = [
|
||||
|
@ -71,10 +74,6 @@ export class FollowersListComponent extends RestTable<ActorFollow> implements On
|
|||
]
|
||||
}
|
||||
|
||||
getIdentifier () {
|
||||
return 'FollowersListComponent'
|
||||
}
|
||||
|
||||
acceptFollower (follows: ActorFollow[]) {
|
||||
this.followService.acceptFollower(follows)
|
||||
.subscribe({
|
||||
|
@ -85,7 +84,7 @@ export class FollowersListComponent extends RestTable<ActorFollow> implements On
|
|||
)
|
||||
this.notifier.success(message)
|
||||
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
|
@ -110,7 +109,7 @@ export class FollowersListComponent extends RestTable<ActorFollow> implements On
|
|||
)
|
||||
this.notifier.success(message)
|
||||
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
|
@ -141,7 +140,7 @@ export class FollowersListComponent extends RestTable<ActorFollow> implements On
|
|||
|
||||
this.notifier.success(message)
|
||||
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
|
@ -152,15 +151,9 @@ export class FollowersListComponent extends RestTable<ActorFollow> implements On
|
|||
return follow.follower.name + '@' + follow.follower.host
|
||||
}
|
||||
|
||||
protected reloadDataInternal () {
|
||||
this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search })
|
||||
.subscribe({
|
||||
next: resultList => {
|
||||
this.followers = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
private _dataLoader (options: DataLoaderOptions) {
|
||||
const { pagination, sort, search } = options
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
return this.followService.getFollowers({ pagination, sort, search })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +1,29 @@
|
|||
<p-table
|
||||
[value]="following" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order"
|
||||
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||
[showCurrentPageReport]="true" [currentPageReportTemplate]="getPaginationTemplate()"
|
||||
[(selection)]="selectedRows"
|
||||
<my-table
|
||||
#table
|
||||
key="FollowingListComponent"
|
||||
[defaultColumns]="columns"
|
||||
i18n-paginatorText
|
||||
paginatorText="subscriptions per page"
|
||||
[dataLoader]="dataLoader"
|
||||
columnConfig="false"
|
||||
[bulkActions]="bulkActions"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="left-buttons">
|
||||
<my-action-dropdown
|
||||
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
|
||||
[actions]="bulkActions" [entry]="selectedRows"
|
||||
>
|
||||
</my-action-dropdown>
|
||||
<ng-template #totalTitle let-totalRecords>
|
||||
<ng-container i18n>{ totalRecords, plural, =0 {No subscription} =1 {1 subscription} other {{{ totalRecords | myNumberFormatter }} subscriptions}}</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<button *ngIf="!isInSelectionMode()" class="peertube-create-button" (click)="openFollowModal()">
|
||||
<ng-template #captionRight>
|
||||
<my-advanced-input-filter [filters]="searchFilters" (search)="table.onSearch($event)"></my-advanced-input-filter>
|
||||
|
||||
<my-button i18n-label label="Refresh" icon="refresh" (click)="table.loadData()"></my-button>
|
||||
|
||||
<button class="peertube-create-button" (click)="openFollowModal()">
|
||||
<my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Follow</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto d-flex gap-2 flex-wrap">
|
||||
<my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
|
||||
<my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px">
|
||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
<th scope="col" style="width: 150px;" i18n>Action</th>
|
||||
<th scope="col" i18n>Following</th>
|
||||
<th scope="col" style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||
<th scope="col" style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th scope="col" style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-follow>
|
||||
<tr pSelectableRow="follow">
|
||||
<td class="checkbox-cell">
|
||||
<p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
|
||||
</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-delete-button label (click)="removeFollowing([ follow ])"></my-delete-button>
|
||||
</td>
|
||||
<ng-template #tableCells let-follow>
|
||||
<td>
|
||||
<a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
|
||||
{{ buildFollowingName(follow) }}
|
||||
|
@ -70,19 +44,19 @@
|
|||
[host]="follow.following.host" [redundancyAllowed]="follow.following.hostRedundancyAllowed"
|
||||
></my-redundancy-checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td myAutoColspan>
|
||||
<div class="no-results">
|
||||
<ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container>
|
||||
<ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #actionCell let-follow>
|
||||
<my-delete-button label (click)="removeFollowing([ follow ])"></my-delete-button>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<ng-template #noResults let-search>
|
||||
@if (search) {
|
||||
<ng-container i18n>No host found matching current filters.</ng-container>
|
||||
} @else {
|
||||
<ng-container i18n>Your instance is not following anyone.</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</my-table>
|
||||
|
||||
<my-follow-modal #followModal></my-follow-modal>
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
a {
|
||||
color: pvar(--fg);
|
||||
}
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
my-delete-button {
|
||||
max-width: 130px;
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||
import { ConfirmService, Notifier } from '@app/core'
|
||||
import { formatICU } from '@app/helpers'
|
||||
import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { ActorFollow } from '@peertube/peertube-models'
|
||||
import { SharedModule, SortMeta } from 'primeng/api'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { ActionDropdownComponent, DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
||||
import { DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||
import { DeleteButtonComponent } from '../../../shared/shared-main/buttons/delete-button.component'
|
||||
import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-colspan.directive'
|
||||
import { NumberFormatterPipe } from '../../../shared/shared-main/common/number-formatter.pipe'
|
||||
import { DataLoaderOptions, TableColumnInfo, TableComponent } from '../../../shared/shared-tables/table.component'
|
||||
import { RedundancyCheckboxComponent } from '../shared/redundancy-checkbox.component'
|
||||
import { FollowModalComponent } from './follow-modal.component'
|
||||
|
||||
|
@ -20,39 +19,44 @@ import { FollowModalComponent } from './follow-modal.component'
|
|||
templateUrl: './following-list.component.html',
|
||||
styleUrls: [ './following-list.component.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GlobalIconComponent,
|
||||
TableModule,
|
||||
SharedModule,
|
||||
NgIf,
|
||||
ActionDropdownComponent,
|
||||
AdvancedInputFilterComponent,
|
||||
DeleteButtonComponent,
|
||||
RedundancyCheckboxComponent,
|
||||
AutoColspanDirective,
|
||||
FollowModalComponent,
|
||||
PTDatePipe,
|
||||
ButtonComponent
|
||||
ButtonComponent,
|
||||
TableComponent,
|
||||
NumberFormatterPipe
|
||||
]
|
||||
})
|
||||
export class FollowingListComponent extends RestTable<ActorFollow> implements OnInit {
|
||||
export class FollowingListComponent implements OnInit {
|
||||
private notifier = inject(Notifier)
|
||||
private confirmService = inject(ConfirmService)
|
||||
private followService = inject(InstanceFollowService)
|
||||
|
||||
readonly followModal = viewChild<FollowModalComponent>('followModal')
|
||||
|
||||
following: ActorFollow[] = []
|
||||
totalRecords = 0
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
readonly table = viewChild<TableComponent<ActorFollow>>('table')
|
||||
|
||||
searchFilters: AdvancedInputFilter[] = []
|
||||
|
||||
bulkActions: DropdownAction<ActorFollow[]>[] = []
|
||||
|
||||
ngOnInit () {
|
||||
this.initialize()
|
||||
columns: TableColumnInfo<string>[] = [
|
||||
{ id: 'following', label: $localize`Following`, sortable: false },
|
||||
{ id: 'state', label: $localize`State`, sortable: true },
|
||||
{ id: 'createdAt', label: $localize`Created`, sortable: true },
|
||||
{ id: 'redundancyAllowed', label: $localize`Redundancy allowed`, sortable: true }
|
||||
]
|
||||
|
||||
dataLoader: typeof this._dataLoader
|
||||
|
||||
constructor () {
|
||||
this.dataLoader = this._dataLoader.bind(this)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.searchFilters = this.followService.buildFollowsListFilters()
|
||||
|
||||
this.bulkActions = [
|
||||
|
@ -63,10 +67,6 @@ export class FollowingListComponent extends RestTable<ActorFollow> implements On
|
|||
]
|
||||
}
|
||||
|
||||
getIdentifier () {
|
||||
return 'FollowingListComponent'
|
||||
}
|
||||
|
||||
openFollowModal () {
|
||||
this.followModal().openModal()
|
||||
}
|
||||
|
@ -99,22 +99,16 @@ export class FollowingListComponent extends RestTable<ActorFollow> implements On
|
|||
)
|
||||
|
||||
this.notifier.success(message)
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
protected reloadDataInternal () {
|
||||
this.followService.getFollowing({ pagination: this.pagination, sort: this.sort, search: this.search })
|
||||
.subscribe({
|
||||
next: resultList => {
|
||||
this.following = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
private _dataLoader (options: DataLoaderOptions) {
|
||||
const { pagination, sort, search } = options
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
return this.followService.getFollowing({ pagination, sort, search })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,21 @@
|
|||
<div class="admin-sub-header">
|
||||
<my-table
|
||||
#table
|
||||
key="VideoRedundanciesListComponent"
|
||||
[defaultColumns]="columns"
|
||||
defaultSort="name"
|
||||
defaultSortOrder="asc"
|
||||
i18n-paginatorText
|
||||
paginatorText="redundancies per page"
|
||||
[dataLoader]="dataLoader"
|
||||
[customParseQueryParams]="customParseQueryParams"
|
||||
[customUpdateUrl]="customUpdateUrl"
|
||||
columnConfig="false"
|
||||
>
|
||||
<ng-template #totalTitle let-totalRecords>
|
||||
<ng-container i18n>{ totalRecords, plural, =0 {No redundancy} =1 {1 redundancy} other {{{ totalRecords | myNumberFormatter }} redundancies}}</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #captionRight>
|
||||
<div class="select-filter-block">
|
||||
<label for="displayType" i18n>Display</label>
|
||||
|
||||
|
@ -9,36 +26,9 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p-table
|
||||
[value]="videoRedundancies" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords"
|
||||
[rows]="rowsPerPage" [first]="pagination.start" [rowsPerPageOptions]="rowsPerPageOptions"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
>
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px;">
|
||||
<span i18n class="visually-hidden">More information</span>
|
||||
</th>
|
||||
<th scope="col" style="width: 150px;" i18n>Action</th>
|
||||
<th scope="col" style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
|
||||
<th scope="col" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th >
|
||||
<th scope="col" style="width: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-expanded="expanded" let-redundancy>
|
||||
<tr>
|
||||
<td class="expand-cell">
|
||||
<my-table-expander-icon [pRowToggler]="redundancy" [expanded]="expanded" i18n-tooltip tooltip="List redundancies"></my-table-expander-icon>
|
||||
</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-delete-button label (click)="removeRedundancy(redundancy)"></my-delete-button>
|
||||
</td>
|
||||
|
||||
<ng-template #tableCells let-redundancy>
|
||||
<td *ngIf="isDisplayingRemoteVideos()">{{ getRedundancyStrategy(redundancy) }}</td>
|
||||
|
||||
<td>
|
||||
|
@ -49,39 +39,26 @@
|
|||
</td>
|
||||
|
||||
<td *ngIf="isDisplayingRemoteVideos()">{{ getTotalSize(redundancy) | bytes: 1 }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="rowexpansion" let-redundancy>
|
||||
<tr *ngIf="redundancy.redundancies.files.length !== 0">
|
||||
<td class="expand-cell" myAutoColspan>
|
||||
<div *ngFor="let file of redundancy.redundancies.files" class="expansion-block">
|
||||
<my-video-redundancy-information [redundancyElement]="file"></my-video-redundancy-information>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #actionCell let-redundancy>
|
||||
<my-delete-button label (click)="removeRedundancy(redundancy)"></my-delete-button>
|
||||
</ng-template>
|
||||
|
||||
<tr *ngIf="redundancy.redundancies.streamingPlaylists.length !== 0">
|
||||
<td class="expand-cell" myAutoColspan>
|
||||
<ng-template #expandedRow let-redundancy>
|
||||
<div *ngFor="let playlist of redundancy.redundancies.streamingPlaylists">
|
||||
<my-video-redundancy-information [redundancyElement]="playlist"></my-video-redundancy-information>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td myAutoColspan>
|
||||
<div class="no-results">
|
||||
<ng-container *ngIf="isDisplayingRemoteVideos()" i18n>Your instance doesn't mirror any video.</ng-container>
|
||||
<ng-container *ngIf="!isDisplayingRemoteVideos()" i18n>Your instance has no mirrored videos.</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #noResults>
|
||||
@if (isDisplayingRemoteVideos()) {
|
||||
<ng-container i18n>Your instance doesn't mirror any video.</ng-container>
|
||||
} @else {
|
||||
<ng-container i18n>Your instance has no mirrored videos.</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
</my-table>
|
||||
|
||||
<div class="redundancies-charts" *ngIf="isDisplayingRemoteVideos() && dataLoaded">
|
||||
<h6 i18n>Enabled strategies stats</h6>
|
||||
|
|
|
@ -1,22 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
|
||||
a {
|
||||
color: pvar(--fg);
|
||||
}
|
||||
|
||||
.expansion-block {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-sub-header {
|
||||
justify-content: flex-end;
|
||||
|
||||
.peertube-select-container {
|
||||
@include peertube-select-container(auto);
|
||||
}
|
||||
}
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
.redundancies-charts {
|
||||
margin-top: 50px;
|
||||
|
|
|
@ -1,55 +1,50 @@
|
|||
import { NgFor, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ConfirmService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
|
||||
import { ConfirmService, Notifier, ServerService } from '@app/core'
|
||||
import { BytesPipe } from '@app/shared/shared-main/common/bytes.pipe'
|
||||
import { RedundancyService } from '@app/shared/shared-main/video/redundancy.service'
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { VideoRedundanciesTarget, VideoRedundancy, VideosRedundancyStats } from '@peertube/peertube-models'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import { ChartData, ChartOptions, TooltipItem } from 'chart.js'
|
||||
import { SharedModule, SortMeta } from 'primeng/api'
|
||||
import { ChartModule } from 'primeng/chart'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { tap } from 'rxjs'
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { DeleteButtonComponent } from '../../../shared/shared-main/buttons/delete-button.component'
|
||||
import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-colspan.directive'
|
||||
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
|
||||
import { NumberFormatterPipe } from '../../../shared/shared-main/common/number-formatter.pipe'
|
||||
import { DataLoaderOptions, TableColumnInfo, TableComponent, TableQueryParams } from '../../../shared/shared-tables/table.component'
|
||||
import { VideoRedundancyInformationComponent } from './video-redundancy-information.component'
|
||||
|
||||
type QueryParams = TableQueryParams & {
|
||||
displayType: VideoRedundanciesTarget
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-redundancies-list',
|
||||
templateUrl: './video-redundancies-list.component.html',
|
||||
styleUrls: [ './video-redundancies-list.component.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GlobalIconComponent,
|
||||
FormsModule,
|
||||
TableModule,
|
||||
SharedModule,
|
||||
NgIf,
|
||||
NgbTooltip,
|
||||
TableExpanderIconComponent,
|
||||
DeleteButtonComponent,
|
||||
AutoColspanDirective,
|
||||
NgFor,
|
||||
VideoRedundancyInformationComponent,
|
||||
ChartModule,
|
||||
BytesPipe
|
||||
BytesPipe,
|
||||
TableComponent,
|
||||
NumberFormatterPipe
|
||||
]
|
||||
})
|
||||
export class VideoRedundanciesListComponent extends RestTable implements OnInit {
|
||||
export class VideoRedundanciesListComponent implements OnInit {
|
||||
private static LS_DISPLAY_TYPE = 'video-redundancies-list-display-type'
|
||||
|
||||
private notifier = inject(Notifier)
|
||||
private confirmService = inject(ConfirmService)
|
||||
private redundancyService = inject(RedundancyService)
|
||||
private serverService = inject(ServerService)
|
||||
|
||||
private static LS_DISPLAY_TYPE = 'video-redundancies-list-display-type'
|
||||
readonly table = viewChild<TableComponent<VideoRedundancy>>('table')
|
||||
|
||||
videoRedundancies: VideoRedundancy[] = []
|
||||
totalRecords = 0
|
||||
|
||||
sort: SortMeta = { field: 'name', order: 1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
displayType: VideoRedundanciesTarget = 'my-videos'
|
||||
|
||||
redundanciesGraphsData: { stats: VideosRedundancyStats, graphData: ChartData, options: ChartOptions, ariaLabel: string }[] = []
|
||||
|
@ -61,20 +56,22 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit
|
|||
|
||||
private bytesPipe: BytesPipe
|
||||
|
||||
columns: TableColumnInfo<string>[]
|
||||
|
||||
dataLoader: typeof this._dataLoader
|
||||
customUpdateUrl: typeof this._customUpdateUrl
|
||||
customParseQueryParams: typeof this._customParseQueryParams
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.bytesPipe = new BytesPipe()
|
||||
}
|
||||
|
||||
getIdentifier () {
|
||||
return 'VideoRedundanciesListComponent'
|
||||
this.customUpdateUrl = this._customUpdateUrl.bind(this)
|
||||
this.customParseQueryParams = this._customParseQueryParams.bind(this)
|
||||
this.dataLoader = this._dataLoader.bind(this)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.loadSelectLocalStorage()
|
||||
|
||||
this.initialize()
|
||||
this.buildColumns()
|
||||
|
||||
this.serverService.getServerStats()
|
||||
.subscribe(res => {
|
||||
|
@ -97,10 +94,12 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit
|
|||
}
|
||||
|
||||
onDisplayTypeChanged () {
|
||||
this.pagination.start = 0
|
||||
this.saveSelectLocalStorage()
|
||||
this.dataLoaded = false
|
||||
|
||||
this.reloadData()
|
||||
this.saveSelectLocalStorage()
|
||||
this.buildColumns()
|
||||
|
||||
this.table().reloadData()
|
||||
}
|
||||
|
||||
getRedundancyStrategy (redundancy: VideoRedundancy) {
|
||||
|
@ -179,33 +178,42 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit
|
|||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Video redundancies removed!`)
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
protected reloadDataInternal () {
|
||||
this.dataLoaded = false
|
||||
|
||||
const options = {
|
||||
pagination: this.pagination,
|
||||
sort: this.sort,
|
||||
target: this.displayType
|
||||
private _dataLoader (options: DataLoaderOptions) {
|
||||
return this.redundancyService.listVideoRedundancies({ ...options, target: this.displayType })
|
||||
.pipe(tap(() => this.dataLoaded = true))
|
||||
}
|
||||
|
||||
this.redundancyService.listVideoRedundancies(options)
|
||||
.subscribe({
|
||||
next: resultList => {
|
||||
this.videoRedundancies = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
private _customUpdateUrl (): Partial<QueryParams> {
|
||||
return {
|
||||
displayType: this.displayType
|
||||
}
|
||||
}
|
||||
|
||||
this.dataLoaded = true
|
||||
},
|
||||
private _customParseQueryParams (queryParams: QueryParams) {
|
||||
if (queryParams.displayType) {
|
||||
this.displayType = queryParams.displayType
|
||||
}
|
||||
}
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
private buildColumns () {
|
||||
this.columns = [ { id: 'name', label: $localize`Name`, sortable: true } ]
|
||||
|
||||
if (this.isDisplayingRemoteVideos()) {
|
||||
this.columns = [
|
||||
{ id: 'strategy', label: $localize`Strategy`, sortable: false },
|
||||
|
||||
...this.columns,
|
||||
|
||||
{ id: 'totalSize', label: $localize`Total size`, sortable: false }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private loadSelectLocalStorage () {
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<div>
|
||||
<span class="label">Url</span>
|
||||
<strong>Url</strong>
|
||||
<a target="_blank" rel="noopener noreferrer" [href]="redundancyElement().fileUrl">{{ redundancyElement().fileUrl }}</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="label">Created on</span>
|
||||
<strong>Created on</strong>
|
||||
<span>{{ redundancyElement().createdAt | ptDate: 'medium' }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="label">Expires on</span>
|
||||
<strong>Expires on</strong>
|
||||
<span>{{ redundancyElement().expiresOn | ptDate: 'medium' }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="label">Size</span>
|
||||
<strong>Size</strong>
|
||||
<span>{{ redundancyElement().size | bytes: 1 }}</span>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
.label {
|
||||
strong {
|
||||
display: inline-block;
|
||||
min-width: 100px;
|
||||
font-weight: $font-semibold;
|
||||
color: pvar(--fg-300);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,14 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { Component } from '@angular/core'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { GenericAccountBlocklistComponent } from '@app/shared/shared-moderation/account-blocklist.component'
|
||||
import { BlocklistComponentType } from '@app/shared/shared-moderation/blocklist.service'
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SharedModule } from 'primeng/api'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { ActorAvatarComponent } from '../../../shared/shared-actor-image/actor-avatar.component'
|
||||
import { AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
||||
import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-colspan.directive'
|
||||
import { GenericAccountBlocklistComponent } from '@app/shared/shared-moderation/generic-account-blocklist.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-account-blocklist',
|
||||
styleUrls: [ '../../../shared/shared-moderation/moderation.scss' ],
|
||||
templateUrl: '../../../shared/shared-moderation/account-blocklist.component.html',
|
||||
template: `<my-generic-account-blocklist [mode]="mode" key="InstanceAccountBlocklistComponent" />`,
|
||||
imports: [
|
||||
TableModule,
|
||||
SharedModule,
|
||||
AdvancedInputFilterComponent,
|
||||
NgbTooltip,
|
||||
ActorAvatarComponent,
|
||||
AutoColspanDirective,
|
||||
NgIf,
|
||||
PTDatePipe
|
||||
GenericAccountBlocklistComponent
|
||||
]
|
||||
})
|
||||
export class InstanceAccountBlocklistComponent extends GenericAccountBlocklistComponent {
|
||||
export class InstanceAccountBlocklistComponent {
|
||||
mode = BlocklistComponentType.Instance
|
||||
|
||||
getIdentifier () {
|
||||
return 'InstanceAccountBlocklistComponent'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,14 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { Component } from '@angular/core'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { BlocklistComponentType } from '@app/shared/shared-moderation/blocklist.service'
|
||||
import { GenericServerBlocklistComponent } from '@app/shared/shared-moderation/server-blocklist.component'
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SharedModule } from 'primeng/api'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-colspan.directive'
|
||||
import { BatchDomainsModalComponent } from '../../../shared/shared-moderation/batch-domains-modal.component'
|
||||
import { GenericServerBlocklistComponent } from '../../../shared/shared-moderation/generic-server-blocklist.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-server-blocklist',
|
||||
styleUrls: [ '../../../shared/shared-moderation/server-blocklist.component.scss' ],
|
||||
templateUrl: '../../../shared/shared-moderation/server-blocklist.component.html',
|
||||
template: `<my-generic-server-blocklist [mode]="mode" key="InstanceServerBlocklistComponent" />`,
|
||||
imports: [
|
||||
GlobalIconComponent,
|
||||
TableModule,
|
||||
SharedModule,
|
||||
AdvancedInputFilterComponent,
|
||||
NgbTooltip,
|
||||
AutoColspanDirective,
|
||||
NgIf,
|
||||
BatchDomainsModalComponent,
|
||||
PTDatePipe
|
||||
GenericServerBlocklistComponent
|
||||
]
|
||||
})
|
||||
export class InstanceServerBlocklistComponent extends GenericServerBlocklistComponent {
|
||||
export class InstanceServerBlocklistComponent {
|
||||
mode = BlocklistComponentType.Instance
|
||||
|
||||
getIdentifier () {
|
||||
return 'InstanceServerBlocklistComponent'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,65 +1,23 @@
|
|||
<p-table
|
||||
[value]="registrations" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
|
||||
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||
[(selection)]="selectedRows" [showCurrentPageReport]="true" [currentPageReportTemplate]="getPaginationTemplate()"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
<my-table
|
||||
#table
|
||||
key="RegistrationListComponent"
|
||||
[defaultColumns]="columns"
|
||||
i18n-paginatorText
|
||||
paginatorText="registrations per page"
|
||||
[dataLoader]="dataLoader"
|
||||
columnConfig="true"
|
||||
[bulkActions]="bulkActions"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="left-buttons">
|
||||
<my-action-dropdown
|
||||
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="primary"
|
||||
[actions]="bulkActions" [entry]="selectedRows"
|
||||
>
|
||||
</my-action-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #totalTitle let-totalRecords>
|
||||
<ng-container i18n>{ totalRecords, plural, =0 {No registration} =1 {1 registration} other {{{ totalRecords | myNumberFormatter }} registrations}}</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr> <!-- header -->
|
||||
<th scope="col" style="width: 40px">
|
||||
<p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
<th scope="col" style="width: 40px;">
|
||||
<span i18n class="visually-hidden">More information</span>
|
||||
</th>
|
||||
<th scope="col" style="width: 150px;">
|
||||
<span i18n class="visually-hidden">Actions</span>
|
||||
</th>
|
||||
<th scope="col" i18n>Account</th>
|
||||
<th scope="col" i18n>Email</th>
|
||||
<th scope="col" i18n>Channel</th>
|
||||
<th scope="col" i18n>Registration reason</th>
|
||||
<th scope="col" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||
<th scope="col" i18n>Moderation response</th>
|
||||
<th scope="col" style="width: 150px;" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="createdAt">Requested on <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
</tr>
|
||||
<ng-template #captionRight>
|
||||
<my-advanced-input-filter (search)="table.onSearch($event)"></my-advanced-input-filter>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-expanded="expanded" let-registration>
|
||||
<tr [pSelectableRow]="registration">
|
||||
<td class="checkbox-cell">
|
||||
<p-tableCheckbox [value]="registration" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
|
||||
</td>
|
||||
|
||||
<td class="expand-cell">
|
||||
<my-table-expander-icon [pRowToggler]="registration" [expanded]="expanded"></my-table-expander-icon>
|
||||
</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown
|
||||
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
|
||||
i18n-label label="Actions" [actions]="registrationActions" [entry]="registration" buttonSize="small"
|
||||
></my-action-dropdown>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<ng-template #tableCells let-registration>
|
||||
<td *ngIf="table.isColumnDisplayed('account')">
|
||||
<div class="chip two-lines">
|
||||
<div>
|
||||
<span>{{ registration.username }}</span>
|
||||
|
@ -68,11 +26,11 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td *ngIf="table.isColumnDisplayed('email')">
|
||||
<my-user-email-info [entry]="registration" [showEmailVerifyInformation]="requiresEmailVerification"></my-user-email-info>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td *ngIf="table.isColumnDisplayed('channel')">
|
||||
<div class="chip two-lines">
|
||||
<div>
|
||||
<span>{{ registration.channelHandle }}</span>
|
||||
|
@ -81,32 +39,37 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td container="body" placement="left auto" [ngbTooltip]="registration.registrationReason">
|
||||
<td *ngIf="table.isColumnDisplayed('registrationReason')" class="max-width-300px ellipsis" container="body" placement="left auto" [ngbTooltip]="registration.registrationReason">
|
||||
{{ registration.registrationReason }}
|
||||
</td>
|
||||
|
||||
<td class="c-hand abuse-states" [pRowToggler]="registration">
|
||||
|
||||
<div *ngIf="isRegistrationAccepted(registration)" [title]="registration.state.label" class="pt-badge badge-success">
|
||||
<td *ngIf="table.isColumnDisplayed('state')" class="abuse-states">
|
||||
@if (isRegistrationAccepted(registration)) {
|
||||
<div [title]="registration.state.label" class="pt-badge badge-success">
|
||||
<my-global-icon iconName="tick"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRegistrationRejected(registration)" [title]="registration.state.label" class="pt-badge badge-danger">
|
||||
} @else if (isRegistrationRejected(registration)) {
|
||||
<div [title]="registration.state.label" class="pt-badge badge-danger">
|
||||
<my-global-icon iconName="cross"></my-global-icon>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
|
||||
<td container="body" placement="left auto" [ngbTooltip]="registration.moderationResponse">
|
||||
<td *ngIf="table.isColumnDisplayed('moderationResponse')" class="max-width-300px ellipsis" container="body" placement="left auto" [ngbTooltip]="registration.moderationResponse">
|
||||
{{ registration.moderationResponse }}
|
||||
</td>
|
||||
|
||||
<td class="c-hand" [pRowToggler]="registration">{{ registration.createdAt | ptDate: 'short' }}</td>
|
||||
</tr>
|
||||
<td *ngIf="table.isColumnDisplayed('createdAt')">{{ registration.createdAt | ptDate: 'short' }}</td>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="rowexpansion" let-registration>
|
||||
<tr>
|
||||
<td myAutoColspan>
|
||||
<ng-template #actionCell let-registration>
|
||||
<my-action-dropdown
|
||||
placement="bottom-right top-right left auto" container="body"
|
||||
i18n-label label="Actions" [actions]="registrationActions" [entry]="registration"
|
||||
></my-action-dropdown>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #expandedRow let-registration>
|
||||
<div class="moderation-expanded">
|
||||
<div class="left">
|
||||
<div class="d-flex">
|
||||
|
@ -120,20 +83,15 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td myAutoColspan>
|
||||
<div class="no-results">
|
||||
<ng-container *ngIf="search" i18n>No registrations found matching current filters.</ng-container>
|
||||
<ng-container *ngIf="!search" i18n>No registrations found.</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #noResults let-search>
|
||||
@if (search) {
|
||||
<ng-container i18n>No registrations found matching current filters.</ng-container>
|
||||
} @else {
|
||||
<ng-container i18n>No registrations found.</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</my-table>
|
||||
|
||||
<my-process-registration-modal #processRegistrationModal (registrationProcessed)="onRegistrationProcessed()"></my-process-registration-modal>
|
||||
|
|
|
@ -1,43 +1,42 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
|
||||
import { ConfirmService, MarkdownService, Notifier, ServerService } from '@app/core'
|
||||
import { formatICU } from '@app/helpers'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { UserRegistration, UserRegistrationState } from '@peertube/peertube-models'
|
||||
import { SharedModule, SortMeta } from 'primeng/api'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { ResultList, UserRegistration as UserRegistrationServer, UserRegistrationState } from '@peertube/peertube-models'
|
||||
import { switchMap } from 'rxjs'
|
||||
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { ActionDropdownComponent, DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
||||
import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-colspan.directive'
|
||||
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
|
||||
import { NumberFormatterPipe } from '../../../shared/shared-main/common/number-formatter.pipe'
|
||||
import { DataLoaderOptions, TableColumnInfo, TableComponent } from '../../../shared/shared-tables/table.component'
|
||||
import { UserEmailInfoComponent } from '../../shared/user-email-info.component'
|
||||
import { AdminRegistrationService } from './admin-registration.service'
|
||||
import { ProcessRegistrationModalComponent } from './process-registration-modal.component'
|
||||
|
||||
type UserRegistration = UserRegistrationServer & { registrationReasonHTML?: string, moderationResponseHTML?: string }
|
||||
type ColumnName = 'account' | 'email' | 'channel' | 'registrationReason' | 'state' | 'moderationResponse' | 'createdAt'
|
||||
|
||||
@Component({
|
||||
selector: 'my-registration-list',
|
||||
templateUrl: './registration-list.component.html',
|
||||
styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './registration-list.component.scss' ],
|
||||
imports: [
|
||||
GlobalIconComponent,
|
||||
TableModule,
|
||||
SharedModule,
|
||||
NgIf,
|
||||
CommonModule,
|
||||
ActionDropdownComponent,
|
||||
AdvancedInputFilterComponent,
|
||||
NgbTooltip,
|
||||
TableExpanderIconComponent,
|
||||
NgClass,
|
||||
UserEmailInfoComponent,
|
||||
AutoColspanDirective,
|
||||
ProcessRegistrationModalComponent,
|
||||
PTDatePipe
|
||||
PTDatePipe,
|
||||
NumberFormatterPipe,
|
||||
TableComponent
|
||||
]
|
||||
})
|
||||
export class RegistrationListComponent extends RestTable<UserRegistration> implements OnInit {
|
||||
export class RegistrationListComponent implements OnInit {
|
||||
protected route = inject(ActivatedRoute)
|
||||
protected router = inject(Router)
|
||||
private server = inject(ServerService)
|
||||
|
@ -47,11 +46,7 @@ export class RegistrationListComponent extends RestTable<UserRegistration> imple
|
|||
private adminRegistrationService = inject(AdminRegistrationService)
|
||||
|
||||
readonly processRegistrationModal = viewChild<ProcessRegistrationModalComponent>('processRegistrationModal')
|
||||
|
||||
registrations: (UserRegistration & { registrationReasonHTML?: string, moderationResponseHTML?: string })[] = []
|
||||
totalRecords = 0
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
readonly table = viewChild<TableComponent<UserRegistration, ColumnName>>('table')
|
||||
|
||||
registrationActions: DropdownAction<UserRegistration>[][] = []
|
||||
bulkActions: DropdownAction<UserRegistration[]>[] = []
|
||||
|
@ -60,8 +55,20 @@ export class RegistrationListComponent extends RestTable<UserRegistration> imple
|
|||
|
||||
requiresEmailVerification: boolean
|
||||
|
||||
columns: TableColumnInfo<ColumnName>[] = [
|
||||
{ id: 'account', label: $localize`Account`, sortable: false },
|
||||
{ id: 'email', label: $localize`Email`, sortable: false },
|
||||
{ id: 'channel', label: $localize`Channel`, sortable: false },
|
||||
{ id: 'registrationReason', label: $localize`Registration reason`, sortable: false },
|
||||
{ id: 'state', label: $localize`State`, sortable: true },
|
||||
{ id: 'moderationResponse', label: $localize`Moderation response`, sortable: false },
|
||||
{ id: 'createdAt', label: $localize`Requested on`, sortable: true }
|
||||
]
|
||||
|
||||
dataLoader: typeof this._dataLoader
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.dataLoader = this._dataLoader.bind(this)
|
||||
|
||||
this.registrationActions = [
|
||||
[
|
||||
|
@ -92,18 +99,12 @@ export class RegistrationListComponent extends RestTable<UserRegistration> imple
|
|||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.initialize()
|
||||
|
||||
this.server.getConfig()
|
||||
.subscribe(config => {
|
||||
this.requiresEmailVerification = config.signup.requiresEmailVerification
|
||||
})
|
||||
}
|
||||
|
||||
getIdentifier () {
|
||||
return 'RegistrationListComponent'
|
||||
}
|
||||
|
||||
isRegistrationAccepted (registration: UserRegistration) {
|
||||
return registration.state.id === UserRegistrationState.ACCEPTED
|
||||
}
|
||||
|
@ -113,27 +114,21 @@ export class RegistrationListComponent extends RestTable<UserRegistration> imple
|
|||
}
|
||||
|
||||
onRegistrationProcessed () {
|
||||
this.reloadData()
|
||||
this.table().reloadData({ field: 'createdAt', order: -1 })
|
||||
}
|
||||
|
||||
protected reloadDataInternal () {
|
||||
this.adminRegistrationService.listRegistrations({
|
||||
pagination: this.pagination,
|
||||
sort: this.sort,
|
||||
search: this.search
|
||||
}).subscribe({
|
||||
next: async resultList => {
|
||||
this.totalRecords = resultList.total
|
||||
this.registrations = resultList.data
|
||||
|
||||
for (const registration of this.registrations) {
|
||||
private _dataLoader (options: DataLoaderOptions) {
|
||||
return this.adminRegistrationService.listRegistrations(options)
|
||||
.pipe(
|
||||
switchMap(async (resultList: ResultList<UserRegistration>) => {
|
||||
for (const registration of resultList.data) {
|
||||
registration.registrationReasonHTML = await this.toHtml(registration.registrationReason)
|
||||
registration.moderationResponseHTML = await this.toHtml(registration.moderationResponse)
|
||||
}
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
return resultList
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private openRegistrationRequestProcessModal (registration: UserRegistration, mode: 'accept' | 'reject') {
|
||||
|
@ -160,7 +155,7 @@ export class RegistrationListComponent extends RestTable<UserRegistration> imple
|
|||
)
|
||||
|
||||
this.notifier.success(message)
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
|
|
|
@ -1,50 +1,21 @@
|
|||
<p-table
|
||||
[value]="blocklist" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
|
||||
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||
[showCurrentPageReport]="true" [currentPageReportTemplate]="getPaginationTemplate()"
|
||||
[expandedRowKeys]="expandedRows"
|
||||
<my-table
|
||||
#table
|
||||
key="VideoBlockListComponent"
|
||||
[defaultColumns]="columns"
|
||||
i18n-paginatorText
|
||||
paginatorText="videos per page"
|
||||
[dataLoader]="dataLoader"
|
||||
columnConfig="false"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="ms-auto">
|
||||
<my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #totalTitle let-totalRecords>
|
||||
<ng-container i18n>{ totalRecords, plural, =0 {No video} =1 {1 video} other {{{ totalRecords | myNumberFormatter }} videos}}</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px;">
|
||||
<span i18n class="visually-hidden">More information</span>
|
||||
</th>
|
||||
<th scope="col" style="width: 150px;">
|
||||
<span i18n class="visually-hidden">Actions</span>
|
||||
</th>
|
||||
<th scope="col" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th scope="col" style="width: 100px;" i18n>Sensitive</th>
|
||||
<th scope="col" style="width: 120px;" i18n>Unfederated</th>
|
||||
<th scope="col" style="width: 150px;" i18n [ngbTooltip]="sortTooltip" container="body" pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
</tr>
|
||||
<ng-template #captionRight>
|
||||
<my-advanced-input-filter [filters]="inputFilters" (search)="table.onSearch($event)"></my-advanced-input-filter>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-videoBlock let-expanded="expanded">
|
||||
<tr>
|
||||
<td *ngIf="!videoBlock.reason">
|
||||
<span i18n class="visually-hidden">No more information is available for this row</span>
|
||||
</td>
|
||||
|
||||
<td class="expand-cell" *ngIf="videoBlock.reason" >
|
||||
<my-table-expander-icon [pRowToggler]="videoBlock" [expanded]="expanded"></my-table-expander-icon>
|
||||
</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown
|
||||
[ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body"
|
||||
i18n-label label="Actions" [actions]="videoBlocklistActions" [entry]="videoBlock" buttonSize="small"
|
||||
></my-action-dropdown>
|
||||
</td>
|
||||
|
||||
<ng-template #tableCells let-videoBlock>
|
||||
<td>
|
||||
<my-video-cell [video]="videoBlock.video" size="small">
|
||||
<div>
|
||||
|
@ -66,14 +37,17 @@
|
|||
<td>
|
||||
{{ videoBlock.createdAt | ptDate: 'short' }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="rowexpansion" let-videoBlock>
|
||||
<tr>
|
||||
<td class="expand-cell" myAutoColspan>
|
||||
<div class="d-flex moderation-expanded">
|
||||
<ng-template #actionCell let-videoBlock>
|
||||
<my-action-dropdown
|
||||
placement="bottom-right auto" container="body"
|
||||
i18n-label label="Actions" [actions]="videoBlocklistActions" [entry]="videoBlock"
|
||||
></my-action-dropdown>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #expandedRow let-videoBlock>
|
||||
<div *ngIf="videoBlock" class="d-flex moderation-expanded">
|
||||
<div class="left">
|
||||
<span class="moderation-expanded-label" i18n>Block reason:</span>
|
||||
<span class="moderation-expanded-text" [innerHTML]="videoBlock.reasonHtml"></span>
|
||||
|
@ -82,21 +56,15 @@
|
|||
<div class="right">
|
||||
<my-embed [video]="videoBlock.video"></my-embed>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td myAutoColspan>
|
||||
<div class="no-results">
|
||||
<ng-container *ngIf="search" i18n>No blocked video found matching current filters.</ng-container>
|
||||
<ng-container *ngIf="!search" i18n>No blocked video found.</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template #noResults let-search>
|
||||
@if (search) {
|
||||
<ng-container i18n>No blocked video found matching current filters.</ng-container>
|
||||
} @else {
|
||||
<ng-container i18n>No blocked video found.</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</my-table>
|
||||
|
||||
|
|
|
@ -1,49 +1,41 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { ConfirmService, MarkdownService, Notifier, ServerService } from '@app/core'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { buildVideoEmbedLink, decorateVideoLink } from '@peertube/peertube-core-utils'
|
||||
import { VideoBlacklist, VideoBlacklistType, VideoBlacklistType_Type } from '@peertube/peertube-models'
|
||||
import { ResultList, VideoBlacklist as VideoBlacklistServer, VideoBlacklistType, VideoBlacklistType_Type } from '@peertube/peertube-models'
|
||||
import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
|
||||
import { SharedModule, SortMeta } from 'primeng/api'
|
||||
import { TableModule } from 'primeng/table'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component'
|
||||
import { ActionDropdownComponent, DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component'
|
||||
import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-colspan.directive'
|
||||
import { NumberFormatterPipe } from '../../../shared/shared-main/common/number-formatter.pipe'
|
||||
import { EmbedComponent } from '../../../shared/shared-main/video/embed.component'
|
||||
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
|
||||
import { DataLoaderOptions, TableColumnInfo, TableComponent } from '../../../shared/shared-tables/table.component'
|
||||
import { VideoCellComponent } from '../../../shared/shared-tables/video-cell.component'
|
||||
import { VideoNSFWBadgeComponent } from '../../../shared/shared-video/video-nsfw-badge.component'
|
||||
|
||||
type VideoBlacklist = VideoBlacklistServer & { reasonHtml?: string }
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-block-list',
|
||||
templateUrl: './video-block-list.component.html',
|
||||
styleUrls: [ '../../../shared/shared-moderation/moderation.scss' ],
|
||||
imports: [
|
||||
TableModule,
|
||||
SharedModule,
|
||||
CommonModule,
|
||||
AdvancedInputFilterComponent,
|
||||
NgbTooltip,
|
||||
NgIf,
|
||||
TableExpanderIconComponent,
|
||||
ActionDropdownComponent,
|
||||
NgClass,
|
||||
VideoCellComponent,
|
||||
AutoColspanDirective,
|
||||
EmbedComponent,
|
||||
PTDatePipe,
|
||||
VideoNSFWBadgeComponent
|
||||
VideoNSFWBadgeComponent,
|
||||
TableComponent,
|
||||
NumberFormatterPipe
|
||||
]
|
||||
})
|
||||
export class VideoBlockListComponent extends RestTable implements OnInit {
|
||||
protected route = inject(ActivatedRoute)
|
||||
protected router = inject(Router)
|
||||
export class VideoBlockListComponent implements OnInit {
|
||||
private notifier = inject(Notifier)
|
||||
private serverService = inject(ServerService)
|
||||
private confirmService = inject(ConfirmService)
|
||||
|
@ -51,10 +43,8 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
private markdownRenderer = inject(MarkdownService)
|
||||
private videoService = inject(VideoService)
|
||||
|
||||
blocklist: (VideoBlacklist & { reasonHtml?: string })[] = []
|
||||
totalRecords = 0
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
readonly table = viewChild<TableComponent<VideoBlacklist>>('table')
|
||||
|
||||
blocklistTypeFilter: VideoBlacklistType_Type
|
||||
|
||||
videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = []
|
||||
|
@ -75,8 +65,17 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
]
|
||||
|
||||
columns: TableColumnInfo<string>[] = [
|
||||
{ id: 'name', label: $localize`Video`, sortable: true },
|
||||
{ id: 'sensitive', label: $localize`Sensitive`, sortable: false },
|
||||
{ id: 'unfederated', label: $localize`Unfederated`, sortable: false },
|
||||
{ id: 'createdAt', label: $localize`Date`, sortable: true }
|
||||
]
|
||||
|
||||
dataLoader: typeof this._dataLoader
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.dataLoader = this._dataLoader.bind(this)
|
||||
|
||||
this.videoBlocklistActions = [
|
||||
[
|
||||
|
@ -93,7 +92,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
).subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Video ${videoBlock.video.name} switched to manual block.`)
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
|
@ -125,7 +124,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Video deleted.`)
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
|
@ -143,12 +142,6 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
if (serverConfig.autoBlacklist.videos.ofUsers.enabled) {
|
||||
this.blocklistTypeFilter = VideoBlacklistType.MANUAL
|
||||
}
|
||||
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
getIdentifier () {
|
||||
return 'VideoBlockListComponent'
|
||||
}
|
||||
|
||||
toHtml (text: string) {
|
||||
|
@ -165,7 +158,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Video ${entry.video.name} unblocked.`)
|
||||
this.reloadData()
|
||||
this.table().loadData()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
|
@ -185,25 +178,16 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
|
|||
})
|
||||
}
|
||||
|
||||
protected reloadDataInternal () {
|
||||
this.videoBlocklistService.listBlocks({
|
||||
pagination: this.pagination,
|
||||
sort: this.sort,
|
||||
search: this.search
|
||||
}).subscribe({
|
||||
next: async resultList => {
|
||||
this.totalRecords = resultList.total
|
||||
|
||||
this.blocklist = resultList.data
|
||||
|
||||
for (const element of this.blocklist) {
|
||||
Object.assign(element, {
|
||||
reasonHtml: await this.toHtml(element.reason)
|
||||
})
|
||||
private _dataLoader (options: DataLoaderOptions) {
|
||||
return this.videoBlocklistService.listBlocks(options)
|
||||
.pipe(
|
||||
switchMap(async (resultList: ResultList<VideoBlacklist>) => {
|
||||
for (const element of resultList.data) {
|
||||
element.reasonHtml = await this.toHtml(element.reason)
|
||||
}
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
return resultList
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<em i18n>This view also shows comments from muted accounts.</em>
|
||||
|
||||
<my-video-comment-list-admin-owner mode="admin"></my-video-comment-list-admin-owner>
|
||||
<my-video-comment-list-admin-owner mode="admin" key="AdminVideoCommentList"></my-video-comment-list-admin-owner>
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common'
|
||||
import { CommonModule, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router, RouterLink } from '@angular/router'
|
||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
||||
import { AuthService, Notifier, ScreenService, ServerService } from '@app/core'
|
||||
import {
|
||||
USER_CHANNEL_NAME_VALIDATOR,
|
||||
|
@ -14,15 +13,16 @@ import {
|
|||
USER_VIDEO_QUOTA_DAILY_VALIDATOR,
|
||||
USER_VIDEO_QUOTA_VALIDATOR
|
||||
} from '@app/shared/form-validators/user-validators'
|
||||
import { AdminConfigService } from '@app/shared/shared-admin/admin-config.service'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { AccountTokenSessionsComponent } from '@app/shared/shared-users/account-token-sessions.component'
|
||||
import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
|
||||
import { UserCreate, UserRole } from '@peertube/peertube-models'
|
||||
import { ActorAvatarEditComponent } from '../../../../shared/shared-actor-image-edit/actor-avatar-edit.component'
|
||||
import { InputTextComponent } from '../../../../shared/shared-forms/input-text.component'
|
||||
import { PeertubeCheckboxComponent } from '../../../../shared/shared-forms/peertube-checkbox.component'
|
||||
import { SelectCustomValueComponent } from '../../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { HelpComponent } from '../../../../shared/shared-main/buttons/help.component'
|
||||
import { BytesPipe } from '../../../../shared/shared-main/common/bytes.pipe'
|
||||
import { UserRealQuotaInfoComponent } from '../../../shared/user-real-quota-info.component'
|
||||
import { UserEdit } from './user-edit'
|
||||
|
@ -34,27 +34,25 @@ import { UserPasswordComponent } from './user-password.component'
|
|||
styleUrls: [ './user-edit.component.scss' ],
|
||||
imports: [
|
||||
RouterLink,
|
||||
NgIf,
|
||||
CommonModule,
|
||||
NgTemplateOutlet,
|
||||
ActorAvatarEditComponent,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgClass,
|
||||
HelpComponent,
|
||||
InputTextComponent,
|
||||
NgFor,
|
||||
SelectCustomValueComponent,
|
||||
UserRealQuotaInfoComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
UserPasswordComponent,
|
||||
BytesPipe,
|
||||
AccountTokenSessionsComponent,
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
export class UserCreateComponent extends UserEdit implements OnInit {
|
||||
protected serverService = inject(ServerService)
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
protected configService = inject(ConfigService)
|
||||
protected configService = inject(AdminConfigService)
|
||||
protected screenService = inject(ScreenService)
|
||||
protected auth = inject(AuthService)
|
||||
private router = inject(Router)
|
||||
|
|
|
@ -123,11 +123,11 @@
|
|||
<div class="form-group" *ngIf="isCreation()">
|
||||
<label i18n for="password">Password</label>
|
||||
|
||||
<my-help *ngIf="isPasswordOptional()">
|
||||
<ng-container i18n>
|
||||
@if (isPasswordOptional()) {
|
||||
<div class="form-group-description" i18n>
|
||||
If you leave the password empty, an email will be sent to the user.
|
||||
</ng-container>
|
||||
</my-help>
|
||||
</div>
|
||||
}
|
||||
|
||||
<my-input-text formControlName="password" inputId="password" [formError]="formErrors['password']" autocomplete="new-password"></my-input-text>
|
||||
</div>
|
||||
|
@ -215,16 +215,27 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="displayDangerZone()" class="pt-two-cols mt-5"> <!-- danger zone grid -->
|
||||
@if (displayTokenSessions()) {
|
||||
<div class="pt-two-cols mt-5">
|
||||
<div class="title-col">
|
||||
<div class="anchor" id="danger"></div> <!-- danger zone anchor -->
|
||||
<div class="anchor" id="token-sessions"></div>
|
||||
<h2 i18n>TOKEN SESSIONS</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
<my-account-token-sessions [user]="user"></my-account-token-sessions>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (displayPasswordZone()) {
|
||||
<div class="pt-two-cols mt-5">
|
||||
<div class="title-col">
|
||||
<div class="anchor" id="danger"></div>
|
||||
<h2 i18n class="pt-title-danger">DANGER ZONE</h2>
|
||||
</div>
|
||||
|
||||
<div class="content-col">
|
||||
|
||||
<div class="danger-zone">
|
||||
<div class="form-group">
|
||||
<div class="mb-1 fw-bold" i18n>Send a link to reset the password by email to the user</div>
|
||||
<button class="peertube-button danger-button" (click)="resetPassword()" i18n>Ask for new password</button>
|
||||
|
@ -240,6 +251,5 @@
|
|||
<button class="peertube-button danger-button" (click)="disableTwoFactorAuth()" i18n>Disable two factor authentication</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue