Merge branch 'develop' into shorter-URLs-channels-accounts
This commit is contained in:
commit
8f608a4cb2
|
@ -88,6 +88,7 @@
|
|||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-extraneous-class": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
// bugged but useful
|
||||
"@typescript-eslint/restrict-plus-operands": "off"
|
||||
},
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💬 IRC
|
||||
url: https://kiwiirc.com/client/irc.freenode.net/#peertube
|
||||
about: Chat with us via IRC for quick Q/A here
|
||||
- name: 💬 Matrix
|
||||
url: https://matrix.to/#/#peertube:matrix.org
|
||||
about: Chat with us via Matrix for quick Q/A here
|
||||
- name: 💬 IRC
|
||||
url: https://kiwiirc.com/client/irc.freenode.net/#peertube
|
||||
about: Chat with us via IRC for quick Q/A here
|
||||
- name: 🤷💻🤦 Forum
|
||||
url: https://framacolibri.org/c/peertube
|
||||
about: You can ask and answer other questions here
|
||||
|
|
|
@ -45,11 +45,6 @@ jobs:
|
|||
branch-base: develop
|
||||
bundlewatch-github-token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
|
||||
|
||||
- name: PeerTube client stats
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
node dist/scripts/client-build-stats.js > client-build-stats.json
|
||||
|
||||
- name: PeerTube code stats
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
|
@ -57,11 +52,24 @@ jobs:
|
|||
unzip "scc-3.0.0-x86_64-unknown-linux.zip"
|
||||
./scc --format=json --exclude-dir .git,node_modules,client/node_modules,client/dist,dist,yarn.lock,client/yarn.lock,client/src/locale,test1,test2,test3,client/src/assets/images,config,storage,server/tests/fixtures,support/openapi,.idea,.vscode,docker-volume,ffmpeg-3,ffmpeg-4 > ./scc.json
|
||||
|
||||
- name: PeerTube client stats
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
node dist/scripts/client-build-stats.js > client-build-stats.json
|
||||
|
||||
- name: PeerTube client lighthouse report
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
sudo apt-get install chromium-browser
|
||||
sudo npm install -g lighthouse
|
||||
lighthouse --chrome-flags="--headless" https://peertube2.cpy.re --output=json --output-path=./lighthouse.json
|
||||
|
||||
- name: Display stats
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
cat client-build-stats.json
|
||||
cat scc.json
|
||||
cat lighthouse.json
|
||||
|
||||
- name: Upload stats
|
||||
if: github.event_name != 'pull_request'
|
||||
|
@ -87,5 +95,5 @@ jobs:
|
|||
|
||||
if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
|
||||
echo "Uploading files"
|
||||
scp client-build-stats.json scc.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats;
|
||||
scp lighthouse.json client-build-stats.json scc.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats;
|
||||
fi
|
||||
|
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
env:
|
||||
PGUSER: peertube
|
||||
PGHOST: localhost
|
||||
NODE_PENDING_JOB_WAIT: 500
|
||||
NODE_PENDING_JOB_WAIT: 250
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -15,7 +15,7 @@ yarn-error.log
|
|||
/server/tests/fixtures/video_59fps.mp4
|
||||
|
||||
# Production
|
||||
/storage/
|
||||
/storage
|
||||
/config/production.yaml
|
||||
/config/local*
|
||||
/ffmpeg/
|
||||
|
|
145
CHANGELOG.md
145
CHANGELOG.md
|
@ -1,5 +1,150 @@
|
|||
# Changelog
|
||||
|
||||
## v3.2.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* **Important:** You must update your nginx configuration to add the `upload-resumable` endpoint: https://github.com/Chocobozzz/PeerTube/blob/develop/support/nginx/peertube#L81
|
||||
* **Important:** Due to a bug in ffmpeg, PeerTube is not compatible with ffmpeg 4.4. See https://github.com/Chocobozzz/PeerTube/issues/3990
|
||||
* **Important:** Drop NodeJS 10 support
|
||||
* PeerTube is not compatible with NodeJS 16 yet
|
||||
* By default, HLS transcoding is now enabled and webtorrent is disabled. We suggest you to reflect this change.
|
||||
See [the documentation](https://docs.joinpeertube.org/admin-configuration?id=webtorrent-transcoding-or-hls-transcoding) for more information
|
||||
* PeerTube client now displays bigger video thumbnails.
|
||||
To fix old thumbnails quality, run `regenerate-thumbnails` script after your PeerTube upgrade: https://docs.joinpeertube.org/maintain-tools?id=regenerate-thumbnailsjs
|
||||
|
||||
### Docker
|
||||
|
||||
* Support SSL database env parameter [#4114](https://github.com/Chocobozzz/PeerTube/pull/4114)
|
||||
|
||||
### Maintenance
|
||||
|
||||
* Support `X-Frame-Options` header, enabled by default in the configuration
|
||||
* Directly use `node` in [systemd template](https://github.com/Chocobozzz/PeerTube/blob/develop/support/systemd/peertube.service)
|
||||
* Check ffmpeg version at PeerTube startup
|
||||
* Add `upload-resumable` nginx endpoint: https://github.com/Chocobozzz/PeerTube/blob/develop/support/nginx/peertube#L81
|
||||
|
||||
### CLI tools
|
||||
|
||||
* Add `regenerate-thumbnails` script to regenerate thumbnails of local videos
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
* Theme:
|
||||
* `--submenuColor` becomes `--submenuBackgroundColor`
|
||||
* Support HTML placeholders for plugins. See [the documentation](https://docs.joinpeertube.org/contribute-plugins?id=html-placeholder-elements) for more information
|
||||
* `player-next` next to the PeerTube player
|
||||
* Support storing files for plugins in a dedicated directory. See [the documentation](https://docs.joinpeertube.org/contribute-plugins?id=storage) for more information
|
||||
* Transcoding:
|
||||
* Add `inputOptions` option support for transcoding profile [#3917](https://github.com/Chocobozzz/PeerTube/pull/3917)
|
||||
* Add `scaleFilter.name` option support for transcoding profile [#3917](https://github.com/Chocobozzz/PeerTube/pull/3917)
|
||||
* Plugin settings:
|
||||
* Add ability to register `html` and `select` setting
|
||||
* Add ability to hide a plugin setting depending on the form state
|
||||
* Plugin form fields (to add inputs to video form...):
|
||||
* Add ability to hide a plugin field depending on the form state using `.hidden` property
|
||||
* Add client helpers:
|
||||
* `getServerConfig()`
|
||||
* `getAuthHeader()`
|
||||
* Add server helpers:
|
||||
* `config.getServerConfig()`
|
||||
* `plugin.getBaseStaticRoute()`
|
||||
* `plugin.getBaseRouterRoute()`
|
||||
* `plugin.getDataDirectoryPath()`
|
||||
* `user.getAuthUser()`
|
||||
* Add client plugin hooks (https://docs.joinpeertube.org/api-plugins):
|
||||
* `action:modal.video-download.shown`
|
||||
* `action:video-upload.init`
|
||||
* `action:video-url-import.init`
|
||||
* `action:video-torrent-import.init`
|
||||
* `action:go-live.init`
|
||||
* `action:auth-user.logged-in` & `action:auth-user.logged-out`
|
||||
* `action:auth-user.information-loaded`
|
||||
* `action:admin-plugin-settings.init`
|
||||
* Add server plugin hooks (https://docs.joinpeertube.org/api-plugins):
|
||||
* `filter:api.download.video.allowed.result` & `filter:api.download.torrent.allowed.result` to forbid download
|
||||
* `filter:html.embed.video-playlist.allowed.result` & `filter:html.embed.video.allowed.result` to forbid embed
|
||||
* `filter:api.search.videos.local.list.params` & `filter:api.search.videos.local.list.result`
|
||||
* `filter:api.search.videos.index.list.params` & `filter:api.search.videos.index.list.result`
|
||||
* `filter:api.search.video-channels.local.list.params` & `filter:api.search.video-channels.local.list.result`
|
||||
* `filter:api.search.video-channels.index.list.params` & `filter:api.search.video-channels.index.list.result`
|
||||
|
||||
### Features
|
||||
|
||||
* :tada: More robust uploads using a resumable upload endpoint [#3933](https://github.com/Chocobozzz/PeerTube/pull/3933)
|
||||
* Accessibility/UI:
|
||||
* :tada: Redesign channel and account page
|
||||
* :tada: Increase video miniature size
|
||||
* :tada: Add channel banner support
|
||||
* Use a square avatar for channels and a round avatar for accounts
|
||||
* Use account initial as default account avatar [#4002](https://github.com/Chocobozzz/PeerTube/pull/4002)
|
||||
* Prefer channel display in video miniature
|
||||
* Add *support* button in channel page
|
||||
* Set direct download as default in video download modal [#3880](https://github.com/Chocobozzz/PeerTube/pull/3880)
|
||||
* Show less information in video download modal by default [#3890](https://github.com/Chocobozzz/PeerTube/pull/3890)
|
||||
* Autofocus admin plugin search input
|
||||
* Add `1.75` playback rate to player [#3888](https://github.com/Chocobozzz/PeerTube/pull/3888)
|
||||
* Add `title` attribute to embed code [#3901](https://github.com/Chocobozzz/PeerTube/pull/3901)
|
||||
* Don't pause player when opening a modal [#3909](https://github.com/Chocobozzz/PeerTube/pull/3909)
|
||||
* Add link below the player to open the video on origin instance [#3624](https://github.com/Chocobozzz/PeerTube/issues/3624)
|
||||
* Notify admins on new available PeerTube version
|
||||
* Notify admins on new available plugin version
|
||||
* Sort channels by last uploaded videos
|
||||
* Video player:
|
||||
* Add loop toggle to context menu [#3949](https://github.com/Chocobozzz/PeerTube/pull/3949)
|
||||
* Add icons to context menu [#3955](https://github.com/Chocobozzz/PeerTube/pull/3955)
|
||||
* Add a *Previous* button in playlist watch page [#3485](https://github.com/Chocobozzz/PeerTube/pull/3485)
|
||||
* Automatically close the settings menu when clicking outside the player
|
||||
* Add "stats for nerds" panel in context menu [#3958](https://github.com/Chocobozzz/PeerTube/pull/3958)
|
||||
* Add channel and playlist stats to stats endpoint [#3747](https://github.com/Chocobozzz/PeerTube/pull/3747)
|
||||
* Support `playlistPosition=last` and negative index (`playlistPosition=-2`) URL query parameters for playlists [#3974](https://github.com/Chocobozzz/PeerTube/pull/3974)
|
||||
* My videos:
|
||||
* Add ability to sort videos (publication date, most viewed...)
|
||||
* Add ability to only display live videos
|
||||
* Automatically resume videos for non logged-in users [#3885](https://github.com/Chocobozzz/PeerTube/pull/3885)
|
||||
* Admin plugins:
|
||||
* Show a modal when upgrading a plugin to a major version
|
||||
* Display a setting button after plugin installation
|
||||
* Add ability to search live videos
|
||||
* Use bigger thumbnails for feeds
|
||||
* Parse video description markdown for Opengraph/Twitter/HTML elements
|
||||
* Open the remote interaction modal when replying to a comment if we are logged-out
|
||||
* Handle `.srt` captions with broken durations
|
||||
* Performance:
|
||||
* Player now lazy loads video captions
|
||||
* Faster admin table filters
|
||||
* Optimize feed endpoint
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* More robust comments fetcher of remote video
|
||||
* Fix database ssl connection
|
||||
* Remove unnecessary black border above and below video in player [#3920](https://github.com/Chocobozzz/PeerTube/pull/3920)
|
||||
* Reduce tag input excessive padding [#3927](https://github.com/Chocobozzz/PeerTube/pull/3927)
|
||||
* Fix disappearing hamburger menu for narrow screens [#3929](https://github.com/Chocobozzz/PeerTube/pull/3929)
|
||||
* Fix Youtube subtitle import with some languages
|
||||
* Fix transcoding profile update in admin config
|
||||
* Fix outbox fetch with subtitled videos
|
||||
* Correctly unload a plugin on update/uninstall [#3940](https://github.com/Chocobozzz/PeerTube/pull/3940)
|
||||
* Ensure to install plugins that are supported by PeerTube
|
||||
* Fix welcome/warning modal displaying twice
|
||||
* Fix h265 video import using CLI
|
||||
* Fix context menu when watching a playlist
|
||||
* Fix transcoding job priority preventing video publication when there are many videos to transcode
|
||||
* Fix remote account/channel "joined at"
|
||||
* Fix CLI plugins list command options [#4055](https://github.com/Chocobozzz/PeerTube/pull/4055)
|
||||
* Fix HTTP player defaulting to audio resolution
|
||||
* Logger warning level is "warn"
|
||||
* Fix default boolean plugin setting [#4107](https://github.com/Chocobozzz/PeerTube/pull/4107)
|
||||
* Fix duplicate ffmpeg preset option for live
|
||||
* Avoid federation error when file has no torrent file
|
||||
* Fix local user auth select
|
||||
* Fix live ending banner display
|
||||
* Fix redundancy max size
|
||||
* Fix broken lives handling
|
||||
|
||||
|
||||
|
||||
## v3.1.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
|
32
CREDITS.md
32
CREDITS.md
|
@ -3,25 +3,27 @@
|
|||
* Chocobozzz
|
||||
* Rigel Kent
|
||||
* Filip Bengtsson
|
||||
* kimsible
|
||||
* josé m
|
||||
* kimsible
|
||||
* Simon Brosdetzko
|
||||
* Александр
|
||||
* Clemens Schielicke
|
||||
* Berto Te
|
||||
* Clemens Schielicke
|
||||
* Jeff Huang
|
||||
* kontrollanten
|
||||
* Phongpanot
|
||||
* Laurent Ettouati
|
||||
* Racida S
|
||||
* Kim
|
||||
* Phongpanot
|
||||
* Marcin Mikołajczak
|
||||
* Kim
|
||||
* Tirifto
|
||||
* Felix Ableitner
|
||||
* Vodoyo Kamal
|
||||
* Felix Ableitner
|
||||
* Gérald Niel
|
||||
* Zet
|
||||
* Duy
|
||||
* GunChleoc
|
||||
* Slimane Selyan AMIRI
|
||||
* Zet
|
||||
* x
|
||||
* Frank Sträter
|
||||
* Julien Maulny
|
||||
|
@ -29,12 +31,11 @@
|
|||
* Jorropo
|
||||
* Josh Morel
|
||||
* BO41
|
||||
* Slimane Selyan AMIRI
|
||||
* Francesc
|
||||
* mando laress
|
||||
* Balázs Meskó
|
||||
* Duy
|
||||
* Francesc
|
||||
* John Livingston
|
||||
* mando laress
|
||||
* Eivind Ødegård
|
||||
* Quentin PAGÈS
|
||||
* Besnik Bleta
|
||||
* Ihor Hordiichuk
|
||||
|
@ -53,7 +54,6 @@
|
|||
* Thomas Citharel
|
||||
* Agron Selimaj
|
||||
* Benjamin Bouvier
|
||||
* Eivind Ødegård
|
||||
* Joe Bill
|
||||
* Kemal Oktay Aktoğan
|
||||
* Luc Didry
|
||||
|
@ -66,6 +66,7 @@
|
|||
* David Libeau
|
||||
* Ewald Arnold
|
||||
* Florent F
|
||||
* Florian CUNY
|
||||
* Nassim Bounouas
|
||||
* NorbiPeti
|
||||
* Rafael Fontenelle
|
||||
|
@ -81,7 +82,6 @@
|
|||
* David Soh
|
||||
* Dimitri Gilbert
|
||||
* Florent Poinsaut
|
||||
* Florian CUNY
|
||||
* Frank Chang
|
||||
* Green-Star
|
||||
* Micah Elizabeth Scott
|
||||
|
@ -94,6 +94,7 @@
|
|||
* test2a
|
||||
* 路过是好事
|
||||
* Ajeje Brazorf
|
||||
* Andrey
|
||||
* Angristan
|
||||
* Ch
|
||||
* Chris Sakura 佐倉くりす on Youtube
|
||||
|
@ -103,6 +104,7 @@
|
|||
* Mildred
|
||||
* Okhin
|
||||
* Pierre-Alain TORET
|
||||
* Poslovitch
|
||||
* Serge Victor
|
||||
* Théo Le Calvar
|
||||
* Ugaitz
|
||||
|
@ -115,7 +117,6 @@
|
|||
* Ahsan Haris Ahmed
|
||||
* Alberto Teira
|
||||
* Aliaksandr Hrankin
|
||||
* Andrey
|
||||
* Andréas Livet
|
||||
* Andrés Maldonado
|
||||
* Arco
|
||||
|
@ -133,6 +134,7 @@
|
|||
* Kiro
|
||||
* LecygneNoir
|
||||
* Leopere
|
||||
* Loukas Stamellos
|
||||
* Lukas Winkler
|
||||
* Manuel Viens
|
||||
* Manuela Silva
|
||||
|
@ -250,6 +252,7 @@
|
|||
* Fabio Agreles Bezerra
|
||||
* Fernandez, ReK2
|
||||
* Florent
|
||||
* Gabriel Scherer
|
||||
* Glandos
|
||||
* Guillaume Pérution-Kihli
|
||||
* Gérald CHATAGNON
|
||||
|
@ -265,6 +268,7 @@
|
|||
* Jacob
|
||||
* Jacques Foucry
|
||||
* Jagannath Bhat
|
||||
* Jan Prunk
|
||||
* Janey Muñoz
|
||||
* Jarosław Maciejewski
|
||||
* Jeena
|
||||
|
@ -315,6 +319,7 @@
|
|||
* PhieF
|
||||
* Philip Durbin
|
||||
* Philipp Fischbeck
|
||||
* Philo van Kemenade
|
||||
* Pierre-Jean
|
||||
* Predatorix Phoenix
|
||||
* Quentin Dupont
|
||||
|
@ -361,6 +366,7 @@
|
|||
* bikepunk
|
||||
* bsky
|
||||
* ctlaltdefeat
|
||||
* decentral1se
|
||||
* dingycle
|
||||
* eduard pintilie
|
||||
* gillux
|
||||
|
|
25
README.md
25
README.md
|
@ -6,7 +6,7 @@
|
|||
|
||||
<p align=center>
|
||||
<strong><a href="https://joinpeertube.org">Website</a></strong>
|
||||
| <strong><a href="https://instances.joinpeertube.org">Join an instance</a></strong>
|
||||
| <strong><a href="https://joinpeertube.org/instances">Join an instance</a></strong>
|
||||
| <strong><a href="#package-create-your-own-instance">Create an instance</a></strong>
|
||||
| <strong><a href="#contact">Chat with us</a></strong>
|
||||
| <strong><a href="https://framasoft.org/en/#soutenir">Donate</a></strong>
|
||||
|
@ -67,23 +67,24 @@ Introduction
|
|||
|
||||
PeerTube is a free, decentralized and federated video platform developed as an alternative to other platforms that centralize our data and attention, such as YouTube, Dailymotion or Vimeo. :clapper:
|
||||
|
||||
But one organization hosting PeerTube alone may not have enough money to pay for bandwidth and video storage of its servers,
|
||||
all servers of PeerTube are interoperable as a federated network, and non-PeerTube servers can be part of the larger Vidiverse
|
||||
(federated video network) by talking our implementation of ActivityPub.
|
||||
Video load is reduced thanks to P2P in the web browser using <a href="https://github.com/webtorrent/webtorrent">WebTorrent</a> or <a href="https://github.com/novage/p2p-media-loader">p2p-media-loader</a>.
|
||||
|
||||
To learn more, see:
|
||||
To learn more:
|
||||
* This [two-minute video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) (hosted on PeerTube) explaining what PeerTube is and how it works
|
||||
* PeerTube's project homepage, [joinpeertube.org](https://joinpeertube.org)
|
||||
* Demonstration instances:
|
||||
* [peertube.cpy.re](https://peertube.cpy.re)
|
||||
* [peertube2.cpy.re](https://peertube2.cpy.re)
|
||||
* [peertube3.cpy.re](https://peertube3.cpy.re)
|
||||
* [peertube.cpy.re](https://peertube.cpy.re) (stable)
|
||||
* [peertube2.cpy.re](https://peertube2.cpy.re) (Nightly)
|
||||
* [peertube3.cpy.re](https://peertube3.cpy.re) (RC)
|
||||
* This [video](https://peertube.cpy.re/videos/watch/da2b08d4-a242-4170-b32a-4ec8cbdca701) demonstrating the communication between PeerTube and [Mastodon](https://github.com/tootsuite/mastodon) (a decentralized Twitter alternative)
|
||||
|
||||
:sparkles: Features
|
||||
----------------------------------------------------------------
|
||||
|
||||
<p align=center>
|
||||
<strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-viewers">All features for viewers</a></strong>
|
||||
| <strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-content-creators">All features for content creators</a></strong>
|
||||
| <strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-administrators">All features for administrators</a></strong>
|
||||
</p>
|
||||
|
||||
<img src="https://lutim.cpy.re/AHbctLjn.png" align="left" height="300px"/>
|
||||
<h3 align="left">Video streaming, even in live!</h3>
|
||||
<p align="left">
|
||||
|
@ -121,6 +122,8 @@ In addition to visitors using WebTorrent to share the load among them, instances
|
|||
Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisements that hurt visitors and <strike>incentivize</strike> alter creativity (more about that in our <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/FAQ.md">FAQ</a>).
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
:raised_hands: Contributing
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
@ -132,8 +135,8 @@ guide](https://github.com/Chocobozzz/PeerTube/blob/develop/.github/CONTRIBUTING.
|
|||
You can also join the cheerful bunch that makes our community:
|
||||
|
||||
* Chat<a name="contact"></a>:
|
||||
* IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)**
|
||||
* Matrix (bridged on IRC and [Discord](https://discord.gg/wj8DDUT)) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)**
|
||||
* IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)**
|
||||
* Forum:
|
||||
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
|
||||
|
||||
|
|
|
@ -24,6 +24,12 @@
|
|||
"rule-empty-line-before": null,
|
||||
"selector-max-id": null,
|
||||
"scss/at-function-pattern": null,
|
||||
"function-parentheses-space-inside": "never-single-line"
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"property-no-vendor-prefix": [
|
||||
true,
|
||||
{
|
||||
"ignoreProperties": [ "mask-image" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,13 +131,14 @@
|
|||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"aot": true,
|
||||
"localize": true,
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"baseHref": "/",
|
||||
"deployUrl": "client/",
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"src/sass/include"
|
||||
|
@ -151,39 +152,24 @@
|
|||
"src/sass/application.scss"
|
||||
],
|
||||
"allowedCommonJsDependencies": [
|
||||
"@angularclass/hmr",
|
||||
"debug",
|
||||
"mousetrap",
|
||||
"qrcode",
|
||||
"chart.js",
|
||||
"linkifyjs/html",
|
||||
"linkifyjs",
|
||||
"markdown-it",
|
||||
"htmlparser2",
|
||||
"markdown-it-emoji/light",
|
||||
"sanitize-html",
|
||||
"socket.io-client",
|
||||
"socket.io-parser",
|
||||
"@app/+about/about-peertube/about-peertube-contributors.component",
|
||||
"path",
|
||||
"video.js",
|
||||
"debug",
|
||||
"p2p-media-loader-hlsjs",
|
||||
"videojs-hotkeys/videojs.hotkeys",
|
||||
"p2p-media-loader-core",
|
||||
"qrcode",
|
||||
"webtorrent",
|
||||
"cache-chunk-store",
|
||||
"global/document",
|
||||
"videojs-vtt.js",
|
||||
"videojs-vtt.js",
|
||||
"@babel/runtime/helpers/possibleConstructorReturn",
|
||||
"@babel/runtime/helpers/inherits",
|
||||
"@babel/runtime/helpers/construct",
|
||||
"@videojs/xhr",
|
||||
"htmlparser2",
|
||||
"url",
|
||||
"parse-srcset",
|
||||
"video.js",
|
||||
"sha1",
|
||||
"postcss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
@ -191,7 +177,6 @@
|
|||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
|
@ -251,8 +236,6 @@
|
|||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"baseHref": "/",
|
||||
"deployUrl": "client/",
|
||||
"browserTarget": "PeerTube:build",
|
||||
"proxyConfig": "proxy.config.json"
|
||||
},
|
||||
|
|
|
@ -26,7 +26,12 @@ export class VideoUploadPage {
|
|||
await elem.sendKeys(fileToUpload)
|
||||
|
||||
// Wait for the upload to finish
|
||||
await browser.wait(browser.ExpectedConditions.elementToBeClickable(this.getSecondStepSubmitButton()))
|
||||
await browser.wait(async () => {
|
||||
const actionButton = this.getSecondStepSubmitButton().element(by.css('.action-button'))
|
||||
|
||||
const klass = await actionButton.getAttribute('class')
|
||||
return !klass.includes('disabled')
|
||||
})
|
||||
}
|
||||
|
||||
async validSecondUploadStep (videoName: string) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
@ -29,20 +29,20 @@
|
|||
"@types/mousetrap": "1.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.1102.2",
|
||||
"@angular/animations": "^11.1.1",
|
||||
"@angular/cdk": "^11.0.0",
|
||||
"@angular/cli": "^11.1.2",
|
||||
"@angular/common": "^11.1.1",
|
||||
"@angular/compiler": "^11.1.1",
|
||||
"@angular/compiler-cli": "^11.1.1",
|
||||
"@angular/core": "^11.1.1",
|
||||
"@angular/forms": "^11.1.1",
|
||||
"@angular/localize": "^11.1.1",
|
||||
"@angular/platform-browser": "^11.1.1",
|
||||
"@angular/platform-browser-dynamic": "^11.1.1",
|
||||
"@angular/router": "^11.1.1",
|
||||
"@angular/service-worker": "^11.1.1",
|
||||
"@angular-devkit/build-angular": "^12.0.0",
|
||||
"@angular/animations": "^12.0.0",
|
||||
"@angular/cdk": "^12.0.0",
|
||||
"@angular/cli": "^12.0.0",
|
||||
"@angular/common": "^12.0.0",
|
||||
"@angular/compiler": "^12.0.0",
|
||||
"@angular/compiler-cli": "^12.0.0",
|
||||
"@angular/core": "^12.0.0",
|
||||
"@angular/forms": "^12.0.0",
|
||||
"@angular/localize": "^12.0.0",
|
||||
"@angular/platform-browser": "^12.0.0",
|
||||
"@angular/platform-browser-dynamic": "^12.0.0",
|
||||
"@angular/router": "^12.0.0",
|
||||
"@angular/service-worker": "^12.0.0",
|
||||
"@neos21/bootstrap3-glyphicons": "^1.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^9.0.2",
|
||||
"@ng-select/ng-select": "^6.0.0",
|
||||
|
@ -51,7 +51,6 @@
|
|||
"@ngx-loading-bar/core": "^5.0.0",
|
||||
"@ngx-loading-bar/http-client": "^5.0.0",
|
||||
"@ngx-loading-bar/router": "^5.0.0",
|
||||
"@ngx-meta/core": "^9.0.0",
|
||||
"@types/chart.js": "^2.9.16",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/debug": "^4.1.5",
|
||||
|
@ -70,19 +69,19 @@
|
|||
"angular2-hotkeys": "^2.1.2",
|
||||
"angularx-qrcode": "11.0.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"buffer": "^6.0.2",
|
||||
"buffer": "^6.0.3",
|
||||
"cache-chunk-store": "^3.0.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"codelyzer": "^6.0.0",
|
||||
"core-js": "^3.1.4",
|
||||
"css-loader": "^5.0.1",
|
||||
"css-loader": "^5.2.6",
|
||||
"debug": "^4.3.1",
|
||||
"dexie": "^3.0.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"focus-visible": "^5.0.2",
|
||||
"hls.js": "^0.14.16",
|
||||
"html-loader": "^1.0.0",
|
||||
"html-webpack-plugin": "^4.0.3",
|
||||
"html-loader": "^2.1.2",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"jasmine-core": "~3.7.1",
|
||||
"jasmine-spec-reporter": "~7.0.0",
|
||||
|
@ -95,42 +94,42 @@
|
|||
"linkifyjs": "^2.1.5",
|
||||
"lodash-es": "^4.17.4",
|
||||
"markdown-it": "12.0.4",
|
||||
"mini-css-extract-plugin": "^1.3.1",
|
||||
"mini-css-extract-plugin": "^1.6.0",
|
||||
"ngx-uploadx": "^4.1.0",
|
||||
"p2p-media-loader-hlsjs": "^0.6.2",
|
||||
"path-browserify": "^1.0.0",
|
||||
"primeng": "^11.0.0-rc.1",
|
||||
"primeng": "^12.0.0-rc.1",
|
||||
"process": "^0.11.10",
|
||||
"protractor": "~7.0.0",
|
||||
"purify-css": "^1.2.5",
|
||||
"raw-loader": "^4.0.0",
|
||||
"rxjs": "^6.5.2",
|
||||
"sanitize-html": "^2.1.2",
|
||||
"sass": "^1.29.0",
|
||||
"sass-loader": "^10",
|
||||
"sass-resources-loader": "^2.0.0",
|
||||
"sass": "^1.34.0",
|
||||
"sass-loader": "^11.1.1",
|
||||
"sha.js": "^2.4.11",
|
||||
"socket.io-client": "^4.0.1",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.0.0",
|
||||
"stylelint": "^13.13.0",
|
||||
"stylelint-config-sass-guidelines": "^8.0.0",
|
||||
"terser-webpack-plugin": "^4",
|
||||
"ts-loader": "^8.0.14",
|
||||
"terser-webpack-plugin": "^5.1.2",
|
||||
"ts-loader": "^9.2.2",
|
||||
"tslib": "^2.0.0",
|
||||
"tslint": "~6.1.0",
|
||||
"tslint-angular": "^3.0.2",
|
||||
"tslint-config-standard": "^9.0.0",
|
||||
"typescript": "~4.1",
|
||||
"typescript": "~4.2.4",
|
||||
"video.js": "^7",
|
||||
"videojs-contextmenu-pt": "^5.4.1",
|
||||
"videojs-contrib-quality-levels": "^2.0.9",
|
||||
"videojs-dock": "^2.0.2",
|
||||
"videojs-hotkeys": "^0.2.27",
|
||||
"videostream": "~3.2.1",
|
||||
"webpack-bundle-analyzer": "^4.1.0",
|
||||
"webpack-cli": "^4.2.0",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"webtorrent": "^0.116.1",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"zone.js": "~0.11.3"
|
||||
"zone.js": "~0.11.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{{ follower}}
|
||||
</a>
|
||||
|
||||
<button i18n class="showMore" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button>
|
||||
<button i18n class="show-more" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6 col-md-12">
|
||||
|
|
|
@ -14,6 +14,6 @@ export class AboutPeertubeContributorsComponent implements OnInit {
|
|||
constructor (private markdownService: MarkdownService) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown)
|
||||
this.creditsHtml = await this.markdownService.unsafeMarkdownToHTML(this.markdown, true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { AboutComponent } from './about.component'
|
||||
import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
|
||||
import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
|
||||
import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
|
||||
import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
|
||||
import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver'
|
||||
import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
|
||||
import { AboutComponent } from './about.component'
|
||||
|
||||
const aboutRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AboutComponent,
|
||||
canActivateChild: [ MetaGuard ],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
}
|
||||
|
||||
a {
|
||||
@include peertube-word-wrap;
|
||||
|
||||
color: pvar(--mainForegroundColor);
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,13 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
loadMoreChannels () {
|
||||
this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination)
|
||||
const options = {
|
||||
account: this.account,
|
||||
componentPagination: this.channelPagination,
|
||||
sort: '-updatedAt'
|
||||
}
|
||||
|
||||
this.videoChannelService.listAccountVideoChannels(options)
|
||||
.pipe(
|
||||
tap(res => this.channelPagination.totalItems = res.total),
|
||||
switchMap(res => from(res.data)),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { AccountSearchComponent } from './account-search/account-search.component'
|
||||
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
|
||||
import { AccountVideosComponent } from './account-videos/account-videos.component'
|
||||
|
@ -14,7 +13,6 @@ const accountsRoutes: Routes = [
|
|||
{
|
||||
path: ':accountId',
|
||||
component: AccountsComponent,
|
||||
canActivateChild: [ MetaGuard ],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
|
|
@ -66,7 +66,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
distinctUntilChanged(),
|
||||
switchMap(accountId => this.accountService.getAccount(accountId)),
|
||||
tap(account => this.onAccount(account)),
|
||||
switchMap(account => this.videoChannelService.listAccountVideoChannels(account)),
|
||||
switchMap(account => this.videoChannelService.listAccountVideoChannels({ account })),
|
||||
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [
|
||||
HttpStatusCode.BAD_REQUEST_400,
|
||||
HttpStatusCode.NOT_FOUND_404
|
||||
|
|
|
@ -4,7 +4,6 @@ import { ConfigRoutes } from '@app/+admin/config'
|
|||
import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes'
|
||||
import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes'
|
||||
import { SystemRoutes } from '@app/+admin/system'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { AdminComponent } from './admin.component'
|
||||
import { FollowsRoutes } from './follows'
|
||||
import { UsersRoutes } from './users'
|
||||
|
@ -13,8 +12,6 @@ const adminRoutes: Routes = [
|
|||
{
|
||||
path: '',
|
||||
component: AdminComponent,
|
||||
canActivate: [ MetaGuard ],
|
||||
canActivateChild: [ MetaGuard ],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
|
|
@ -4,12 +4,13 @@ import { TableModule } from 'primeng/table'
|
|||
import { NgModule } from '@angular/core'
|
||||
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
|
||||
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
|
||||
import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
|
||||
import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
|
||||
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
||||
import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
|
||||
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
|
||||
import { AdminRoutingModule } from './admin-routing.module'
|
||||
import { AdminComponent } from './admin.component'
|
||||
import {
|
||||
|
@ -18,6 +19,7 @@ import {
|
|||
EditBasicConfigurationComponent,
|
||||
EditConfigurationService,
|
||||
EditCustomConfigComponent,
|
||||
EditHomepageComponent,
|
||||
EditInstanceInformationComponent,
|
||||
EditLiveConfigurationComponent,
|
||||
EditVODTranscodingComponent
|
||||
|
@ -53,6 +55,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
|||
SharedVideoCommentModule,
|
||||
SharedActorImageModule,
|
||||
SharedActorImageEditModule,
|
||||
SharedCustomMarkupModule,
|
||||
|
||||
TableModule,
|
||||
SelectButtonModule,
|
||||
|
@ -100,7 +103,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
|||
EditVODTranscodingComponent,
|
||||
EditLiveConfigurationComponent,
|
||||
EditAdvancedConfigurationComponent,
|
||||
EditInstanceInformationComponent
|
||||
EditInstanceInformationComponent,
|
||||
EditHomepageComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
|
|
@ -26,22 +26,13 @@
|
|||
<div class="form-group" formGroupName="instance">
|
||||
<label i18n for="instanceDefaultClientRoute">Landing page</label>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
|
||||
<option i18n value="/videos/overview">Discover videos</option>
|
||||
|
||||
<optgroup i18n-label label="Trending pages">
|
||||
<option i18n value="/videos/trending">Default trending page</option>
|
||||
<option i18n value="/videos/trending?alg=best" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('best')">Best videos</option>
|
||||
<option i18n value="/videos/trending?alg=hot" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('hot')">Hot videos</option>
|
||||
<option i18n value="/videos/trending?alg=most-viewed" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-viewed')">Most viewed videos</option>
|
||||
<option i18n value="/videos/trending?alg=most-liked" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-liked')">Most liked videos</option>
|
||||
</optgroup>
|
||||
|
||||
<option i18n value="/videos/recently-added">Recently added videos</option>
|
||||
<option i18n value="/videos/local">Local videos</option>
|
||||
</select>
|
||||
</div>
|
||||
<my-select-custom-value
|
||||
id="instanceDefaultClientRoute"
|
||||
[items]="defaultLandingPageOptions"
|
||||
formControlName="defaultClientRoute"
|
||||
inputType="text"
|
||||
[clearable]="false"
|
||||
></my-select-custom-value>
|
||||
|
||||
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { MenuService } from '@app/core'
|
||||
import { ServerConfig } from '@shared/models'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
|
||||
|
@ -10,22 +12,31 @@ import { ConfigService } from '../shared/config.service'
|
|||
templateUrl: './edit-basic-configuration.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ]
|
||||
})
|
||||
export class EditBasicConfigurationComponent implements OnInit {
|
||||
export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
||||
@Input() form: FormGroup
|
||||
@Input() formErrors: any
|
||||
|
||||
@Input() serverConfig: ServerConfig
|
||||
|
||||
signupAlertMessage: string
|
||||
defaultLandingPageOptions: SelectOptionsItem[] = []
|
||||
|
||||
constructor (
|
||||
private configService: ConfigService
|
||||
private configService: ConfigService,
|
||||
private menuService: MenuService
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.buildLandingPageOptions()
|
||||
this.checkSignupField()
|
||||
}
|
||||
|
||||
ngOnChanges (changes: SimpleChanges) {
|
||||
if (changes['serverConfig']) {
|
||||
this.buildLandingPageOptions()
|
||||
}
|
||||
}
|
||||
|
||||
getVideoQuotaOptions () {
|
||||
return this.configService.videoQuotaOptions
|
||||
}
|
||||
|
@ -70,6 +81,15 @@ export class EditBasicConfigurationComponent implements OnInit {
|
|||
return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
|
||||
}
|
||||
|
||||
buildLandingPageOptions () {
|
||||
this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig)
|
||||
.map(o => ({
|
||||
id: o.path,
|
||||
label: o.label,
|
||||
description: o.path
|
||||
}))
|
||||
}
|
||||
|
||||
private checkSignupField () {
|
||||
const signupControl = this.form.get('signup.enabled')
|
||||
|
||||
|
|
|
@ -3,8 +3,16 @@
|
|||
|
||||
<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>Instance information</a>
|
||||
<a ngbNavLink i18n>Information</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
|
||||
|
@ -13,7 +21,7 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="basic-configuration">
|
||||
<a ngbNavLink i18n>Basic configuration</a>
|
||||
<a ngbNavLink i18n>Basic</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
||||
|
@ -40,7 +48,7 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="advanced-configuration">
|
||||
<a ngbNavLink i18n>Advanced configuration</a>
|
||||
<a ngbNavLink i18n>Advanced</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
import omit from 'lodash-es/omit'
|
||||
import { forkJoin } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
|
@ -24,9 +25,14 @@ import {
|
|||
} 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, FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { CustomConfig, ServerConfig } from '@shared/models'
|
||||
import { CustomPageService } from '@app/shared/shared-main/custom-page'
|
||||
import { CustomConfig, CustomPage, ServerConfig } from '@shared/models'
|
||||
import { EditConfigurationService } from './edit-configuration.service'
|
||||
|
||||
type ComponentCustomConfig = CustomConfig & {
|
||||
instanceCustomHomepage: CustomPage
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-custom-config',
|
||||
templateUrl: './edit-custom-config.component.html',
|
||||
|
@ -35,9 +41,11 @@ import { EditConfigurationService } from './edit-configuration.service'
|
|||
export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||
activeNav: string
|
||||
|
||||
customConfig: CustomConfig
|
||||
customConfig: ComponentCustomConfig
|
||||
serverConfig: ServerConfig
|
||||
|
||||
homepage: CustomPage
|
||||
|
||||
languageItems: SelectOptionsItem[] = []
|
||||
categoryItems: SelectOptionsItem[] = []
|
||||
|
||||
|
@ -47,6 +55,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
protected formValidatorService: FormValidatorService,
|
||||
private notifier: Notifier,
|
||||
private configService: ConfigService,
|
||||
private customPage: CustomPageService,
|
||||
private serverService: ServerService,
|
||||
private editConfigurationService: EditConfigurationService
|
||||
) {
|
||||
|
@ -56,11 +65,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
ngOnInit () {
|
||||
this.serverConfig = this.serverService.getTmpConfig()
|
||||
this.serverService.getConfig()
|
||||
.subscribe(config => {
|
||||
this.serverConfig = config
|
||||
})
|
||||
.subscribe(config => this.serverConfig = config)
|
||||
|
||||
const formGroupData: { [key in keyof CustomConfig ]: any } = {
|
||||
const formGroupData: { [key in keyof ComponentCustomConfig ]: any } = {
|
||||
instance: {
|
||||
name: INSTANCE_NAME_VALIDATOR,
|
||||
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
||||
|
@ -215,6 +222,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
disableLocalSearch: null,
|
||||
isDefaultSearch: null
|
||||
}
|
||||
},
|
||||
|
||||
instanceCustomHomepage: {
|
||||
content: null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,15 +261,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
}
|
||||
|
||||
async formValidated () {
|
||||
const value: CustomConfig = this.form.getRawValue()
|
||||
const value: ComponentCustomConfig = this.form.getRawValue()
|
||||
|
||||
this.configService.updateCustomConfig(value)
|
||||
forkJoin([
|
||||
this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')),
|
||||
this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content)
|
||||
])
|
||||
.subscribe(
|
||||
res => {
|
||||
this.customConfig = res
|
||||
([ resConfig ]) => {
|
||||
const instanceCustomHomepage = {
|
||||
content: value.instanceCustomHomepage.content
|
||||
}
|
||||
|
||||
this.customConfig = { ...resConfig, instanceCustomHomepage }
|
||||
|
||||
// Reload general configuration
|
||||
this.serverService.resetConfig()
|
||||
.subscribe(config => this.serverConfig = config)
|
||||
|
||||
this.updateForm()
|
||||
|
||||
|
@ -317,9 +336,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
}
|
||||
|
||||
private loadConfigAndUpdateForm () {
|
||||
this.configService.getCustomConfig()
|
||||
.subscribe(config => {
|
||||
this.customConfig = config
|
||||
forkJoin([
|
||||
this.configService.getCustomConfig(),
|
||||
this.customPage.getInstanceHomepage()
|
||||
])
|
||||
.subscribe(([ config, homepage ]) => {
|
||||
this.customConfig = { ...config, instanceCustomHomepage: homepage }
|
||||
|
||||
this.updateForm()
|
||||
// Force form validation
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<ng-container [formGroup]="form">
|
||||
|
||||
<ng-container formGroupName="instanceCustomHomepage">
|
||||
|
||||
<div class="form-row mt-5"> <!-- homepage grid -->
|
||||
<div class="form-group col-12 col-lg-4 col-xl-3">
|
||||
<div i18n class="inner-form-title">INSTANCE HOMEPAGE</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceCustomHomepageContent">Homepage</label>
|
||||
|
||||
<my-markdown-textarea
|
||||
name="instanceCustomHomepageContent" formControlName="content" textareaMaxWidth="90%" textareaHeight="300px"
|
||||
[customMarkdownRenderer]="customMarkdownRenderer"
|
||||
[classes]="{ 'input-error': formErrors['instanceCustomHomepage.content'] }"
|
||||
></my-markdown-textarea>
|
||||
|
||||
<div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error">{{ formErrors.instanceCustomHomepage.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
|
@ -0,0 +1,25 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-homepage',
|
||||
templateUrl: './edit-homepage.component.html',
|
||||
styleUrls: [ './edit-custom-config.component.scss' ]
|
||||
})
|
||||
export class EditHomepageComponent implements OnInit {
|
||||
@Input() form: FormGroup
|
||||
@Input() formErrors: any
|
||||
|
||||
customMarkdownRenderer: (text: string) => Promise<HTMLElement>
|
||||
|
||||
constructor (private customMarkup: CustomMarkupService) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.customMarkdownRenderer = async (text: string) => {
|
||||
return this.customMarkup.buildElement(text)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ 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'
|
||||
|
|
|
@ -5,8 +5,7 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
|||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
|
||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||
import { compareSemVer } from '@shared/core-utils/miscs/miscs'
|
||||
import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
|
||||
import { PluginType } from '@shared/models/plugins/plugin.type'
|
||||
import { PeerTubePlugin, PluginType } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-plugin-list-installed',
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<my-global-icon iconName="search"></my-global-icon>
|
||||
|
||||
<ng-container i18n>
|
||||
{{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}"
|
||||
{{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}"
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
|
|
@ -4,8 +4,7 @@ import { Component, OnInit } from '@angular/core'
|
|||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
||||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
|
||||
import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
|
||||
import { PluginType } from '@shared/models/plugins/plugin.type'
|
||||
import { PeerTubePluginIndex, PluginType } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-plugin-search',
|
||||
|
|
|
@ -81,6 +81,8 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
|||
userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10)
|
||||
userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10)
|
||||
|
||||
if (userUpdate.pluginAuth === 'null') userUpdate.pluginAuth = null
|
||||
|
||||
this.userService.updateUser(this.user.id, userUpdate).subscribe(
|
||||
() => {
|
||||
this.notifier.success($localize`User ${this.user.username} updated.`)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { HomeComponent } from './home.component'
|
||||
|
||||
const homeRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: HomeComponent,
|
||||
canActivateChild: [ MetaGuard ]
|
||||
}
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [ RouterModule.forChild(homeRoutes) ],
|
||||
exports: [ RouterModule ]
|
||||
})
|
||||
export class HomeRoutingModule {}
|
|
@ -0,0 +1,4 @@
|
|||
<div class="root margin-content">
|
||||
<div #contentWrapper></div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.root {
|
||||
padding-top: 20px;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
|
||||
import { CustomPageService } from '@app/shared/shared-main/custom-page'
|
||||
|
||||
@Component({
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: [ './home.component.scss' ]
|
||||
})
|
||||
|
||||
export class HomeComponent implements OnInit {
|
||||
@ViewChild('contentWrapper') contentWrapper: ElementRef<HTMLInputElement>
|
||||
|
||||
constructor (
|
||||
private customMarkupService: CustomMarkupService,
|
||||
private customPageService: CustomPageService
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.customPageService.getInstanceHomepage()
|
||||
.subscribe(async ({ content }) => {
|
||||
const element = await this.customMarkupService.buildElement(content)
|
||||
this.contentWrapper.nativeElement.appendChild(element)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
|
||||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
import { HomeRoutingModule } from './home-routing.module'
|
||||
import { HomeComponent } from './home.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
HomeRoutingModule,
|
||||
|
||||
SharedMainModule,
|
||||
SharedCustomMarkupModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
HomeComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
HomeComponent
|
||||
],
|
||||
|
||||
providers: [ ]
|
||||
})
|
||||
export class HomeModule { }
|
|
@ -0,0 +1,3 @@
|
|||
export * from './home-routing.module'
|
||||
export * from './home.component'
|
||||
export * from './home.module'
|
|
@ -1,14 +1,12 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { LoginComponent } from './login.component'
|
||||
import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service'
|
||||
import { LoginComponent } from './login.component'
|
||||
|
||||
const loginRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LoginComponent,
|
||||
canActivate: [ MetaGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Login`
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { LoginGuard } from '../core'
|
||||
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
|
||||
import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
|
||||
import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
|
||||
import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
|
||||
import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
|
||||
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
|
||||
import { MyAccountComponent } from './my-account.component'
|
||||
import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
|
||||
|
||||
const myAccountRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MyAccountComponent,
|
||||
canActivateChild: [ MetaGuard, LoginGuard ],
|
||||
canActivateChild: [ LoginGuard ],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common'
|
|||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { AfterViewChecked, Component, OnInit } from '@angular/core'
|
||||
import { AuthService, Notifier, User, UserService } from '@app/core'
|
||||
import { uploadErrorHandler } from '@app/helpers'
|
||||
import { genericUploadErrorHandler } from '@app/helpers'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-settings',
|
||||
|
@ -46,7 +46,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked {
|
|||
this.user.updateAccountAvatar(data.avatar)
|
||||
},
|
||||
|
||||
(err: HttpErrorResponse) => uploadErrorHandler({
|
||||
(err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`avatar`,
|
||||
notifier: this.notifier
|
||||
|
|
|
@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http'
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { uploadErrorHandler } from '@app/helpers'
|
||||
import { genericUploadErrorHandler } from '@app/helpers'
|
||||
import {
|
||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||
|
@ -109,7 +109,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
|
|||
this.videoChannel.updateAvatar(data.avatar)
|
||||
},
|
||||
|
||||
(err: HttpErrorResponse) => uploadErrorHandler({
|
||||
(err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`avatar`,
|
||||
notifier: this.notifier
|
||||
|
@ -139,7 +139,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
|
|||
this.videoChannel.updateBanner(data.banner)
|
||||
},
|
||||
|
||||
(err: HttpErrorResponse) => uploadErrorHandler({
|
||||
(err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`banner`,
|
||||
notifier: this.notifier
|
||||
|
|
|
@ -68,8 +68,14 @@ channel with the same name (${videoChannel.name})!`,
|
|||
this.authService.userInformationLoaded
|
||||
.pipe(mergeMap(() => {
|
||||
const user = this.authService.getUser()
|
||||
const options = {
|
||||
account: user.account,
|
||||
withStats: true,
|
||||
search: this.search,
|
||||
sort: '-updatedAt'
|
||||
}
|
||||
|
||||
return this.videoChannelService.listAccountVideoChannels(user.account, null, true, this.search)
|
||||
return this.videoChannelService.listAccountVideoChannels(options)
|
||||
})).subscribe(res => {
|
||||
this.videoChannels = res.data
|
||||
this.totalItems = res.total
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { LoginGuard } from '../core'
|
||||
import { MyHistoryComponent } from './my-history/my-history.component'
|
||||
import { MyLibraryComponent } from './my-library.component'
|
||||
|
@ -17,7 +16,7 @@ const myLibraryRoutes: Routes = [
|
|||
{
|
||||
path: '',
|
||||
component: MyLibraryComponent,
|
||||
canActivateChild: [ MetaGuard, LoginGuard ],
|
||||
canActivateChild: [ LoginGuard ],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
|
|
@ -8,13 +8,8 @@
|
|||
<div class="modal-body" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
<label i18n for="channel">Select a channel to receive the video</label>
|
||||
<div class="peertube-select-container">
|
||||
<select formControlName="channel" id="channel" class="form-control">
|
||||
<option i18n value="undefined" disabled>Channel that will receive the video</option>
|
||||
<option *ngFor="let channel of videoChannels" [value]="channel.id">{{ channel.displayName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<my-select-channel labelForId="channel" formControlName="channel" [items]="videoChannels"></my-select-channel>
|
||||
|
||||
<div *ngIf="formErrors.channel" class="form-error">{{ formErrors.channel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { switchMap } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||
import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { AuthService, Notifier } from '@app/core'
|
||||
import { listUserChannels } from '@app/helpers'
|
||||
import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
|
||||
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main'
|
||||
import { VideoOwnershipService } from '@app/shared/shared-main'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { VideoChangeOwnership, VideoChannel } from '@shared/models'
|
||||
import { VideoChangeOwnership } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-accept-ownership',
|
||||
|
@ -18,8 +19,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
|
|||
@ViewChild('modal', { static: true }) modal: ElementRef
|
||||
|
||||
videoChangeOwnership: VideoChangeOwnership | undefined = undefined
|
||||
|
||||
videoChannels: VideoChannel[]
|
||||
videoChannels: SelectChannelItem[]
|
||||
|
||||
error: string = null
|
||||
|
||||
|
@ -28,7 +28,6 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
|
|||
private videoOwnershipService: VideoOwnershipService,
|
||||
private notifier: Notifier,
|
||||
private authService: AuthService,
|
||||
private videoChannelService: VideoChannelService,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
super()
|
||||
|
@ -37,9 +36,8 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
|
|||
ngOnInit () {
|
||||
this.videoChannels = []
|
||||
|
||||
this.authService.userInformationLoaded
|
||||
.pipe(switchMap(() => this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account)))
|
||||
.subscribe(videoChannels => this.videoChannels = videoChannels.data)
|
||||
listUserChannels(this.authService)
|
||||
.subscribe(channels => this.videoChannels = channels)
|
||||
|
||||
this.buildForm({
|
||||
channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { ResetPasswordComponent } from './reset-password.component'
|
||||
|
||||
const resetPasswordRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ResetPasswordComponent,
|
||||
canActivate: [ MetaGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: `Reset password`
|
||||
title: $localize`Reset password`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="radio-label label-container">
|
||||
<label i18n>Display only</label>
|
||||
<button i18n class="reset-button reset-button-small" (click)="resetField('isLive')" *ngIf="advancedSearch.isLive !== undefined">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="peertube-radio-container">
|
||||
<input type="radio" name="isLive" id="isLiveTrue" value="true" [(ngModel)]="advancedSearch.isLive">
|
||||
<label i18n for="isLiveTrue" class="radio">Live videos</label>
|
||||
</div>
|
||||
|
||||
<div class="peertube-radio-container">
|
||||
<input type="radio" name="isLive" id="isLiveFalse" value="false" [(ngModel)]="advancedSearch.isLive">
|
||||
<label i18n for="isLiveFalse" class="radio">VOD videos</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="radio-label label-container">
|
||||
<label i18n>Display sensitive content</label>
|
||||
|
@ -44,7 +63,7 @@
|
|||
</div>
|
||||
|
||||
<div class="peertube-radio-container" *ngFor="let date of publishedDateRanges">
|
||||
<input type="radio" (change)="inputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
|
||||
<input type="radio" (change)="onInputUpdated()" name="publishedDateRange" [id]="date.id" [value]="date.id" [(ngModel)]="publishedDateRange">
|
||||
<label [for]="date.id" class="radio">{{ date.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -60,7 +79,7 @@
|
|||
<div class="row">
|
||||
<div class="pl-0 col-sm-6">
|
||||
<input
|
||||
(change)="inputUpdated()"
|
||||
(change)="onInputUpdated()"
|
||||
(keydown.enter)="$event.preventDefault()"
|
||||
type="text" id="original-publication-after" name="original-publication-after"
|
||||
i18n-placeholder placeholder="After..."
|
||||
|
@ -70,7 +89,7 @@
|
|||
</div>
|
||||
<div class="pr-0 col-sm-6">
|
||||
<input
|
||||
(change)="inputUpdated()"
|
||||
(change)="onInputUpdated()"
|
||||
(keydown.enter)="$event.preventDefault()"
|
||||
type="text" id="original-publication-before" name="original-publication-before"
|
||||
i18n-placeholder placeholder="Before..."
|
||||
|
@ -93,7 +112,7 @@
|
|||
</div>
|
||||
|
||||
<div class="peertube-radio-container" *ngFor="let duration of durationRanges">
|
||||
<input type="radio" (change)="inputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
|
||||
<input type="radio" (change)="onInputUpdated()" name="durationRange" [id]="duration.id" [value]="duration.id" [(ngModel)]="durationRange">
|
||||
<label [for]="duration.id" class="radio">{{ duration.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,8 @@ import { ServerService } from '@app/core'
|
|||
import { AdvancedSearch } from '@app/shared/shared-search'
|
||||
import { ServerConfig, VideoConstant } from '@shared/models'
|
||||
|
||||
type FormOption = { id: string, label: string }
|
||||
|
||||
@Component({
|
||||
selector: 'my-search-filters',
|
||||
styleUrls: [ './search-filters.component.scss' ],
|
||||
|
@ -17,9 +19,10 @@ export class SearchFiltersComponent implements OnInit {
|
|||
videoLicences: VideoConstant<number>[] = []
|
||||
videoLanguages: VideoConstant<string>[] = []
|
||||
|
||||
publishedDateRanges: { id: string, label: string }[] = []
|
||||
sorts: { id: string, label: string }[] = []
|
||||
durationRanges: { id: string, label: string }[] = []
|
||||
publishedDateRanges: FormOption[] = []
|
||||
sorts: FormOption[] = []
|
||||
durationRanges: FormOption[] = []
|
||||
videoType: FormOption[] = []
|
||||
|
||||
publishedDateRange: string
|
||||
durationRange: string
|
||||
|
@ -33,10 +36,6 @@ export class SearchFiltersComponent implements OnInit {
|
|||
private serverService: ServerService
|
||||
) {
|
||||
this.publishedDateRanges = [
|
||||
{
|
||||
id: 'any_published_date',
|
||||
label: $localize`Any`
|
||||
},
|
||||
{
|
||||
id: 'today',
|
||||
label: $localize`Today`
|
||||
|
@ -55,11 +54,18 @@ export class SearchFiltersComponent implements OnInit {
|
|||
}
|
||||
]
|
||||
|
||||
this.durationRanges = [
|
||||
this.videoType = [
|
||||
{
|
||||
id: 'any_duration',
|
||||
label: $localize`Any`
|
||||
id: 'vod',
|
||||
label: $localize`VOD videos`
|
||||
},
|
||||
{
|
||||
id: 'live',
|
||||
label: $localize`Live videos`
|
||||
}
|
||||
]
|
||||
|
||||
this.durationRanges = [
|
||||
{
|
||||
id: 'short',
|
||||
label: $localize`Short (< 4 min)`
|
||||
|
@ -104,24 +110,26 @@ export class SearchFiltersComponent implements OnInit {
|
|||
this.loadOriginallyPublishedAtYears()
|
||||
}
|
||||
|
||||
inputUpdated () {
|
||||
onInputUpdated () {
|
||||
this.updateModelFromDurationRange()
|
||||
this.updateModelFromPublishedRange()
|
||||
this.updateModelFromOriginallyPublishedAtYears()
|
||||
}
|
||||
|
||||
formUpdated () {
|
||||
this.inputUpdated()
|
||||
this.onInputUpdated()
|
||||
this.filtered.emit(this.advancedSearch)
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.advancedSearch.reset()
|
||||
|
||||
this.resetOriginalPublicationYears()
|
||||
|
||||
this.durationRange = undefined
|
||||
this.publishedDateRange = undefined
|
||||
this.originallyPublishedStartYear = undefined
|
||||
this.originallyPublishedEndYear = undefined
|
||||
this.inputUpdated()
|
||||
|
||||
this.onInputUpdated()
|
||||
}
|
||||
|
||||
resetField (fieldName: string, value?: any) {
|
||||
|
@ -130,7 +138,7 @@ export class SearchFiltersComponent implements OnInit {
|
|||
|
||||
resetLocalField (fieldName: string, value?: any) {
|
||||
this[fieldName] = value
|
||||
this.inputUpdated()
|
||||
this.onInputUpdated()
|
||||
}
|
||||
|
||||
resetOriginalPublicationYears () {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
|
||||
import { SearchComponent } from './search.component'
|
||||
import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
|
||||
|
@ -9,7 +8,6 @@ const searchRoutes: Routes = [
|
|||
{
|
||||
path: '',
|
||||
component: SearchComponent,
|
||||
canActivate: [ MetaGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Search`
|
||||
|
@ -19,7 +17,6 @@ const searchRoutes: Routes = [
|
|||
{
|
||||
path: 'lazy-load-video',
|
||||
component: SearchComponent,
|
||||
canActivate: [ MetaGuard ],
|
||||
resolve: {
|
||||
data: VideoLazyLoadResolver
|
||||
}
|
||||
|
@ -27,7 +24,6 @@ const searchRoutes: Routes = [
|
|||
{
|
||||
path: 'lazy-load-channel',
|
||||
component: SearchComponent,
|
||||
canActivate: [ MetaGuard ],
|
||||
resolve: {
|
||||
data: ChannelLazyLoadResolver
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { forkJoin, of, Subscription } from 'rxjs'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core'
|
||||
import { AuthService, ComponentPagination, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
|
||||
import { immutableAssign } from '@app/helpers'
|
||||
import { Video, VideoChannel } from '@app/shared/shared-main'
|
||||
import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
|
||||
import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature'
|
||||
import { MetaService } from '@ngx-meta/core'
|
||||
import { SearchTargetType, ServerConfig } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
|
@ -238,7 +237,10 @@ export class SearchComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private updateTitle () {
|
||||
const suffix = this.currentSearch ? ' ' + this.currentSearch : ''
|
||||
const suffix = this.currentSearch
|
||||
? ' ' + this.currentSearch
|
||||
: ''
|
||||
|
||||
this.metaService.setTitle($localize`Search` + suffix)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { ServerConfigResolver, UnloggedGuard } from '@app/core'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { RegisterComponent } from './register.component'
|
||||
|
||||
const registerRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: RegisterComponent,
|
||||
canActivate: [ MetaGuard, UnloggedGuard ],
|
||||
canActivate: [ UnloggedGuard ],
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Register`
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
|
||||
import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component'
|
||||
import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
|
||||
|
||||
const verifyAccountRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
canActivateChild: [ MetaGuard ],
|
||||
children: [
|
||||
{
|
||||
path: 'email',
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
|
||||
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
|
||||
import { VideoChannelsComponent } from './video-channels.component'
|
||||
|
@ -9,7 +8,6 @@ const videoChannelsRoutes: Routes = [
|
|||
{
|
||||
path: ':videoChannelName',
|
||||
component: VideoChannelsComponent,
|
||||
canActivateChild: [ MetaGuard ],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<a ngbNavLink i18n>Basic info</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row">
|
||||
<div class="form-columns">
|
||||
<div class="col-video-edit">
|
||||
<div class="form-group">
|
||||
<label i18n for="name">Title</label>
|
||||
|
@ -76,7 +76,7 @@
|
|||
<my-help>
|
||||
<ng-template ptTemplate="customHtml">
|
||||
<ng-container i18n>
|
||||
<a href="https://chooser-beta.creativecommons.org/" target="_blank" rel="noopener noreferrer">Choose</a> the appropriate license for your work.
|
||||
<a href="https://chooser-beta.creativecommons.org/" target="_blank" rel="noopener noreferrer">Choose</a> the appropriate licence for your work.
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</my-help>
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
// Bootstrap grid utilities require functions, variables and mixins
|
||||
@import 'node_modules/bootstrap/scss/functions';
|
||||
@import 'node_modules/bootstrap/scss/variables';
|
||||
@import 'node_modules/bootstrap/scss/mixins';
|
||||
@import 'node_modules/bootstrap/scss/grid';
|
||||
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
|
@ -57,63 +51,60 @@ my-peertube-checkbox {
|
|||
}
|
||||
}
|
||||
|
||||
.captions {
|
||||
.captions-header {
|
||||
text-align: right;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.captions-header {
|
||||
text-align: right;
|
||||
margin-bottom: 1rem;
|
||||
.create-caption {
|
||||
@include create-button;
|
||||
}
|
||||
|
||||
.create-caption {
|
||||
@include create-button;
|
||||
.caption-entry {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
|
||||
a.caption-entry-label {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
flex-grow: 1;
|
||||
color: #000;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.caption-entry {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
|
||||
a.caption-entry-label {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
flex-grow: 1;
|
||||
color: #000;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.caption-entry-label {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
|
||||
margin-right: 20px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.caption-entry-state {
|
||||
width: 200px;
|
||||
|
||||
&.caption-entry-state-create {
|
||||
color: #39CC0B;
|
||||
}
|
||||
|
||||
&.caption-entry-state-delete {
|
||||
color: #FF0000;
|
||||
}
|
||||
}
|
||||
|
||||
.caption-entry-delete {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
}
|
||||
}
|
||||
|
||||
.no-caption {
|
||||
text-align: center;
|
||||
.caption-entry-label {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
|
||||
margin-right: 20px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.caption-entry-state {
|
||||
width: 200px;
|
||||
|
||||
&.caption-entry-state-create {
|
||||
color: #39CC0B;
|
||||
}
|
||||
|
||||
&.caption-entry-state-delete {
|
||||
color: #FF0000;
|
||||
}
|
||||
}
|
||||
|
||||
.caption-entry-delete {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
}
|
||||
}
|
||||
|
||||
.no-caption {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.submit-container {
|
||||
|
@ -143,35 +134,15 @@ p-calendar {
|
|||
}
|
||||
}
|
||||
|
||||
// columns for the video
|
||||
.col-video-edit {
|
||||
@include make-col-ready();
|
||||
.form-columns {
|
||||
display: grid;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
@include make-col(7);
|
||||
|
||||
+ .col-video-edit {
|
||||
@include make-col(5);
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
@include make-col(8);
|
||||
|
||||
+ .col-video-edit {
|
||||
@include make-col(4);
|
||||
}
|
||||
}
|
||||
grid-template-columns: 66% 1fr;
|
||||
grid-gap: 30px;
|
||||
}
|
||||
|
||||
:host-context(.expanded) {
|
||||
.col-video-edit {
|
||||
@include media-breakpoint-up(md) {
|
||||
@include make-col(8);
|
||||
|
||||
+ .col-video-edit {
|
||||
@include make-col(4);
|
||||
}
|
||||
}
|
||||
@include on-small-main-col {
|
||||
.form-columns {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,15 @@ import {
|
|||
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { InstanceService } from '@app/shared/shared-instance'
|
||||
import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||
import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models'
|
||||
import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
|
||||
import {
|
||||
LiveVideo,
|
||||
RegisterClientFormFieldOptions,
|
||||
RegisterClientVideoFieldOptions,
|
||||
ServerConfig,
|
||||
VideoConstant,
|
||||
VideoDetails,
|
||||
VideoPrivacy
|
||||
} from '@shared/models'
|
||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||
import { VideoEditType } from './video-edit.type'
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import { objectToFormData } from '@app/helpers'
|
||||
import { resolveUrl, UploaderX } from 'ngx-uploadx'
|
||||
|
||||
/**
|
||||
* multipart/form-data uploader extending the UploaderX implementation of Google Resumable
|
||||
* for use with multer
|
||||
*
|
||||
* @see https://github.com/kukhariev/ngx-uploadx/blob/637e258fe366b8095203f387a6101a230ee4f8e6/src/uploadx/lib/uploaderx.ts
|
||||
* @example
|
||||
*
|
||||
* options: UploadxOptions = {
|
||||
* uploaderClass: UploaderXFormData
|
||||
* };
|
||||
*/
|
||||
export class UploaderXFormData extends UploaderX {
|
||||
|
||||
async getFileUrl (): Promise<string> {
|
||||
const headers = {
|
||||
'X-Upload-Content-Length': this.size.toString(),
|
||||
'X-Upload-Content-Type': this.file.type || 'application/octet-stream'
|
||||
}
|
||||
|
||||
const previewfile = this.metadata.previewfile as any as File
|
||||
delete this.metadata.previewfile
|
||||
|
||||
const data = objectToFormData(this.metadata)
|
||||
if (previewfile !== undefined) {
|
||||
data.append('previewfile', previewfile, previewfile.name)
|
||||
data.append('thumbnailfile', previewfile, previewfile.name)
|
||||
}
|
||||
|
||||
await this.request({
|
||||
method: 'POST',
|
||||
body: data,
|
||||
url: this.endpoint,
|
||||
headers
|
||||
})
|
||||
|
||||
const location = this.getValueFromResponse('location')
|
||||
if (!location) {
|
||||
throw new Error('Invalid or missing Location header')
|
||||
}
|
||||
|
||||
this.offset = this.responseStatus === 201 ? 0 : undefined
|
||||
|
||||
return resolveUrl(location, this.endpoint)
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import { scrollToTop } from '@app/helpers'
|
|||
import { FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import { VideoPrivacy, VideoUpdate } from '@shared/models'
|
||||
import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
|
||||
import { hydrateFormFromVideo } from '../shared/video-edit-utils'
|
||||
import { VideoSend } from './video-send'
|
||||
|
||||
|
@ -113,7 +113,13 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
|
|||
this.loadingBar.useRef().complete()
|
||||
this.isImportingVideo = false
|
||||
this.firstStepError.emit()
|
||||
this.notifier.error(err.message)
|
||||
|
||||
let message = err.message
|
||||
if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
|
||||
message = $localize`Torrents with only 1 file are supported.`
|
||||
}
|
||||
|
||||
this.notifier.error(message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
<div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="setVideoFile($event)">
|
||||
<div *ngIf="!isUploadingVideo" class="upload-video-container" dragDrop (fileDropped)="onFileDropped($event)">
|
||||
<div class="first-step-block">
|
||||
<my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
|
||||
|
||||
<div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'">
|
||||
<span i18n>Select the file to upload</span>
|
||||
<input
|
||||
aria-label="Select the file to upload" i18n-aria-label
|
||||
#videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" autofocus
|
||||
aria-label="Select the file to upload"
|
||||
i18n-aria-label
|
||||
#videofileInput
|
||||
[accept]="videoExtensions"
|
||||
(change)="onFileChange($event)"
|
||||
id="videofile"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -41,7 +46,13 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group upload-audio-button">
|
||||
<my-button className="orange-button" i18n-label [label]="getAudioUploadLabel()" icon="upload" (click)="uploadFirstStep(true)"></my-button>
|
||||
<my-button
|
||||
className="orange-button"
|
||||
[label]="getAudioUploadLabel()"
|
||||
icon="upload"
|
||||
(click)="uploadAudio()"
|
||||
>
|
||||
</my-button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -64,6 +75,7 @@
|
|||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" />
|
||||
<input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" />
|
||||
|
|
|
@ -47,8 +47,4 @@
|
|||
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-group > input:not(:first-child) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { Subscription } from 'rxjs'
|
||||
import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
|
||||
import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx'
|
||||
import { UploaderXFormData } from './uploaderx-form-data'
|
||||
import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
|
||||
import { scrollToTop, uploadErrorHandler } from '@app/helpers'
|
||||
import { scrollToTop, genericUploadErrorHandler } from '@app/helpers'
|
||||
import { FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||
import { VideoPrivacy } from '@shared/models'
|
||||
import { VideoSend } from './video-send'
|
||||
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-upload',
|
||||
|
@ -20,23 +21,18 @@ import { VideoSend } from './video-send'
|
|||
'./video-send.scss'
|
||||
]
|
||||
})
|
||||
export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate {
|
||||
export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, AfterViewInit, CanComponentDeactivate {
|
||||
@Output() firstStepDone = new EventEmitter<string>()
|
||||
@Output() firstStepError = new EventEmitter<void>()
|
||||
@ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
|
||||
|
||||
// So that it can be accessed in the template
|
||||
readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
|
||||
|
||||
userVideoQuotaUsed = 0
|
||||
userVideoQuotaUsedDaily = 0
|
||||
|
||||
isUploadingAudioFile = false
|
||||
isUploadingVideo = false
|
||||
isUpdatingVideo = false
|
||||
|
||||
videoUploaded = false
|
||||
videoUploadObservable: Subscription = null
|
||||
videoUploadPercents = 0
|
||||
videoUploadedIds = {
|
||||
id: 0,
|
||||
|
@ -49,7 +45,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
|||
error: string
|
||||
enableRetryAfterError: boolean
|
||||
|
||||
// So that it can be accessed in the template
|
||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
||||
protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + 'upload-resumable'
|
||||
|
||||
private uploadxOptions: UploadxOptions
|
||||
private isUpdatingVideo = false
|
||||
private fileToUpload: File
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
|
@ -61,15 +63,77 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
|||
protected videoCaptionService: VideoCaptionService,
|
||||
private userService: UserService,
|
||||
private router: Router,
|
||||
private hooks: HooksService
|
||||
) {
|
||||
private hooks: HooksService,
|
||||
private resumableUploadService: UploadxService
|
||||
) {
|
||||
super()
|
||||
|
||||
this.uploadxOptions = {
|
||||
endpoint: this.BASE_VIDEO_UPLOAD_URL,
|
||||
multiple: false,
|
||||
token: this.authService.getAccessToken(),
|
||||
uploaderClass: UploaderXFormData,
|
||||
retryConfig: {
|
||||
maxAttempts: 6,
|
||||
shouldRetry: (code: number) => {
|
||||
return code < 400 || code >= 501
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get videoExtensions () {
|
||||
return this.serverConfig.video.file.extensions.join(', ')
|
||||
}
|
||||
|
||||
onUploadVideoOngoing (state: UploadState) {
|
||||
switch (state.status) {
|
||||
case 'error':
|
||||
const error = state.response?.error || 'Unknow error'
|
||||
|
||||
this.handleUploadError({
|
||||
error: new Error(error),
|
||||
name: 'HttpErrorResponse',
|
||||
message: error,
|
||||
ok: false,
|
||||
headers: new HttpHeaders(state.responseHeaders),
|
||||
status: +state.responseStatus,
|
||||
statusText: error,
|
||||
type: HttpEventType.Response,
|
||||
url: state.url
|
||||
})
|
||||
break
|
||||
|
||||
case 'cancelled':
|
||||
this.isUploadingVideo = false
|
||||
this.videoUploadPercents = 0
|
||||
|
||||
this.firstStepError.emit()
|
||||
this.enableRetryAfterError = false
|
||||
this.error = ''
|
||||
break
|
||||
|
||||
case 'queue':
|
||||
this.closeFirstStep(state.name)
|
||||
break
|
||||
|
||||
case 'uploading':
|
||||
this.videoUploadPercents = state.progress
|
||||
break
|
||||
|
||||
case 'paused':
|
||||
this.notifier.info($localize`Upload on hold`)
|
||||
break
|
||||
|
||||
case 'complete':
|
||||
this.videoUploaded = true
|
||||
this.videoUploadPercents = 100
|
||||
|
||||
this.videoUploadedIds = state?.response.video
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
super.ngOnInit()
|
||||
|
||||
|
@ -78,6 +142,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
|||
this.userVideoQuotaUsed = data.videoQuotaUsed
|
||||
this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
|
||||
})
|
||||
|
||||
this.resumableUploadService.events
|
||||
.subscribe(state => this.onUploadVideoOngoing(state))
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
|
@ -85,7 +152,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
|||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
|
||||
this.cancelUpload()
|
||||
}
|
||||
|
||||
canDeactivate () {
|
||||
|
@ -105,137 +172,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
|||
}
|
||||
}
|
||||
|
||||
getVideoFile () {
|
||||
return this.videofileInput.nativeElement.files[0]
|
||||
}
|
||||
|
||||
setVideoFile (files: FileList) {
|
||||
onFileDropped (files: FileList) {
|
||||
this.videofileInput.nativeElement.files = files
|
||||
this.fileChange()
|
||||
|
||||
this.onFileChange({ target: this.videofileInput.nativeElement })
|
||||
}
|
||||
|
||||
getAudioUploadLabel () {
|
||||
const videofile = this.getVideoFile()
|
||||
if (!videofile) return $localize`Upload`
|
||||
onFileChange (event: Event | { target: HTMLInputElement }) {
|
||||
const file = (event.target as HTMLInputElement).files[0]
|
||||
|
||||
return $localize`Upload ${videofile.name}`
|
||||
if (!file) return
|
||||
|
||||
if (!this.checkGlobalUserQuota(file)) return
|
||||
if (!this.checkDailyUserQuota(file)) return
|
||||
|
||||
if (this.isAudioFile(file.name)) {
|
||||
this.isUploadingAudioFile = true
|
||||
return
|
||||
}
|
||||
|
||||
this.isUploadingVideo = true
|
||||
this.fileToUpload = file
|
||||
|
||||
this.uploadFile(file)
|
||||
}
|
||||
|
||||
fileChange () {
|
||||
this.uploadFirstStep()
|
||||
uploadAudio () {
|
||||
this.uploadFile(this.getInputVideoFile(), this.previewfileUpload)
|
||||
}
|
||||
|
||||
retryUpload () {
|
||||
this.enableRetryAfterError = false
|
||||
this.error = ''
|
||||
this.uploadVideo()
|
||||
this.uploadFile(this.fileToUpload)
|
||||
}
|
||||
|
||||
cancelUpload () {
|
||||
if (this.videoUploadObservable !== null) {
|
||||
this.videoUploadObservable.unsubscribe()
|
||||
}
|
||||
|
||||
this.isUploadingVideo = false
|
||||
this.videoUploadPercents = 0
|
||||
this.videoUploadObservable = null
|
||||
|
||||
this.firstStepError.emit()
|
||||
this.enableRetryAfterError = false
|
||||
this.error = ''
|
||||
|
||||
this.notifier.info($localize`Upload cancelled`)
|
||||
}
|
||||
|
||||
uploadFirstStep (clickedOnButton = false) {
|
||||
const videofile = this.getVideoFile()
|
||||
if (!videofile) return
|
||||
|
||||
if (!this.checkGlobalUserQuota(videofile)) return
|
||||
if (!this.checkDailyUserQuota(videofile)) return
|
||||
|
||||
if (clickedOnButton === false && this.isAudioFile(videofile.name)) {
|
||||
this.isUploadingAudioFile = true
|
||||
return
|
||||
}
|
||||
|
||||
// Build name field
|
||||
const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
|
||||
let name: string
|
||||
|
||||
// If the name of the file is very small, keep the extension
|
||||
if (nameWithoutExtension.length < 3) name = videofile.name
|
||||
else name = nameWithoutExtension
|
||||
|
||||
const nsfw = this.serverConfig.instance.isNSFW
|
||||
const waitTranscoding = true
|
||||
const commentsEnabled = true
|
||||
const downloadEnabled = true
|
||||
const channelId = this.firstStepChannelId.toString()
|
||||
|
||||
this.formData = new FormData()
|
||||
this.formData.append('name', name)
|
||||
// Put the video "private" -> we are waiting the user validation of the second step
|
||||
this.formData.append('privacy', VideoPrivacy.PRIVATE.toString())
|
||||
this.formData.append('nsfw', '' + nsfw)
|
||||
this.formData.append('commentsEnabled', '' + commentsEnabled)
|
||||
this.formData.append('downloadEnabled', '' + downloadEnabled)
|
||||
this.formData.append('waitTranscoding', '' + waitTranscoding)
|
||||
this.formData.append('channelId', '' + channelId)
|
||||
this.formData.append('videofile', videofile)
|
||||
|
||||
if (this.previewfileUpload) {
|
||||
this.formData.append('previewfile', this.previewfileUpload)
|
||||
this.formData.append('thumbnailfile', this.previewfileUpload)
|
||||
}
|
||||
|
||||
this.isUploadingVideo = true
|
||||
this.firstStepDone.emit(name)
|
||||
|
||||
this.form.patchValue({
|
||||
name,
|
||||
privacy: this.firstStepPrivacyId,
|
||||
nsfw,
|
||||
channelId: this.firstStepChannelId,
|
||||
previewfile: this.previewfileUpload
|
||||
})
|
||||
|
||||
this.uploadVideo()
|
||||
}
|
||||
|
||||
uploadVideo () {
|
||||
this.videoUploadObservable = this.videoService.uploadVideo(this.formData).subscribe(
|
||||
event => {
|
||||
if (event.type === HttpEventType.UploadProgress) {
|
||||
this.videoUploadPercents = Math.round(100 * event.loaded / event.total)
|
||||
} else if (event instanceof HttpResponse) {
|
||||
this.videoUploaded = true
|
||||
|
||||
this.videoUploadedIds = event.body.video
|
||||
|
||||
this.videoUploadObservable = null
|
||||
}
|
||||
},
|
||||
|
||||
(err: HttpErrorResponse) => {
|
||||
// Reset progress (but keep isUploadingVideo true)
|
||||
this.videoUploadPercents = 0
|
||||
this.videoUploadObservable = null
|
||||
this.enableRetryAfterError = true
|
||||
|
||||
this.error = uploadErrorHandler({
|
||||
err,
|
||||
name: $localize`video`,
|
||||
notifier: this.notifier,
|
||||
sticky: false
|
||||
})
|
||||
|
||||
if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413 ||
|
||||
err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) {
|
||||
this.cancelUpload()
|
||||
}
|
||||
}
|
||||
)
|
||||
this.resumableUploadService.control({ action: 'cancel' })
|
||||
}
|
||||
|
||||
isPublishingButtonDisabled () {
|
||||
|
@ -245,6 +218,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
|||
!this.videoUploadedIds.id
|
||||
}
|
||||
|
||||
getAudioUploadLabel () {
|
||||
const videofile = this.getInputVideoFile()
|
||||
if (!videofile) return $localize`Upload`
|
||||
|
||||
return $localize`Upload ${videofile.name}`
|
||||
}
|
||||
|
||||
updateSecondStep () {
|
||||
if (this.isPublishingButtonDisabled() || !this.checkForm()) {
|
||||
return
|
||||
|
@ -275,6 +255,62 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
|||
)
|
||||
}
|
||||
|
||||
private getInputVideoFile () {
|
||||
return this.videofileInput.nativeElement.files[0]
|
||||
}
|
||||
|
||||
private uploadFile (file: File, previewfile?: File) {
|
||||
const metadata = {
|
||||
waitTranscoding: true,
|
||||
commentsEnabled: true,
|
||||
downloadEnabled: true,
|
||||
channelId: this.firstStepChannelId,
|
||||
nsfw: this.serverConfig.instance.isNSFW,
|
||||
privacy: VideoPrivacy.PRIVATE.toString(),
|
||||
filename: file.name,
|
||||
previewfile: previewfile as any
|
||||
}
|
||||
|
||||
this.resumableUploadService.handleFiles(file, {
|
||||
...this.uploadxOptions,
|
||||
metadata
|
||||
})
|
||||
|
||||
this.isUploadingVideo = true
|
||||
}
|
||||
|
||||
private handleUploadError (err: HttpErrorResponse) {
|
||||
// Reset progress (but keep isUploadingVideo true)
|
||||
this.videoUploadPercents = 0
|
||||
this.enableRetryAfterError = true
|
||||
|
||||
this.error = genericUploadErrorHandler({
|
||||
err,
|
||||
name: $localize`video`,
|
||||
notifier: this.notifier,
|
||||
sticky: false
|
||||
})
|
||||
|
||||
if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) {
|
||||
this.cancelUpload()
|
||||
}
|
||||
}
|
||||
|
||||
private closeFirstStep (filename: string) {
|
||||
const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '')
|
||||
const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension
|
||||
|
||||
this.form.patchValue({
|
||||
name,
|
||||
privacy: this.firstStepPrivacyId,
|
||||
nsfw: this.serverConfig.instance.isNSFW,
|
||||
channelId: this.firstStepChannelId,
|
||||
previewfile: this.previewfileUpload
|
||||
})
|
||||
|
||||
this.firstStepDone.emit(name)
|
||||
}
|
||||
|
||||
private checkGlobalUserQuota (videofile: File) {
|
||||
const bytePipes = new BytesPipe()
|
||||
|
||||
|
@ -285,8 +321,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
|||
const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0)
|
||||
const videoQuotaBytes = bytePipes.transform(videoQuota, 0)
|
||||
|
||||
const msg = $localize`Your video quota is exceeded with this video (
|
||||
video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
|
||||
const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
|
||||
this.notifier.error(msg)
|
||||
|
||||
return false
|
||||
|
@ -304,9 +339,7 @@ video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuota
|
|||
const videoSizeBytes = bytePipes.transform(videofile.size, 0)
|
||||
const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0)
|
||||
const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0)
|
||||
|
||||
const msg = $localize`Your daily video quota is exceeded with this video (
|
||||
video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})`
|
||||
const msg = $localize`Your daily video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})`
|
||||
this.notifier.error(msg)
|
||||
|
||||
return false
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { VideoAddComponent } from './video-add.component'
|
||||
|
||||
const videoAddRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: VideoAddComponent,
|
||||
canActivate: [ MetaGuard, LoginGuard ],
|
||||
canActivate: [ LoginGuard ],
|
||||
canDeactivate: [ CanDeactivateGuard ]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
|
||||
</div>
|
||||
|
||||
<div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
|
||||
<ng-container ngbNavItem>
|
||||
<div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }">
|
||||
<ng-container ngbNavItem="upload">
|
||||
<a ngbNavLink>
|
||||
<span i18n>Upload a file</span>
|
||||
</a>
|
||||
|
@ -31,7 +31,7 @@
|
|||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem *ngIf="isVideoImportHttpEnabled()">
|
||||
<ng-container ngbNavItem="import-url" *ngIf="isVideoImportHttpEnabled()">
|
||||
<a ngbNavLink>
|
||||
<span i18n>Import with URL</span>
|
||||
</a>
|
||||
|
@ -41,7 +41,7 @@
|
|||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem *ngIf="isVideoImportTorrentEnabled()">
|
||||
<ng-container ngbNavItem="import-torrent" *ngIf="isVideoImportTorrentEnabled()">
|
||||
<a ngbNavLink>
|
||||
<span i18n>Import with torrent</span>
|
||||
</a>
|
||||
|
@ -51,7 +51,7 @@
|
|||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem *ngIf="isVideoLiveEnabled()">
|
||||
<ng-container ngbNavItem="go-live" *ngIf="isVideoLiveEnabled()">
|
||||
<a ngbNavLink>
|
||||
<span i18n>Go live</span>
|
||||
</a>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
|
||||
import { ServerConfig } from '@shared/models'
|
||||
import { VideoEditType } from './shared/video-edit.type'
|
||||
|
@ -22,11 +23,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
|
|||
|
||||
secondStepType: VideoEditType
|
||||
videoName: string
|
||||
serverConfig: ServerConfig
|
||||
|
||||
activeNav: string
|
||||
|
||||
private serverConfig: ServerConfig
|
||||
|
||||
constructor (
|
||||
private auth: AuthService,
|
||||
private serverService: ServerService
|
||||
private serverService: ServerService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
get userInformationLoaded () {
|
||||
|
@ -42,6 +48,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
|
|||
.subscribe(config => this.serverConfig = config)
|
||||
|
||||
this.user = this.auth.getUser()
|
||||
|
||||
if (this.route.snapshot.fragment) {
|
||||
this.onNavChange(this.route.snapshot.fragment)
|
||||
}
|
||||
}
|
||||
|
||||
onNavChange (newActiveNav: string) {
|
||||
this.activeNav = newActiveNav
|
||||
|
||||
this.router.navigate([], { fragment: this.activeNav })
|
||||
}
|
||||
|
||||
onFirstStepDone (type: VideoEditType, videoName: string) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { CanDeactivateGuard } from '@app/core'
|
||||
import { UploadxModule } from 'ngx-uploadx'
|
||||
import { VideoEditModule } from './shared/video-edit.module'
|
||||
import { DragDropDirective } from './video-add-components/drag-drop.directive'
|
||||
import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
|
||||
|
@ -13,7 +14,9 @@ import { VideoAddComponent } from './video-add.component'
|
|||
imports: [
|
||||
VideoAddRoutingModule,
|
||||
|
||||
VideoEditModule
|
||||
VideoEditModule,
|
||||
|
||||
UploadxModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { VideoUpdateComponent } from './video-update.component'
|
||||
import { VideoUpdateResolver } from './video-update.resolver'
|
||||
|
||||
|
@ -9,7 +8,7 @@ const videoUpdateRoutes: Routes = [
|
|||
{
|
||||
path: '',
|
||||
component: VideoUpdateComponent,
|
||||
canActivate: [ MetaGuard, LoginGuard ],
|
||||
canActivate: [ LoginGuard ],
|
||||
canDeactivate: [ CanDeactivateGuard ],
|
||||
resolve: {
|
||||
videoData: VideoUpdateResolver
|
||||
|
|
|
@ -2,7 +2,9 @@ import { forkJoin, of } from 'rxjs'
|
|||
import { map, switchMap } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
|
||||
import { VideoCaptionService, VideoChannelService, VideoDetails, VideoService } from '@app/shared/shared-main'
|
||||
import { AuthService } from '@app/core'
|
||||
import { listUserChannels } from '@app/helpers'
|
||||
import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||
|
||||
@Injectable()
|
||||
|
@ -10,7 +12,7 @@ export class VideoUpdateResolver implements Resolve<any> {
|
|||
constructor (
|
||||
private videoService: VideoService,
|
||||
private liveVideoService: LiveVideoService,
|
||||
private videoChannelService: VideoChannelService,
|
||||
private authService: AuthService,
|
||||
private videoCaptionService: VideoCaptionService
|
||||
) {
|
||||
}
|
||||
|
@ -31,17 +33,7 @@ export class VideoUpdateResolver implements Resolve<any> {
|
|||
.loadCompleteDescription(video.descriptionPath)
|
||||
.pipe(map(description => Object.assign(video, { description }))),
|
||||
|
||||
this.videoChannelService
|
||||
.listAccountVideoChannels(video.account)
|
||||
.pipe(
|
||||
map(result => result.data),
|
||||
map(videoChannels => videoChannels.map(c => ({
|
||||
id: c.id,
|
||||
label: c.displayName,
|
||||
support: c.support,
|
||||
avatarPath: c.avatar?.path
|
||||
})))
|
||||
),
|
||||
listUserChannels(this.authService),
|
||||
|
||||
this.videoCaptionService
|
||||
.listCaptions(video.id)
|
||||
|
|
|
@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
|
|||
// Before HTML rendering restore line feed for markdown list compatibility
|
||||
const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
|
||||
const html = await this.markdownService.textMarkdownToHTML(commentText, true, true)
|
||||
this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html)
|
||||
this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(html)
|
||||
this.newParentComments = this.parentComments.concat([ this.comment ])
|
||||
|
||||
if (this.comment.account) {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { VideoWatchComponent } from './video-watch.component'
|
||||
|
||||
const videoWatchRoutes: Routes = [
|
||||
{
|
||||
path: 'playlist/:playlistId',
|
||||
component: VideoWatchComponent,
|
||||
canActivate: [ MetaGuard ]
|
||||
component: VideoWatchComponent
|
||||
},
|
||||
{
|
||||
path: ':videoId/comments/:commentId',
|
||||
|
@ -15,8 +13,7 @@ const videoWatchRoutes: Routes = [
|
|||
},
|
||||
{
|
||||
path: ':videoId',
|
||||
component: VideoWatchComponent,
|
||||
canActivate: [ MetaGuard ]
|
||||
component: VideoWatchComponent
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -146,6 +146,8 @@ $video-info-margin-left: 44px;
|
|||
}
|
||||
|
||||
.video-info-name {
|
||||
@include peertube-word-wrap;
|
||||
|
||||
margin-right: 30px;
|
||||
min-height: 40px; // Align with the action buttons
|
||||
font-size: 27px;
|
||||
|
@ -173,6 +175,7 @@ $video-info-margin-left: 44px;
|
|||
|
||||
a {
|
||||
@include disable-default-a-behaviour;
|
||||
@include peertube-word-wrap;
|
||||
|
||||
color: pvar(--mainForegroundColor);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
AuthUser,
|
||||
ConfirmService,
|
||||
MarkdownService,
|
||||
MetaService,
|
||||
Notifier,
|
||||
PeerTubeSocket,
|
||||
RestExtractor,
|
||||
|
@ -25,7 +26,6 @@ import { SupportModalComponent } from '@app/shared/shared-support-modal'
|
|||
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
|
||||
import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature'
|
||||
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
|
||||
import { MetaService } from '@ngx-meta/core'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||
import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
|
||||
|
@ -509,7 +509,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
private async setVideoDescriptionHTML () {
|
||||
const html = await this.markdownService.textMarkdownToHTML(this.video.description)
|
||||
this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html)
|
||||
this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html)
|
||||
}
|
||||
|
||||
private setVideoLikesBarTooltipText () {
|
||||
|
@ -674,7 +674,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.player.one('ended', () => {
|
||||
if (this.video.isLive) {
|
||||
this.video.state.id = VideoState.LIVE_ENDED
|
||||
this.zone.run(() => this.video.state.id = VideoState.LIVE_ENDED)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
|
|||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private auth: AuthService,
|
||||
private serverService: ServerService
|
||||
private serverService: ServerService,
|
||||
private redirectService: RedirectService
|
||||
) {
|
||||
super(data)
|
||||
|
||||
|
@ -84,12 +85,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
|
|||
|
||||
this.algorithmChangeSub = this.route.queryParams.subscribe(
|
||||
queryParams => {
|
||||
const algorithm = queryParams['alg']
|
||||
if (algorithm) {
|
||||
this.data.model = algorithm
|
||||
} else {
|
||||
this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM
|
||||
}
|
||||
this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -99,7 +95,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
|
|||
}
|
||||
|
||||
setSort () {
|
||||
const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM
|
||||
const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm()
|
||||
? this.data.model
|
||||
: undefined
|
||||
|
||||
|
|
|
@ -35,11 +35,12 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
|||
protected storageService: LocalStorageService,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
private videoService: VideoService,
|
||||
private redirectService: RedirectService,
|
||||
private hooks: HooksService
|
||||
) {
|
||||
super()
|
||||
|
||||
this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM)
|
||||
this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
|
||||
|
||||
this.headerComponentInjector = this.getInjector()
|
||||
}
|
||||
|
@ -106,7 +107,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
|||
}
|
||||
|
||||
protected loadPageRouteParams (queryParams: Params) {
|
||||
const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM
|
||||
const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
|
||||
|
||||
this.sort = this.parseAlgorithm(algorithm)
|
||||
}
|
||||
|
@ -115,8 +116,10 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
|||
switch (algorithm) {
|
||||
case 'most-viewed':
|
||||
return '-trending'
|
||||
|
||||
case 'most-liked':
|
||||
return '-likes'
|
||||
|
||||
default:
|
||||
return '-' + algorithm as VideoSortField
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { LoginGuard } from '@app/core'
|
||||
import { MetaGuard } from '@ngx-meta/core'
|
||||
import { VideoTrendingComponent } from './video-list'
|
||||
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
|
||||
import { VideoLocalComponent } from './video-list/video-local.component'
|
||||
|
@ -13,7 +12,6 @@ const videosRoutes: Routes = [
|
|||
{
|
||||
path: '',
|
||||
component: VideosComponent,
|
||||
canActivateChild: [ MetaGuard ],
|
||||
children: [
|
||||
{
|
||||
path: 'overview',
|
||||
|
|
|
@ -3,7 +3,7 @@ import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment }
|
|||
import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
|
||||
import { MenuGuards } from '@app/core/routing/menu-guard.service'
|
||||
import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
|
||||
import { PreloadSelectedModulesList } from './core'
|
||||
import { MetaGuard, PreloadSelectedModulesList } from './core'
|
||||
import { EmptyComponent } from './empty.component'
|
||||
import { RootComponent } from './root.component'
|
||||
|
||||
|
@ -12,55 +12,72 @@ const routes: Routes = [
|
|||
path: 'admin',
|
||||
canActivate: [ MenuGuards.close() ],
|
||||
canDeactivate: [ MenuGuards.open() ],
|
||||
loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule)
|
||||
loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () => import('./+home/home.module').then(m => m.HomeModule)
|
||||
},
|
||||
{
|
||||
path: 'my-account',
|
||||
loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule)
|
||||
loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'my-library',
|
||||
loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule)
|
||||
loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'verify-account',
|
||||
loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule)
|
||||
loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'a',
|
||||
loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule)
|
||||
loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'c',
|
||||
loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule)
|
||||
loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadChildren: () => import('./+about/about.module').then(m => m.AboutModule)
|
||||
loadChildren: () => import('./+about/about.module').then(m => m.AboutModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
loadChildren: () => import('./+signup/+register/register.module').then(m => m.RegisterModule)
|
||||
loadChildren: () => import('./+signup/+register/register.module').then(m => m.RegisterModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'reset-password',
|
||||
loadChildren: () => import('./+reset-password/reset-password.module').then(m => m.ResetPasswordModule)
|
||||
loadChildren: () => import('./+reset-password/reset-password.module').then(m => m.ResetPasswordModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
loadChildren: () => import('./+login/login.module').then(m => m.LoginModule)
|
||||
loadChildren: () => import('./+login/login.module').then(m => m.LoginModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadChildren: () => import('./+search/search.module').then(m => m.SearchModule)
|
||||
loadChildren: () => import('./+search/search.module').then(m => m.SearchModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'videos',
|
||||
loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule)
|
||||
loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'remote-interaction',
|
||||
loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule)
|
||||
loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
},
|
||||
{
|
||||
path: 'video-playlists/watch',
|
||||
|
|
|
@ -40,8 +40,10 @@
|
|||
}
|
||||
|
||||
.icon-menu {
|
||||
background-color: pvar(--mainForegroundColor);
|
||||
mask-image: url('../assets/images/misc/menu.svg');
|
||||
-webkit-mask-image: url('../assets/images/misc/menu.svg');
|
||||
|
||||
background-color: pvar(--mainForegroundColor);
|
||||
margin: 0 18px 0 20px;
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
|
|
|
@ -67,7 +67,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||
}
|
||||
|
||||
goToDefaultRoute () {
|
||||
return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE)
|
||||
return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
|
@ -231,7 +231,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||
}
|
||||
|
||||
this.broadcastMessage = {
|
||||
message: await this.markdownService.completeMarkdownToHTML(messageConfig.message),
|
||||
message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true),
|
||||
dismissable: messageConfig.dismissable,
|
||||
class: classes[messageConfig.level]
|
||||
}
|
||||
|
|
|
@ -4,9 +4,7 @@ import { APP_BASE_HREF, registerLocaleData } from '@angular/common'
|
|||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||
import { ServerService } from '@app/core'
|
||||
import localeOc from '@app/helpers/locales/oc'
|
||||
import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
|
||||
import { AppRoutingModule } from './app-routing.module'
|
||||
import { AppComponent } from './app.component'
|
||||
import { CoreModule } from './core'
|
||||
|
@ -19,12 +17,12 @@ import { CustomModalComponent } from './modal/custom-modal.component'
|
|||
import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
|
||||
import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
|
||||
import { WelcomeModalComponent } from './modal/welcome-modal.component'
|
||||
import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
|
||||
import { SharedFormModule } from './shared/shared-forms'
|
||||
import { SharedGlobalIconModule } from './shared/shared-icons'
|
||||
import { SharedInstanceModule } from './shared/shared-instance'
|
||||
import { SharedMainModule } from './shared/shared-main'
|
||||
import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings'
|
||||
import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
|
||||
|
||||
registerLocaleData(localeOc, 'oc')
|
||||
|
||||
|
@ -62,22 +60,6 @@ registerLocaleData(localeOc, 'oc')
|
|||
SharedInstanceModule,
|
||||
SharedActorImageModule,
|
||||
|
||||
MetaModule.forRoot({
|
||||
provide: MetaLoader,
|
||||
useFactory: (serverService: ServerService) => {
|
||||
return new MetaStaticLoader({
|
||||
pageTitlePositioning: PageTitlePositioning.PrependPageTitle,
|
||||
pageTitleSeparator: ' - ',
|
||||
get applicationName () { return serverService.getTmpConfig().instance.name },
|
||||
defaults: {
|
||||
get title () { return serverService.getTmpConfig().instance.name },
|
||||
get description () { return serverService.getTmpConfig().instance.shortDescription }
|
||||
}
|
||||
})
|
||||
},
|
||||
deps: [ ServerService ]
|
||||
}),
|
||||
|
||||
AppRoutingModule // Put it after all the module because it has the 404 route
|
||||
],
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
|
|||
import { Notifier } from './notification'
|
||||
import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
|
||||
import { RestExtractor, RestService } from './rest'
|
||||
import { LoginGuard, RedirectService, UnloggedGuard, UserRightGuard } from './routing'
|
||||
import { LoginGuard, MetaGuard, MetaService, RedirectService, UnloggedGuard, UserRightGuard } from './routing'
|
||||
import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
|
||||
import { ServerConfigResolver } from './routing/server-config-resolver.service'
|
||||
import { ScopedTokensService } from './scoped-tokens'
|
||||
|
@ -77,7 +77,10 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
|
|||
MessageService,
|
||||
PeerTubeSocket,
|
||||
ServerConfigResolver,
|
||||
CanDeactivateGuard
|
||||
CanDeactivateGuard,
|
||||
|
||||
MetaService,
|
||||
MetaGuard
|
||||
]
|
||||
})
|
||||
export class CoreModule {
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
import { fromEvent } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { GlobalIconName } from '@app/shared/shared-icons'
|
||||
import { sortObjectComparator } from '@shared/core-utils/miscs/miscs'
|
||||
import { ServerConfig } from '@shared/models/server'
|
||||
import { ScreenService } from '../wrappers'
|
||||
|
||||
export type MenuLink = {
|
||||
icon: GlobalIconName
|
||||
label: string
|
||||
menuLabel: string
|
||||
path: string
|
||||
priority: number
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MenuService {
|
||||
isMenuDisplayed = true
|
||||
|
@ -48,6 +59,53 @@ export class MenuService {
|
|||
this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
|
||||
}
|
||||
|
||||
buildCommonLinks (config: ServerConfig) {
|
||||
let entries: MenuLink[] = [
|
||||
{
|
||||
icon: 'globe' as 'globe',
|
||||
label: $localize`Discover videos`,
|
||||
menuLabel: $localize`Discover`,
|
||||
path: '/videos/overview',
|
||||
priority: 150
|
||||
},
|
||||
{
|
||||
icon: 'trending' as 'trending',
|
||||
label: $localize`Trending videos`,
|
||||
menuLabel: $localize`Trending`,
|
||||
path: '/videos/trending',
|
||||
priority: 140
|
||||
},
|
||||
{
|
||||
icon: 'recently-added' as 'recently-added',
|
||||
label: $localize`Recently added videos`,
|
||||
menuLabel: $localize`Recently added`,
|
||||
path: '/videos/recently-added',
|
||||
priority: 130
|
||||
},
|
||||
{
|
||||
icon: 'octagon' as 'octagon',
|
||||
label: $localize`Local videos`,
|
||||
menuLabel: $localize`Local videos`,
|
||||
path: '/videos/local',
|
||||
priority: 120
|
||||
}
|
||||
]
|
||||
|
||||
if (config.homepage.enabled) {
|
||||
entries.push({
|
||||
icon: 'home' as 'home',
|
||||
label: $localize`Home`,
|
||||
menuLabel: $localize`Home`,
|
||||
path: '/home',
|
||||
priority: 160
|
||||
})
|
||||
}
|
||||
|
||||
entries = entries.sort(sortObjectComparator('priority', 'desc'))
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
private handleWindowResize () {
|
||||
// On touch screens, do not handle window resize event since opened menu is handled with a content overlay
|
||||
if (this.screenService.isInTouchScreen()) return
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable } from '@angular/core'
|
||||
import { LinkifierService } from './linkifier.service'
|
||||
import { SANITIZE_OPTIONS } from '@shared/core-utils/renderer/html'
|
||||
import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html'
|
||||
|
||||
@Injectable()
|
||||
export class HtmlRendererService {
|
||||
|
@ -20,7 +20,7 @@ export class HtmlRendererService {
|
|||
})
|
||||
}
|
||||
|
||||
async toSafeHtml (text: string) {
|
||||
async toSafeHtml (text: string, additionalAllowedTags: string[] = []) {
|
||||
const [ html ] = await Promise.all([
|
||||
// Convert possible markdown to html
|
||||
this.linkifier.linkify(text),
|
||||
|
@ -28,7 +28,11 @@ export class HtmlRendererService {
|
|||
this.loadSanitizeHtml()
|
||||
])
|
||||
|
||||
return this.sanitizeHtml(html, SANITIZE_OPTIONS)
|
||||
const options = additionalAllowedTags.length !== 0
|
||||
? getCustomMarkupSanitizeOptions(additionalAllowedTags)
|
||||
: getSanitizeOptions()
|
||||
|
||||
return this.sanitizeHtml(html, options)
|
||||
}
|
||||
|
||||
private async loadSanitizeHtml () {
|
||||
|
|
|
@ -17,12 +17,15 @@ type MarkdownParsers = {
|
|||
enhancedMarkdownIt: MarkdownIt
|
||||
enhancedWithHTMLMarkdownIt: MarkdownIt
|
||||
|
||||
completeMarkdownIt: MarkdownIt
|
||||
unsafeMarkdownIt: MarkdownIt
|
||||
|
||||
customPageMarkdownIt: MarkdownIt
|
||||
}
|
||||
|
||||
type MarkdownConfig = {
|
||||
rules: string[]
|
||||
html: boolean
|
||||
breaks: boolean
|
||||
escape?: boolean
|
||||
}
|
||||
|
||||
|
@ -35,18 +38,24 @@ export class MarkdownService {
|
|||
private markdownParsers: MarkdownParsers = {
|
||||
textMarkdownIt: null,
|
||||
textWithHTMLMarkdownIt: null,
|
||||
|
||||
enhancedMarkdownIt: null,
|
||||
enhancedWithHTMLMarkdownIt: null,
|
||||
completeMarkdownIt: null
|
||||
|
||||
unsafeMarkdownIt: null,
|
||||
|
||||
customPageMarkdownIt: null
|
||||
}
|
||||
private parsersConfig: MarkdownParserConfigs = {
|
||||
textMarkdownIt: { rules: TEXT_RULES, html: false },
|
||||
textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, html: true, escape: true },
|
||||
textMarkdownIt: { rules: TEXT_RULES, breaks: true, html: false },
|
||||
textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, breaks: true, html: true, escape: true },
|
||||
|
||||
enhancedMarkdownIt: { rules: ENHANCED_RULES, html: false },
|
||||
enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, html: true, escape: true },
|
||||
enhancedMarkdownIt: { rules: ENHANCED_RULES, breaks: true, html: false },
|
||||
enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, breaks: true, html: true, escape: true },
|
||||
|
||||
completeMarkdownIt: { rules: COMPLETE_RULES, html: true }
|
||||
unsafeMarkdownIt: { rules: COMPLETE_RULES, breaks: true, html: true, escape: false },
|
||||
|
||||
customPageMarkdownIt: { rules: COMPLETE_RULES, breaks: false, html: true, escape: true }
|
||||
}
|
||||
|
||||
private emojiModule: any
|
||||
|
@ -54,22 +63,26 @@ export class MarkdownService {
|
|||
constructor (private htmlRenderer: HtmlRendererService) {}
|
||||
|
||||
textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
|
||||
if (withHtml) return this.render('textWithHTMLMarkdownIt', markdown, withEmoji)
|
||||
if (withHtml) return this.render({ name: 'textWithHTMLMarkdownIt', markdown, withEmoji })
|
||||
|
||||
return this.render('textMarkdownIt', markdown, withEmoji)
|
||||
return this.render({ name: 'textMarkdownIt', markdown, withEmoji })
|
||||
}
|
||||
|
||||
enhancedMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
|
||||
if (withHtml) return this.render('enhancedWithHTMLMarkdownIt', markdown, withEmoji)
|
||||
if (withHtml) return this.render({ name: 'enhancedWithHTMLMarkdownIt', markdown, withEmoji })
|
||||
|
||||
return this.render('enhancedMarkdownIt', markdown, withEmoji)
|
||||
return this.render({ name: 'enhancedMarkdownIt', markdown, withEmoji })
|
||||
}
|
||||
|
||||
completeMarkdownToHTML (markdown: string) {
|
||||
return this.render('completeMarkdownIt', markdown, true)
|
||||
unsafeMarkdownToHTML (markdown: string, _trustedInput: true) {
|
||||
return this.render({ name: 'unsafeMarkdownIt', markdown, withEmoji: true })
|
||||
}
|
||||
|
||||
async processVideoTimestamps (html: string) {
|
||||
customPageMarkdownToHTML (markdown: string, additionalAllowedTags: string[]) {
|
||||
return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags })
|
||||
}
|
||||
|
||||
processVideoTimestamps (html: string) {
|
||||
return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
|
||||
const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
|
||||
const url = buildVideoLink({ startTime: t })
|
||||
|
@ -77,7 +90,13 @@ export class MarkdownService {
|
|||
})
|
||||
}
|
||||
|
||||
private async render (name: keyof MarkdownParsers, markdown: string, withEmoji = false) {
|
||||
private async render (options: {
|
||||
name: keyof MarkdownParsers
|
||||
markdown: string
|
||||
withEmoji: boolean
|
||||
additionalAllowedTags?: string[]
|
||||
}) {
|
||||
const { name, markdown, withEmoji, additionalAllowedTags } = options
|
||||
if (!markdown) return ''
|
||||
|
||||
const config = this.parsersConfig[ name ]
|
||||
|
@ -96,7 +115,7 @@ export class MarkdownService {
|
|||
let html = this.markdownParsers[ name ].render(markdown)
|
||||
html = this.avoidTruncatedTags(html)
|
||||
|
||||
if (config.escape) return this.htmlRenderer.toSafeHtml(html)
|
||||
if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags)
|
||||
|
||||
return html
|
||||
}
|
||||
|
@ -105,7 +124,7 @@ export class MarkdownService {
|
|||
// FIXME: import('...') returns a struct module, containing a "default" field
|
||||
const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
|
||||
|
||||
const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html })
|
||||
const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: config.breaks, html: config.html })
|
||||
|
||||
for (const rule of config.rules) {
|
||||
markdownIt.enable(rule)
|
||||
|
|
|
@ -3,6 +3,8 @@ export * from './custom-reuse-strategy'
|
|||
export * from './disable-for-reuse-hook'
|
||||
export * from './login-guard.service'
|
||||
export * from './menu-guard.service'
|
||||
export * from './meta-guard.service'
|
||||
export * from './meta.service'
|
||||
export * from './preload-selected-modules-list'
|
||||
export * from './redirect.service'
|
||||
export * from './server-config-resolver.service'
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'
|
||||
import { MetaService } from './meta.service'
|
||||
|
||||
@Injectable()
|
||||
export class MetaGuard implements CanActivate, CanActivateChild {
|
||||
|
||||
constructor (private meta: MetaService) { }
|
||||
|
||||
canActivate (route: ActivatedRouteSnapshot): boolean {
|
||||
const metaSettings = route.data?.meta
|
||||
|
||||
if (metaSettings) {
|
||||
this.meta.update(metaSettings)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
canActivateChild (route: ActivatedRouteSnapshot): boolean {
|
||||
return this.canActivate(route)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { Injectable } from '@angular/core'
|
||||
import { Meta, Title } from '@angular/platform-browser'
|
||||
import { HTMLServerConfig } from '@shared/models/server'
|
||||
import { ServerService } from '../server'
|
||||
|
||||
export interface MetaSettings {
|
||||
title?: string
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MetaService {
|
||||
private config: HTMLServerConfig
|
||||
|
||||
constructor (
|
||||
private titleService: Title,
|
||||
private meta: Meta,
|
||||
private server: ServerService
|
||||
) {
|
||||
this.config = this.server.getTmpConfig()
|
||||
this.server.getConfig()
|
||||
.subscribe(config => this.config = config)
|
||||
}
|
||||
|
||||
setTitle (subTitle?: string) {
|
||||
let title = ''
|
||||
if (subTitle) title += `${subTitle} - `
|
||||
|
||||
title += this.config.instance.name
|
||||
|
||||
this.titleService.setTitle(title)
|
||||
}
|
||||
|
||||
setTag (name: string, value: string) {
|
||||
this.meta.addTag({ name, content: value })
|
||||
}
|
||||
|
||||
update (meta: MetaSettings) {
|
||||
this.setTitle(meta.title)
|
||||
}
|
||||
}
|
|
@ -6,14 +6,14 @@ import { ServerService } from '../server'
|
|||
export class RedirectService {
|
||||
// Default route could change according to the instance configuration
|
||||
static INIT_DEFAULT_ROUTE = '/videos/trending'
|
||||
static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
|
||||
static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
|
||||
static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
|
||||
|
||||
private previousUrl: string
|
||||
private currentUrl: string
|
||||
|
||||
private redirectingToHomepage = false
|
||||
private defaultTrendingAlgorithm = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
|
||||
private defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
|
||||
|
||||
constructor (
|
||||
private router: Router,
|
||||
|
@ -22,10 +22,10 @@ export class RedirectService {
|
|||
// The config is first loaded from the cache so try to get the default route
|
||||
const tmpConfig = this.serverService.getTmpConfig()
|
||||
if (tmpConfig?.instance?.defaultClientRoute) {
|
||||
RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
|
||||
this.defaultRoute = tmpConfig.instance.defaultClientRoute
|
||||
}
|
||||
if (tmpConfig?.trending?.videos?.algorithms?.default) {
|
||||
RedirectService.DEFAULT_TRENDING_ALGORITHM = tmpConfig.trending.videos.algorithms.default
|
||||
this.defaultTrendingAlgorithm = tmpConfig.trending.videos.algorithms.default
|
||||
}
|
||||
|
||||
// Load default route
|
||||
|
@ -34,13 +34,8 @@ export class RedirectService {
|
|||
const defaultRouteConfig = config.instance.defaultClientRoute
|
||||
const defaultTrendingConfig = config.trending.videos.algorithms.default
|
||||
|
||||
if (defaultRouteConfig) {
|
||||
RedirectService.DEFAULT_ROUTE = defaultRouteConfig
|
||||
}
|
||||
|
||||
if (defaultTrendingConfig) {
|
||||
RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig
|
||||
}
|
||||
if (defaultRouteConfig) this.defaultRoute = defaultRouteConfig
|
||||
if (defaultTrendingConfig) this.defaultTrendingAlgorithm = defaultTrendingConfig
|
||||
})
|
||||
|
||||
// Track previous url
|
||||
|
@ -53,6 +48,14 @@ export class RedirectService {
|
|||
})
|
||||
}
|
||||
|
||||
getDefaultRoute () {
|
||||
return this.defaultRoute
|
||||
}
|
||||
|
||||
getDefaultTrendingAlgorithm () {
|
||||
return this.defaultTrendingAlgorithm
|
||||
}
|
||||
|
||||
redirectToPreviousRoute () {
|
||||
const exceptions = [
|
||||
'/verify-account',
|
||||
|
@ -72,21 +75,21 @@ export class RedirectService {
|
|||
|
||||
this.redirectingToHomepage = true
|
||||
|
||||
console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
|
||||
console.log('Redirecting to %s...', this.defaultRoute)
|
||||
|
||||
this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
|
||||
this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
|
||||
.then(() => this.redirectingToHomepage = false)
|
||||
.catch(() => {
|
||||
this.redirectingToHomepage = false
|
||||
|
||||
console.error(
|
||||
'Cannot navigate to %s, resetting default route to %s.',
|
||||
RedirectService.DEFAULT_ROUTE,
|
||||
this.defaultRoute,
|
||||
RedirectService.INIT_DEFAULT_ROUTE
|
||||
)
|
||||
|
||||
RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
|
||||
return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
|
||||
this.defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
|
||||
return this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators'
|
|||
import { HttpClient } from '@angular/common/http'
|
||||
import { Inject, Injectable, LOCALE_ID } from '@angular/core'
|
||||
import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
|
||||
import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
|
||||
import { environment } from '../../../environments/environment'
|
||||
|
@ -16,8 +15,6 @@ export class ServerService {
|
|||
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
|
||||
private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
|
||||
|
||||
private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
|
||||
|
||||
configReloaded = new Subject<ServerConfig>()
|
||||
|
||||
private localeObservable: Observable<any>
|
||||
|
@ -176,6 +173,9 @@ export class ServerService {
|
|||
disableLocalSearch: false,
|
||||
isDefaultSearch: false
|
||||
}
|
||||
},
|
||||
homepage: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,9 +201,7 @@ export class ServerService {
|
|||
this.configReset = true
|
||||
|
||||
// Notify config update
|
||||
this.getConfig().subscribe(() => {
|
||||
// empty, to fire a reset config event
|
||||
})
|
||||
return this.getConfig()
|
||||
}
|
||||
|
||||
getConfig () {
|
||||
|
@ -212,7 +210,6 @@ export class ServerService {
|
|||
if (!this.configObservable) {
|
||||
this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
|
||||
.pipe(
|
||||
tap(config => this.saveConfigLocally(config)),
|
||||
tap(config => {
|
||||
this.config = config
|
||||
this.configLoaded = true
|
||||
|
@ -343,20 +340,15 @@ export class ServerService {
|
|||
)
|
||||
}
|
||||
|
||||
private saveConfigLocally (config: ServerConfig) {
|
||||
peertubeLocalStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config))
|
||||
}
|
||||
|
||||
private loadConfigLocally () {
|
||||
const configString = peertubeLocalStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY)
|
||||
const configString = window['PeerTubeServerConfig']
|
||||
if (!configString) return
|
||||
|
||||
if (configString) {
|
||||
try {
|
||||
const parsed = JSON.parse(configString)
|
||||
Object.assign(this.config, parsed)
|
||||
} catch (err) {
|
||||
console.error('Cannot parse config saved in local storage.', err)
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(configString)
|
||||
Object.assign(this.config, parsed)
|
||||
} catch (err) {
|
||||
console.error('Cannot parse config saved in from index.html.', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,19 @@ export class ThemeService {
|
|||
: this.userService.getAnonymousUser().theme
|
||||
|
||||
if (theme !== 'instance-default') return theme
|
||||
return this.serverConfig.theme.default
|
||||
|
||||
const instanceTheme = this.serverConfig.theme.default
|
||||
if (instanceTheme !== 'default') return instanceTheme
|
||||
|
||||
// Default to dark theme if available and wanted by the user
|
||||
if (
|
||||
this.themes.find(t => t.name === 'dark') &&
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
) {
|
||||
return 'dark'
|
||||
}
|
||||
|
||||
return instanceTheme
|
||||
}
|
||||
|
||||
private loadTheme (name: string) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { map } from 'rxjs/operators'
|
||||
import { first, map } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
|
@ -23,20 +23,29 @@ function getParameterByName (name: string, url: string) {
|
|||
|
||||
function listUserChannels (authService: AuthService) {
|
||||
return authService.userInformationLoaded
|
||||
.pipe(map(() => {
|
||||
const user = authService.getUser()
|
||||
if (!user) return undefined
|
||||
.pipe(
|
||||
first(),
|
||||
map(() => {
|
||||
const user = authService.getUser()
|
||||
if (!user) return undefined
|
||||
|
||||
const videoChannels = user.videoChannels
|
||||
if (Array.isArray(videoChannels) === false) return undefined
|
||||
const videoChannels = user.videoChannels
|
||||
if (Array.isArray(videoChannels) === false) return undefined
|
||||
|
||||
return videoChannels.map(c => ({
|
||||
id: c.id,
|
||||
label: c.displayName,
|
||||
support: c.support,
|
||||
avatarPath: c.avatar?.path
|
||||
}) as SelectChannelItem)
|
||||
}))
|
||||
return videoChannels
|
||||
.sort((a, b) => {
|
||||
if (a.updatedAt < b.updatedAt) return 1
|
||||
if (a.updatedAt > b.updatedAt) return -1
|
||||
return 0
|
||||
})
|
||||
.map(c => ({
|
||||
id: c.id,
|
||||
label: c.displayName,
|
||||
support: c.support,
|
||||
avatarPath: c.avatar?.path
|
||||
}) as SelectChannelItem)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function getAbsoluteAPIUrl () {
|
||||
|
@ -167,8 +176,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) {
|
|||
)
|
||||
}
|
||||
|
||||
function uploadErrorHandler (parameters: {
|
||||
err: HttpErrorResponse
|
||||
function genericUploadErrorHandler (parameters: {
|
||||
err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
|
||||
name: string
|
||||
notifier: Notifier
|
||||
sticky?: boolean
|
||||
|
@ -180,6 +189,9 @@ function uploadErrorHandler (parameters: {
|
|||
if (err instanceof ErrorEvent) { // network error
|
||||
message = $localize`The connection was interrupted`
|
||||
notifier.error(message, title, null, sticky)
|
||||
} else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) {
|
||||
message = $localize`The server encountered an error`
|
||||
notifier.error(message, title, null, sticky)
|
||||
} else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
|
||||
message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
|
||||
notifier.error(message, title, null, sticky)
|
||||
|
@ -210,5 +222,5 @@ export {
|
|||
isInViewport,
|
||||
isXPercentInViewport,
|
||||
listUserChannels,
|
||||
uploadErrorHandler
|
||||
genericUploadErrorHandler
|
||||
}
|
||||
|
|
|
@ -123,24 +123,9 @@
|
|||
<div class="on-instance">
|
||||
<div i18n class="block-title">ON {{instanceName}}</div>
|
||||
|
||||
<a class="menu-link" routerLink="/videos/overview" routerLinkActive="active">
|
||||
<my-global-icon iconName="globe" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Discover</ng-container>
|
||||
</a>
|
||||
|
||||
<a class="menu-link" routerLink="/videos/trending" routerLinkActive="active">
|
||||
<my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Trending</ng-container>
|
||||
</a>
|
||||
|
||||
<a class="menu-link" routerLink="/videos/recently-added" routerLinkActive="active">
|
||||
<my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Recently added</ng-container>
|
||||
</a>
|
||||
|
||||
<a class="menu-link" routerLink="/videos/local" routerLinkActive="active">
|
||||
<my-global-icon iconName="home" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Local videos</ng-container>
|
||||
<a class="menu-link" *ngFor="let commonLink of commonMenuLinks" [routerLink]="commonLink.path" routerLinkActive="active">
|
||||
<my-global-icon [iconName]="commonLink.icon" aria-hidden="true"></my-global-icon>
|
||||
<ng-container>{{ commonLink.menuLabel }}</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,17 @@ import { switchMap } from 'rxjs/operators'
|
|||
import { ViewportScroller } from '@angular/common'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { AuthService, AuthStatus, AuthUser, MenuService, RedirectService, ScreenService, ServerService, UserService } from '@app/core'
|
||||
import {
|
||||
AuthService,
|
||||
AuthStatus,
|
||||
AuthUser,
|
||||
MenuLink,
|
||||
MenuService,
|
||||
RedirectService,
|
||||
ScreenService,
|
||||
ServerService,
|
||||
UserService
|
||||
} from '@app/core'
|
||||
import { scrollToTop } from '@app/helpers'
|
||||
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
|
||||
import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
|
||||
|
@ -35,6 +45,8 @@ export class MenuComponent implements OnInit {
|
|||
|
||||
currentInterfaceLanguage: string
|
||||
|
||||
commonMenuLinks: MenuLink[] = []
|
||||
|
||||
private languages: VideoConstant<string>[] = []
|
||||
private serverConfig: ServerConfig
|
||||
private routesPerRight: { [role in UserRight]?: string } = {
|
||||
|
@ -80,7 +92,10 @@ export class MenuComponent implements OnInit {
|
|||
ngOnInit () {
|
||||
this.serverConfig = this.serverService.getTmpConfig()
|
||||
this.serverService.getConfig()
|
||||
.subscribe(config => this.serverConfig = config)
|
||||
.subscribe(config => {
|
||||
this.serverConfig = config
|
||||
this.buildMenuLinks()
|
||||
})
|
||||
|
||||
this.isLoggedIn = this.authService.isLoggedIn()
|
||||
if (this.isLoggedIn === true) {
|
||||
|
@ -241,6 +256,10 @@ export class MenuComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
private buildMenuLinks () {
|
||||
this.commonMenuLinks = this.menuService.buildCommonLinks(this.serverConfig)
|
||||
}
|
||||
|
||||
private buildUserLanguages () {
|
||||
if (!this.user) {
|
||||
this.videoLanguages = []
|
||||
|
|
|
@ -42,7 +42,7 @@ export class ActorBannerEditComponent implements OnInit {
|
|||
this.bannerExtensions = config.banner.file.extensions.join(', ')
|
||||
|
||||
// tslint:disable:max-line-length
|
||||
this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
|
||||
this.bannerFormat = $localize`ratio 6/1, recommended size: 1920x317, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<div *ngIf="channel" class="channel">
|
||||
<my-actor-avatar [channel]="channel" size="34"></my-actor-avatar>
|
||||
|
||||
<div class="display-name">{{ channel.displayName }}</div>
|
||||
<div class="username">{{ channel.name }}</div>
|
||||
|
||||
<div class="description">{{ channel.description }}</div>
|
||||
</div>
|
|
@ -0,0 +1,9 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.channel {
|
||||
border-radius: 15px;
|
||||
padding: 10px;
|
||||
width: min-content;
|
||||
border: 1px solid pvar(--mainColor);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { VideoChannel, VideoChannelService } from '../shared-main'
|
||||
|
||||
/*
|
||||
* Markup component that creates a channel miniature only
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'my-channel-miniature-markup',
|
||||
templateUrl: 'channel-miniature-markup.component.html',
|
||||
styleUrls: [ 'channel-miniature-markup.component.scss' ]
|
||||
})
|
||||
export class ChannelMiniatureMarkupComponent implements OnInit {
|
||||
@Input() name: string
|
||||
|
||||
channel: VideoChannel
|
||||
|
||||
constructor (
|
||||
private channelService: VideoChannelService
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.channelService.getVideoChannel(this.name)
|
||||
.subscribe(channel => this.channel = channel)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
import { ComponentRef, Injectable } from '@angular/core'
|
||||
import { MarkdownService } from '@app/core'
|
||||
import {
|
||||
ChannelMiniatureMarkupData,
|
||||
EmbedMarkupData,
|
||||
PlaylistMiniatureMarkupData,
|
||||
VideoMiniatureMarkupData,
|
||||
VideosListMarkupData
|
||||
} from '@shared/models'
|
||||
import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
|
||||
import { DynamicElementService } from './dynamic-element.service'
|
||||
import { EmbedMarkupComponent } from './embed-markup.component'
|
||||
import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
|
||||
import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
|
||||
import { VideosListMarkupComponent } from './videos-list-markup.component'
|
||||
|
||||
type BuilderFunction = (el: HTMLElement) => ComponentRef<any>
|
||||
|
||||
@Injectable()
|
||||
export class CustomMarkupService {
|
||||
private builders: { [ selector: string ]: BuilderFunction } = {
|
||||
'peertube-video-embed': el => this.embedBuilder(el, 'video'),
|
||||
'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
|
||||
'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
|
||||
'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
|
||||
'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
|
||||
'peertube-videos-list': el => this.videosListBuilder(el)
|
||||
}
|
||||
|
||||
constructor (
|
||||
private dynamicElementService: DynamicElementService,
|
||||
private markdown: MarkdownService
|
||||
) { }
|
||||
|
||||
async buildElement (text: string) {
|
||||
const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags())
|
||||
|
||||
const rootElement = document.createElement('div')
|
||||
rootElement.innerHTML = html
|
||||
|
||||
for (const selector of this.getSupportedTags()) {
|
||||
rootElement.querySelectorAll(selector)
|
||||
.forEach((e: HTMLElement) => {
|
||||
try {
|
||||
const component = this.execBuilder(selector, e)
|
||||
|
||||
this.dynamicElementService.injectElement(e, component)
|
||||
} catch (err) {
|
||||
console.error('Cannot inject component %s.', selector, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return rootElement
|
||||
}
|
||||
|
||||
private getSupportedTags () {
|
||||
return Object.keys(this.builders)
|
||||
}
|
||||
|
||||
private execBuilder (selector: string, el: HTMLElement) {
|
||||
return this.builders[selector](el)
|
||||
}
|
||||
|
||||
private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
|
||||
const data = el.dataset as EmbedMarkupData
|
||||
const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
|
||||
|
||||
this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
private videoMiniatureBuilder (el: HTMLElement) {
|
||||
const data = el.dataset as VideoMiniatureMarkupData
|
||||
const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
|
||||
|
||||
this.dynamicElementService.setModel(component, { uuid: data.uuid })
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
private playlistMiniatureBuilder (el: HTMLElement) {
|
||||
const data = el.dataset as PlaylistMiniatureMarkupData
|
||||
const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
|
||||
|
||||
this.dynamicElementService.setModel(component, { uuid: data.uuid })
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
private channelMiniatureBuilder (el: HTMLElement) {
|
||||
const data = el.dataset as ChannelMiniatureMarkupData
|
||||
const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
|
||||
|
||||
this.dynamicElementService.setModel(component, { name: data.name })
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
private videosListBuilder (el: HTMLElement) {
|
||||
const data = el.dataset as VideosListMarkupData
|
||||
const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
|
||||
|
||||
const model = {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
sort: data.sort,
|
||||
categoryOneOf: this.buildArrayNumber(data.categoryOneOf),
|
||||
languageOneOf: this.buildArrayString(data.languageOneOf),
|
||||
count: this.buildNumber(data.count) || 10
|
||||
}
|
||||
|
||||
this.dynamicElementService.setModel(component, model)
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
private buildNumber (value: string) {
|
||||
if (!value) return undefined
|
||||
|
||||
return parseInt(value, 10)
|
||||
}
|
||||
|
||||
private buildArrayNumber (value: string) {
|
||||
if (!value) return undefined
|
||||
|
||||
return value.split(',').map(v => parseInt(v, 10))
|
||||
}
|
||||
|
||||
private buildArrayString (value: string) {
|
||||
if (!value) return undefined
|
||||
|
||||
return value.split(',')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import {
|
||||
ApplicationRef,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
EmbeddedViewRef,
|
||||
Injectable,
|
||||
Injector,
|
||||
OnChanges,
|
||||
SimpleChange,
|
||||
SimpleChanges,
|
||||
Type
|
||||
} from '@angular/core'
|
||||
|
||||
@Injectable()
|
||||
export class DynamicElementService {
|
||||
|
||||
constructor (
|
||||
private injector: Injector,
|
||||
private applicationRef: ApplicationRef,
|
||||
private componentFactoryResolver: ComponentFactoryResolver
|
||||
) { }
|
||||
|
||||
createElement <T> (ofComponent: Type<T>) {
|
||||
const div = document.createElement('div')
|
||||
|
||||
const component = this.componentFactoryResolver.resolveComponentFactory(ofComponent)
|
||||
.create(this.injector, [], div)
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
injectElement <T> (wrapper: HTMLElement, componentRef: ComponentRef<T>) {
|
||||
const hostView = componentRef.hostView as EmbeddedViewRef<any>
|
||||
|
||||
this.applicationRef.attachView(hostView)
|
||||
wrapper.appendChild(hostView.rootNodes[0])
|
||||
}
|
||||
|
||||
setModel <T> (componentRef: ComponentRef<T>, attributes: Partial<T>) {
|
||||
const changes: SimpleChanges = {}
|
||||
|
||||
for (const key of Object.keys(attributes)) {
|
||||
const previousValue = componentRef.instance[key]
|
||||
const newValue = attributes[key]
|
||||
|
||||
componentRef.instance[key] = newValue
|
||||
changes[key] = new SimpleChange(previousValue, newValue, previousValue === undefined)
|
||||
}
|
||||
|
||||
const component = componentRef.instance
|
||||
if (typeof (component as unknown as OnChanges).ngOnChanges === 'function') {
|
||||
(component as unknown as OnChanges).ngOnChanges(changes)
|
||||
}
|
||||
|
||||
componentRef.changeDetectorRef.detectChanges()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { Component, ElementRef, Input, OnInit } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-embed-markup',
|
||||
template: ''
|
||||
})
|
||||
export class EmbedMarkupComponent implements OnInit {
|
||||
@Input() uuid: string
|
||||
@Input() type: 'video' | 'playlist' = 'video'
|
||||
|
||||
constructor (private el: ElementRef) { }
|
||||
|
||||
ngOnInit () {
|
||||
const link = this.type === 'video'
|
||||
? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` })
|
||||
: buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` })
|
||||
|
||||
this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './custom-markup.service'
|
||||
export * from './dynamic-element.service'
|
||||
export * from './shared-custom-markup.module'
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue