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-namespace": "off",
|
||||||
"@typescript-eslint/no-empty-interface": "off",
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
"@typescript-eslint/no-extraneous-class": "off",
|
"@typescript-eslint/no-extraneous-class": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
// bugged but useful
|
// bugged but useful
|
||||||
"@typescript-eslint/restrict-plus-operands": "off"
|
"@typescript-eslint/restrict-plus-operands": "off"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
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
|
- name: 💬 Matrix
|
||||||
url: https://matrix.to/#/#peertube:matrix.org
|
url: https://matrix.to/#/#peertube:matrix.org
|
||||||
about: Chat with us via Matrix for quick Q/A here
|
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
|
- name: 🤷💻🤦 Forum
|
||||||
url: https://framacolibri.org/c/peertube
|
url: https://framacolibri.org/c/peertube
|
||||||
about: You can ask and answer other questions here
|
about: You can ask and answer other questions here
|
||||||
|
|
|
@ -45,11 +45,6 @@ jobs:
|
||||||
branch-base: develop
|
branch-base: develop
|
||||||
bundlewatch-github-token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
|
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
|
- name: PeerTube code stats
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: |
|
run: |
|
||||||
|
@ -57,11 +52,24 @@ jobs:
|
||||||
unzip "scc-3.0.0-x86_64-unknown-linux.zip"
|
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
|
./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
|
- name: Display stats
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: |
|
run: |
|
||||||
cat client-build-stats.json
|
cat client-build-stats.json
|
||||||
cat scc.json
|
cat scc.json
|
||||||
|
cat lighthouse.json
|
||||||
|
|
||||||
- name: Upload stats
|
- name: Upload stats
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
@ -87,5 +95,5 @@ jobs:
|
||||||
|
|
||||||
if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
|
if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
|
||||||
echo "Uploading files"
|
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
|
fi
|
||||||
|
|
|
@ -44,7 +44,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
PGUSER: peertube
|
PGUSER: peertube
|
||||||
PGHOST: localhost
|
PGHOST: localhost
|
||||||
NODE_PENDING_JOB_WAIT: 500
|
NODE_PENDING_JOB_WAIT: 250
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
|
@ -15,7 +15,7 @@ yarn-error.log
|
||||||
/server/tests/fixtures/video_59fps.mp4
|
/server/tests/fixtures/video_59fps.mp4
|
||||||
|
|
||||||
# Production
|
# Production
|
||||||
/storage/
|
/storage
|
||||||
/config/production.yaml
|
/config/production.yaml
|
||||||
/config/local*
|
/config/local*
|
||||||
/ffmpeg/
|
/ffmpeg/
|
||||||
|
|
145
CHANGELOG.md
145
CHANGELOG.md
|
@ -1,5 +1,150 @@
|
||||||
# Changelog
|
# 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
|
## v3.1.0
|
||||||
|
|
||||||
### IMPORTANT NOTES
|
### IMPORTANT NOTES
|
||||||
|
|
32
CREDITS.md
32
CREDITS.md
|
@ -3,25 +3,27 @@
|
||||||
* Chocobozzz
|
* Chocobozzz
|
||||||
* Rigel Kent
|
* Rigel Kent
|
||||||
* Filip Bengtsson
|
* Filip Bengtsson
|
||||||
* kimsible
|
|
||||||
* josé m
|
* josé m
|
||||||
|
* kimsible
|
||||||
* Simon Brosdetzko
|
* Simon Brosdetzko
|
||||||
* Александр
|
* Александр
|
||||||
* Clemens Schielicke
|
|
||||||
* Berto Te
|
* Berto Te
|
||||||
|
* Clemens Schielicke
|
||||||
* Jeff Huang
|
* Jeff Huang
|
||||||
* kontrollanten
|
* kontrollanten
|
||||||
* Phongpanot
|
|
||||||
* Laurent Ettouati
|
* Laurent Ettouati
|
||||||
* Racida S
|
* Racida S
|
||||||
* Kim
|
* Phongpanot
|
||||||
* Marcin Mikołajczak
|
* Marcin Mikołajczak
|
||||||
|
* Kim
|
||||||
* Tirifto
|
* Tirifto
|
||||||
* Felix Ableitner
|
|
||||||
* Vodoyo Kamal
|
* Vodoyo Kamal
|
||||||
|
* Felix Ableitner
|
||||||
* Gérald Niel
|
* Gérald Niel
|
||||||
* Zet
|
* Duy
|
||||||
* GunChleoc
|
* GunChleoc
|
||||||
|
* Slimane Selyan AMIRI
|
||||||
|
* Zet
|
||||||
* x
|
* x
|
||||||
* Frank Sträter
|
* Frank Sträter
|
||||||
* Julien Maulny
|
* Julien Maulny
|
||||||
|
@ -29,12 +31,11 @@
|
||||||
* Jorropo
|
* Jorropo
|
||||||
* Josh Morel
|
* Josh Morel
|
||||||
* BO41
|
* BO41
|
||||||
* Slimane Selyan AMIRI
|
|
||||||
* Francesc
|
|
||||||
* mando laress
|
|
||||||
* Balázs Meskó
|
* Balázs Meskó
|
||||||
* Duy
|
* Francesc
|
||||||
* John Livingston
|
* John Livingston
|
||||||
|
* mando laress
|
||||||
|
* Eivind Ødegård
|
||||||
* Quentin PAGÈS
|
* Quentin PAGÈS
|
||||||
* Besnik Bleta
|
* Besnik Bleta
|
||||||
* Ihor Hordiichuk
|
* Ihor Hordiichuk
|
||||||
|
@ -53,7 +54,6 @@
|
||||||
* Thomas Citharel
|
* Thomas Citharel
|
||||||
* Agron Selimaj
|
* Agron Selimaj
|
||||||
* Benjamin Bouvier
|
* Benjamin Bouvier
|
||||||
* Eivind Ødegård
|
|
||||||
* Joe Bill
|
* Joe Bill
|
||||||
* Kemal Oktay Aktoğan
|
* Kemal Oktay Aktoğan
|
||||||
* Luc Didry
|
* Luc Didry
|
||||||
|
@ -66,6 +66,7 @@
|
||||||
* David Libeau
|
* David Libeau
|
||||||
* Ewald Arnold
|
* Ewald Arnold
|
||||||
* Florent F
|
* Florent F
|
||||||
|
* Florian CUNY
|
||||||
* Nassim Bounouas
|
* Nassim Bounouas
|
||||||
* NorbiPeti
|
* NorbiPeti
|
||||||
* Rafael Fontenelle
|
* Rafael Fontenelle
|
||||||
|
@ -81,7 +82,6 @@
|
||||||
* David Soh
|
* David Soh
|
||||||
* Dimitri Gilbert
|
* Dimitri Gilbert
|
||||||
* Florent Poinsaut
|
* Florent Poinsaut
|
||||||
* Florian CUNY
|
|
||||||
* Frank Chang
|
* Frank Chang
|
||||||
* Green-Star
|
* Green-Star
|
||||||
* Micah Elizabeth Scott
|
* Micah Elizabeth Scott
|
||||||
|
@ -94,6 +94,7 @@
|
||||||
* test2a
|
* test2a
|
||||||
* 路过是好事
|
* 路过是好事
|
||||||
* Ajeje Brazorf
|
* Ajeje Brazorf
|
||||||
|
* Andrey
|
||||||
* Angristan
|
* Angristan
|
||||||
* Ch
|
* Ch
|
||||||
* Chris Sakura 佐倉くりす on Youtube
|
* Chris Sakura 佐倉くりす on Youtube
|
||||||
|
@ -103,6 +104,7 @@
|
||||||
* Mildred
|
* Mildred
|
||||||
* Okhin
|
* Okhin
|
||||||
* Pierre-Alain TORET
|
* Pierre-Alain TORET
|
||||||
|
* Poslovitch
|
||||||
* Serge Victor
|
* Serge Victor
|
||||||
* Théo Le Calvar
|
* Théo Le Calvar
|
||||||
* Ugaitz
|
* Ugaitz
|
||||||
|
@ -115,7 +117,6 @@
|
||||||
* Ahsan Haris Ahmed
|
* Ahsan Haris Ahmed
|
||||||
* Alberto Teira
|
* Alberto Teira
|
||||||
* Aliaksandr Hrankin
|
* Aliaksandr Hrankin
|
||||||
* Andrey
|
|
||||||
* Andréas Livet
|
* Andréas Livet
|
||||||
* Andrés Maldonado
|
* Andrés Maldonado
|
||||||
* Arco
|
* Arco
|
||||||
|
@ -133,6 +134,7 @@
|
||||||
* Kiro
|
* Kiro
|
||||||
* LecygneNoir
|
* LecygneNoir
|
||||||
* Leopere
|
* Leopere
|
||||||
|
* Loukas Stamellos
|
||||||
* Lukas Winkler
|
* Lukas Winkler
|
||||||
* Manuel Viens
|
* Manuel Viens
|
||||||
* Manuela Silva
|
* Manuela Silva
|
||||||
|
@ -250,6 +252,7 @@
|
||||||
* Fabio Agreles Bezerra
|
* Fabio Agreles Bezerra
|
||||||
* Fernandez, ReK2
|
* Fernandez, ReK2
|
||||||
* Florent
|
* Florent
|
||||||
|
* Gabriel Scherer
|
||||||
* Glandos
|
* Glandos
|
||||||
* Guillaume Pérution-Kihli
|
* Guillaume Pérution-Kihli
|
||||||
* Gérald CHATAGNON
|
* Gérald CHATAGNON
|
||||||
|
@ -265,6 +268,7 @@
|
||||||
* Jacob
|
* Jacob
|
||||||
* Jacques Foucry
|
* Jacques Foucry
|
||||||
* Jagannath Bhat
|
* Jagannath Bhat
|
||||||
|
* Jan Prunk
|
||||||
* Janey Muñoz
|
* Janey Muñoz
|
||||||
* Jarosław Maciejewski
|
* Jarosław Maciejewski
|
||||||
* Jeena
|
* Jeena
|
||||||
|
@ -315,6 +319,7 @@
|
||||||
* PhieF
|
* PhieF
|
||||||
* Philip Durbin
|
* Philip Durbin
|
||||||
* Philipp Fischbeck
|
* Philipp Fischbeck
|
||||||
|
* Philo van Kemenade
|
||||||
* Pierre-Jean
|
* Pierre-Jean
|
||||||
* Predatorix Phoenix
|
* Predatorix Phoenix
|
||||||
* Quentin Dupont
|
* Quentin Dupont
|
||||||
|
@ -361,6 +366,7 @@
|
||||||
* bikepunk
|
* bikepunk
|
||||||
* bsky
|
* bsky
|
||||||
* ctlaltdefeat
|
* ctlaltdefeat
|
||||||
|
* decentral1se
|
||||||
* dingycle
|
* dingycle
|
||||||
* eduard pintilie
|
* eduard pintilie
|
||||||
* gillux
|
* gillux
|
||||||
|
|
25
README.md
25
README.md
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<p align=center>
|
<p align=center>
|
||||||
<strong><a href="https://joinpeertube.org">Website</a></strong>
|
<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="#package-create-your-own-instance">Create an instance</a></strong>
|
||||||
| <strong><a href="#contact">Chat with us</a></strong>
|
| <strong><a href="#contact">Chat with us</a></strong>
|
||||||
| <strong><a href="https://framasoft.org/en/#soutenir">Donate</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:
|
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,
|
To learn more:
|
||||||
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:
|
|
||||||
* 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
|
* 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)
|
* PeerTube's project homepage, [joinpeertube.org](https://joinpeertube.org)
|
||||||
* Demonstration instances:
|
* Demonstration instances:
|
||||||
* [peertube.cpy.re](https://peertube.cpy.re)
|
* [peertube.cpy.re](https://peertube.cpy.re) (stable)
|
||||||
* [peertube2.cpy.re](https://peertube2.cpy.re)
|
* [peertube2.cpy.re](https://peertube2.cpy.re) (Nightly)
|
||||||
* [peertube3.cpy.re](https://peertube3.cpy.re)
|
* [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)
|
* 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
|
: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"/>
|
<img src="https://lutim.cpy.re/AHbctLjn.png" align="left" height="300px"/>
|
||||||
<h3 align="left">Video streaming, even in live!</h3>
|
<h3 align="left">Video streaming, even in live!</h3>
|
||||||
<p align="left">
|
<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>).
|
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>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
:raised_hands: Contributing
|
: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:
|
You can also join the cheerful bunch that makes our community:
|
||||||
|
|
||||||
* Chat<a name="contact"></a>:
|
* 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)**
|
* 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:
|
* Forum:
|
||||||
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
|
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,12 @@
|
||||||
"rule-empty-line-before": null,
|
"rule-empty-line-before": null,
|
||||||
"selector-max-id": null,
|
"selector-max-id": null,
|
||||||
"scss/at-function-pattern": 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": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
"options": {
|
"options": {
|
||||||
"aot": true,
|
|
||||||
"localize": true,
|
"localize": true,
|
||||||
"outputPath": "dist",
|
"outputPath": "dist",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"baseHref": "/",
|
||||||
|
"deployUrl": "client/",
|
||||||
"stylePreprocessorOptions": {
|
"stylePreprocessorOptions": {
|
||||||
"includePaths": [
|
"includePaths": [
|
||||||
"src/sass/include"
|
"src/sass/include"
|
||||||
|
@ -151,39 +152,24 @@
|
||||||
"src/sass/application.scss"
|
"src/sass/application.scss"
|
||||||
],
|
],
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
"@angularclass/hmr",
|
"qrcode",
|
||||||
"debug",
|
|
||||||
"mousetrap",
|
|
||||||
"chart.js",
|
"chart.js",
|
||||||
"linkifyjs/html",
|
"htmlparser2",
|
||||||
"linkifyjs",
|
|
||||||
"markdown-it",
|
|
||||||
"markdown-it-emoji/light",
|
"markdown-it-emoji/light",
|
||||||
"sanitize-html",
|
"sanitize-html",
|
||||||
"socket.io-client",
|
"debug",
|
||||||
"socket.io-parser",
|
|
||||||
"@app/+about/about-peertube/about-peertube-contributors.component",
|
|
||||||
"path",
|
|
||||||
"video.js",
|
|
||||||
"p2p-media-loader-hlsjs",
|
"p2p-media-loader-hlsjs",
|
||||||
"videojs-hotkeys/videojs.hotkeys",
|
"video.js",
|
||||||
"p2p-media-loader-core",
|
"sha1",
|
||||||
"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",
|
|
||||||
"postcss"
|
"postcss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false,
|
||||||
|
"namedChunks": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
@ -191,7 +177,6 @@
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
"aot": true,
|
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
"vendorChunk": false,
|
"vendorChunk": false,
|
||||||
"buildOptimizer": true,
|
"buildOptimizer": true,
|
||||||
|
@ -251,8 +236,6 @@
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"baseHref": "/",
|
|
||||||
"deployUrl": "client/",
|
|
||||||
"browserTarget": "PeerTube:build",
|
"browserTarget": "PeerTube:build",
|
||||||
"proxyConfig": "proxy.config.json"
|
"proxyConfig": "proxy.config.json"
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,7 +26,12 @@ export class VideoUploadPage {
|
||||||
await elem.sendKeys(fileToUpload)
|
await elem.sendKeys(fileToUpload)
|
||||||
|
|
||||||
// Wait for the upload to finish
|
// 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) {
|
async validSecondUploadStep (videoName: string) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "peertube-client",
|
"name": "peertube-client",
|
||||||
"version": "3.1.0",
|
"version": "3.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -29,20 +29,20 @@
|
||||||
"@types/mousetrap": "1.6.3"
|
"@types/mousetrap": "1.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^0.1102.2",
|
"@angular-devkit/build-angular": "^12.0.0",
|
||||||
"@angular/animations": "^11.1.1",
|
"@angular/animations": "^12.0.0",
|
||||||
"@angular/cdk": "^11.0.0",
|
"@angular/cdk": "^12.0.0",
|
||||||
"@angular/cli": "^11.1.2",
|
"@angular/cli": "^12.0.0",
|
||||||
"@angular/common": "^11.1.1",
|
"@angular/common": "^12.0.0",
|
||||||
"@angular/compiler": "^11.1.1",
|
"@angular/compiler": "^12.0.0",
|
||||||
"@angular/compiler-cli": "^11.1.1",
|
"@angular/compiler-cli": "^12.0.0",
|
||||||
"@angular/core": "^11.1.1",
|
"@angular/core": "^12.0.0",
|
||||||
"@angular/forms": "^11.1.1",
|
"@angular/forms": "^12.0.0",
|
||||||
"@angular/localize": "^11.1.1",
|
"@angular/localize": "^12.0.0",
|
||||||
"@angular/platform-browser": "^11.1.1",
|
"@angular/platform-browser": "^12.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^11.1.1",
|
"@angular/platform-browser-dynamic": "^12.0.0",
|
||||||
"@angular/router": "^11.1.1",
|
"@angular/router": "^12.0.0",
|
||||||
"@angular/service-worker": "^11.1.1",
|
"@angular/service-worker": "^12.0.0",
|
||||||
"@neos21/bootstrap3-glyphicons": "^1.0.1",
|
"@neos21/bootstrap3-glyphicons": "^1.0.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^9.0.2",
|
"@ng-bootstrap/ng-bootstrap": "^9.0.2",
|
||||||
"@ng-select/ng-select": "^6.0.0",
|
"@ng-select/ng-select": "^6.0.0",
|
||||||
|
@ -51,7 +51,6 @@
|
||||||
"@ngx-loading-bar/core": "^5.0.0",
|
"@ngx-loading-bar/core": "^5.0.0",
|
||||||
"@ngx-loading-bar/http-client": "^5.0.0",
|
"@ngx-loading-bar/http-client": "^5.0.0",
|
||||||
"@ngx-loading-bar/router": "^5.0.0",
|
"@ngx-loading-bar/router": "^5.0.0",
|
||||||
"@ngx-meta/core": "^9.0.0",
|
|
||||||
"@types/chart.js": "^2.9.16",
|
"@types/chart.js": "^2.9.16",
|
||||||
"@types/core-js": "^2.5.2",
|
"@types/core-js": "^2.5.2",
|
||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
|
@ -70,19 +69,19 @@
|
||||||
"angular2-hotkeys": "^2.1.2",
|
"angular2-hotkeys": "^2.1.2",
|
||||||
"angularx-qrcode": "11.0.0",
|
"angularx-qrcode": "11.0.0",
|
||||||
"bootstrap": "^4.1.3",
|
"bootstrap": "^4.1.3",
|
||||||
"buffer": "^6.0.2",
|
"buffer": "^6.0.3",
|
||||||
"cache-chunk-store": "^3.0.0",
|
"cache-chunk-store": "^3.0.0",
|
||||||
"chart.js": "^2.9.3",
|
"chart.js": "^2.9.3",
|
||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"core-js": "^3.1.4",
|
"core-js": "^3.1.4",
|
||||||
"css-loader": "^5.0.1",
|
"css-loader": "^5.2.6",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"dexie": "^3.0.0",
|
"dexie": "^3.0.0",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"focus-visible": "^5.0.2",
|
"focus-visible": "^5.0.2",
|
||||||
"hls.js": "^0.14.16",
|
"hls.js": "^0.14.16",
|
||||||
"html-loader": "^1.0.0",
|
"html-loader": "^2.1.2",
|
||||||
"html-webpack-plugin": "^4.0.3",
|
"html-webpack-plugin": "^5.3.1",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
"jasmine-core": "~3.7.1",
|
"jasmine-core": "~3.7.1",
|
||||||
"jasmine-spec-reporter": "~7.0.0",
|
"jasmine-spec-reporter": "~7.0.0",
|
||||||
|
@ -95,42 +94,42 @@
|
||||||
"linkifyjs": "^2.1.5",
|
"linkifyjs": "^2.1.5",
|
||||||
"lodash-es": "^4.17.4",
|
"lodash-es": "^4.17.4",
|
||||||
"markdown-it": "12.0.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",
|
"p2p-media-loader-hlsjs": "^0.6.2",
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
"primeng": "^11.0.0-rc.1",
|
"primeng": "^12.0.0-rc.1",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"protractor": "~7.0.0",
|
"protractor": "~7.0.0",
|
||||||
"purify-css": "^1.2.5",
|
"purify-css": "^1.2.5",
|
||||||
"raw-loader": "^4.0.0",
|
"raw-loader": "^4.0.0",
|
||||||
"rxjs": "^6.5.2",
|
"rxjs": "^6.5.2",
|
||||||
"sanitize-html": "^2.1.2",
|
"sanitize-html": "^2.1.2",
|
||||||
"sass": "^1.29.0",
|
"sass": "^1.34.0",
|
||||||
"sass-loader": "^10",
|
"sass-loader": "^11.1.1",
|
||||||
"sass-resources-loader": "^2.0.0",
|
|
||||||
"sha.js": "^2.4.11",
|
"sha.js": "^2.4.11",
|
||||||
"socket.io-client": "^4.0.1",
|
"socket.io-client": "^4.0.1",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"stream-http": "^3.0.0",
|
"stream-http": "^3.0.0",
|
||||||
"stylelint": "^13.13.0",
|
"stylelint": "^13.13.0",
|
||||||
"stylelint-config-sass-guidelines": "^8.0.0",
|
"stylelint-config-sass-guidelines": "^8.0.0",
|
||||||
"terser-webpack-plugin": "^4",
|
"terser-webpack-plugin": "^5.1.2",
|
||||||
"ts-loader": "^8.0.14",
|
"ts-loader": "^9.2.2",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"tslint": "~6.1.0",
|
"tslint": "~6.1.0",
|
||||||
"tslint-angular": "^3.0.2",
|
"tslint-angular": "^3.0.2",
|
||||||
"tslint-config-standard": "^9.0.0",
|
"tslint-config-standard": "^9.0.0",
|
||||||
"typescript": "~4.1",
|
"typescript": "~4.2.4",
|
||||||
"video.js": "^7",
|
"video.js": "^7",
|
||||||
"videojs-contextmenu-pt": "^5.4.1",
|
"videojs-contextmenu-pt": "^5.4.1",
|
||||||
"videojs-contrib-quality-levels": "^2.0.9",
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
"videojs-dock": "^2.0.2",
|
"videojs-dock": "^2.0.2",
|
||||||
"videojs-hotkeys": "^0.2.27",
|
"videojs-hotkeys": "^0.2.27",
|
||||||
"videostream": "~3.2.1",
|
"videostream": "~3.2.1",
|
||||||
"webpack-bundle-analyzer": "^4.1.0",
|
"webpack-bundle-analyzer": "^4.4.2",
|
||||||
"webpack-cli": "^4.2.0",
|
"webpack-cli": "^4.7.0",
|
||||||
"webtorrent": "^0.116.1",
|
"webtorrent": "^0.116.1",
|
||||||
"whatwg-fetch": "^3.0.0",
|
"whatwg-fetch": "^3.0.0",
|
||||||
"zone.js": "~0.11.3"
|
"zone.js": "~0.11.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{{ follower}}
|
{{ follower}}
|
||||||
</a>
|
</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>
|
||||||
|
|
||||||
<div class="col-xl-6 col-md-12">
|
<div class="col-xl-6 col-md-12">
|
||||||
|
|
|
@ -14,6 +14,6 @@ export class AboutPeertubeContributorsComponent implements OnInit {
|
||||||
constructor (private markdownService: MarkdownService) { }
|
constructor (private markdownService: MarkdownService) { }
|
||||||
|
|
||||||
async ngOnInit () {
|
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 { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
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 { 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 { 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 = [
|
const aboutRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: AboutComponent,
|
component: AboutComponent,
|
||||||
canActivateChild: [ MetaGuard ],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|
|
@ -36,6 +36,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@include peertube-word-wrap;
|
||||||
|
|
||||||
color: pvar(--mainForegroundColor);
|
color: pvar(--mainForegroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,13 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMoreChannels () {
|
loadMoreChannels () {
|
||||||
this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination)
|
const options = {
|
||||||
|
account: this.account,
|
||||||
|
componentPagination: this.channelPagination,
|
||||||
|
sort: '-updatedAt'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.videoChannelService.listAccountVideoChannels(options)
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(res => this.channelPagination.totalItems = res.total),
|
tap(res => this.channelPagination.totalItems = res.total),
|
||||||
switchMap(res => from(res.data)),
|
switchMap(res => from(res.data)),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { AccountSearchComponent } from './account-search/account-search.component'
|
import { AccountSearchComponent } from './account-search/account-search.component'
|
||||||
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
|
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
|
||||||
import { AccountVideosComponent } from './account-videos/account-videos.component'
|
import { AccountVideosComponent } from './account-videos/account-videos.component'
|
||||||
|
@ -14,7 +13,6 @@ const accountsRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: ':accountId',
|
path: ':accountId',
|
||||||
component: AccountsComponent,
|
component: AccountsComponent,
|
||||||
canActivateChild: [ MetaGuard ],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|
|
@ -66,7 +66,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
switchMap(accountId => this.accountService.getAccount(accountId)),
|
switchMap(accountId => this.accountService.getAccount(accountId)),
|
||||||
tap(account => this.onAccount(account)),
|
tap(account => this.onAccount(account)),
|
||||||
switchMap(account => this.videoChannelService.listAccountVideoChannels(account)),
|
switchMap(account => this.videoChannelService.listAccountVideoChannels({ account })),
|
||||||
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [
|
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [
|
||||||
HttpStatusCode.BAD_REQUEST_400,
|
HttpStatusCode.BAD_REQUEST_400,
|
||||||
HttpStatusCode.NOT_FOUND_404
|
HttpStatusCode.NOT_FOUND_404
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { ConfigRoutes } from '@app/+admin/config'
|
||||||
import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes'
|
import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes'
|
||||||
import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes'
|
import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes'
|
||||||
import { SystemRoutes } from '@app/+admin/system'
|
import { SystemRoutes } from '@app/+admin/system'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { AdminComponent } from './admin.component'
|
import { AdminComponent } from './admin.component'
|
||||||
import { FollowsRoutes } from './follows'
|
import { FollowsRoutes } from './follows'
|
||||||
import { UsersRoutes } from './users'
|
import { UsersRoutes } from './users'
|
||||||
|
@ -13,8 +12,6 @@ const adminRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: AdminComponent,
|
component: AdminComponent,
|
||||||
canActivate: [ MetaGuard ],
|
|
||||||
canActivateChild: [ MetaGuard ],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|
|
@ -4,12 +4,13 @@ import { TableModule } from 'primeng/table'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
|
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
|
||||||
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
|
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 { SharedFormModule } from '@app/shared/shared-forms'
|
||||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||||
import { SharedMainModule } from '@app/shared/shared-main'
|
import { SharedMainModule } from '@app/shared/shared-main'
|
||||||
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
||||||
import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
|
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 { AdminRoutingModule } from './admin-routing.module'
|
||||||
import { AdminComponent } from './admin.component'
|
import { AdminComponent } from './admin.component'
|
||||||
import {
|
import {
|
||||||
|
@ -18,6 +19,7 @@ import {
|
||||||
EditBasicConfigurationComponent,
|
EditBasicConfigurationComponent,
|
||||||
EditConfigurationService,
|
EditConfigurationService,
|
||||||
EditCustomConfigComponent,
|
EditCustomConfigComponent,
|
||||||
|
EditHomepageComponent,
|
||||||
EditInstanceInformationComponent,
|
EditInstanceInformationComponent,
|
||||||
EditLiveConfigurationComponent,
|
EditLiveConfigurationComponent,
|
||||||
EditVODTranscodingComponent
|
EditVODTranscodingComponent
|
||||||
|
@ -53,6 +55,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
||||||
SharedVideoCommentModule,
|
SharedVideoCommentModule,
|
||||||
SharedActorImageModule,
|
SharedActorImageModule,
|
||||||
SharedActorImageEditModule,
|
SharedActorImageEditModule,
|
||||||
|
SharedCustomMarkupModule,
|
||||||
|
|
||||||
TableModule,
|
TableModule,
|
||||||
SelectButtonModule,
|
SelectButtonModule,
|
||||||
|
@ -100,7 +103,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
||||||
EditVODTranscodingComponent,
|
EditVODTranscodingComponent,
|
||||||
EditLiveConfigurationComponent,
|
EditLiveConfigurationComponent,
|
||||||
EditAdvancedConfigurationComponent,
|
EditAdvancedConfigurationComponent,
|
||||||
EditInstanceInformationComponent
|
EditInstanceInformationComponent,
|
||||||
|
EditHomepageComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
@ -26,22 +26,13 @@
|
||||||
<div class="form-group" formGroupName="instance">
|
<div class="form-group" formGroupName="instance">
|
||||||
<label i18n for="instanceDefaultClientRoute">Landing page</label>
|
<label i18n for="instanceDefaultClientRoute">Landing page</label>
|
||||||
|
|
||||||
<div class="peertube-select-container">
|
<my-select-custom-value
|
||||||
<select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
|
id="instanceDefaultClientRoute"
|
||||||
<option i18n value="/videos/overview">Discover videos</option>
|
[items]="defaultLandingPageOptions"
|
||||||
|
formControlName="defaultClientRoute"
|
||||||
<optgroup i18n-label label="Trending pages">
|
inputType="text"
|
||||||
<option i18n value="/videos/trending">Default trending page</option>
|
[clearable]="false"
|
||||||
<option i18n value="/videos/trending?alg=best" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('best')">Best videos</option>
|
></my-select-custom-value>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
|
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
|
||||||
import { pairwise } from 'rxjs/operators'
|
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 { FormGroup } from '@angular/forms'
|
||||||
|
import { MenuService } from '@app/core'
|
||||||
import { ServerConfig } from '@shared/models'
|
import { ServerConfig } from '@shared/models'
|
||||||
import { ConfigService } from '../shared/config.service'
|
import { ConfigService } from '../shared/config.service'
|
||||||
|
|
||||||
|
@ -10,22 +12,31 @@ import { ConfigService } from '../shared/config.service'
|
||||||
templateUrl: './edit-basic-configuration.component.html',
|
templateUrl: './edit-basic-configuration.component.html',
|
||||||
styleUrls: [ './edit-custom-config.component.scss' ]
|
styleUrls: [ './edit-custom-config.component.scss' ]
|
||||||
})
|
})
|
||||||
export class EditBasicConfigurationComponent implements OnInit {
|
export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
||||||
@Input() form: FormGroup
|
@Input() form: FormGroup
|
||||||
@Input() formErrors: any
|
@Input() formErrors: any
|
||||||
|
|
||||||
@Input() serverConfig: ServerConfig
|
@Input() serverConfig: ServerConfig
|
||||||
|
|
||||||
signupAlertMessage: string
|
signupAlertMessage: string
|
||||||
|
defaultLandingPageOptions: SelectOptionsItem[] = []
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private configService: ConfigService
|
private configService: ConfigService,
|
||||||
|
private menuService: MenuService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
this.buildLandingPageOptions()
|
||||||
this.checkSignupField()
|
this.checkSignupField()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnChanges (changes: SimpleChanges) {
|
||||||
|
if (changes['serverConfig']) {
|
||||||
|
this.buildLandingPageOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getVideoQuotaOptions () {
|
getVideoQuotaOptions () {
|
||||||
return this.configService.videoQuotaOptions
|
return this.configService.videoQuotaOptions
|
||||||
}
|
}
|
||||||
|
@ -70,6 +81,15 @@ export class EditBasicConfigurationComponent implements OnInit {
|
||||||
return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
|
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 () {
|
private checkSignupField () {
|
||||||
const signupControl = this.form.get('signup.enabled')
|
const signupControl = this.form.get('signup.enabled')
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,16 @@
|
||||||
|
|
||||||
<div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs">
|
<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">
|
<ng-container ngbNavItem="instance-information">
|
||||||
<a ngbNavLink i18n>Instance information</a>
|
<a ngbNavLink i18n>Information</a>
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
|
<my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
|
||||||
|
@ -13,7 +21,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngbNavItem="basic-configuration">
|
<ng-container ngbNavItem="basic-configuration">
|
||||||
<a ngbNavLink i18n>Basic configuration</a>
|
<a ngbNavLink i18n>Basic</a>
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
<my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
|
||||||
|
@ -40,7 +48,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngbNavItem="advanced-configuration">
|
<ng-container ngbNavItem="advanced-configuration">
|
||||||
<a ngbNavLink i18n>Advanced configuration</a>
|
<a ngbNavLink i18n>Advanced</a>
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
|
<my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
|
import omit from 'lodash-es/omit'
|
||||||
import { forkJoin } from 'rxjs'
|
import { forkJoin } from 'rxjs'
|
||||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
@ -24,9 +25,14 @@ import {
|
||||||
} from '@app/shared/form-validators/custom-config-validators'
|
} 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 { 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 { 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'
|
import { EditConfigurationService } from './edit-configuration.service'
|
||||||
|
|
||||||
|
type ComponentCustomConfig = CustomConfig & {
|
||||||
|
instanceCustomHomepage: CustomPage
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-edit-custom-config',
|
selector: 'my-edit-custom-config',
|
||||||
templateUrl: './edit-custom-config.component.html',
|
templateUrl: './edit-custom-config.component.html',
|
||||||
|
@ -35,9 +41,11 @@ import { EditConfigurationService } from './edit-configuration.service'
|
||||||
export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
activeNav: string
|
activeNav: string
|
||||||
|
|
||||||
customConfig: CustomConfig
|
customConfig: ComponentCustomConfig
|
||||||
serverConfig: ServerConfig
|
serverConfig: ServerConfig
|
||||||
|
|
||||||
|
homepage: CustomPage
|
||||||
|
|
||||||
languageItems: SelectOptionsItem[] = []
|
languageItems: SelectOptionsItem[] = []
|
||||||
categoryItems: SelectOptionsItem[] = []
|
categoryItems: SelectOptionsItem[] = []
|
||||||
|
|
||||||
|
@ -47,6 +55,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private customPage: CustomPageService,
|
||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
private editConfigurationService: EditConfigurationService
|
private editConfigurationService: EditConfigurationService
|
||||||
) {
|
) {
|
||||||
|
@ -56,11 +65,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.serverConfig = this.serverService.getTmpConfig()
|
this.serverConfig = this.serverService.getTmpConfig()
|
||||||
this.serverService.getConfig()
|
this.serverService.getConfig()
|
||||||
.subscribe(config => {
|
.subscribe(config => this.serverConfig = config)
|
||||||
this.serverConfig = config
|
|
||||||
})
|
|
||||||
|
|
||||||
const formGroupData: { [key in keyof CustomConfig ]: any } = {
|
const formGroupData: { [key in keyof ComponentCustomConfig ]: any } = {
|
||||||
instance: {
|
instance: {
|
||||||
name: INSTANCE_NAME_VALIDATOR,
|
name: INSTANCE_NAME_VALIDATOR,
|
||||||
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
|
||||||
|
@ -215,6 +222,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
disableLocalSearch: null,
|
disableLocalSearch: null,
|
||||||
isDefaultSearch: null
|
isDefaultSearch: null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
instanceCustomHomepage: {
|
||||||
|
content: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,15 +261,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async formValidated () {
|
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(
|
.subscribe(
|
||||||
res => {
|
([ resConfig ]) => {
|
||||||
this.customConfig = res
|
const instanceCustomHomepage = {
|
||||||
|
content: value.instanceCustomHomepage.content
|
||||||
|
}
|
||||||
|
|
||||||
|
this.customConfig = { ...resConfig, instanceCustomHomepage }
|
||||||
|
|
||||||
// Reload general configuration
|
// Reload general configuration
|
||||||
this.serverService.resetConfig()
|
this.serverService.resetConfig()
|
||||||
|
.subscribe(config => this.serverConfig = config)
|
||||||
|
|
||||||
this.updateForm()
|
this.updateForm()
|
||||||
|
|
||||||
|
@ -317,9 +336,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadConfigAndUpdateForm () {
|
private loadConfigAndUpdateForm () {
|
||||||
this.configService.getCustomConfig()
|
forkJoin([
|
||||||
.subscribe(config => {
|
this.configService.getCustomConfig(),
|
||||||
this.customConfig = config
|
this.customPage.getInstanceHomepage()
|
||||||
|
])
|
||||||
|
.subscribe(([ config, homepage ]) => {
|
||||||
|
this.customConfig = { ...config, instanceCustomHomepage: homepage }
|
||||||
|
|
||||||
this.updateForm()
|
this.updateForm()
|
||||||
// Force form validation
|
// 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-basic-configuration.component'
|
||||||
export * from './edit-configuration.service'
|
export * from './edit-configuration.service'
|
||||||
export * from './edit-custom-config.component'
|
export * from './edit-custom-config.component'
|
||||||
|
export * from './edit-homepage.component'
|
||||||
export * from './edit-instance-information.component'
|
export * from './edit-instance-information.component'
|
||||||
export * from './edit-live-configuration.component'
|
export * from './edit-live-configuration.component'
|
||||||
export * from './edit-vod-transcoding.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 { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
|
||||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||||
import { compareSemVer } from '@shared/core-utils/miscs/miscs'
|
import { compareSemVer } from '@shared/core-utils/miscs/miscs'
|
||||||
import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
|
import { PeerTubePlugin, PluginType } from '@shared/models'
|
||||||
import { PluginType } from '@shared/models/plugins/plugin.type'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-plugin-list-installed',
|
selector: 'my-plugin-list-installed',
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<my-global-icon iconName="search"></my-global-icon>
|
<my-global-icon iconName="search"></my-global-icon>
|
||||||
|
|
||||||
<ng-container i18n>
|
<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>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,8 +4,7 @@ import { Component, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
||||||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
|
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
|
||||||
import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
|
import { PeerTubePluginIndex, PluginType } from '@shared/models'
|
||||||
import { PluginType } from '@shared/models/plugins/plugin.type'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-plugin-search',
|
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.videoQuota = parseInt(this.form.value['videoQuota'], 10)
|
||||||
userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 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.userService.updateUser(this.user.id, userUpdate).subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success($localize`User ${this.user.username} updated.`)
|
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 { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
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 { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service'
|
||||||
|
import { LoginComponent } from './login.component'
|
||||||
|
|
||||||
const loginRoutes: Routes = [
|
const loginRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: LoginComponent,
|
component: LoginComponent,
|
||||||
canActivate: [ MetaGuard ],
|
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: $localize`Login`
|
title: $localize`Login`
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { LoginGuard } from '../core'
|
import { LoginGuard } from '../core'
|
||||||
import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
|
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 { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
|
||||||
import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
|
import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
|
||||||
import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
|
import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
|
||||||
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
|
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
|
||||||
import { MyAccountComponent } from './my-account.component'
|
import { MyAccountComponent } from './my-account.component'
|
||||||
import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
|
|
||||||
|
|
||||||
const myAccountRoutes: Routes = [
|
const myAccountRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: MyAccountComponent,
|
component: MyAccountComponent,
|
||||||
canActivateChild: [ MetaGuard, LoginGuard ],
|
canActivateChild: [ LoginGuard ],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common'
|
||||||
import { HttpErrorResponse } from '@angular/common/http'
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
import { AfterViewChecked, Component, OnInit } from '@angular/core'
|
import { AfterViewChecked, Component, OnInit } from '@angular/core'
|
||||||
import { AuthService, Notifier, User, UserService } from '@app/core'
|
import { AuthService, Notifier, User, UserService } from '@app/core'
|
||||||
import { uploadErrorHandler } from '@app/helpers'
|
import { genericUploadErrorHandler } from '@app/helpers'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-settings',
|
selector: 'my-account-settings',
|
||||||
|
@ -46,7 +46,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked {
|
||||||
this.user.updateAccountAvatar(data.avatar)
|
this.user.updateAccountAvatar(data.avatar)
|
||||||
},
|
},
|
||||||
|
|
||||||
(err: HttpErrorResponse) => uploadErrorHandler({
|
(err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||||
err,
|
err,
|
||||||
name: $localize`avatar`,
|
name: $localize`avatar`,
|
||||||
notifier: this.notifier
|
notifier: this.notifier
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||||
import { uploadErrorHandler } from '@app/helpers'
|
import { genericUploadErrorHandler } from '@app/helpers'
|
||||||
import {
|
import {
|
||||||
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
|
||||||
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
|
||||||
|
@ -109,7 +109,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
|
||||||
this.videoChannel.updateAvatar(data.avatar)
|
this.videoChannel.updateAvatar(data.avatar)
|
||||||
},
|
},
|
||||||
|
|
||||||
(err: HttpErrorResponse) => uploadErrorHandler({
|
(err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||||
err,
|
err,
|
||||||
name: $localize`avatar`,
|
name: $localize`avatar`,
|
||||||
notifier: this.notifier
|
notifier: this.notifier
|
||||||
|
@ -139,7 +139,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
|
||||||
this.videoChannel.updateBanner(data.banner)
|
this.videoChannel.updateBanner(data.banner)
|
||||||
},
|
},
|
||||||
|
|
||||||
(err: HttpErrorResponse) => uploadErrorHandler({
|
(err: HttpErrorResponse) => genericUploadErrorHandler({
|
||||||
err,
|
err,
|
||||||
name: $localize`banner`,
|
name: $localize`banner`,
|
||||||
notifier: this.notifier
|
notifier: this.notifier
|
||||||
|
|
|
@ -68,8 +68,14 @@ channel with the same name (${videoChannel.name})!`,
|
||||||
this.authService.userInformationLoaded
|
this.authService.userInformationLoaded
|
||||||
.pipe(mergeMap(() => {
|
.pipe(mergeMap(() => {
|
||||||
const user = this.authService.getUser()
|
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 => {
|
})).subscribe(res => {
|
||||||
this.videoChannels = res.data
|
this.videoChannels = res.data
|
||||||
this.totalItems = res.total
|
this.totalItems = res.total
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { LoginGuard } from '../core'
|
import { LoginGuard } from '../core'
|
||||||
import { MyHistoryComponent } from './my-history/my-history.component'
|
import { MyHistoryComponent } from './my-history/my-history.component'
|
||||||
import { MyLibraryComponent } from './my-library.component'
|
import { MyLibraryComponent } from './my-library.component'
|
||||||
|
@ -17,7 +16,7 @@ const myLibraryRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: MyLibraryComponent,
|
component: MyLibraryComponent,
|
||||||
canActivateChild: [ MetaGuard, LoginGuard ],
|
canActivateChild: [ LoginGuard ],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|
|
@ -8,13 +8,8 @@
|
||||||
<div class="modal-body" [formGroup]="form">
|
<div class="modal-body" [formGroup]="form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="channel">Select a channel to receive the video</label>
|
<label i18n for="channel">Select a channel to receive the video</label>
|
||||||
<div class="peertube-select-container">
|
<my-select-channel labelForId="channel" formControlName="channel" [items]="videoChannels"></my-select-channel>
|
||||||
<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>
|
|
||||||
<div *ngIf="formErrors.channel" class="form-error">{{ formErrors.channel }}</div>
|
<div *ngIf="formErrors.channel" class="form-error">{{ formErrors.channel }}</div>
|
||||||
</div>
|
</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 { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { AuthService, Notifier } from '@app/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 { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
|
||||||
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { VideoChangeOwnership, VideoChannel } from '@shared/models'
|
import { VideoChangeOwnership } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-accept-ownership',
|
selector: 'my-accept-ownership',
|
||||||
|
@ -18,8 +19,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
|
||||||
@ViewChild('modal', { static: true }) modal: ElementRef
|
@ViewChild('modal', { static: true }) modal: ElementRef
|
||||||
|
|
||||||
videoChangeOwnership: VideoChangeOwnership | undefined = undefined
|
videoChangeOwnership: VideoChangeOwnership | undefined = undefined
|
||||||
|
videoChannels: SelectChannelItem[]
|
||||||
videoChannels: VideoChannel[]
|
|
||||||
|
|
||||||
error: string = null
|
error: string = null
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
|
||||||
private videoOwnershipService: VideoOwnershipService,
|
private videoOwnershipService: VideoOwnershipService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private videoChannelService: VideoChannelService,
|
|
||||||
private modalService: NgbModal
|
private modalService: NgbModal
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -37,9 +36,8 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.videoChannels = []
|
this.videoChannels = []
|
||||||
|
|
||||||
this.authService.userInformationLoaded
|
listUserChannels(this.authService)
|
||||||
.pipe(switchMap(() => this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account)))
|
.subscribe(channels => this.videoChannels = channels)
|
||||||
.subscribe(videoChannels => this.videoChannels = videoChannels.data)
|
|
||||||
|
|
||||||
this.buildForm({
|
this.buildForm({
|
||||||
channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR
|
channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { ResetPasswordComponent } from './reset-password.component'
|
import { ResetPasswordComponent } from './reset-password.component'
|
||||||
|
|
||||||
const resetPasswordRoutes: Routes = [
|
const resetPasswordRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: ResetPasswordComponent,
|
component: ResetPasswordComponent,
|
||||||
canActivate: [ MetaGuard ],
|
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: `Reset password`
|
title: $localize`Reset password`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,25 @@
|
||||||
</div>
|
</div>
|
||||||
</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="form-group">
|
||||||
<div class="radio-label label-container">
|
<div class="radio-label label-container">
|
||||||
<label i18n>Display sensitive content</label>
|
<label i18n>Display sensitive content</label>
|
||||||
|
@ -44,7 +63,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="peertube-radio-container" *ngFor="let date of publishedDateRanges">
|
<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>
|
<label [for]="date.id" class="radio">{{ date.label }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,7 +79,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="pl-0 col-sm-6">
|
<div class="pl-0 col-sm-6">
|
||||||
<input
|
<input
|
||||||
(change)="inputUpdated()"
|
(change)="onInputUpdated()"
|
||||||
(keydown.enter)="$event.preventDefault()"
|
(keydown.enter)="$event.preventDefault()"
|
||||||
type="text" id="original-publication-after" name="original-publication-after"
|
type="text" id="original-publication-after" name="original-publication-after"
|
||||||
i18n-placeholder placeholder="After..."
|
i18n-placeholder placeholder="After..."
|
||||||
|
@ -70,7 +89,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="pr-0 col-sm-6">
|
<div class="pr-0 col-sm-6">
|
||||||
<input
|
<input
|
||||||
(change)="inputUpdated()"
|
(change)="onInputUpdated()"
|
||||||
(keydown.enter)="$event.preventDefault()"
|
(keydown.enter)="$event.preventDefault()"
|
||||||
type="text" id="original-publication-before" name="original-publication-before"
|
type="text" id="original-publication-before" name="original-publication-before"
|
||||||
i18n-placeholder placeholder="Before..."
|
i18n-placeholder placeholder="Before..."
|
||||||
|
@ -93,7 +112,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="peertube-radio-container" *ngFor="let duration of durationRanges">
|
<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>
|
<label [for]="duration.id" class="radio">{{ duration.label }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { ServerService } from '@app/core'
|
||||||
import { AdvancedSearch } from '@app/shared/shared-search'
|
import { AdvancedSearch } from '@app/shared/shared-search'
|
||||||
import { ServerConfig, VideoConstant } from '@shared/models'
|
import { ServerConfig, VideoConstant } from '@shared/models'
|
||||||
|
|
||||||
|
type FormOption = { id: string, label: string }
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-search-filters',
|
selector: 'my-search-filters',
|
||||||
styleUrls: [ './search-filters.component.scss' ],
|
styleUrls: [ './search-filters.component.scss' ],
|
||||||
|
@ -17,9 +19,10 @@ export class SearchFiltersComponent implements OnInit {
|
||||||
videoLicences: VideoConstant<number>[] = []
|
videoLicences: VideoConstant<number>[] = []
|
||||||
videoLanguages: VideoConstant<string>[] = []
|
videoLanguages: VideoConstant<string>[] = []
|
||||||
|
|
||||||
publishedDateRanges: { id: string, label: string }[] = []
|
publishedDateRanges: FormOption[] = []
|
||||||
sorts: { id: string, label: string }[] = []
|
sorts: FormOption[] = []
|
||||||
durationRanges: { id: string, label: string }[] = []
|
durationRanges: FormOption[] = []
|
||||||
|
videoType: FormOption[] = []
|
||||||
|
|
||||||
publishedDateRange: string
|
publishedDateRange: string
|
||||||
durationRange: string
|
durationRange: string
|
||||||
|
@ -33,10 +36,6 @@ export class SearchFiltersComponent implements OnInit {
|
||||||
private serverService: ServerService
|
private serverService: ServerService
|
||||||
) {
|
) {
|
||||||
this.publishedDateRanges = [
|
this.publishedDateRanges = [
|
||||||
{
|
|
||||||
id: 'any_published_date',
|
|
||||||
label: $localize`Any`
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'today',
|
id: 'today',
|
||||||
label: $localize`Today`
|
label: $localize`Today`
|
||||||
|
@ -55,11 +54,18 @@ export class SearchFiltersComponent implements OnInit {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
this.durationRanges = [
|
this.videoType = [
|
||||||
{
|
{
|
||||||
id: 'any_duration',
|
id: 'vod',
|
||||||
label: $localize`Any`
|
label: $localize`VOD videos`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'live',
|
||||||
|
label: $localize`Live videos`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
this.durationRanges = [
|
||||||
{
|
{
|
||||||
id: 'short',
|
id: 'short',
|
||||||
label: $localize`Short (< 4 min)`
|
label: $localize`Short (< 4 min)`
|
||||||
|
@ -104,24 +110,26 @@ export class SearchFiltersComponent implements OnInit {
|
||||||
this.loadOriginallyPublishedAtYears()
|
this.loadOriginallyPublishedAtYears()
|
||||||
}
|
}
|
||||||
|
|
||||||
inputUpdated () {
|
onInputUpdated () {
|
||||||
this.updateModelFromDurationRange()
|
this.updateModelFromDurationRange()
|
||||||
this.updateModelFromPublishedRange()
|
this.updateModelFromPublishedRange()
|
||||||
this.updateModelFromOriginallyPublishedAtYears()
|
this.updateModelFromOriginallyPublishedAtYears()
|
||||||
}
|
}
|
||||||
|
|
||||||
formUpdated () {
|
formUpdated () {
|
||||||
this.inputUpdated()
|
this.onInputUpdated()
|
||||||
this.filtered.emit(this.advancedSearch)
|
this.filtered.emit(this.advancedSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
reset () {
|
reset () {
|
||||||
this.advancedSearch.reset()
|
this.advancedSearch.reset()
|
||||||
|
|
||||||
|
this.resetOriginalPublicationYears()
|
||||||
|
|
||||||
this.durationRange = undefined
|
this.durationRange = undefined
|
||||||
this.publishedDateRange = undefined
|
this.publishedDateRange = undefined
|
||||||
this.originallyPublishedStartYear = undefined
|
|
||||||
this.originallyPublishedEndYear = undefined
|
this.onInputUpdated()
|
||||||
this.inputUpdated()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetField (fieldName: string, value?: any) {
|
resetField (fieldName: string, value?: any) {
|
||||||
|
@ -130,7 +138,7 @@ export class SearchFiltersComponent implements OnInit {
|
||||||
|
|
||||||
resetLocalField (fieldName: string, value?: any) {
|
resetLocalField (fieldName: string, value?: any) {
|
||||||
this[fieldName] = value
|
this[fieldName] = value
|
||||||
this.inputUpdated()
|
this.onInputUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
resetOriginalPublicationYears () {
|
resetOriginalPublicationYears () {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
|
import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
|
||||||
import { SearchComponent } from './search.component'
|
import { SearchComponent } from './search.component'
|
||||||
import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
|
import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
|
||||||
|
@ -9,7 +8,6 @@ const searchRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: SearchComponent,
|
component: SearchComponent,
|
||||||
canActivate: [ MetaGuard ],
|
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: $localize`Search`
|
title: $localize`Search`
|
||||||
|
@ -19,7 +17,6 @@ const searchRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'lazy-load-video',
|
path: 'lazy-load-video',
|
||||||
component: SearchComponent,
|
component: SearchComponent,
|
||||||
canActivate: [ MetaGuard ],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
data: VideoLazyLoadResolver
|
data: VideoLazyLoadResolver
|
||||||
}
|
}
|
||||||
|
@ -27,7 +24,6 @@ const searchRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'lazy-load-channel',
|
path: 'lazy-load-channel',
|
||||||
component: SearchComponent,
|
component: SearchComponent,
|
||||||
canActivate: [ MetaGuard ],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
data: ChannelLazyLoadResolver
|
data: ChannelLazyLoadResolver
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { forkJoin, of, Subscription } from 'rxjs'
|
import { forkJoin, of, Subscription } from 'rxjs'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
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 { immutableAssign } from '@app/helpers'
|
||||||
import { Video, VideoChannel } from '@app/shared/shared-main'
|
import { Video, VideoChannel } from '@app/shared/shared-main'
|
||||||
import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
|
import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
|
||||||
import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature'
|
import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature'
|
||||||
import { MetaService } from '@ngx-meta/core'
|
|
||||||
import { SearchTargetType, ServerConfig } from '@shared/models'
|
import { SearchTargetType, ServerConfig } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -238,7 +237,10 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTitle () {
|
private updateTitle () {
|
||||||
const suffix = this.currentSearch ? ' ' + this.currentSearch : ''
|
const suffix = this.currentSearch
|
||||||
|
? ' ' + this.currentSearch
|
||||||
|
: ''
|
||||||
|
|
||||||
this.metaService.setTitle($localize`Search` + suffix)
|
this.metaService.setTitle($localize`Search` + suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { ServerConfigResolver, UnloggedGuard } from '@app/core'
|
import { ServerConfigResolver, UnloggedGuard } from '@app/core'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { RegisterComponent } from './register.component'
|
import { RegisterComponent } from './register.component'
|
||||||
|
|
||||||
const registerRoutes: Routes = [
|
const registerRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: RegisterComponent,
|
component: RegisterComponent,
|
||||||
canActivate: [ MetaGuard, UnloggedGuard ],
|
canActivate: [ UnloggedGuard ],
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
title: $localize`Register`
|
title: $localize`Register`
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
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 { 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 = [
|
const verifyAccountRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
canActivateChild: [ MetaGuard ],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'email',
|
path: 'email',
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
|
import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
|
||||||
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
|
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
|
||||||
import { VideoChannelsComponent } from './video-channels.component'
|
import { VideoChannelsComponent } from './video-channels.component'
|
||||||
|
@ -9,7 +8,6 @@ const videoChannelsRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: ':videoChannelName',
|
path: ':videoChannelName',
|
||||||
component: VideoChannelsComponent,
|
component: VideoChannelsComponent,
|
||||||
canActivateChild: [ MetaGuard ],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<a ngbNavLink i18n>Basic info</a>
|
<a ngbNavLink i18n>Basic info</a>
|
||||||
|
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="row">
|
<div class="form-columns">
|
||||||
<div class="col-video-edit">
|
<div class="col-video-edit">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="name">Title</label>
|
<label i18n for="name">Title</label>
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
<my-help>
|
<my-help>
|
||||||
<ng-template ptTemplate="customHtml">
|
<ng-template ptTemplate="customHtml">
|
||||||
<ng-container i18n>
|
<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-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</my-help>
|
</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 'variables';
|
||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
|
@ -57,16 +51,14 @@ my-peertube-checkbox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.captions {
|
|
||||||
|
|
||||||
.captions-header {
|
.captions-header {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.create-caption {
|
.create-caption {
|
||||||
@include create-button;
|
@include create-button;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.caption-entry {
|
.caption-entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -114,7 +106,6 @@ my-peertube-checkbox {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.submit-container {
|
.submit-container {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
@ -143,35 +134,15 @@ p-calendar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// columns for the video
|
.form-columns {
|
||||||
.col-video-edit {
|
display: grid;
|
||||||
@include make-col-ready();
|
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
grid-template-columns: 66% 1fr;
|
||||||
@include make-col(7);
|
grid-gap: 30px;
|
||||||
|
|
||||||
+ .col-video-edit {
|
|
||||||
@include make-col(5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
@include on-small-main-col {
|
||||||
@include make-col(8);
|
.form-columns {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
+ .col-video-edit {
|
|
||||||
@include make-col(4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(.expanded) {
|
|
||||||
.col-video-edit {
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
@include make-col(8);
|
|
||||||
|
|
||||||
+ .col-video-edit {
|
|
||||||
@include make-col(4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,15 @@ import {
|
||||||
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
|
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { InstanceService } from '@app/shared/shared-instance'
|
import { InstanceService } from '@app/shared/shared-instance'
|
||||||
import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
|
import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||||
import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models'
|
import {
|
||||||
import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
|
LiveVideo,
|
||||||
|
RegisterClientFormFieldOptions,
|
||||||
|
RegisterClientVideoFieldOptions,
|
||||||
|
ServerConfig,
|
||||||
|
VideoConstant,
|
||||||
|
VideoDetails,
|
||||||
|
VideoPrivacy
|
||||||
|
} from '@shared/models'
|
||||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||||
import { VideoEditType } from './video-edit.type'
|
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 { FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
|
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
|
||||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
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 { hydrateFormFromVideo } from '../shared/video-edit-utils'
|
||||||
import { VideoSend } from './video-send'
|
import { VideoSend } from './video-send'
|
||||||
|
|
||||||
|
@ -113,7 +113,13 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
|
||||||
this.loadingBar.useRef().complete()
|
this.loadingBar.useRef().complete()
|
||||||
this.isImportingVideo = false
|
this.isImportingVideo = false
|
||||||
this.firstStepError.emit()
|
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">
|
<div class="first-step-block">
|
||||||
<my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
|
<my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
<div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'">
|
<div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'">
|
||||||
<span i18n>Select the file to upload</span>
|
<span i18n>Select the file to upload</span>
|
||||||
<input
|
<input
|
||||||
aria-label="Select the file to upload" i18n-aria-label
|
aria-label="Select the file to upload"
|
||||||
#videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" autofocus
|
i18n-aria-label
|
||||||
|
#videofileInput
|
||||||
|
[accept]="videoExtensions"
|
||||||
|
(change)="onFileChange($event)"
|
||||||
|
id="videofile"
|
||||||
|
type="file"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -41,7 +46,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group upload-audio-button">
|
<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>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,6 +75,7 @@
|
||||||
<span>{{ error }}</span>
|
<span>{{ error }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group" role="group">
|
<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="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()" />
|
<input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" />
|
||||||
|
|
|
@ -47,8 +47,4 @@
|
||||||
|
|
||||||
margin-left: 10px;
|
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 { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
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 { 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 { FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||||
import { VideoPrivacy } from '@shared/models'
|
import { VideoPrivacy } from '@shared/models'
|
||||||
import { VideoSend } from './video-send'
|
import { VideoSend } from './video-send'
|
||||||
|
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-upload',
|
selector: 'my-video-upload',
|
||||||
|
@ -20,23 +21,18 @@ import { VideoSend } from './video-send'
|
||||||
'./video-send.scss'
|
'./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() firstStepDone = new EventEmitter<string>()
|
||||||
@Output() firstStepError = new EventEmitter<void>()
|
@Output() firstStepError = new EventEmitter<void>()
|
||||||
@ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
|
@ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
|
||||||
|
|
||||||
// So that it can be accessed in the template
|
|
||||||
readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
|
|
||||||
|
|
||||||
userVideoQuotaUsed = 0
|
userVideoQuotaUsed = 0
|
||||||
userVideoQuotaUsedDaily = 0
|
userVideoQuotaUsedDaily = 0
|
||||||
|
|
||||||
isUploadingAudioFile = false
|
isUploadingAudioFile = false
|
||||||
isUploadingVideo = false
|
isUploadingVideo = false
|
||||||
isUpdatingVideo = false
|
|
||||||
|
|
||||||
videoUploaded = false
|
videoUploaded = false
|
||||||
videoUploadObservable: Subscription = null
|
|
||||||
videoUploadPercents = 0
|
videoUploadPercents = 0
|
||||||
videoUploadedIds = {
|
videoUploadedIds = {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
@ -49,7 +45,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
||||||
error: string
|
error: string
|
||||||
enableRetryAfterError: boolean
|
enableRetryAfterError: boolean
|
||||||
|
|
||||||
|
// So that it can be accessed in the template
|
||||||
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
|
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 (
|
constructor (
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
|
@ -61,15 +63,77 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
||||||
protected videoCaptionService: VideoCaptionService,
|
protected videoCaptionService: VideoCaptionService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private hooks: HooksService
|
private hooks: HooksService,
|
||||||
|
private resumableUploadService: UploadxService
|
||||||
) {
|
) {
|
||||||
super()
|
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 () {
|
get videoExtensions () {
|
||||||
return this.serverConfig.video.file.extensions.join(', ')
|
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 () {
|
ngOnInit () {
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
|
|
||||||
|
@ -78,6 +142,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
||||||
this.userVideoQuotaUsed = data.videoQuotaUsed
|
this.userVideoQuotaUsed = data.videoQuotaUsed
|
||||||
this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
|
this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.resumableUploadService.events
|
||||||
|
.subscribe(state => this.onUploadVideoOngoing(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit () {
|
||||||
|
@ -85,7 +152,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
|
this.cancelUpload()
|
||||||
}
|
}
|
||||||
|
|
||||||
canDeactivate () {
|
canDeactivate () {
|
||||||
|
@ -105,137 +172,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoFile () {
|
onFileDropped (files: FileList) {
|
||||||
return this.videofileInput.nativeElement.files[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
setVideoFile (files: FileList) {
|
|
||||||
this.videofileInput.nativeElement.files = files
|
this.videofileInput.nativeElement.files = files
|
||||||
this.fileChange()
|
|
||||||
|
this.onFileChange({ target: this.videofileInput.nativeElement })
|
||||||
}
|
}
|
||||||
|
|
||||||
getAudioUploadLabel () {
|
onFileChange (event: Event | { target: HTMLInputElement }) {
|
||||||
const videofile = this.getVideoFile()
|
const file = (event.target as HTMLInputElement).files[0]
|
||||||
if (!videofile) return $localize`Upload`
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
fileChange () {
|
this.isUploadingVideo = true
|
||||||
this.uploadFirstStep()
|
this.fileToUpload = file
|
||||||
|
|
||||||
|
this.uploadFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadAudio () {
|
||||||
|
this.uploadFile(this.getInputVideoFile(), this.previewfileUpload)
|
||||||
}
|
}
|
||||||
|
|
||||||
retryUpload () {
|
retryUpload () {
|
||||||
this.enableRetryAfterError = false
|
this.enableRetryAfterError = false
|
||||||
this.error = ''
|
this.error = ''
|
||||||
this.uploadVideo()
|
this.uploadFile(this.fileToUpload)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelUpload () {
|
cancelUpload () {
|
||||||
if (this.videoUploadObservable !== null) {
|
this.resumableUploadService.control({ action: 'cancel' })
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isPublishingButtonDisabled () {
|
isPublishingButtonDisabled () {
|
||||||
|
@ -245,6 +218,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
||||||
!this.videoUploadedIds.id
|
!this.videoUploadedIds.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAudioUploadLabel () {
|
||||||
|
const videofile = this.getInputVideoFile()
|
||||||
|
if (!videofile) return $localize`Upload`
|
||||||
|
|
||||||
|
return $localize`Upload ${videofile.name}`
|
||||||
|
}
|
||||||
|
|
||||||
updateSecondStep () {
|
updateSecondStep () {
|
||||||
if (this.isPublishingButtonDisabled() || !this.checkForm()) {
|
if (this.isPublishingButtonDisabled() || !this.checkForm()) {
|
||||||
return
|
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) {
|
private checkGlobalUserQuota (videofile: File) {
|
||||||
const bytePipes = new BytesPipe()
|
const bytePipes = new BytesPipe()
|
||||||
|
|
||||||
|
@ -285,8 +321,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView
|
||||||
const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0)
|
const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0)
|
||||||
const videoQuotaBytes = bytePipes.transform(videoQuota, 0)
|
const videoQuotaBytes = bytePipes.transform(videoQuota, 0)
|
||||||
|
|
||||||
const msg = $localize`Your video quota is exceeded with this video (
|
const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
|
||||||
video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
|
|
||||||
this.notifier.error(msg)
|
this.notifier.error(msg)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
@ -304,9 +339,7 @@ video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuota
|
||||||
const videoSizeBytes = bytePipes.transform(videofile.size, 0)
|
const videoSizeBytes = bytePipes.transform(videofile.size, 0)
|
||||||
const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0)
|
const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0)
|
||||||
const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 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)
|
this.notifier.error(msg)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { VideoAddComponent } from './video-add.component'
|
import { VideoAddComponent } from './video-add.component'
|
||||||
|
|
||||||
const videoAddRoutes: Routes = [
|
const videoAddRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: VideoAddComponent,
|
component: VideoAddComponent,
|
||||||
canActivate: [ MetaGuard, LoginGuard ],
|
canActivate: [ LoginGuard ],
|
||||||
canDeactivate: [ CanDeactivateGuard ]
|
canDeactivate: [ CanDeactivateGuard ]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
<ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
|
<ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
|
<div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }">
|
||||||
<ng-container ngbNavItem>
|
<ng-container ngbNavItem="upload">
|
||||||
<a ngbNavLink>
|
<a ngbNavLink>
|
||||||
<span i18n>Upload a file</span>
|
<span i18n>Upload a file</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngbNavItem *ngIf="isVideoImportHttpEnabled()">
|
<ng-container ngbNavItem="import-url" *ngIf="isVideoImportHttpEnabled()">
|
||||||
<a ngbNavLink>
|
<a ngbNavLink>
|
||||||
<span i18n>Import with URL</span>
|
<span i18n>Import with URL</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngbNavItem *ngIf="isVideoImportTorrentEnabled()">
|
<ng-container ngbNavItem="import-torrent" *ngIf="isVideoImportTorrentEnabled()">
|
||||||
<a ngbNavLink>
|
<a ngbNavLink>
|
||||||
<span i18n>Import with torrent</span>
|
<span i18n>Import with torrent</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngbNavItem *ngIf="isVideoLiveEnabled()">
|
<ng-container ngbNavItem="go-live" *ngIf="isVideoLiveEnabled()">
|
||||||
<a ngbNavLink>
|
<a ngbNavLink>
|
||||||
<span i18n>Go live</span>
|
<span i18n>Go live</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
|
import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
|
import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
|
||||||
import { ServerConfig } from '@shared/models'
|
import { ServerConfig } from '@shared/models'
|
||||||
import { VideoEditType } from './shared/video-edit.type'
|
import { VideoEditType } from './shared/video-edit.type'
|
||||||
|
@ -22,11 +23,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
|
||||||
|
|
||||||
secondStepType: VideoEditType
|
secondStepType: VideoEditType
|
||||||
videoName: string
|
videoName: string
|
||||||
serverConfig: ServerConfig
|
|
||||||
|
activeNav: string
|
||||||
|
|
||||||
|
private serverConfig: ServerConfig
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private serverService: ServerService
|
private serverService: ServerService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get userInformationLoaded () {
|
get userInformationLoaded () {
|
||||||
|
@ -42,6 +48,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
|
||||||
.subscribe(config => this.serverConfig = config)
|
.subscribe(config => this.serverConfig = config)
|
||||||
|
|
||||||
this.user = this.auth.getUser()
|
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) {
|
onFirstStepDone (type: VideoEditType, videoName: string) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { CanDeactivateGuard } from '@app/core'
|
import { CanDeactivateGuard } from '@app/core'
|
||||||
|
import { UploadxModule } from 'ngx-uploadx'
|
||||||
import { VideoEditModule } from './shared/video-edit.module'
|
import { VideoEditModule } from './shared/video-edit.module'
|
||||||
import { DragDropDirective } from './video-add-components/drag-drop.directive'
|
import { DragDropDirective } from './video-add-components/drag-drop.directive'
|
||||||
import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
|
import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
|
||||||
|
@ -13,7 +14,9 @@ import { VideoAddComponent } from './video-add.component'
|
||||||
imports: [
|
imports: [
|
||||||
VideoAddRoutingModule,
|
VideoAddRoutingModule,
|
||||||
|
|
||||||
VideoEditModule
|
VideoEditModule,
|
||||||
|
|
||||||
|
UploadxModule
|
||||||
],
|
],
|
||||||
|
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { VideoUpdateComponent } from './video-update.component'
|
import { VideoUpdateComponent } from './video-update.component'
|
||||||
import { VideoUpdateResolver } from './video-update.resolver'
|
import { VideoUpdateResolver } from './video-update.resolver'
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ const videoUpdateRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: VideoUpdateComponent,
|
component: VideoUpdateComponent,
|
||||||
canActivate: [ MetaGuard, LoginGuard ],
|
canActivate: [ LoginGuard ],
|
||||||
canDeactivate: [ CanDeactivateGuard ],
|
canDeactivate: [ CanDeactivateGuard ],
|
||||||
resolve: {
|
resolve: {
|
||||||
videoData: VideoUpdateResolver
|
videoData: VideoUpdateResolver
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { forkJoin, of } from 'rxjs'
|
||||||
import { map, switchMap } from 'rxjs/operators'
|
import { map, switchMap } from 'rxjs/operators'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
|
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'
|
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -10,7 +12,7 @@ export class VideoUpdateResolver implements Resolve<any> {
|
||||||
constructor (
|
constructor (
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private liveVideoService: LiveVideoService,
|
private liveVideoService: LiveVideoService,
|
||||||
private videoChannelService: VideoChannelService,
|
private authService: AuthService,
|
||||||
private videoCaptionService: VideoCaptionService
|
private videoCaptionService: VideoCaptionService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -31,17 +33,7 @@ export class VideoUpdateResolver implements Resolve<any> {
|
||||||
.loadCompleteDescription(video.descriptionPath)
|
.loadCompleteDescription(video.descriptionPath)
|
||||||
.pipe(map(description => Object.assign(video, { description }))),
|
.pipe(map(description => Object.assign(video, { description }))),
|
||||||
|
|
||||||
this.videoChannelService
|
listUserChannels(this.authService),
|
||||||
.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
|
|
||||||
})))
|
|
||||||
),
|
|
||||||
|
|
||||||
this.videoCaptionService
|
this.videoCaptionService
|
||||||
.listCaptions(video.id)
|
.listCaptions(video.id)
|
||||||
|
|
|
@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
|
||||||
// Before HTML rendering restore line feed for markdown list compatibility
|
// Before HTML rendering restore line feed for markdown list compatibility
|
||||||
const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
|
const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
|
||||||
const html = await this.markdownService.textMarkdownToHTML(commentText, true, true)
|
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 ])
|
this.newParentComments = this.parentComments.concat([ this.comment ])
|
||||||
|
|
||||||
if (this.comment.account) {
|
if (this.comment.account) {
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { VideoWatchComponent } from './video-watch.component'
|
import { VideoWatchComponent } from './video-watch.component'
|
||||||
|
|
||||||
const videoWatchRoutes: Routes = [
|
const videoWatchRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'playlist/:playlistId',
|
path: 'playlist/:playlistId',
|
||||||
component: VideoWatchComponent,
|
component: VideoWatchComponent
|
||||||
canActivate: [ MetaGuard ]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':videoId/comments/:commentId',
|
path: ':videoId/comments/:commentId',
|
||||||
|
@ -15,8 +13,7 @@ const videoWatchRoutes: Routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':videoId',
|
path: ':videoId',
|
||||||
component: VideoWatchComponent,
|
component: VideoWatchComponent
|
||||||
canActivate: [ MetaGuard ]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -146,6 +146,8 @@ $video-info-margin-left: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-info-name {
|
.video-info-name {
|
||||||
|
@include peertube-word-wrap;
|
||||||
|
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
min-height: 40px; // Align with the action buttons
|
min-height: 40px; // Align with the action buttons
|
||||||
font-size: 27px;
|
font-size: 27px;
|
||||||
|
@ -173,6 +175,7 @@ $video-info-margin-left: 44px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@include disable-default-a-behaviour;
|
@include disable-default-a-behaviour;
|
||||||
|
@include peertube-word-wrap;
|
||||||
|
|
||||||
color: pvar(--mainForegroundColor);
|
color: pvar(--mainForegroundColor);
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
AuthUser,
|
AuthUser,
|
||||||
ConfirmService,
|
ConfirmService,
|
||||||
MarkdownService,
|
MarkdownService,
|
||||||
|
MetaService,
|
||||||
Notifier,
|
Notifier,
|
||||||
PeerTubeSocket,
|
PeerTubeSocket,
|
||||||
RestExtractor,
|
RestExtractor,
|
||||||
|
@ -25,7 +26,6 @@ import { SupportModalComponent } from '@app/shared/shared-support-modal'
|
||||||
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
|
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
|
||||||
import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature'
|
import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature'
|
||||||
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
|
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
|
||||||
import { MetaService } from '@ngx-meta/core'
|
|
||||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||||
import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
|
import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
|
||||||
|
@ -509,7 +509,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private async setVideoDescriptionHTML () {
|
private async setVideoDescriptionHTML () {
|
||||||
const html = await this.markdownService.textMarkdownToHTML(this.video.description)
|
const html = await this.markdownService.textMarkdownToHTML(this.video.description)
|
||||||
this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html)
|
this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
private setVideoLikesBarTooltipText () {
|
private setVideoLikesBarTooltipText () {
|
||||||
|
@ -674,7 +674,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
this.player.one('ended', () => {
|
this.player.one('ended', () => {
|
||||||
if (this.video.isLive) {
|
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 route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private serverService: ServerService
|
private serverService: ServerService,
|
||||||
|
private redirectService: RedirectService
|
||||||
) {
|
) {
|
||||||
super(data)
|
super(data)
|
||||||
|
|
||||||
|
@ -84,12 +85,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
|
||||||
|
|
||||||
this.algorithmChangeSub = this.route.queryParams.subscribe(
|
this.algorithmChangeSub = this.route.queryParams.subscribe(
|
||||||
queryParams => {
|
queryParams => {
|
||||||
const algorithm = queryParams['alg']
|
this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
|
||||||
if (algorithm) {
|
|
||||||
this.data.model = algorithm
|
|
||||||
} else {
|
|
||||||
this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +95,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
|
||||||
}
|
}
|
||||||
|
|
||||||
setSort () {
|
setSort () {
|
||||||
const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM
|
const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm()
|
||||||
? this.data.model
|
? this.data.model
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
|
|
@ -35,11 +35,12 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
||||||
protected storageService: LocalStorageService,
|
protected storageService: LocalStorageService,
|
||||||
protected cfr: ComponentFactoryResolver,
|
protected cfr: ComponentFactoryResolver,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
|
private redirectService: RedirectService,
|
||||||
private hooks: HooksService
|
private hooks: HooksService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM)
|
this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
|
||||||
|
|
||||||
this.headerComponentInjector = this.getInjector()
|
this.headerComponentInjector = this.getInjector()
|
||||||
}
|
}
|
||||||
|
@ -106,7 +107,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadPageRouteParams (queryParams: Params) {
|
protected loadPageRouteParams (queryParams: Params) {
|
||||||
const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM
|
const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
|
||||||
|
|
||||||
this.sort = this.parseAlgorithm(algorithm)
|
this.sort = this.parseAlgorithm(algorithm)
|
||||||
}
|
}
|
||||||
|
@ -115,8 +116,10 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
|
||||||
switch (algorithm) {
|
switch (algorithm) {
|
||||||
case 'most-viewed':
|
case 'most-viewed':
|
||||||
return '-trending'
|
return '-trending'
|
||||||
|
|
||||||
case 'most-liked':
|
case 'most-liked':
|
||||||
return '-likes'
|
return '-likes'
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return '-' + algorithm as VideoSortField
|
return '-' + algorithm as VideoSortField
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
import { LoginGuard } from '@app/core'
|
import { LoginGuard } from '@app/core'
|
||||||
import { MetaGuard } from '@ngx-meta/core'
|
|
||||||
import { VideoTrendingComponent } from './video-list'
|
import { VideoTrendingComponent } from './video-list'
|
||||||
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
|
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
|
||||||
import { VideoLocalComponent } from './video-list/video-local.component'
|
import { VideoLocalComponent } from './video-list/video-local.component'
|
||||||
|
@ -13,7 +12,6 @@ const videosRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: VideosComponent,
|
component: VideosComponent,
|
||||||
canActivateChild: [ MetaGuard ],
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'overview',
|
path: 'overview',
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment }
|
||||||
import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
|
import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
|
||||||
import { MenuGuards } from '@app/core/routing/menu-guard.service'
|
import { MenuGuards } from '@app/core/routing/menu-guard.service'
|
||||||
import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
|
import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
|
||||||
import { PreloadSelectedModulesList } from './core'
|
import { MetaGuard, PreloadSelectedModulesList } from './core'
|
||||||
import { EmptyComponent } from './empty.component'
|
import { EmptyComponent } from './empty.component'
|
||||||
import { RootComponent } from './root.component'
|
import { RootComponent } from './root.component'
|
||||||
|
|
||||||
|
@ -12,55 +12,72 @@ const routes: Routes = [
|
||||||
path: 'admin',
|
path: 'admin',
|
||||||
canActivate: [ MenuGuards.close() ],
|
canActivate: [ MenuGuards.close() ],
|
||||||
canDeactivate: [ MenuGuards.open() ],
|
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',
|
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',
|
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',
|
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',
|
path: 'a',
|
||||||
loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule)
|
loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule),
|
||||||
|
canActivateChild: [ MetaGuard ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'c',
|
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',
|
path: 'about',
|
||||||
loadChildren: () => import('./+about/about.module').then(m => m.AboutModule)
|
loadChildren: () => import('./+about/about.module').then(m => m.AboutModule),
|
||||||
|
canActivateChild: [ MetaGuard ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'signup',
|
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',
|
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',
|
path: 'login',
|
||||||
loadChildren: () => import('./+login/login.module').then(m => m.LoginModule)
|
loadChildren: () => import('./+login/login.module').then(m => m.LoginModule),
|
||||||
|
canActivateChild: [ MetaGuard ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'search',
|
path: 'search',
|
||||||
loadChildren: () => import('./+search/search.module').then(m => m.SearchModule)
|
loadChildren: () => import('./+search/search.module').then(m => m.SearchModule),
|
||||||
|
canActivateChild: [ MetaGuard ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'videos',
|
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',
|
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',
|
path: 'video-playlists/watch',
|
||||||
|
|
|
@ -40,8 +40,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-menu {
|
.icon-menu {
|
||||||
background-color: pvar(--mainForegroundColor);
|
|
||||||
mask-image: url('../assets/images/misc/menu.svg');
|
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;
|
margin: 0 18px 0 20px;
|
||||||
|
|
||||||
@media screen and (max-width: $mobile-view) {
|
@media screen and (max-width: $mobile-view) {
|
||||||
|
|
|
@ -67,7 +67,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
goToDefaultRoute () {
|
goToDefaultRoute () {
|
||||||
return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE)
|
return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
@ -231,7 +231,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.broadcastMessage = {
|
this.broadcastMessage = {
|
||||||
message: await this.markdownService.completeMarkdownToHTML(messageConfig.message),
|
message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true),
|
||||||
dismissable: messageConfig.dismissable,
|
dismissable: messageConfig.dismissable,
|
||||||
class: classes[messageConfig.level]
|
class: classes[messageConfig.level]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,7 @@ import { APP_BASE_HREF, registerLocaleData } from '@angular/common'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { BrowserModule } from '@angular/platform-browser'
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker'
|
import { ServiceWorkerModule } from '@angular/service-worker'
|
||||||
import { ServerService } from '@app/core'
|
|
||||||
import localeOc from '@app/helpers/locales/oc'
|
import localeOc from '@app/helpers/locales/oc'
|
||||||
import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
|
|
||||||
import { AppRoutingModule } from './app-routing.module'
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
import { CoreModule } from './core'
|
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 { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
|
||||||
import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
|
import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
|
||||||
import { WelcomeModalComponent } from './modal/welcome-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 { SharedFormModule } from './shared/shared-forms'
|
||||||
import { SharedGlobalIconModule } from './shared/shared-icons'
|
import { SharedGlobalIconModule } from './shared/shared-icons'
|
||||||
import { SharedInstanceModule } from './shared/shared-instance'
|
import { SharedInstanceModule } from './shared/shared-instance'
|
||||||
import { SharedMainModule } from './shared/shared-main'
|
import { SharedMainModule } from './shared/shared-main'
|
||||||
import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings'
|
import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings'
|
||||||
import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
|
|
||||||
|
|
||||||
registerLocaleData(localeOc, 'oc')
|
registerLocaleData(localeOc, 'oc')
|
||||||
|
|
||||||
|
@ -62,22 +60,6 @@ registerLocaleData(localeOc, 'oc')
|
||||||
SharedInstanceModule,
|
SharedInstanceModule,
|
||||||
SharedActorImageModule,
|
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
|
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 { Notifier } from './notification'
|
||||||
import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
|
import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
|
||||||
import { RestExtractor, RestService } from './rest'
|
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 { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
|
||||||
import { ServerConfigResolver } from './routing/server-config-resolver.service'
|
import { ServerConfigResolver } from './routing/server-config-resolver.service'
|
||||||
import { ScopedTokensService } from './scoped-tokens'
|
import { ScopedTokensService } from './scoped-tokens'
|
||||||
|
@ -77,7 +77,10 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
|
||||||
MessageService,
|
MessageService,
|
||||||
PeerTubeSocket,
|
PeerTubeSocket,
|
||||||
ServerConfigResolver,
|
ServerConfigResolver,
|
||||||
CanDeactivateGuard
|
CanDeactivateGuard,
|
||||||
|
|
||||||
|
MetaService,
|
||||||
|
MetaGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
import { fromEvent } from 'rxjs'
|
import { fromEvent } from 'rxjs'
|
||||||
import { debounceTime } from 'rxjs/operators'
|
import { debounceTime } from 'rxjs/operators'
|
||||||
import { Injectable } from '@angular/core'
|
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'
|
import { ScreenService } from '../wrappers'
|
||||||
|
|
||||||
|
export type MenuLink = {
|
||||||
|
icon: GlobalIconName
|
||||||
|
label: string
|
||||||
|
menuLabel: string
|
||||||
|
path: string
|
||||||
|
priority: number
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MenuService {
|
export class MenuService {
|
||||||
isMenuDisplayed = true
|
isMenuDisplayed = true
|
||||||
|
@ -48,6 +59,53 @@ export class MenuService {
|
||||||
this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
|
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 () {
|
private handleWindowResize () {
|
||||||
// On touch screens, do not handle window resize event since opened menu is handled with a content overlay
|
// On touch screens, do not handle window resize event since opened menu is handled with a content overlay
|
||||||
if (this.screenService.isInTouchScreen()) return
|
if (this.screenService.isInTouchScreen()) return
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { LinkifierService } from './linkifier.service'
|
import { LinkifierService } from './linkifier.service'
|
||||||
import { SANITIZE_OPTIONS } from '@shared/core-utils/renderer/html'
|
import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HtmlRendererService {
|
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([
|
const [ html ] = await Promise.all([
|
||||||
// Convert possible markdown to html
|
// Convert possible markdown to html
|
||||||
this.linkifier.linkify(text),
|
this.linkifier.linkify(text),
|
||||||
|
@ -28,7 +28,11 @@ export class HtmlRendererService {
|
||||||
this.loadSanitizeHtml()
|
this.loadSanitizeHtml()
|
||||||
])
|
])
|
||||||
|
|
||||||
return this.sanitizeHtml(html, SANITIZE_OPTIONS)
|
const options = additionalAllowedTags.length !== 0
|
||||||
|
? getCustomMarkupSanitizeOptions(additionalAllowedTags)
|
||||||
|
: getSanitizeOptions()
|
||||||
|
|
||||||
|
return this.sanitizeHtml(html, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadSanitizeHtml () {
|
private async loadSanitizeHtml () {
|
||||||
|
|
|
@ -17,12 +17,15 @@ type MarkdownParsers = {
|
||||||
enhancedMarkdownIt: MarkdownIt
|
enhancedMarkdownIt: MarkdownIt
|
||||||
enhancedWithHTMLMarkdownIt: MarkdownIt
|
enhancedWithHTMLMarkdownIt: MarkdownIt
|
||||||
|
|
||||||
completeMarkdownIt: MarkdownIt
|
unsafeMarkdownIt: MarkdownIt
|
||||||
|
|
||||||
|
customPageMarkdownIt: MarkdownIt
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarkdownConfig = {
|
type MarkdownConfig = {
|
||||||
rules: string[]
|
rules: string[]
|
||||||
html: boolean
|
html: boolean
|
||||||
|
breaks: boolean
|
||||||
escape?: boolean
|
escape?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,18 +38,24 @@ export class MarkdownService {
|
||||||
private markdownParsers: MarkdownParsers = {
|
private markdownParsers: MarkdownParsers = {
|
||||||
textMarkdownIt: null,
|
textMarkdownIt: null,
|
||||||
textWithHTMLMarkdownIt: null,
|
textWithHTMLMarkdownIt: null,
|
||||||
|
|
||||||
enhancedMarkdownIt: null,
|
enhancedMarkdownIt: null,
|
||||||
enhancedWithHTMLMarkdownIt: null,
|
enhancedWithHTMLMarkdownIt: null,
|
||||||
completeMarkdownIt: null
|
|
||||||
|
unsafeMarkdownIt: null,
|
||||||
|
|
||||||
|
customPageMarkdownIt: null
|
||||||
}
|
}
|
||||||
private parsersConfig: MarkdownParserConfigs = {
|
private parsersConfig: MarkdownParserConfigs = {
|
||||||
textMarkdownIt: { rules: TEXT_RULES, html: false },
|
textMarkdownIt: { rules: TEXT_RULES, breaks: true, html: false },
|
||||||
textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, html: true, escape: true },
|
textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, breaks: true, html: true, escape: true },
|
||||||
|
|
||||||
enhancedMarkdownIt: { rules: ENHANCED_RULES, html: false },
|
enhancedMarkdownIt: { rules: ENHANCED_RULES, breaks: true, html: false },
|
||||||
enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, html: true, escape: true },
|
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
|
private emojiModule: any
|
||||||
|
@ -54,22 +63,26 @@ export class MarkdownService {
|
||||||
constructor (private htmlRenderer: HtmlRendererService) {}
|
constructor (private htmlRenderer: HtmlRendererService) {}
|
||||||
|
|
||||||
textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
|
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) {
|
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) {
|
unsafeMarkdownToHTML (markdown: string, _trustedInput: true) {
|
||||||
return this.render('completeMarkdownIt', markdown, 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) {
|
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 t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
|
||||||
const url = buildVideoLink({ startTime: t })
|
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 ''
|
if (!markdown) return ''
|
||||||
|
|
||||||
const config = this.parsersConfig[ name ]
|
const config = this.parsersConfig[ name ]
|
||||||
|
@ -96,7 +115,7 @@ export class MarkdownService {
|
||||||
let html = this.markdownParsers[ name ].render(markdown)
|
let html = this.markdownParsers[ name ].render(markdown)
|
||||||
html = this.avoidTruncatedTags(html)
|
html = this.avoidTruncatedTags(html)
|
||||||
|
|
||||||
if (config.escape) return this.htmlRenderer.toSafeHtml(html)
|
if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags)
|
||||||
|
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
@ -105,7 +124,7 @@ export class MarkdownService {
|
||||||
// FIXME: import('...') returns a struct module, containing a "default" field
|
// FIXME: import('...') returns a struct module, containing a "default" field
|
||||||
const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
|
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) {
|
for (const rule of config.rules) {
|
||||||
markdownIt.enable(rule)
|
markdownIt.enable(rule)
|
||||||
|
|
|
@ -3,6 +3,8 @@ export * from './custom-reuse-strategy'
|
||||||
export * from './disable-for-reuse-hook'
|
export * from './disable-for-reuse-hook'
|
||||||
export * from './login-guard.service'
|
export * from './login-guard.service'
|
||||||
export * from './menu-guard.service'
|
export * from './menu-guard.service'
|
||||||
|
export * from './meta-guard.service'
|
||||||
|
export * from './meta.service'
|
||||||
export * from './preload-selected-modules-list'
|
export * from './preload-selected-modules-list'
|
||||||
export * from './redirect.service'
|
export * from './redirect.service'
|
||||||
export * from './server-config-resolver.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 {
|
export class RedirectService {
|
||||||
// Default route could change according to the instance configuration
|
// Default route could change according to the instance configuration
|
||||||
static INIT_DEFAULT_ROUTE = '/videos/trending'
|
static INIT_DEFAULT_ROUTE = '/videos/trending'
|
||||||
static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
|
|
||||||
static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
|
static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
|
||||||
static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
|
|
||||||
|
|
||||||
private previousUrl: string
|
private previousUrl: string
|
||||||
private currentUrl: string
|
private currentUrl: string
|
||||||
|
|
||||||
private redirectingToHomepage = false
|
private redirectingToHomepage = false
|
||||||
|
private defaultTrendingAlgorithm = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
|
||||||
|
private defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private router: Router,
|
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
|
// The config is first loaded from the cache so try to get the default route
|
||||||
const tmpConfig = this.serverService.getTmpConfig()
|
const tmpConfig = this.serverService.getTmpConfig()
|
||||||
if (tmpConfig?.instance?.defaultClientRoute) {
|
if (tmpConfig?.instance?.defaultClientRoute) {
|
||||||
RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
|
this.defaultRoute = tmpConfig.instance.defaultClientRoute
|
||||||
}
|
}
|
||||||
if (tmpConfig?.trending?.videos?.algorithms?.default) {
|
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
|
// Load default route
|
||||||
|
@ -34,13 +34,8 @@ export class RedirectService {
|
||||||
const defaultRouteConfig = config.instance.defaultClientRoute
|
const defaultRouteConfig = config.instance.defaultClientRoute
|
||||||
const defaultTrendingConfig = config.trending.videos.algorithms.default
|
const defaultTrendingConfig = config.trending.videos.algorithms.default
|
||||||
|
|
||||||
if (defaultRouteConfig) {
|
if (defaultRouteConfig) this.defaultRoute = defaultRouteConfig
|
||||||
RedirectService.DEFAULT_ROUTE = defaultRouteConfig
|
if (defaultTrendingConfig) this.defaultTrendingAlgorithm = defaultTrendingConfig
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultTrendingConfig) {
|
|
||||||
RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Track previous url
|
// Track previous url
|
||||||
|
@ -53,6 +48,14 @@ export class RedirectService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDefaultRoute () {
|
||||||
|
return this.defaultRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultTrendingAlgorithm () {
|
||||||
|
return this.defaultTrendingAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
redirectToPreviousRoute () {
|
redirectToPreviousRoute () {
|
||||||
const exceptions = [
|
const exceptions = [
|
||||||
'/verify-account',
|
'/verify-account',
|
||||||
|
@ -72,21 +75,21 @@ export class RedirectService {
|
||||||
|
|
||||||
this.redirectingToHomepage = true
|
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)
|
.then(() => this.redirectingToHomepage = false)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.redirectingToHomepage = false
|
this.redirectingToHomepage = false
|
||||||
|
|
||||||
console.error(
|
console.error(
|
||||||
'Cannot navigate to %s, resetting default route to %s.',
|
'Cannot navigate to %s, resetting default route to %s.',
|
||||||
RedirectService.DEFAULT_ROUTE,
|
this.defaultRoute,
|
||||||
RedirectService.INIT_DEFAULT_ROUTE
|
RedirectService.INIT_DEFAULT_ROUTE
|
||||||
)
|
)
|
||||||
|
|
||||||
RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
|
this.defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
|
||||||
return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
|
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 { HttpClient } from '@angular/common/http'
|
||||||
import { Inject, Injectable, LOCALE_ID } from '@angular/core'
|
import { Inject, Injectable, LOCALE_ID } from '@angular/core'
|
||||||
import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers'
|
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 { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
|
||||||
import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
|
import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
|
@ -16,8 +15,6 @@ export class ServerService {
|
||||||
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
|
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
|
||||||
private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
|
private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
|
||||||
|
|
||||||
private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
|
|
||||||
|
|
||||||
configReloaded = new Subject<ServerConfig>()
|
configReloaded = new Subject<ServerConfig>()
|
||||||
|
|
||||||
private localeObservable: Observable<any>
|
private localeObservable: Observable<any>
|
||||||
|
@ -176,6 +173,9 @@ export class ServerService {
|
||||||
disableLocalSearch: false,
|
disableLocalSearch: false,
|
||||||
isDefaultSearch: false
|
isDefaultSearch: false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
homepage: {
|
||||||
|
enabled: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,9 +201,7 @@ export class ServerService {
|
||||||
this.configReset = true
|
this.configReset = true
|
||||||
|
|
||||||
// Notify config update
|
// Notify config update
|
||||||
this.getConfig().subscribe(() => {
|
return this.getConfig()
|
||||||
// empty, to fire a reset config event
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig () {
|
getConfig () {
|
||||||
|
@ -212,7 +210,6 @@ export class ServerService {
|
||||||
if (!this.configObservable) {
|
if (!this.configObservable) {
|
||||||
this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
|
this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(config => this.saveConfigLocally(config)),
|
|
||||||
tap(config => {
|
tap(config => {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.configLoaded = true
|
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 () {
|
private loadConfigLocally () {
|
||||||
const configString = peertubeLocalStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY)
|
const configString = window['PeerTubeServerConfig']
|
||||||
|
if (!configString) return
|
||||||
|
|
||||||
if (configString) {
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(configString)
|
const parsed = JSON.parse(configString)
|
||||||
Object.assign(this.config, parsed)
|
Object.assign(this.config, parsed)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Cannot parse config saved in local storage.', err)
|
console.error('Cannot parse config saved in from index.html.', err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,19 @@ export class ThemeService {
|
||||||
: this.userService.getAnonymousUser().theme
|
: this.userService.getAnonymousUser().theme
|
||||||
|
|
||||||
if (theme !== 'instance-default') return 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) {
|
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 { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||||
import { DatePipe } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
import { HttpErrorResponse } from '@angular/common/http'
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
|
@ -23,20 +23,29 @@ function getParameterByName (name: string, url: string) {
|
||||||
|
|
||||||
function listUserChannels (authService: AuthService) {
|
function listUserChannels (authService: AuthService) {
|
||||||
return authService.userInformationLoaded
|
return authService.userInformationLoaded
|
||||||
.pipe(map(() => {
|
.pipe(
|
||||||
|
first(),
|
||||||
|
map(() => {
|
||||||
const user = authService.getUser()
|
const user = authService.getUser()
|
||||||
if (!user) return undefined
|
if (!user) return undefined
|
||||||
|
|
||||||
const videoChannels = user.videoChannels
|
const videoChannels = user.videoChannels
|
||||||
if (Array.isArray(videoChannels) === false) return undefined
|
if (Array.isArray(videoChannels) === false) return undefined
|
||||||
|
|
||||||
return videoChannels.map(c => ({
|
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,
|
id: c.id,
|
||||||
label: c.displayName,
|
label: c.displayName,
|
||||||
support: c.support,
|
support: c.support,
|
||||||
avatarPath: c.avatar?.path
|
avatarPath: c.avatar?.path
|
||||||
}) as SelectChannelItem)
|
}) as SelectChannelItem)
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAbsoluteAPIUrl () {
|
function getAbsoluteAPIUrl () {
|
||||||
|
@ -167,8 +176,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadErrorHandler (parameters: {
|
function genericUploadErrorHandler (parameters: {
|
||||||
err: HttpErrorResponse
|
err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
|
||||||
name: string
|
name: string
|
||||||
notifier: Notifier
|
notifier: Notifier
|
||||||
sticky?: boolean
|
sticky?: boolean
|
||||||
|
@ -180,6 +189,9 @@ function uploadErrorHandler (parameters: {
|
||||||
if (err instanceof ErrorEvent) { // network error
|
if (err instanceof ErrorEvent) { // network error
|
||||||
message = $localize`The connection was interrupted`
|
message = $localize`The connection was interrupted`
|
||||||
notifier.error(message, title, null, sticky)
|
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) {
|
} else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) {
|
||||||
message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
|
message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)`
|
||||||
notifier.error(message, title, null, sticky)
|
notifier.error(message, title, null, sticky)
|
||||||
|
@ -210,5 +222,5 @@ export {
|
||||||
isInViewport,
|
isInViewport,
|
||||||
isXPercentInViewport,
|
isXPercentInViewport,
|
||||||
listUserChannels,
|
listUserChannels,
|
||||||
uploadErrorHandler
|
genericUploadErrorHandler
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,24 +123,9 @@
|
||||||
<div class="on-instance">
|
<div class="on-instance">
|
||||||
<div i18n class="block-title">ON {{instanceName}}</div>
|
<div i18n class="block-title">ON {{instanceName}}</div>
|
||||||
|
|
||||||
<a class="menu-link" routerLink="/videos/overview" routerLinkActive="active">
|
<a class="menu-link" *ngFor="let commonLink of commonMenuLinks" [routerLink]="commonLink.path" routerLinkActive="active">
|
||||||
<my-global-icon iconName="globe" aria-hidden="true"></my-global-icon>
|
<my-global-icon [iconName]="commonLink.icon" aria-hidden="true"></my-global-icon>
|
||||||
<ng-container i18n>Discover</ng-container>
|
<ng-container>{{ commonLink.menuLabel }}</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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,17 @@ import { switchMap } from 'rxjs/operators'
|
||||||
import { ViewportScroller } from '@angular/common'
|
import { ViewportScroller } from '@angular/common'
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
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 { scrollToTop } from '@app/helpers'
|
||||||
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
|
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
|
||||||
import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
|
import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
|
||||||
|
@ -35,6 +45,8 @@ export class MenuComponent implements OnInit {
|
||||||
|
|
||||||
currentInterfaceLanguage: string
|
currentInterfaceLanguage: string
|
||||||
|
|
||||||
|
commonMenuLinks: MenuLink[] = []
|
||||||
|
|
||||||
private languages: VideoConstant<string>[] = []
|
private languages: VideoConstant<string>[] = []
|
||||||
private serverConfig: ServerConfig
|
private serverConfig: ServerConfig
|
||||||
private routesPerRight: { [role in UserRight]?: string } = {
|
private routesPerRight: { [role in UserRight]?: string } = {
|
||||||
|
@ -80,7 +92,10 @@ export class MenuComponent implements OnInit {
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.serverConfig = this.serverService.getTmpConfig()
|
this.serverConfig = this.serverService.getTmpConfig()
|
||||||
this.serverService.getConfig()
|
this.serverService.getConfig()
|
||||||
.subscribe(config => this.serverConfig = config)
|
.subscribe(config => {
|
||||||
|
this.serverConfig = config
|
||||||
|
this.buildMenuLinks()
|
||||||
|
})
|
||||||
|
|
||||||
this.isLoggedIn = this.authService.isLoggedIn()
|
this.isLoggedIn = this.authService.isLoggedIn()
|
||||||
if (this.isLoggedIn === true) {
|
if (this.isLoggedIn === true) {
|
||||||
|
@ -241,6 +256,10 @@ export class MenuComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildMenuLinks () {
|
||||||
|
this.commonMenuLinks = this.menuService.buildCommonLinks(this.serverConfig)
|
||||||
|
}
|
||||||
|
|
||||||
private buildUserLanguages () {
|
private buildUserLanguages () {
|
||||||
if (!this.user) {
|
if (!this.user) {
|
||||||
this.videoLanguages = []
|
this.videoLanguages = []
|
||||||
|
|
|
@ -42,7 +42,7 @@ export class ActorBannerEditComponent implements OnInit {
|
||||||
this.bannerExtensions = config.banner.file.extensions.join(', ')
|
this.bannerExtensions = config.banner.file.extensions.join(', ')
|
||||||
|
|
||||||
// tslint:disable:max-line-length
|
// 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