Merge branch 'develop' into feat-6264-spam-plugin
This commit is contained in:
commit
cd417ddefa
|
@ -1,5 +1,7 @@
|
|||
name: Benchmark
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
#
|
||||
name: "CodeQL"
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, next ]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
name: Docker
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
name: Nightly
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
name: Stats
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
name: Test
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
@ -10,7 +12,7 @@ on:
|
|||
jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
services:
|
||||
redis:
|
||||
|
|
119
CHANGELOG.md
119
CHANGELOG.md
|
@ -1,5 +1,124 @@
|
|||
# Changelog
|
||||
|
||||
## v7.0.1
|
||||
|
||||
### Features
|
||||
|
||||
* Update translations
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix banner/avatar edit buttons
|
||||
* Fix banner margin in channels page
|
||||
* Textarea font size consistency
|
||||
* Fix subscribe button radius
|
||||
* Fix channel avatar info username
|
||||
* Fix maximized markdown textarea
|
||||
* Remove confusing channel message in *My playlists* pages
|
||||
* Fix broken infinite scroll when deleting items (Videos, Channels...)
|
||||
* Fix broadcast message overflow
|
||||
* Fix adding videos in playlist from discover page
|
||||
* Fix my videos edit/delete buttons display
|
||||
* Fix header components overflow in admin log page
|
||||
|
||||
|
||||
## v7.0.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* **Classic install only** (for Docker admins see [v6.3 IMPORTANT NOTES](https://github.com/Chocobozzz/PeerTube/releases/tag/v6.3.0)) Ensure you have `storage.original_video_files` set in your configuration file: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L159.
|
||||
If you did not configure this key but have already enabled "Keep a version of the input file" configuration, original files may have been saved in `versions/peertube-v6.x.x/storage/original-video-files/` directories. If this is the case, you must move these files in the new directory location specified by your `storage.original_video_files` configuration
|
||||
* Safari desktop versions < 13 are not supported anymore
|
||||
* iOS versions < 14.5 are not supported anymore
|
||||
* PeerTube instance requires python >= 3.8 for transcription
|
||||
|
||||
### Docker
|
||||
|
||||
* Fix private IPv6 subnet (we used a subnet reserved for examples)
|
||||
|
||||
### Plugins/Themes/Embed API
|
||||
|
||||
* Remove client plugin hooks: `filter:api.recently-added-videos.videos.list.{params,result}`, `filter:api.local-videos.videos.list.{params,result}`, `filter:api.trending-videos.videos.list.{params,result}` `filter:api.trending-videos.videos.list.result` in favour of `filter:api.browse-videos.videos.list.{params,result}`
|
||||
* Header logo doesn't have the `.icon` class anymore (it still has the `icon-logo` class)
|
||||
* All CSS variables have been replaced so it's easier to theme PeerTube:
|
||||
* PeerTube generates a color palette based on a few main colors (`primary`, `fg`, `bg`, `bg-secondary`...): https://github.com/Chocobozzz/PeerTube/blob/develop/client/src/sass/application.scss#L27
|
||||
* Some new variables fallback to old variables to limit theme breaks
|
||||
|
||||
### Admin config (non-exhaustive)
|
||||
|
||||
* Ensure `instance.default_client_route` (in web admin -> `Configuration` -> `Basic` -> `Landing page`) has a correct path: `/videos/trending`, `/videos/local` and `/videos/recently-added` have been removed in favour of `/videos/browse`
|
||||
* Add ability to configure STUN servers IPs: `webrtc.stun_servers`
|
||||
* Remove `client.videos.miniature.display_author_avatar` config: author avatars are now always displayed
|
||||
|
||||
### Features
|
||||
|
||||
* :tada: Global client redesign :tada:
|
||||
* Introduce a new *Light/Beige* theme that replaces the current one (black/orange)
|
||||
* Add a *Dark/Brown* theme directly in PeerTube core
|
||||
* Split *My library* pages into:
|
||||
* *Video Space* pages (that contains account channels, videos...)
|
||||
* *My library* pages (that contains account playlists, subscriptions...)
|
||||
* Split *Administration* pages into:
|
||||
* *Overview* pages (to list instance users, videos...)
|
||||
* *Moderation* pages (to list abuses, blocks, registrations...)
|
||||
* *Settings* pages (instance configuration, list runners...)
|
||||
* Reorganize the header and the left menu:
|
||||
* Account settings and notifications are now in the header
|
||||
* Add instance name and description in the left menu for anonymous users
|
||||
* Merge *Recently Added* and *Trending* and *Local videos* videos pages into a *Browse videos* page that includes quick filters
|
||||
* Improve *Discover videos* page UX
|
||||
* Redesign the left menu, the horizontal menus, form controls, buttons and video filters panel
|
||||
* Replace/remove/add some icons
|
||||
* :tada: Introduce a modal to easily add/edit/remove subtitle segments :tada:
|
||||
* Improve accessibility:
|
||||
* Fix contrast issues
|
||||
* Add missing labels
|
||||
* Fix progress bar, custom select components, tag input components, notification component accessibility
|
||||
* Add underlining to links
|
||||
* Add "skip menu" links
|
||||
* Improve keyboard navigation
|
||||
* Fix various screen readers issues
|
||||
* Add Slovakian language support to the client
|
||||
* SEO:
|
||||
* Add instance avatar to OpenGraph tags
|
||||
* Hide empty accounts/channels from sitemap [#6633](https://github.com/Chocobozzz/PeerTube/pull/6633)
|
||||
* Inject additional video tags to sitemap [#6633](https://github.com/Chocobozzz/PeerTube/pull/6633)
|
||||
* Various UX improvements:
|
||||
* Improve player control bar responsive
|
||||
* Add refresh button to following list
|
||||
* Clearer signup limit label
|
||||
* Add `0.25` playback rate in player
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix *My channel* search
|
||||
* Fix channel sync edition/listing
|
||||
* Fix adding video tags on Android
|
||||
* Fix fetching client comment URL using ActivityPub resolver (Mastodon search bar...)
|
||||
* Fix crash when logging SQL requests and enabled prettify option
|
||||
* Correctly delete web videos with hls without audio
|
||||
* Fix auto blacklisting unlisted videos
|
||||
* Fix *ERR_BUFFER_OUT_OF_BOUNDS* error on some node version
|
||||
* Add ability to set max channel sync in admin config
|
||||
* Allow plugins to pass client params when listing videos (`filter:api.browse-videos.videos.list.params` hook)
|
||||
* Respect user export expiration admin configuration
|
||||
* Fix studio edition on an audio only file
|
||||
* Fix embed crash on telegram web browser
|
||||
|
||||
|
||||
## v6.3.3
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix broken thumbnails on live replay
|
||||
* Fix detecting portrait rotation of some video
|
||||
* Don't allow to select a frame from a live to set the thumbnail
|
||||
* Fix lost video stream with specific transcoding settings and video input
|
||||
* Fix creating playlist without thumbnail when using the REST API
|
||||
* Fix `.mov` video upload on some Windows versions
|
||||
* Fix `video-plugin-metadata.result` client plugin hook
|
||||
|
||||
|
||||
## v6.3.2
|
||||
|
||||
### Bug fixes
|
||||
|
|
30
CREDITS.md
30
CREDITS.md
|
@ -5,8 +5,8 @@
|
|||
* DignifiedSilence
|
||||
* Александр
|
||||
* T.S
|
||||
* Hồ Nhất Duy
|
||||
* josé m
|
||||
* Hồ Nhất Duy
|
||||
* Jeff Huang
|
||||
* Ihor Hordiichuk
|
||||
* Filip Bengtsson
|
||||
|
@ -36,10 +36,10 @@
|
|||
* John Livingston
|
||||
* Hannes Ylä-Jääski
|
||||
* Kim
|
||||
* Besnik Bleta
|
||||
* Vodoyo Kamal
|
||||
* Armin
|
||||
* Fontan 030
|
||||
* Besnik Bleta
|
||||
* Mohamad Reza
|
||||
* Quentin PAGÈS
|
||||
* Kimsible
|
||||
|
@ -56,12 +56,12 @@
|
|||
* Julien Maulny
|
||||
* Mark Van den Borre
|
||||
* x
|
||||
* Manuel Viens
|
||||
* Jorropo
|
||||
* Josh Morel
|
||||
* Manuel Viens
|
||||
* Renne Rocha
|
||||
* BO41
|
||||
* Ettore Atalan
|
||||
* Renne Rocha
|
||||
* vachan
|
||||
* Elegant Codes
|
||||
* Florian CUNY
|
||||
|
@ -78,6 +78,7 @@
|
|||
* Ch
|
||||
* J. Lavoie
|
||||
* YILDIRIM YAPRAK
|
||||
* alex gabilondo
|
||||
* barzofarev2
|
||||
* jan Seli
|
||||
* 李奕寯
|
||||
|
@ -92,9 +93,9 @@
|
|||
* Echo Kilo
|
||||
* Erik Guldberg
|
||||
* Jan Keromnes
|
||||
* Jiří Podhorecký
|
||||
* Luc Didry
|
||||
* Siourdakis Thanos
|
||||
* alex gabilondo
|
||||
* knuxify
|
||||
* Agron Selimaj
|
||||
* Attila F
|
||||
|
@ -102,6 +103,7 @@
|
|||
* David Soh
|
||||
* Diazepan Medina
|
||||
* Jason Zhou
|
||||
* Kerim Demirkaynak
|
||||
* Loukas Stamellos
|
||||
* Ms Kimsible
|
||||
* NorbiPeti
|
||||
|
@ -115,6 +117,7 @@
|
|||
* Lucas Declercq
|
||||
* Sirxy
|
||||
* matograine
|
||||
* Adrià Martín
|
||||
* Ahmed ABERWAG
|
||||
* Daniel Santos
|
||||
* David Libeau
|
||||
|
@ -132,7 +135,6 @@
|
|||
* nexi
|
||||
* owiox8+1viroxeaziaxw@sharklasers.com
|
||||
* yns bag
|
||||
* Adrià Martín
|
||||
* Anne-Gaelle Moulun
|
||||
* Arman
|
||||
* Asier Iturralde Sarasola
|
||||
|
@ -146,7 +148,6 @@
|
|||
* Green-Star
|
||||
* I_Automne
|
||||
* Ilia
|
||||
* Kerim Demirkaynak
|
||||
* Micah Elizabeth Scott
|
||||
* Pierre-Jean
|
||||
* Ret Samys
|
||||
|
@ -159,6 +160,7 @@
|
|||
* boris joeson
|
||||
* frankstrater
|
||||
* mater
|
||||
* spf
|
||||
* test2a
|
||||
* think4web
|
||||
* 路过是好事
|
||||
|
@ -171,9 +173,11 @@
|
|||
* Cokelat8
|
||||
* DontUseGithub
|
||||
* Farooq Karimi Zadeh
|
||||
* Frederic Bezies
|
||||
* Iñigo
|
||||
* Jim Kats
|
||||
* Joan Montané
|
||||
* José M
|
||||
* Kristoffer Grundström
|
||||
* LecygneNoir
|
||||
* Lukas
|
||||
|
@ -195,7 +199,6 @@
|
|||
* helabasa
|
||||
* kaiyou
|
||||
* roberto marcolin
|
||||
* spf
|
||||
* Ahsan Haris Ahmed
|
||||
* Alberto Teira
|
||||
* Alejandro
|
||||
|
@ -230,6 +233,7 @@
|
|||
* Kiro
|
||||
* Leopere
|
||||
* Linus
|
||||
* Lionel Caylat
|
||||
* Lukas Winkler
|
||||
* M Z
|
||||
* Manuela Silva
|
||||
|
@ -292,13 +296,14 @@
|
|||
* HHY
|
||||
* Hange
|
||||
* Hjalte
|
||||
* HugeFrog24
|
||||
* Hugo Peixoto
|
||||
* HybridGlucose
|
||||
* J C Worm
|
||||
* Jan Ainali
|
||||
* Jan Hartig
|
||||
* Jan Marsalek
|
||||
* Jerguš Fonfer
|
||||
* José M
|
||||
* Joël Galeran
|
||||
* Julien Lemaire
|
||||
* Julien Rabier
|
||||
|
@ -309,11 +314,13 @@
|
|||
* Mondo Xíbaro
|
||||
* Moritz Warning
|
||||
* Mostafa Ahangarha
|
||||
* Murat Özalp
|
||||
* Neko Nekowazarashi
|
||||
* Nicolai Larsen
|
||||
* Nojus
|
||||
* Olivier Bouillet
|
||||
* Pierre Jaury
|
||||
* Piotr Strębski
|
||||
* Puryx
|
||||
* Quentin
|
||||
* ROBERT MCDOWELL
|
||||
|
@ -396,8 +403,10 @@
|
|||
* Clément Brizard
|
||||
* DLP
|
||||
* Daniel Dutra
|
||||
* David Baumgold
|
||||
* David Dobryakov
|
||||
* DeeJayBro
|
||||
* Deval
|
||||
* Dimitri DI GUSTO
|
||||
* Dimitrios Glentadakis
|
||||
* Durgaraj Karki
|
||||
|
@ -455,6 +464,7 @@
|
|||
* JustAnotherArchivist
|
||||
* Kent Anderson
|
||||
* Kevin Cope
|
||||
* Kevin Pliester
|
||||
* Knackie
|
||||
* Kody
|
||||
* Konstantinos Agiannis
|
||||
|
@ -490,7 +500,6 @@
|
|||
* Mikel Gartzia Santamaria
|
||||
* Milo van der Linden
|
||||
* MrGiga
|
||||
* Murat Özalp
|
||||
* Mélanie Pin
|
||||
* Nataly Rocha
|
||||
* Nathanaël J
|
||||
|
@ -558,6 +567,7 @@
|
|||
* Vagelis F
|
||||
* Varik Valefor
|
||||
* Vegard Fjeldberg
|
||||
* VeryREAL
|
||||
* Vik
|
||||
* Vincent Stakenburg
|
||||
* WhiredPlanck
|
||||
|
|
|
@ -36,9 +36,9 @@ cli-table3@^0.6.0:
|
|||
"@colors/colors" "1.5.0"
|
||||
|
||||
cross-spawn@^6.0.0:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
|
||||
version "6.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
|
||||
integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==
|
||||
dependencies:
|
||||
nice-try "^1.0.4"
|
||||
path-key "^2.0.1"
|
||||
|
@ -174,9 +174,9 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
|
|||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||
|
||||
sort-keys@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-5.0.0.tgz#5d775f8ae93ecc29bc7312bbf3acac4e36e3c446"
|
||||
integrity sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-5.1.0.tgz#50a3f3d1ad3c5a76d043e0aeeba7299241e9aa5c"
|
||||
integrity sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==
|
||||
dependencies:
|
||||
is-plain-obj "^4.0.0"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@peertube/peertube-runner",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.23",
|
||||
"type": "module",
|
||||
"main": "dist/peertube-runner.js",
|
||||
"bin": "dist/peertube-runner.js",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { Command, InvalidArgumentError } from '@commander-js/extra-typings'
|
||||
import { RunnerJobType } from '@peertube/peertube-models'
|
||||
import { listRegistered, registerRunner, unregisterRunner } from './register/index.js'
|
||||
import { gracefulShutdown } from './register/shutdown.js'
|
||||
import { RunnerServer } from './server/index.js'
|
||||
import { getSupportedJobsList } from './server/shared/supported-job.js'
|
||||
import { ConfigManager, logger } from './shared/index.js'
|
||||
|
@ -98,6 +99,18 @@ program.command('list-registered')
|
|||
}
|
||||
})
|
||||
|
||||
program.command('graceful-shutdown')
|
||||
.description('Exit runner when all processing tasks are finished')
|
||||
.action(async () => {
|
||||
try {
|
||||
await gracefulShutdown()
|
||||
} catch (err) {
|
||||
console.error('Cannot graceful shutdown the runner.')
|
||||
console.error(err)
|
||||
process.exit(-1)
|
||||
}
|
||||
})
|
||||
|
||||
program.parse()
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { IPCClient } from '../shared/ipc/index.js'
|
||||
|
||||
export async function gracefulShutdown () {
|
||||
const client = new IPCClient()
|
||||
await client.run()
|
||||
|
||||
await client.askGracefulShutdown()
|
||||
|
||||
client.stop()
|
||||
}
|
|
@ -61,8 +61,12 @@ export function scheduleTranscodingProgress (options: {
|
|||
: 60000
|
||||
|
||||
const update = () => {
|
||||
server.runnerJobs.update({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken, progress: progressGetter() })
|
||||
.catch(err => logger.error({ err }, 'Cannot send job progress'))
|
||||
server.runnerJobs.update({
|
||||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
runnerToken,
|
||||
progress: progressGetter()
|
||||
}).catch(err => logger.error({ err }, 'Cannot send job progress'))
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
|
|
|
@ -50,22 +50,23 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
logger.debug(`Using ${this.outputPath} to process live rtmp hls transcoding job ${options.job.uuid}`)
|
||||
}
|
||||
|
||||
process () {
|
||||
const job = this.options.job
|
||||
const payload = job.payload
|
||||
private get payload () {
|
||||
return this.options.job.payload
|
||||
}
|
||||
|
||||
process () {
|
||||
return new Promise<void>(async (res, rej) => {
|
||||
try {
|
||||
await ensureDir(this.outputPath)
|
||||
|
||||
logger.info(`Probing ${payload.input.rtmpUrl}`)
|
||||
const probe = await ffprobePromise(payload.input.rtmpUrl)
|
||||
logger.info({ probe }, `Probed ${payload.input.rtmpUrl}`)
|
||||
logger.info(`Probing ${this.payload.input.rtmpUrl}`)
|
||||
const probe = await ffprobePromise(this.payload.input.rtmpUrl)
|
||||
logger.info({ probe }, `Probed ${this.payload.input.rtmpUrl}`)
|
||||
|
||||
const hasAudio = await hasAudioStream(payload.input.rtmpUrl, probe)
|
||||
const hasVideo = await hasVideoStream(payload.input.rtmpUrl, probe)
|
||||
const bitrate = await getVideoStreamBitrate(payload.input.rtmpUrl, probe)
|
||||
const { ratio } = await getVideoStreamDimensionsInfo(payload.input.rtmpUrl, probe)
|
||||
const hasAudio = await hasAudioStream(this.payload.input.rtmpUrl, probe)
|
||||
const hasVideo = await hasVideoStream(this.payload.input.rtmpUrl, probe)
|
||||
const bitrate = await getVideoStreamBitrate(this.payload.input.rtmpUrl, probe)
|
||||
const { ratio } = await getVideoStreamDimensionsInfo(this.payload.input.rtmpUrl, probe)
|
||||
|
||||
const m3u8Watcher = watch(this.outputPath + '/*.m3u8')
|
||||
this.fsWatchers.push(m3u8Watcher)
|
||||
|
@ -107,15 +108,15 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
})
|
||||
|
||||
this.ffmpegCommand = await buildFFmpegLive().getLiveTranscodingCommand({
|
||||
inputUrl: payload.input.rtmpUrl,
|
||||
inputUrl: this.payload.input.rtmpUrl,
|
||||
|
||||
outPath: this.outputPath,
|
||||
masterPlaylistName: 'master.m3u8',
|
||||
|
||||
segmentListSize: payload.output.segmentListSize,
|
||||
segmentDuration: payload.output.segmentDuration,
|
||||
segmentListSize: this.payload.output.segmentListSize,
|
||||
segmentDuration: this.payload.output.segmentDuration,
|
||||
|
||||
toTranscode: payload.output.toTranscode,
|
||||
toTranscode: this.payload.output.toTranscode,
|
||||
splitAudioAndVideo: true,
|
||||
|
||||
bitrate,
|
||||
|
@ -126,7 +127,7 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
probe
|
||||
})
|
||||
|
||||
logger.info(`Running live transcoding for ${payload.input.rtmpUrl}`)
|
||||
logger.info(`Running live transcoding for ${this.payload.input.rtmpUrl}`)
|
||||
|
||||
this.ffmpegCommand.on('error', (err, stdout, stderr) => {
|
||||
this.onFFmpegError({ err, stdout, stderr })
|
||||
|
@ -241,7 +242,8 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
jobToken: this.options.job.jobToken,
|
||||
jobUUID: this.options.job.uuid,
|
||||
runnerToken: this.options.runnerToken,
|
||||
payload: successBody
|
||||
payload: successBody,
|
||||
reqPayload: this.payload
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -324,7 +326,7 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
await Promise.all(parallelPromises)
|
||||
}
|
||||
|
||||
private async updateWithRetry (payload: CustomLiveRTMPHLSTranscodingUpdatePayload, currentTry = 1): Promise<any> {
|
||||
private async updateWithRetry (updatePayload: CustomLiveRTMPHLSTranscodingUpdatePayload, currentTry = 1): Promise<any> {
|
||||
if (this.ended || this.errored) return
|
||||
|
||||
try {
|
||||
|
@ -332,7 +334,8 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
jobToken: this.options.job.jobToken,
|
||||
jobUUID: this.options.job.uuid,
|
||||
runnerToken: this.options.runnerToken,
|
||||
payload: payload as any
|
||||
payload: updatePayload as any,
|
||||
reqPayload: this.payload
|
||||
})
|
||||
} catch (err) {
|
||||
if (currentTry >= 3) throw err
|
||||
|
@ -341,7 +344,7 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
logger.warn({ err }, 'Will retry update after error')
|
||||
await wait(250)
|
||||
|
||||
return this.updateWithRetry(payload, currentTry + 1)
|
||||
return this.updateWithRetry(updatePayload, currentTry + 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,8 @@ export async function processStudioTranscoding (options: ProcessOptions<RunnerJo
|
|||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
runnerToken,
|
||||
payload: successBody
|
||||
payload: successBody,
|
||||
reqPayload: payload
|
||||
})
|
||||
} finally {
|
||||
if (tmpVideoInputFilePath) await remove(tmpVideoInputFilePath)
|
||||
|
|
|
@ -69,7 +69,8 @@ export async function processVideoTranscription (options: ProcessOptions<RunnerJ
|
|||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
runnerToken,
|
||||
payload: successBody
|
||||
payload: successBody,
|
||||
reqPayload: payload
|
||||
})
|
||||
} finally {
|
||||
if (inputPath) await remove(inputPath)
|
||||
|
|
|
@ -71,7 +71,8 @@ export async function processWebVideoTranscoding (options: ProcessOptions<Runner
|
|||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
runnerToken,
|
||||
payload: successBody
|
||||
payload: successBody,
|
||||
reqPayload: payload
|
||||
})
|
||||
} finally {
|
||||
if (videoInputPath) await remove(videoInputPath)
|
||||
|
@ -139,7 +140,8 @@ export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVO
|
|||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
runnerToken,
|
||||
payload: successBody
|
||||
payload: successBody,
|
||||
reqPayload: payload
|
||||
})
|
||||
} finally {
|
||||
if (videoInputPath) await remove(videoInputPath)
|
||||
|
@ -207,7 +209,8 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
|
|||
jobToken: job.jobToken,
|
||||
jobUUID: job.uuid,
|
||||
runnerToken,
|
||||
payload: successBody
|
||||
payload: successBody,
|
||||
reqPayload: payload
|
||||
})
|
||||
} finally {
|
||||
if (audioPath) await remove(audioPath)
|
||||
|
|
|
@ -23,21 +23,24 @@ export class RunnerServer {
|
|||
|
||||
private checkingAvailableJobs = false
|
||||
|
||||
private gracefulShutdown = false
|
||||
private cleaningUp = false
|
||||
private initialized = false
|
||||
|
||||
private readonly enabledJobsArray: RunnerJobType[]
|
||||
|
||||
private readonly sockets = new Map<PeerTubeServer, Socket>()
|
||||
|
||||
constructor (private readonly enabledJobs?: Set<RunnerJobType>) {}
|
||||
constructor (private readonly enabledJobs?: Set<RunnerJobType>) {
|
||||
this.enabledJobsArray = enabledJobs
|
||||
? Array.from(enabledJobs)
|
||||
: getSupportedJobsList()
|
||||
}
|
||||
|
||||
async run () {
|
||||
logger.info('Running PeerTube runner in server mode')
|
||||
|
||||
const enabledJobsArray = this.enabledJobs
|
||||
? Array.from(this.enabledJobs)
|
||||
: getSupportedJobsList()
|
||||
|
||||
logger.info('Supported and enabled job types: ' + enabledJobsArray.join(', '))
|
||||
logger.info('Supported and enabled job types: ' + this.enabledJobsArray.join(', '))
|
||||
|
||||
await ConfigManager.Instance.load()
|
||||
|
||||
|
@ -180,6 +183,15 @@ export class RunnerServer {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
requestGracefulShutdown () {
|
||||
logger.info('Received graceful shutdown request')
|
||||
|
||||
this.gracefulShutdown = true
|
||||
this.exitGracefullyIfNoProcessingJobs()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private safeAsyncCheckAvailableJobs () {
|
||||
this.checkAvailableJobs()
|
||||
.catch(err => logger.error({ err }, `Cannot check available jobs`))
|
||||
|
@ -188,6 +200,7 @@ export class RunnerServer {
|
|||
private async checkAvailableJobs () {
|
||||
if (!this.initialized) return
|
||||
if (this.checkingAvailableJobs) return
|
||||
if (this.gracefulShutdown) return
|
||||
|
||||
this.checkingAvailableJobs = true
|
||||
|
||||
|
@ -237,8 +250,15 @@ export class RunnerServer {
|
|||
private async requestJob (server: PeerTubeServer) {
|
||||
logger.debug(`Requesting jobs on ${server.url} for runner ${server.runnerName}`)
|
||||
|
||||
const { availableJobs } = await server.runnerJobs.request({ runnerToken: server.runnerToken })
|
||||
const { availableJobs } = await server.runnerJobs.request({
|
||||
runnerToken: server.runnerToken,
|
||||
|
||||
jobTypes: this.enabledJobsArray.length !== getSupportedJobsList().length
|
||||
? this.enabledJobsArray
|
||||
: undefined
|
||||
})
|
||||
|
||||
// FIXME: remove in PeerTube v8: jobTypes has been introduced in PeerTube v7, so do the filter here too
|
||||
const filtered = availableJobs.filter(j => isJobSupported(j, this.enabledJobs))
|
||||
|
||||
if (filtered.length === 0) {
|
||||
|
@ -250,7 +270,15 @@ export class RunnerServer {
|
|||
}
|
||||
|
||||
private async tryToExecuteJobAsync (server: PeerTubeServer, jobToAccept: { uuid: string }) {
|
||||
if (!this.canProcessMoreJobs()) return
|
||||
if (!this.canProcessMoreJobs()) {
|
||||
if (!this.gracefulShutdown) {
|
||||
logger.info(
|
||||
`Do not process more jobs (processing ${this.processingJobs.length} / ${ConfigManager.Instance.getConfig().jobs.concurrency})`
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const { job } = await server.runnerJobs.accept({ runnerToken: server.runnerToken, jobUUID: jobToAccept.uuid })
|
||||
|
||||
|
@ -267,6 +295,8 @@ export class RunnerServer {
|
|||
.finally(() => {
|
||||
this.processingJobs = this.processingJobs.filter(p => p !== processingJob)
|
||||
|
||||
if (this.gracefulShutdown) this.exitGracefullyIfNoProcessingJobs()
|
||||
|
||||
return this.checkAvailableJobs()
|
||||
})
|
||||
}
|
||||
|
@ -282,6 +312,9 @@ export class RunnerServer {
|
|||
}
|
||||
|
||||
private canProcessMoreJobs () {
|
||||
if (this.cleaningUp) return false
|
||||
if (this.gracefulShutdown) return false
|
||||
|
||||
return this.processingJobs.length < ConfigManager.Instance.getConfig().jobs.concurrency
|
||||
}
|
||||
|
||||
|
@ -295,6 +328,15 @@ export class RunnerServer {
|
|||
}
|
||||
}
|
||||
|
||||
private exitGracefullyIfNoProcessingJobs () {
|
||||
if (this.processingJobs.length !== 0) return
|
||||
|
||||
logger.info('Shutting down the runner after graceful shutdown request')
|
||||
|
||||
this.onExit()
|
||||
.catch(err => logger.error({ err }, 'Cannot exit runner'))
|
||||
}
|
||||
|
||||
private async onExit () {
|
||||
if (this.cleaningUp) return
|
||||
this.cleaningUp = true
|
||||
|
|
|
@ -46,5 +46,5 @@ export function isJobSupported (job: { type: RunnerJobType, payload: RunnerJobPa
|
|||
}
|
||||
|
||||
export function getSupportedJobsList () {
|
||||
return Object.keys(supportedMatrix)
|
||||
return Object.keys(supportedMatrix) as unknown as RunnerJobType[]
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import CliTable3 from 'cli-table3'
|
|||
import { ensureDir } from 'fs-extra/esm'
|
||||
import { Client as NetIPC } from '@peertube/net-ipc'
|
||||
import { ConfigManager } from '../config-manager.js'
|
||||
import { IPCReponse, IPCReponseData, IPCRequest } from './shared/index.js'
|
||||
import { IPCResponse, IPCResponseData, IPCRequest } from './shared/index.js'
|
||||
|
||||
export class IPCClient {
|
||||
private netIPC: NetIPC
|
||||
|
@ -39,7 +39,7 @@ export class IPCClient {
|
|||
...options
|
||||
}
|
||||
|
||||
const { success, error } = await this.netIPC.request(req) as IPCReponse
|
||||
const { success, error } = await this.netIPC.request(req) as IPCResponse
|
||||
|
||||
if (success) console.log('PeerTube instance registered')
|
||||
else console.error('Could not register PeerTube instance on runner server side', error)
|
||||
|
@ -54,7 +54,7 @@ export class IPCClient {
|
|||
...options
|
||||
}
|
||||
|
||||
const { success, error } = await this.netIPC.request(req) as IPCReponse
|
||||
const { success, error } = await this.netIPC.request(req) as IPCResponse
|
||||
|
||||
if (success) console.log('PeerTube instance unregistered')
|
||||
else console.error('Could not unregister PeerTube instance on runner server side', error)
|
||||
|
@ -65,7 +65,7 @@ export class IPCClient {
|
|||
type: 'list-registered'
|
||||
}
|
||||
|
||||
const { success, error, data } = await this.netIPC.request(req) as IPCReponse<IPCReponseData>
|
||||
const { success, error, data } = await this.netIPC.request(req) as IPCResponse<IPCResponseData>
|
||||
if (!success) {
|
||||
console.error('Could not list registered PeerTube instances', error)
|
||||
return
|
||||
|
@ -82,6 +82,19 @@ export class IPCClient {
|
|||
console.log(table.toString())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async askGracefulShutdown () {
|
||||
const req: IPCRequest = { type: 'graceful-shutdown' }
|
||||
|
||||
const { success, error } = await this.netIPC.request(req) as IPCResponse
|
||||
|
||||
if (success) console.log('Graceful shutdown acknowledged by the runner')
|
||||
else console.error('Could not graceful shutdown runner', error)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
stop () {
|
||||
this.netIPC.destroy()
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { pick } from '@peertube/peertube-core-utils'
|
|||
import { RunnerServer } from '../../server/index.js'
|
||||
import { ConfigManager } from '../config-manager.js'
|
||||
import { logger } from '../logger.js'
|
||||
import { IPCReponse, IPCReponseData, IPCRequest } from './shared/index.js'
|
||||
import { IPCResponse, IPCResponseData, IPCRequest } from './shared/index.js'
|
||||
|
||||
export class IPCServer {
|
||||
private netIPC: NetIPC
|
||||
|
@ -25,10 +25,10 @@ export class IPCServer {
|
|||
try {
|
||||
const data = await this.process(req)
|
||||
|
||||
this.sendReponse(res, { success: true, data })
|
||||
this.sendResponse(res, { success: true, data })
|
||||
} catch (err) {
|
||||
logger.error('Cannot execute RPC call', err)
|
||||
this.sendReponse(res, { success: false, error: err.message })
|
||||
logger.error({ err }, 'Cannot execute RPC call')
|
||||
this.sendResponse(res, { success: false, error: err.message })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -46,14 +46,18 @@ export class IPCServer {
|
|||
case 'list-registered':
|
||||
return Promise.resolve(this.runnerServer.listRegistered())
|
||||
|
||||
case 'graceful-shutdown':
|
||||
this.runnerServer.requestGracefulShutdown()
|
||||
return undefined
|
||||
|
||||
default:
|
||||
throw new Error('Unknown RPC call ' + (req as any).type)
|
||||
}
|
||||
}
|
||||
|
||||
private sendReponse <T extends IPCReponseData> (
|
||||
private sendResponse <T extends IPCResponseData> (
|
||||
response: (data: any) => Promise<void>,
|
||||
body: IPCReponse<T>
|
||||
body: IPCResponse<T>
|
||||
) {
|
||||
response(body)
|
||||
.catch(err => logger.error('Cannot send response after IPC request', err))
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
export type IPCRequest =
|
||||
IPCRequestRegister |
|
||||
IPCRequestUnregister |
|
||||
IPCRequestListRegistered
|
||||
IPCRequestListRegistered |
|
||||
IPCRequestGracefulShutdown
|
||||
|
||||
export type IPCRequestRegister = {
|
||||
type: 'register'
|
||||
|
@ -13,3 +14,5 @@ export type IPCRequestRegister = {
|
|||
|
||||
export type IPCRequestUnregister = { type: 'unregister', url: string, runnerName: string }
|
||||
export type IPCRequestListRegistered = { type: 'list-registered' }
|
||||
|
||||
export type IPCRequestGracefulShutdown = { type: 'graceful-shutdown' }
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
export type IPCReponse <T extends IPCReponseData = undefined> = {
|
||||
export type IPCResponse <T extends IPCResponseData = undefined> = {
|
||||
success: boolean
|
||||
error?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
export type IPCReponseData =
|
||||
export type IPCResponseData =
|
||||
// list registered
|
||||
{
|
||||
servers: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { pino } from 'pino'
|
||||
import pretty from 'pino-pretty'
|
||||
|
||||
const logger = pino(pretty.default({
|
||||
const logger = pino(pretty({
|
||||
colorize: true
|
||||
}))
|
||||
|
||||
|
|
|
@ -53,11 +53,11 @@
|
|||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "20.14.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.7.tgz#342cada27f97509eb8eb2dbc003edf21ce8ab5a8"
|
||||
integrity sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==
|
||||
version "22.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.3.tgz#08f3d64b3bc6d74b162d36f60213e8a6704ef2b4"
|
||||
integrity sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
undici-types "~6.19.8"
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
@ -142,9 +142,9 @@ fast-zlib@^2.0.1:
|
|||
integrity sha512-DCoYgNagM2Bt1VIpXpdGnRx4LzqJeYG0oh6Nf/7cWo6elTXkFGMw9CrRCYYUIapYNrozYMoyDRflx9mgT3Awyw==
|
||||
|
||||
follow-redirects@^1.15.5:
|
||||
version "1.15.6"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
|
||||
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
|
||||
version "1.15.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
help-me@^5.0.0:
|
||||
version "5.0.0"
|
||||
|
@ -181,9 +181,9 @@ msgpackr-extract@^3.0.2:
|
|||
"@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.3"
|
||||
|
||||
msgpackr@^1.3.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.10.2.tgz#a73de4767f76659e8c69cf9c80fdfce83937a44a"
|
||||
integrity sha512-L60rsPynBvNE+8BWipKKZ9jHcSGbtyJYIwjRq0VrIvQ08cRjntGXJYW/tmciZ2IHWIY8WEW32Qa2xbh5+SKBZA==
|
||||
version "1.11.2"
|
||||
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.11.2.tgz#4463b7f7d68f2e24865c395664973562ad24473d"
|
||||
integrity sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==
|
||||
optionalDependencies:
|
||||
msgpackr-extract "^3.0.2"
|
||||
|
||||
|
@ -206,18 +206,17 @@ once@^1.3.1, once@^1.4.0:
|
|||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5"
|
||||
integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==
|
||||
pino-abstract-transport@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60"
|
||||
integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==
|
||||
dependencies:
|
||||
readable-stream "^4.0.0"
|
||||
split2 "^4.0.0"
|
||||
|
||||
pino-pretty@^11.2.1:
|
||||
version "11.2.1"
|
||||
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.2.1.tgz#de9a42ff8ea7b26da93506bb9e49d0b566c5ae96"
|
||||
integrity sha512-O05NuD9tkRasFRWVaF/uHLOvoRDFD7tb5VMertr78rbsYFjYp48Vg3477EshVAF5eZaEw+OpDl/tu+B0R5o+7g==
|
||||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.3.0.tgz#390b3be044cf3d2e9192c7d19d44f6b690468f2e"
|
||||
integrity sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==
|
||||
dependencies:
|
||||
colorette "^2.0.7"
|
||||
dateformat "^4.6.3"
|
||||
|
@ -227,7 +226,7 @@ pino-pretty@^11.2.1:
|
|||
joycon "^3.1.1"
|
||||
minimist "^1.2.6"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^1.0.0"
|
||||
pino-abstract-transport "^2.0.0"
|
||||
pump "^3.0.0"
|
||||
readable-stream "^4.0.0"
|
||||
secure-json-parse "^2.4.0"
|
||||
|
@ -240,26 +239,26 @@ pino-std-serializers@^7.0.0:
|
|||
integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
|
||||
|
||||
pino@^9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-9.2.0.tgz#e77a9516f3a3e5550d9b76d9f65ac6118ef02bdd"
|
||||
integrity sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==
|
||||
version "9.5.0"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-9.5.0.tgz#a7ef0fea868d22d52d8a4ce46e6e03c5dc46fdd6"
|
||||
integrity sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
fast-redact "^3.1.1"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^1.2.0"
|
||||
pino-abstract-transport "^2.0.0"
|
||||
pino-std-serializers "^7.0.0"
|
||||
process-warning "^3.0.0"
|
||||
process-warning "^4.0.0"
|
||||
quick-format-unescaped "^4.0.3"
|
||||
real-require "^0.2.0"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
sonic-boom "^4.0.1"
|
||||
thread-stream "^3.0.0"
|
||||
|
||||
process-warning@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b"
|
||||
integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==
|
||||
process-warning@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.0.tgz#581e3a7a1fb456c5f4fd239f76bce75897682d5a"
|
||||
integrity sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==
|
||||
|
||||
process@^0.11.10:
|
||||
version "0.11.10"
|
||||
|
@ -267,9 +266,9 @@ process@^0.11.10:
|
|||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
|
||||
integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
@ -301,9 +300,9 @@ safe-buffer@~5.2.0:
|
|||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-stable-stringify@^2.3.1:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886"
|
||||
integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
|
||||
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
|
||||
|
||||
secure-json-parse@^2.4.0:
|
||||
version "2.7.0"
|
||||
|
@ -311,9 +310,9 @@ secure-json-parse@^2.4.0:
|
|||
integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
|
||||
|
||||
sonic-boom@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30"
|
||||
integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d"
|
||||
integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
|
||||
|
@ -341,10 +340,10 @@ thread-stream@^3.0.0:
|
|||
dependencies:
|
||||
real-require "^0.2.0"
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
undici-types@~6.19.8:
|
||||
version "6.19.8"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
|
||||
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
"translation": "src/locale/angular.ar.xlf",
|
||||
"baseHref": "/client/ar/"
|
||||
},
|
||||
"sk": {
|
||||
"translation": "src/locale/angular.sk-SK.xlf",
|
||||
"baseHref": "/client/sk-SK/"
|
||||
},
|
||||
"fa": {
|
||||
"translation": "src/locale/angular.fa-IR.xlf",
|
||||
"baseHref": "/client/fa-IR/"
|
||||
|
@ -208,11 +212,7 @@
|
|||
"is-plain-object",
|
||||
"parse-srcset",
|
||||
"deepmerge",
|
||||
"core-js/features/reflect",
|
||||
"@formatjs/intl-locale/polyfill",
|
||||
"@formatjs/intl-locale/should-polyfill",
|
||||
"@formatjs/intl-pluralrules/polyfill-force",
|
||||
"@formatjs/intl-pluralrules/should-polyfill"
|
||||
"core-js/features/reflect"
|
||||
],
|
||||
"scripts": [],
|
||||
"extractLicenses": false,
|
||||
|
|
|
@ -8,7 +8,7 @@ export class AdminConfigPage {
|
|||
'basic-configuration': 'APPEARANCE',
|
||||
'instance-information': 'INSTANCE'
|
||||
}
|
||||
await go('/admin/config/edit-custom#' + tab)
|
||||
await go('/admin/settings/config/edit-custom#' + tab)
|
||||
|
||||
await $('h2=' + waitTitles[tab]).waitForDisplayed()
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { browserSleep, go } from '../utils'
|
|||
export class AdminPluginPage {
|
||||
|
||||
async navigateToPluginSearch () {
|
||||
await go('/admin/plugins/search')
|
||||
await go('/admin/settings/plugins/search')
|
||||
|
||||
await $('my-plugin-search').waitForDisplayed()
|
||||
}
|
||||
|
|
|
@ -3,10 +3,8 @@ import { getCheckbox } from '../utils'
|
|||
export class AnonymousSettingsPage {
|
||||
|
||||
async openSettings () {
|
||||
const link = await $$('.menu-link').filter(async i => {
|
||||
return await i.getText() === 'My settings'
|
||||
}).then(links => links[0])
|
||||
|
||||
const link = await $('my-header .settings-button')
|
||||
await link.waitForClickable()
|
||||
await link.click()
|
||||
|
||||
await $('my-user-video-settings').waitForDisplayed()
|
||||
|
|
|
@ -35,19 +35,7 @@ export class LoginPage {
|
|||
await submit.click()
|
||||
}
|
||||
|
||||
if (this.isMobileDevice) {
|
||||
const menuToggle = $('.top-left-block button')
|
||||
|
||||
await $('h2=Our content selection').waitForDisplayed()
|
||||
|
||||
await menuToggle.click()
|
||||
|
||||
await this.ensureIsLoggedInAs(displayName)
|
||||
|
||||
await menuToggle.click()
|
||||
} else {
|
||||
await this.ensureIsLoggedInAs(displayName)
|
||||
}
|
||||
}
|
||||
|
||||
async getLoginError (username: string, password: string) {
|
||||
|
@ -76,7 +64,7 @@ export class LoginPage {
|
|||
}
|
||||
|
||||
async logout () {
|
||||
const loggedInDropdown = $('.logged-in-more .logged-in-info')
|
||||
const loggedInDropdown = $('.logged-in-container .logged-in-info')
|
||||
|
||||
await loggedInDropdown.waitForClickable()
|
||||
await loggedInDropdown.click()
|
||||
|
@ -87,18 +75,16 @@ export class LoginPage {
|
|||
await logout.click()
|
||||
|
||||
await browser.waitUntil(() => {
|
||||
return $$('.login-buttons-block, my-error-page a[href="/login"]').some(e => e.isDisplayed())
|
||||
return $$('my-login-link, my-error-page a[href="/login"]').some(e => e.isDisplayed())
|
||||
})
|
||||
}
|
||||
|
||||
async ensureIsLoggedInAs (displayName: string) {
|
||||
await this.getLoggedInInfoElem().waitForExist()
|
||||
|
||||
await expect(this.getLoggedInInfoElem()).toHaveText(displayName)
|
||||
await this.getLoggedInInfoElem(displayName).waitForExist()
|
||||
}
|
||||
|
||||
private getLoggedInInfoElem () {
|
||||
return $('.logged-in-display-name')
|
||||
private getLoggedInInfoElem (displayName: string) {
|
||||
return $('.logged-in-info').$('.display-name*=' + displayName)
|
||||
}
|
||||
|
||||
private getSuffix () {
|
||||
|
|
|
@ -77,7 +77,7 @@ export class MyAccountPage {
|
|||
|
||||
async countVideos (names: string[]) {
|
||||
const elements = await $$('.video').filter(async e => {
|
||||
const t = await e.$('.video-miniature-name').getText()
|
||||
const t = await e.$('.video-name').getText()
|
||||
|
||||
return names.some(n => t.includes(n))
|
||||
})
|
||||
|
@ -140,7 +140,7 @@ export class MyAccountPage {
|
|||
private async getVideoElement (name: string) {
|
||||
const video = async () => {
|
||||
const videos = await $$('.video').filter(async e => {
|
||||
const t = await e.$('.video-miniature-name').getText()
|
||||
const t = await e.$('.video-name').getText()
|
||||
|
||||
return t.includes(name)
|
||||
})
|
||||
|
|
|
@ -6,7 +6,7 @@ export class SignupPage {
|
|||
return $('.create-account-button')
|
||||
}
|
||||
|
||||
async clickOnRegisterInMenu () {
|
||||
async clickOnRegisterButton () {
|
||||
const button = this.getRegisterMenuButton()
|
||||
|
||||
await button.waitForClickable()
|
||||
|
@ -74,7 +74,7 @@ export class SignupPage {
|
|||
name: string
|
||||
}
|
||||
}) {
|
||||
await this.clickOnRegisterInMenu()
|
||||
await this.clickOnRegisterButton()
|
||||
await this.validateStep()
|
||||
await this.checkTerms()
|
||||
await this.validateStep()
|
||||
|
|
|
@ -11,9 +11,9 @@ export class VideoListPage {
|
|||
|
||||
// We did not upload a file on a mobile device
|
||||
if (this.isMobileDevice === true || this.isSafari === true) {
|
||||
url = 'https://peertube2.cpy.re/videos/local'
|
||||
url = 'https://peertube2.cpy.re/videos/browse?scope=local'
|
||||
} else {
|
||||
url = '/videos/recently-added'
|
||||
url = '/videos/browse'
|
||||
}
|
||||
|
||||
await go(url)
|
||||
|
@ -24,19 +24,13 @@ export class VideoListPage {
|
|||
await this.waitForList()
|
||||
}
|
||||
|
||||
async goOnLocal () {
|
||||
await $('.menu-link[href="/videos/local"]').click()
|
||||
await this.waitForTitle('Local videos')
|
||||
}
|
||||
async goOnBrowseVideos () {
|
||||
await $('.menu-link*=Home').click()
|
||||
|
||||
async goOnRecentlyAdded () {
|
||||
await $('.menu-link[href="/videos/recently-added"]').click()
|
||||
await this.waitForTitle('Recently added')
|
||||
}
|
||||
|
||||
async goOnTrending () {
|
||||
await $('.menu-link[href="/videos/trending"]').click()
|
||||
await this.waitForTitle('Trending')
|
||||
const browseVideos = $('a*=Browse videos')
|
||||
await browseVideos.waitForClickable()
|
||||
await browseVideos.click()
|
||||
await this.waitForList()
|
||||
}
|
||||
|
||||
async goOnHomepage () {
|
||||
|
@ -59,32 +53,33 @@ export class VideoListPage {
|
|||
await this.waitForList()
|
||||
}
|
||||
|
||||
getNSFWFilter () {
|
||||
return $$('.active-filter').filter(async a => {
|
||||
return (await a.getText()).includes('Sensitive')
|
||||
}).then(f => f[0])
|
||||
async getNSFWFilter () {
|
||||
const el = $('.active-filter*=Sensitive')
|
||||
await el.waitForDisplayed()
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
async getVideosListName () {
|
||||
const elems = await $$('.videos .video-miniature .video-miniature-name')
|
||||
const elems = await $$('.videos .video-miniature .video-name')
|
||||
const texts = await elems.map(e => e.getText())
|
||||
|
||||
return texts.map(t => t.trim())
|
||||
}
|
||||
|
||||
videoExists (name: string) {
|
||||
return $('.video-miniature-name=' + name).isDisplayed()
|
||||
return $('.video-name=' + name).isDisplayed()
|
||||
}
|
||||
|
||||
async videoIsBlurred (name: string) {
|
||||
const filter = await $('.video-miniature-name=' + name).getCSSProperty('filter')
|
||||
const filter = await $('.video-name=' + name).getCSSProperty('filter')
|
||||
|
||||
return filter.value !== 'none'
|
||||
}
|
||||
|
||||
async clickOnVideo (videoName: string) {
|
||||
const video = async () => {
|
||||
const videos = await $$('.videos .video-miniature .video-miniature-name').filter(async e => {
|
||||
const videos = await $$('.videos .video-miniature .video-name').filter(async e => {
|
||||
const t = await e.getText()
|
||||
|
||||
return t === videoName
|
||||
|
@ -106,7 +101,7 @@ export class VideoListPage {
|
|||
|
||||
async clickOnFirstVideo () {
|
||||
const video = () => $('.videos .video-miniature .video-thumbnail')
|
||||
const videoName = () => $('.videos .video-miniature .video-miniature-name')
|
||||
const videoName = () => $('.videos .video-miniature .video-name')
|
||||
|
||||
await video().waitForClickable()
|
||||
|
||||
|
@ -119,7 +114,7 @@ export class VideoListPage {
|
|||
}
|
||||
|
||||
private waitForList () {
|
||||
return $('.videos .video-miniature .video-miniature-name').waitForDisplayed()
|
||||
return $('.videos .video-miniature .video-name').waitForDisplayed()
|
||||
}
|
||||
|
||||
private waitForTitle (title: string) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { getCheckbox, selectCustomSelect } from '../utils'
|
|||
|
||||
export class VideoUploadPage {
|
||||
async navigateTo () {
|
||||
const publishButton = await $('.root-header .publish-button')
|
||||
const publishButton = await $('.publish-button > a')
|
||||
|
||||
await publishButton.waitForClickable()
|
||||
await publishButton.click()
|
||||
|
@ -28,7 +28,7 @@ export class VideoUploadPage {
|
|||
// Wait for the upload to finish
|
||||
await browser.waitUntil(async () => {
|
||||
const warning = await $('=Publish will be available when upload is finished').isDisplayed()
|
||||
const progress = await $('.progress-bar=100%').isDisplayed()
|
||||
const progress = await $('.progress-container=100%').isDisplayed()
|
||||
|
||||
return !warning && progress
|
||||
})
|
||||
|
|
|
@ -199,7 +199,7 @@ export class VideoWatchPage {
|
|||
|
||||
await textarea.setValue(comment)
|
||||
|
||||
const confirmButton = await $('.comment-buttons .orange-button')
|
||||
const confirmButton = await $('.comment-buttons .primary-button')
|
||||
await confirmButton.waitForClickable()
|
||||
await confirmButton.click()
|
||||
|
||||
|
@ -211,19 +211,21 @@ export class VideoWatchPage {
|
|||
async createReply (comment: string) {
|
||||
const replyButton = await $('button.comment-action-reply')
|
||||
await replyButton.waitForClickable()
|
||||
await replyButton.scrollIntoView()
|
||||
await replyButton.scrollIntoView({ block: 'center' })
|
||||
await replyButton.click()
|
||||
|
||||
const textarea = await $('my-video-comment my-video-comment-add textarea')
|
||||
await textarea.waitForClickable()
|
||||
await textarea.setValue(comment)
|
||||
|
||||
const confirmButton = await $('my-video-comment .comment-buttons .orange-button')
|
||||
const confirmButton = await $('my-video-comment .comment-buttons .primary-button')
|
||||
await confirmButton.waitForClickable()
|
||||
await replyButton.scrollIntoView({ block: 'center' })
|
||||
await confirmButton.click()
|
||||
|
||||
const createdComment = await (await $('.is-child .comment-html p')).getText()
|
||||
const createdComment = await $('.is-child .comment-html p')
|
||||
await createdComment.waitForDisplayed()
|
||||
|
||||
return expect(createdComment).toBe(comment)
|
||||
return expect(await createdComment.getText()).toBe(comment)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('Plugins', () => {
|
|||
}
|
||||
|
||||
async function expectSubmitState ({ disabled }: { disabled: boolean }) {
|
||||
const disabledSubmit = await $('my-button .disabled')
|
||||
const disabledSubmit = await $('my-button [disabled]')
|
||||
|
||||
if (disabled) expect(await disabledSubmit.isDisplayed()).toBeTruthy()
|
||||
else expect(await disabledSubmit.isDisplayed()).toBeFalsy()
|
||||
|
|
|
@ -126,7 +126,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
it('Should go on signup page', async function () {
|
||||
await signupPage.clickOnRegisterInMenu()
|
||||
await signupPage.clickOnRegisterButton()
|
||||
})
|
||||
|
||||
it('Should validate the first step (about page)', async function () {
|
||||
|
@ -179,7 +179,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
it('Should go on signup page', async function () {
|
||||
await signupPage.clickOnRegisterInMenu()
|
||||
await signupPage.clickOnRegisterButton()
|
||||
})
|
||||
|
||||
it('Should validate the first step (about page)', async function () {
|
||||
|
@ -260,7 +260,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
it('Should go on signup page', async function () {
|
||||
await signupPage.clickOnRegisterInMenu()
|
||||
await signupPage.clickOnRegisterButton()
|
||||
})
|
||||
|
||||
it('Should validate the first step (about page)', async function () {
|
||||
|
@ -328,7 +328,7 @@ describe('Signup', () => {
|
|||
})
|
||||
|
||||
it('Should go on signup page', async function () {
|
||||
await signupPage.clickOnRegisterInMenu()
|
||||
await signupPage.clickOnRegisterButton()
|
||||
})
|
||||
|
||||
it('Should validate the first step (about page)', async function () {
|
||||
|
|
|
@ -52,9 +52,7 @@ describe('Videos list', () => {
|
|||
async function checkCommonVideoListPages (policy: NSFWPolicy) {
|
||||
const promisesWithFilters = [
|
||||
videoListPage.goOnRootAccount.bind(videoListPage),
|
||||
videoListPage.goOnLocal.bind(videoListPage),
|
||||
videoListPage.goOnRecentlyAdded.bind(videoListPage),
|
||||
videoListPage.goOnTrending.bind(videoListPage),
|
||||
videoListPage.goOnBrowseVideos.bind(videoListPage),
|
||||
videoListPage.goOnRootChannel.bind(videoListPage)
|
||||
]
|
||||
|
||||
|
|
|
@ -10,12 +10,12 @@ function isCheckboxSelected (name: string) {
|
|||
}
|
||||
|
||||
async function selectCustomSelect (id: string, valueLabel: string) {
|
||||
const wrapper = $(`[formcontrolname=${id}] .ng-arrow-wrapper`)
|
||||
const wrapper = $(`[formcontrolname=${id}] span[role=combobox]`)
|
||||
|
||||
await wrapper.waitForClickable()
|
||||
await wrapper.click()
|
||||
|
||||
const option = await $$(`[formcontrolname=${id}] .ng-option`).filter(async o => {
|
||||
const option = await $$(`[formcontrolname=${id}] li[role=option]`).filter(async o => {
|
||||
const text = await o.getText()
|
||||
|
||||
return text.trimStart().startsWith(valueLabel)
|
||||
|
|
|
@ -77,7 +77,7 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
browserName: 'Safari',
|
||||
browserVersion: '12.1',
|
||||
browserVersion: '13',
|
||||
|
||||
...buildBStackDesktopOptions({ sessionName: 'Safari Desktop', resolution: '1280x1024' })
|
||||
},
|
||||
|
@ -102,10 +102,11 @@ module.exports = {
|
|||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPhone', deviceName: 'iPhone 11', osVersion: '13' })
|
||||
},
|
||||
|
||||
{
|
||||
browserName: 'Safari',
|
||||
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad 7th', osVersion: '13' })
|
||||
...buildBStackMobileOptions({ sessionName: 'Safari iPad', deviceName: 'iPad Pro 11 2020', osVersion: '13' })
|
||||
}
|
||||
],
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "6.3.2",
|
||||
"version": "7.0.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
@ -18,8 +18,6 @@
|
|||
"lint-scss": "stylelint 'src/**/*.scss'",
|
||||
"eslint": "eslint",
|
||||
"ng": "ng",
|
||||
"webdriver-manager": "webdriver-manager",
|
||||
"ngx-extractor": "ngx-extractor",
|
||||
"stylelint": "stylelint"
|
||||
},
|
||||
"browser": {
|
||||
|
@ -53,8 +51,6 @@
|
|||
"@angular/platform-browser-dynamic": "^18.0.4",
|
||||
"@angular/router": "^18.0.4",
|
||||
"@angular/service-worker": "^18.0.4",
|
||||
"@formatjs/intl-locale": "^4.0.0",
|
||||
"@formatjs/intl-pluralrules": "^5.2.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^17.0.0",
|
||||
"@ngx-loading-bar/core": "^6.0.0",
|
||||
"@ngx-loading-bar/http-client": "^6.0.0",
|
||||
|
@ -68,7 +64,6 @@
|
|||
"@types/chart.js": "^2.9.37",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jschannel": "^1.0.0",
|
||||
"@types/linkifyjs": "^2.1.2",
|
||||
"@types/lodash-es": "^4.17.0",
|
||||
|
@ -91,6 +86,7 @@
|
|||
"buffer": "^6.0.3",
|
||||
"chart.js": "^4.3.0",
|
||||
"chartjs-plugin-zoom": "~2.0.1",
|
||||
"color-bits": "^1.0.4",
|
||||
"core-js": "^3.22.8",
|
||||
"debug": "^4.3.1",
|
||||
"dompurify": "^3.1.6",
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<div class="margin-content mt-4">
|
||||
<h3 class="fs-3 fw-semibold mb-3" i18n>Contact {{ instanceName }} administrators</h3>
|
||||
|
||||
@if (isContactFormEnabled()) {
|
||||
@if (!success) {
|
||||
<form novalidate [formGroup]="form" (ngSubmit)="sendForm()">
|
||||
<div class="form-group">
|
||||
<label i18n for="fromName">Your name</label>
|
||||
<input
|
||||
type="text" id="fromName" class="form-control"
|
||||
formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
|
||||
autocomplete="name"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.fromName" class="form-error" role="alert">{{ formErrors.fromName }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="fromEmail">Your email</label>
|
||||
<input
|
||||
type="text" id="fromEmail" class="form-control"
|
||||
formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
|
||||
i18n-placeholder placeholder="Example: john@example.com" autocomplete="email"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.fromEmail" class="form-error" role="alert">{{ formErrors.fromEmail }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="subject">Subject</label>
|
||||
<input
|
||||
type="text" id="subject" class="form-control"
|
||||
formControlName="subject" [ngClass]="{ 'input-error': formErrors['subject'] }"
|
||||
>
|
||||
|
||||
<div *ngIf="formErrors.subject" class="form-error" role="alert">{{ formErrors.subject }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="body">Your message</label>
|
||||
<textarea id="body" formControlName="body" class="form-control" [ngClass]="{ 'input-error': formErrors['body'] }"></textarea>
|
||||
|
||||
<div *ngIf="formErrors.body" class="form-error" role="alert">{{ formErrors.body }}</div>
|
||||
</div>
|
||||
|
||||
<my-alert *ngIf="error" type="danger">{{ error }}</my-alert>
|
||||
|
||||
<input type="submit" i18n-value value="Submit" class="peertube-button primary-button" [disabled]="!form.valid" />
|
||||
</form>
|
||||
} @else {
|
||||
<my-alert type="success">{{ success }}</my-alert>
|
||||
}
|
||||
} @else {
|
||||
<my-alert type="danger" i18n>The contact form is not enabled on this instance.</my-alert>
|
||||
}
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_form-mixins' as *;
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(340px);
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(500px, 200px);
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ServerService } from '@app/core'
|
||||
import {
|
||||
BODY_VALIDATOR,
|
||||
FROM_EMAIL_VALIDATOR,
|
||||
|
@ -13,10 +13,7 @@ import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
|||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { InstanceService } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||
import { HTMLServerConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
|
||||
|
||||
type Prefill = {
|
||||
subject?: string
|
||||
|
@ -24,27 +21,22 @@ type Prefill = {
|
|||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-contact-admin-modal',
|
||||
templateUrl: './contact-admin-modal.component.html',
|
||||
styleUrls: [ './contact-admin-modal.component.scss' ],
|
||||
templateUrl: './about-contact.component.html',
|
||||
styleUrls: [ './about-contact.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ GlobalIconComponent, NgIf, FormsModule, ReactiveFormsModule, NgClass, AlertComponent ]
|
||||
imports: [ NgIf, FormsModule, ReactiveFormsModule, NgClass, AlertComponent ]
|
||||
})
|
||||
export class ContactAdminModalComponent extends FormReactive implements OnInit {
|
||||
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||
|
||||
export class AboutContactComponent extends FormReactive implements OnInit {
|
||||
error: string
|
||||
success: string
|
||||
|
||||
private openedModal: NgbModalRef
|
||||
private serverConfig: HTMLServerConfig
|
||||
|
||||
constructor (
|
||||
protected formReactiveService: FormReactiveService,
|
||||
private router: Router,
|
||||
private modalService: NgbModal,
|
||||
private route: ActivatedRoute,
|
||||
private instanceService: InstanceService,
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier
|
||||
private serverService: ServerService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
@ -62,27 +54,14 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit {
|
|||
subject: SUBJECT_VALIDATOR,
|
||||
body: BODY_VALIDATOR
|
||||
})
|
||||
|
||||
this.prefillForm(this.route.snapshot.queryParams)
|
||||
}
|
||||
|
||||
isContactFormEnabled () {
|
||||
return this.serverConfig.email.enabled && this.serverConfig.contactForm.enabled
|
||||
}
|
||||
|
||||
show (prefill: Prefill = {}) {
|
||||
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
|
||||
|
||||
this.openedModal.shown.subscribe(() => this.prefillForm(prefill))
|
||||
this.openedModal.result.finally(() => this.router.navigateByUrl('/about/instance'))
|
||||
}
|
||||
|
||||
hide () {
|
||||
this.form.reset()
|
||||
this.error = undefined
|
||||
|
||||
this.openedModal.close()
|
||||
this.openedModal = null
|
||||
}
|
||||
|
||||
sendForm () {
|
||||
const fromName = this.form.value['fromName']
|
||||
const fromEmail = this.form.value['fromEmail']
|
||||
|
@ -92,8 +71,7 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit {
|
|||
this.instanceService.contactAdministrator(fromEmail, fromName, subject, body)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`Your message has been sent.`)
|
||||
this.hide()
|
||||
this.success = $localize`Your message has been sent.`
|
||||
},
|
||||
|
||||
error: err => {
|
|
@ -1,30 +1,81 @@
|
|||
<div class="margin-content mt-4">
|
||||
<div class="row">
|
||||
<h1 class="visually-hidden" i18n>Follows</h1>
|
||||
<div class="margin-content mt-5">
|
||||
|
||||
<div class="col-xl-6 col-md-12">
|
||||
<h2 i18n class="fs-5-5 mb-4 fw-semibold">Followers of {{ instanceName }} ({{ followersPagination.totalItems }})</h2>
|
||||
<div class="subscriptions me-3 mb-3">
|
||||
<div class="block-header mb-4 d-flex">
|
||||
<div class="flex-grow-1 me-2">
|
||||
<h3 i18n>{{ subscriptionsPagination.totalItems }} {subscriptionsPagination.totalItems, plural, =1 {subscription} other {subscriptions}}</h3>
|
||||
|
||||
<div i18n class="text-content">
|
||||
This is content to which we have subscribed. This allows us to display their videos directly on {{ instanceName }}.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-subscription-image></my-subscription-image>
|
||||
</div>
|
||||
|
||||
<div class="follows">
|
||||
<div i18n class="no-results" *ngIf="subscriptionsPagination.totalItems === 0">{{ instanceName }} does not have subscriptions.</div>
|
||||
|
||||
<div *ngFor="let subscription of subscriptions" class="follow-block">
|
||||
<my-actor-avatar [actor]="subscription" actorType="instance" size="32"></my-actor-avatar>
|
||||
|
||||
<div>
|
||||
<a class="follow-name" [href]="subscription.url" target="_blank" rel="noopener noreferrer">{{ subscription.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<my-button *ngIf="canLoadMoreSubscriptions()" class="mt-3 mx-auto" (click)="loadMoreSubscriptions()" theme="secondary" i18n>Show more subscriptions</my-button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="serverStats" class="stats mt-4">
|
||||
<h4 i18n>Our network in figures</h4>
|
||||
|
||||
<div myPluginSelector pluginSelectorId="about-instance-network-statistics">
|
||||
<div class="stat">
|
||||
<strong>{{ serverStats.totalVideos | number }}</strong>
|
||||
<a routerLink="/videos/browse" [queryParams]="{ scope: 'federated' }" i18n>total videos</a>
|
||||
<my-global-icon iconName="videos"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<strong>{{ serverStats.totalVideoComments | number }}</strong>
|
||||
<div i18n>total comments</div>
|
||||
<my-global-icon iconName="message-circle"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="followers">
|
||||
<div class="block-header mb-4 d-flex">
|
||||
<div class="flex-grow-1 me-2">
|
||||
<h3 i18n>{{ followersPagination.totalItems }} {followersPagination.totalItems, plural, =1 {follower} other {followers}}</h3>
|
||||
|
||||
<div i18n class="text-content">
|
||||
Our subscribers automatically display videos of {{ instanceName }} on their platforms.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-follower-image></my-follower-image>
|
||||
</div>
|
||||
|
||||
<div class="follows">
|
||||
<div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">{{ instanceName }} does not have followers.</div>
|
||||
|
||||
<a *ngFor="let follower of followers" [href]="follower.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ follower.name }}
|
||||
</a>
|
||||
|
||||
<button i18n class="peertube-button-link grey-button mt-1" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-6 col-md-12">
|
||||
<h2 i18n class="fs-5-5 mb-4 fw-semibold">Subscriptions of {{ instanceName }} ({{ followingsPagination.totalItems }})</h2>
|
||||
|
||||
<div i18n class="no-results" *ngIf="followingsPagination.totalItems === 0">{{ instanceName }} does not have subscriptions.</div>
|
||||
|
||||
<a *ngFor="let following of followings" [href]="following.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ following.name }}
|
||||
</a>
|
||||
|
||||
<button i18n class="peertube-button-link grey-button mt-1" *ngIf="!loadedAllFollowings && canLoadMoreFollowings()" (click)="loadAllFollowings()">Show full list</button>
|
||||
</div>
|
||||
<div *ngFor="let follower of followers" class="follow-block">
|
||||
<my-actor-avatar [actor]="follower" actorType="instance" size="32"></my-actor-avatar>
|
||||
|
||||
<div>
|
||||
<a class="follow-name" [href]="follower.url" target="_blank" rel="noopener noreferrer">{{ follower.name }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<my-button *ngIf="canLoadMoreFollowers()" class="mt-3 mx-auto" (click)="loadMoreFollowers()" theme="secondary" i18n>Show more followers</my-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,85 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_bootstrap-variables' as *;
|
||||
@use '_components' as *;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin-top: 3px;
|
||||
.margin-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
.text-content {
|
||||
color: pvar(--fg-300);
|
||||
}
|
||||
|
||||
.stat {
|
||||
@include stats-card;
|
||||
}
|
||||
|
||||
.stats > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.followers,
|
||||
.subscriptions {
|
||||
flex-basis: 50%;
|
||||
background-color: pvar(--bg-secondary-400);
|
||||
padding: 1.5rem;
|
||||
border-radius: 14px;
|
||||
|
||||
h3 {
|
||||
font-weight: $font-bold;
|
||||
color: pvar(--fg-400);
|
||||
|
||||
@include font-size(2rem);
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: pvar(--fg-300);
|
||||
font-weight: $font-bold;
|
||||
|
||||
@include font-size(1.25rem);
|
||||
}
|
||||
}
|
||||
|
||||
.follows {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.follow-block {
|
||||
width: calc(50% - 1rem);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background-color: pvar(--bg-secondary-450);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
my-actor-avatar {
|
||||
@include margin-right(1rem);
|
||||
}
|
||||
}
|
||||
|
||||
.follow-name {
|
||||
font-weight: $font-bold;
|
||||
color: pvar(--fg-400);
|
||||
}
|
||||
|
||||
@media screen and (max-width: #{breakpoint(xl)}) {
|
||||
.margin-content {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.followers,
|
||||
.subscriptions {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@include on-small-main-col {
|
||||
.follow-block {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,41 @@
|
|||
import { SortMeta } from 'primeng/api'
|
||||
import { DecimalPipe, NgFor, NgIf } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { ComponentPagination, hasMoreItems, Notifier, RestService, ServerService } from '@app/core'
|
||||
import { Actor } from '@peertube/peertube-models'
|
||||
import { NgIf, NgFor } from '@angular/common'
|
||||
import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service'
|
||||
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
|
||||
import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-selector.directive'
|
||||
import { Actor, ServerStats } from '@peertube/peertube-models'
|
||||
import { SortMeta } from 'primeng/api'
|
||||
import { FollowerImageComponent } from './follower-image.component'
|
||||
import { SubscriptionImageComponent } from './subscription-image.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-about-follows',
|
||||
templateUrl: './about-follows.component.html',
|
||||
styleUrls: [ './about-follows.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ NgIf, NgFor ]
|
||||
imports: [
|
||||
NgIf,
|
||||
NgFor,
|
||||
ActorAvatarComponent,
|
||||
ButtonComponent,
|
||||
PluginSelectorDirective,
|
||||
GlobalIconComponent,
|
||||
DecimalPipe,
|
||||
RouterLink,
|
||||
SubscriptionImageComponent,
|
||||
FollowerImageComponent
|
||||
]
|
||||
})
|
||||
|
||||
export class AboutFollowsComponent implements OnInit {
|
||||
instanceName: string
|
||||
|
||||
followers: { name: string, url: string }[] = []
|
||||
followings: { name: string, url: string }[] = []
|
||||
|
||||
loadedAllFollowers = false
|
||||
loadedAllFollowings = false
|
||||
followers: Actor[] = []
|
||||
subscriptions: Actor[] = []
|
||||
|
||||
followersPagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
|
@ -28,13 +43,18 @@ export class AboutFollowsComponent implements OnInit {
|
|||
totalItems: 0
|
||||
}
|
||||
|
||||
followingsPagination: ComponentPagination = {
|
||||
subscriptionsPagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
totalItems: 0
|
||||
}
|
||||
|
||||
sort: SortMeta = {
|
||||
serverStats: ServerStats
|
||||
|
||||
private loadingFollowers = false
|
||||
private loadingSubscriptions = false
|
||||
|
||||
private sort: SortMeta = {
|
||||
field: 'createdAt',
|
||||
order: -1
|
||||
}
|
||||
|
@ -47,41 +67,12 @@ export class AboutFollowsComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.loadMoreFollowers()
|
||||
|
||||
this.loadMoreFollowings()
|
||||
this.loadMoreFollowers(true)
|
||||
this.loadMoreSubscriptions(true)
|
||||
|
||||
this.instanceName = this.server.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
loadAllFollowings () {
|
||||
if (this.loadedAllFollowings) return
|
||||
|
||||
this.loadedAllFollowings = true
|
||||
this.followingsPagination.itemsPerPage = 100
|
||||
|
||||
this.loadMoreFollowings(true)
|
||||
|
||||
while (hasMoreItems(this.followingsPagination)) {
|
||||
this.followingsPagination.currentPage += 1
|
||||
|
||||
this.loadMoreFollowings()
|
||||
}
|
||||
}
|
||||
|
||||
loadAllFollowers () {
|
||||
if (this.loadedAllFollowers) return
|
||||
|
||||
this.loadedAllFollowers = true
|
||||
this.followersPagination.itemsPerPage = 100
|
||||
|
||||
this.loadMoreFollowers(true)
|
||||
|
||||
while (hasMoreItems(this.followersPagination)) {
|
||||
this.followersPagination.currentPage += 1
|
||||
|
||||
this.loadMoreFollowers()
|
||||
}
|
||||
this.server.getServerStats().subscribe(stats => this.serverStats = stats)
|
||||
}
|
||||
|
||||
buildLink (host: string) {
|
||||
|
@ -89,14 +80,20 @@ export class AboutFollowsComponent implements OnInit {
|
|||
}
|
||||
|
||||
canLoadMoreFollowers () {
|
||||
return this.loadedAllFollowers || this.followersPagination.totalItems > this.followersPagination.itemsPerPage
|
||||
return hasMoreItems(this.followersPagination)
|
||||
}
|
||||
|
||||
canLoadMoreFollowings () {
|
||||
return this.loadedAllFollowings || this.followingsPagination.totalItems > this.followingsPagination.itemsPerPage
|
||||
canLoadMoreSubscriptions () {
|
||||
return hasMoreItems(this.subscriptionsPagination)
|
||||
}
|
||||
|
||||
private loadMoreFollowers (reset = false) {
|
||||
loadMoreFollowers (reset = false) {
|
||||
if (this.loadingFollowers) return
|
||||
this.loadingFollowers = true
|
||||
|
||||
if (reset) this.followersPagination.currentPage = 1
|
||||
else this.followersPagination.currentPage++
|
||||
|
||||
const pagination = this.restService.componentToRestPagination(this.followersPagination)
|
||||
|
||||
this.followService.getFollowers({ pagination, sort: this.sort, state: 'accepted' })
|
||||
|
@ -110,36 +107,46 @@ export class AboutFollowsComponent implements OnInit {
|
|||
this.followersPagination.totalItems = resultList.total
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
error: err => this.notifier.error(err.message),
|
||||
|
||||
complete: () => this.loadingFollowers = false
|
||||
})
|
||||
}
|
||||
|
||||
private loadMoreFollowings (reset = false) {
|
||||
const pagination = this.restService.componentToRestPagination(this.followingsPagination)
|
||||
loadMoreSubscriptions (reset = false) {
|
||||
if (this.loadingSubscriptions) return
|
||||
this.loadingSubscriptions = true
|
||||
|
||||
if (reset) this.subscriptionsPagination.currentPage = 1
|
||||
else this.subscriptionsPagination.currentPage++
|
||||
|
||||
const pagination = this.restService.componentToRestPagination(this.subscriptionsPagination)
|
||||
|
||||
this.followService.getFollowing({ pagination, sort: this.sort, state: 'accepted' })
|
||||
.subscribe({
|
||||
next: resultList => {
|
||||
if (reset) this.followings = []
|
||||
if (reset) this.subscriptions = []
|
||||
|
||||
const newFollowings = resultList.data.map(r => this.formatFollow(r.following))
|
||||
this.followings = this.followings.concat(newFollowings)
|
||||
this.subscriptions = this.subscriptions.concat(newFollowings)
|
||||
|
||||
this.followingsPagination.totalItems = resultList.total
|
||||
this.subscriptionsPagination.totalItems = resultList.total
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
error: err => this.notifier.error(err.message),
|
||||
|
||||
complete: () => this.loadingSubscriptions = false
|
||||
})
|
||||
}
|
||||
|
||||
private formatFollow (actor: Actor) {
|
||||
return {
|
||||
...actor,
|
||||
|
||||
// Instance follow, only display host
|
||||
name: actor.name === 'peertube'
|
||||
? actor.host
|
||||
: actor.name + '@' + actor.host,
|
||||
|
||||
url: actor.url
|
||||
: actor.name + '@' + actor.host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<div class="root" aria-hidden="true">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M42.2928 87.2622L10.5359 19.0679L28.4902 11.6038L89.4271 62.8128L42.2928 87.2622Z"
|
||||
fill="url(#paint0_linear_1305_16041)" />
|
||||
<path d="M57.3679 68.7467L87 26L89.4588 26L89.4588 77.6445L57.3679 68.7467Z"
|
||||
fill="url(#paint1_linear_1305_16041)" />
|
||||
<rect x="2.30959" y="14.776" width="37.2961" height="37.2961" rx="9" transform="rotate(-22.8223 2.30959 14.776)"
|
||||
fill="var(--bg-secondary-400)" stroke="var(--secondary-icon-color)" stroke-width="2" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0393 26.7067L15.8565 16.767L25.4049 18.5988L20.0393 26.7067Z"
|
||||
fill="var(--secondary-icon-color)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.2229 36.6467L20.0401 26.7069L29.5885 28.5387L24.2229 36.6467Z"
|
||||
fill="var(--secondary-icon-color)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.5872 28.5378L25.4043 18.598L34.9528 20.4299L29.5872 28.5378Z"
|
||||
fill="var(--secondary-icon-color)" />
|
||||
<path
|
||||
d="M95.2828 29.4743L94.6821 27.4769C94.3634 26.4174 93.637 25.5279 92.6625 25.0041C91.6881 24.4802 90.5454 24.3649 89.4859 24.6835L83.4938 26.4856C82.4343 26.8043 81.5448 27.5307 81.0209 28.5052C80.4971 29.4796 80.3818 30.6223 80.7004 31.6818L81.3011 33.6792"
|
||||
stroke="var(--bg-secondary-500)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M85.2882 21.5899C87.4944 20.9264 88.745 18.6 88.0815 16.3937C87.418 14.1875 85.0916 12.9369 82.8854 13.6004C80.6791 14.2639 79.4285 16.5903 80.092 18.7965C80.7555 21.0028 83.0819 22.2534 85.2882 21.5899Z"
|
||||
stroke="var(--bg-secondary-500)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M68.0007 92.0002L22.5001 99.4999L18.0003 99.4996L52.5006 67L68.0007 92.0002Z"
|
||||
fill="url(#paint2_linear_1305_16041)" />
|
||||
<rect x="7.78809" y="86.0605" width="28.6783" height="28.6783" rx="6" transform="rotate(-6.5522 7.78809 86.0605)"
|
||||
fill="var(--secondary-icon-color)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.5762 98.6849L17.6782 90.8661L23.993 94.1018L18.5762 98.6849Z"
|
||||
fill="var(--bg)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.4756 106.504L18.5776 98.685L24.8924 101.921L19.4756 106.504Z"
|
||||
fill="var(--bg)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.8916 101.92L23.9936 94.1015L30.3084 97.3371L24.8916 101.92Z"
|
||||
fill="var(--bg)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1305_16041" x1="10.3339" y1="3.88906" x2="70.4056" y2="83.557"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.39" stop-color="var(--bg)" />
|
||||
<stop offset="0.905" stop-color="var(--bg)" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1305_16041" x1="87.5" y1="22.5" x2="75.6241" y2="83.0273"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="var(--bg)" />
|
||||
<stop offset="0.905" stop-color="var(--bg)" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1305_16041" x1="5.83634" y1="113.619" x2="65.326" y2="61.6047"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="var(--bg)" />
|
||||
<stop offset="0.905" stop-color="var(--bg)" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<img [src]="avatarUrl" alt="">
|
||||
</div>
|
|
@ -0,0 +1,19 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_bootstrap-variables' as *;
|
||||
@use '_components' as *;
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid pvar(--bg-secondary-400);
|
||||
border-radius: $instance-img-radius;
|
||||
position: absolute;
|
||||
right: 36px;
|
||||
bottom: 25px;
|
||||
transform: rotate(18deg);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { ServerService } from '@app/core'
|
||||
import { Actor } from '@app/shared/shared-main/account/actor.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-follower-image',
|
||||
templateUrl: './follower-image.component.html',
|
||||
styleUrls: [ './follower-image.component.scss' ],
|
||||
standalone: true
|
||||
})
|
||||
export class FollowerImageComponent implements OnInit {
|
||||
avatarUrl: string
|
||||
|
||||
constructor (private server: ServerService) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.server.getHTMLConfig().instance, 30)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<div class="root" aria-hidden="true">
|
||||
<img [src]="avatarUrl" alt="">
|
||||
|
||||
<svg width="125" height="129" viewBox="0 0 125 129" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M43.4996 129L18.9946 27.2709L36.8651 13.7961L124.924 67.6037L43.4996 129Z"
|
||||
fill="url(#paint0_linear_1305_16148)" />
|
||||
<rect x="57.9766" y="33.5923" width="39.9544" height="39.9544" rx="10"
|
||||
transform="rotate(-17.7787 57.9766 33.5923)" fill="var(--bg-secondary-450)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M76.1448 47.9191L72.7968 37.478L82.3039 40.1868L76.1448 47.9191Z" fill="var(--secondary-icon-color)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M79.4925 58.3605L76.1445 47.9194L85.6515 50.6282L79.4925 58.3605Z" fill="var(--secondary-icon-color)" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M85.6507 50.6276L82.3027 40.1865L91.8097 42.8953L85.6507 50.6276Z" fill="var(--secondary-icon-color)" />
|
||||
<g clip-path="url(#clip0_1305_16148)">
|
||||
<path
|
||||
d="M39.7866 86.6025L40.6519 83.7089C41.1109 82.1741 40.9414 80.5198 40.1807 79.11C39.4199 77.7002 38.1303 76.6503 36.5955 76.1913L27.915 73.5954C26.3801 73.1364 24.7259 73.3059 23.316 74.0666C21.9062 74.8273 20.8563 76.117 20.3973 77.6518L19.532 80.5453"
|
||||
stroke="var(--bg-secondary-500)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M33.9859 69.1059C37.182 70.0617 40.5478 68.2456 41.5036 65.0495C42.4594 61.8534 40.6433 58.4877 37.4472 57.5319C34.2511 56.5761 30.8853 58.3922 29.9295 61.5883C28.9737 64.7844 30.7899 68.1501 33.9859 69.1059Z"
|
||||
stroke="var(--bg-secondary-500)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</g>
|
||||
<path
|
||||
d="M64.0505 89.509C62.4526 89.3031 60.9752 90.5494 60.7506 92.2926L59.3273 103.34C59.1027 105.083 60.216 106.663 61.814 106.869L80.6204 109.292C82.2183 109.498 83.7001 108.218 83.9203 106.508"
|
||||
stroke="var(--secondary-icon-color)" stroke-width="2" />
|
||||
<path
|
||||
d="M69.7048 83.8923L91.83 86.7428C93.0902 86.9051 94.0965 88.1833 93.8955 89.7441L92.235 102.632C92.0339 104.193 90.7364 105.175 89.4763 105.012L67.3511 102.162C66.0909 101.999 65.0845 100.721 65.2856 99.1604L66.9461 86.2721C67.1472 84.7112 68.4447 83.7299 69.7048 83.8923Z"
|
||||
stroke="var(--secondary-icon-color)" stroke-width="2" />
|
||||
<path d="M77.2559 89.7397L76.2523 97.5294L82.9857 94.4381L77.2559 89.7397Z" fill="var(--secondary-icon-color)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1305_16148" x1="27.7121" y1="20.698" x2="75.2879" y2="83.7937"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.293252" stop-color="var(--bg)" />
|
||||
<stop offset="1" stop-color="var(--bg)" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1305_16148">
|
||||
<rect width="36.2416" height="36.2416" fill="var(--bg)"
|
||||
transform="translate(21.3838 48) rotate(16.6494)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_bootstrap-variables' as *;
|
||||
@use '_components' as *;
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid pvar(--bg-secondary-400);
|
||||
border-radius: $instance-img-radius;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 15px;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { ServerService } from '@app/core'
|
||||
import { Actor } from '@app/shared/shared-main/account/actor.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-subscription-image',
|
||||
templateUrl: './subscription-image.component.html',
|
||||
styleUrls: [ './subscription-image.component.scss' ],
|
||||
standalone: true
|
||||
})
|
||||
export class SubscriptionImageComponent implements OnInit {
|
||||
avatarUrl: string
|
||||
|
||||
constructor (private server: ServerService) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.server.getHTMLConfig().instance, 30)
|
||||
}
|
||||
}
|
|
@ -1,233 +1,12 @@
|
|||
<div class="banner" *ngIf="instanceBannerUrl">
|
||||
<img [src]="instanceBannerUrl" alt="Instance banner">
|
||||
<div class="margin-content">
|
||||
<my-horizontal-menu [menuEntries]="menuEntries" areChildren="true"></my-horizontal-menu>
|
||||
|
||||
<div class="content">
|
||||
<div>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
<div class="margin-content mt-4">
|
||||
<div class="row ">
|
||||
<div class="col-md-12 col-xl-6">
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<h1 i18n class="fw-semibold fs-5">About {{ instanceName }}</h1>
|
||||
|
||||
<a routerLink="/about/contact" i18n *ngIf="isContactFormEnabled" class="peertube-button-link orange-button h-100 d-flex align-items-center">Contact us</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-4" *ngIf="categories.length !== 0 || languages.length !== 0">
|
||||
<span *ngFor="let category of categories" class="pt-badge badge-primary">{{ category }}</span>
|
||||
|
||||
<span *ngFor="let language of languages" class="pt-badge badge-secondary">{{ language }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="block">{{ shortDescription }}</div>
|
||||
|
||||
<div i18n *ngIf="isNSFW" class="block mt-4 fw-semibold">This instance is dedicated to sensitive/NSFW content.</div>
|
||||
</div>
|
||||
|
||||
<div class="anchor" id="administrators-and-sustainability"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.administrator || aboutHTML.maintenanceLifetime || aboutHTML.businessModel"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="administrators-and-sustainability"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)"
|
||||
>
|
||||
<h2 i18n class="middle-title">
|
||||
ADMINISTRATORS & SUSTAINABILITY
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block administrator" *ngIf="aboutHTML.administrator">
|
||||
<div class="anchor" id="administrators"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="administrators"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Who we are</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.administrator"></div>
|
||||
</div>
|
||||
|
||||
<div class="block creation-reason" *ngIf="aboutHTML.creationReason">
|
||||
<div class="anchor" id="creation-reason"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="creation-reason"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Why we created this instance</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.creationReason"></div>
|
||||
</div>
|
||||
|
||||
<div class="block maintenance-lifetime" *ngIf="aboutHTML.maintenanceLifetime">
|
||||
<div class="anchor" id="maintenance-lifetime"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="maintenance-lifetime"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">How long we plan to maintain this instance</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.maintenanceLifetime"></div>
|
||||
</div>
|
||||
|
||||
<div class="block business-model" *ngIf="aboutHTML.businessModel">
|
||||
<div class="anchor" id="business-model"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="business-model"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">How we will pay for keeping our instance running</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.businessModel"></div>
|
||||
</div>
|
||||
|
||||
<div class="anchor" id="information"></div>
|
||||
<a
|
||||
*ngIf="descriptionElement"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
INFORMATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block description">
|
||||
<div class="anchor" id="description"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="description"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Description</h3>
|
||||
</a>
|
||||
|
||||
<my-custom-markup-container [content]="descriptionElement"></my-custom-markup-container>
|
||||
</div>
|
||||
|
||||
<div myPluginSelector pluginSelectorId="about-instance-moderation">
|
||||
<div class="anchor" id="moderation"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.moderationInformation || aboutHTML.codeOfConduct || aboutHTML.terms"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
MODERATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block moderation-information" *ngIf="aboutHTML.moderationInformation">
|
||||
<div class="anchor" id="moderation-information"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Moderation information</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.moderationInformation"></div>
|
||||
</div>
|
||||
|
||||
<div class="block code-of-conduct" *ngIf="aboutHTML.codeOfConduct">
|
||||
<div class="anchor" id="code-of-conduct"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="code-of-conduct"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Code of conduct</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.codeOfConduct"></div>
|
||||
</div>
|
||||
|
||||
<div class="block terms">
|
||||
<div class="anchor" id="terms"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="terms"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Terms</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.terms"></div>
|
||||
<my-instance-stat-rules [stats]="serverStats" [config]="serverConfig" [aboutHTML]="aboutHTML"></my-instance-stat-rules>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div myPluginSelector pluginSelectorId="about-instance-other-information">
|
||||
<div class="anchor" id="other-information"></div>
|
||||
<a
|
||||
*ngIf="aboutHTML.hardwareInformation"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="other-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
OTHER INFORMATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block hardware-information" *ngIf="aboutHTML.hardwareInformation">
|
||||
<div class="anchor" id="hardware-information"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="hardware-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Hardware information</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="aboutHTML.hardwareInformation"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
|
||||
<h2 class="visually-hidden" i18n>FEATURES</h2>
|
||||
<my-instance-features-table></my-instance-features-table>
|
||||
</div>
|
||||
|
||||
<div class="col" myPluginSelector pluginSelectorId="about-instance-statistics">
|
||||
<div class="anchor" id="statistics"></div>
|
||||
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="statistics"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">STATISTICS</h2>
|
||||
</a>
|
||||
|
||||
<my-instance-statistics [serverStats]="serverStats"></my-instance-statistics>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-contact-admin-modal #contactAdminModal></my-contact-admin-modal>
|
||||
|
|
|
@ -1,50 +1,23 @@
|
|||
@use '_variables' as *;
|
||||
@use '_bootstrap-variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.pt-badge {
|
||||
@include margin-right(5px);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: $font-semibold;
|
||||
margin-bottom: 5px;
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
|
||||
@include rfs(4rem, gap);
|
||||
}
|
||||
|
||||
.middle-title {
|
||||
margin-top: 0;
|
||||
|
||||
@include in-content-small-title;
|
||||
@include margin-bottom(1.5rem);
|
||||
my-instance-stat-rules {
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.block {
|
||||
@include margin-bottom(4.5rem);
|
||||
@media screen and (max-width: #{breakpoint(xl)}) {
|
||||
.content {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.anchor-link {
|
||||
position: relative;
|
||||
|
||||
@include disable-outline;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
&::after {
|
||||
content: '#';
|
||||
display: inline-block;
|
||||
|
||||
@include margin-left(0.2em);
|
||||
}
|
||||
}
|
||||
|
||||
.middle-title,
|
||||
.section-title {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--mainForegroundColor);
|
||||
my-instance-stat-rules {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
import { NgFor, NgIf, ViewportScroller } from '@angular/common'
|
||||
import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, RouterOutlet } from '@angular/router'
|
||||
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { maxBy } from '@peertube/peertube-core-utils'
|
||||
import { HTMLServerConfig, ServerStats } from '@peertube/peertube-models'
|
||||
import { copyToClipboard } from '@root-helpers/utils'
|
||||
import { CustomMarkupContainerComponent } from '../../shared/shared-custom-markup/custom-markup-container.component'
|
||||
import { InstanceFeaturesTableComponent } from '../../shared/shared-instance/instance-features-table.component'
|
||||
import { PluginSelectorDirective } from '../../shared/shared-main/plugins/plugin-selector.directive'
|
||||
import { ServerConfig, ServerStats } from '@peertube/peertube-models'
|
||||
import { ResolverData } from './about-instance.resolver'
|
||||
import { ContactAdminModalComponent } from './contact-admin-modal.component'
|
||||
import { InstanceStatisticsComponent } from './instance-statistics.component'
|
||||
import { InstanceStatRulesComponent } from './instance-stat-rules.component'
|
||||
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-about-instance',
|
||||
|
@ -19,97 +12,61 @@ import { InstanceStatisticsComponent } from './instance-statistics.component'
|
|||
styleUrls: [ './about-instance.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIf,
|
||||
RouterLink,
|
||||
NgFor,
|
||||
CustomMarkupContainerComponent,
|
||||
PluginSelectorDirective,
|
||||
InstanceFeaturesTableComponent,
|
||||
InstanceStatisticsComponent,
|
||||
ContactAdminModalComponent
|
||||
InstanceStatRulesComponent,
|
||||
HorizontalMenuComponent,
|
||||
RouterOutlet
|
||||
]
|
||||
})
|
||||
export class AboutInstanceComponent implements OnInit, AfterViewChecked {
|
||||
export class AboutInstanceComponent implements OnInit {
|
||||
@ViewChild('descriptionWrapper') descriptionWrapper: ElementRef<HTMLInputElement>
|
||||
@ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent
|
||||
|
||||
aboutHTML: AboutHTML
|
||||
descriptionElement: HTMLDivElement
|
||||
|
||||
instanceBannerUrl: string
|
||||
|
||||
languages: string[] = []
|
||||
categories: string[] = []
|
||||
shortDescription = ''
|
||||
|
||||
initialized = false
|
||||
|
||||
serverStats: ServerStats
|
||||
|
||||
private serverConfig: HTMLServerConfig
|
||||
|
||||
private lastScrollHash: string
|
||||
serverConfig: ServerConfig
|
||||
menuEntries: HorizontalMenuEntry[] = []
|
||||
|
||||
constructor (
|
||||
private viewportScroller: ViewportScroller,
|
||||
private route: ActivatedRoute,
|
||||
private notifier: Notifier,
|
||||
private serverService: ServerService
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
get instanceName () {
|
||||
return this.serverConfig.instance.name
|
||||
}
|
||||
|
||||
get isContactFormEnabled () {
|
||||
return this.serverConfig.email.enabled && this.serverConfig.contactForm.enabled
|
||||
}
|
||||
|
||||
get isNSFW () {
|
||||
return this.serverConfig.instance.isNSFW
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
const { about, languages, categories, aboutHTML, descriptionElement, serverStats }: ResolverData = this.route.snapshot.data.instanceData
|
||||
const {
|
||||
aboutHTML,
|
||||
serverStats,
|
||||
serverConfig
|
||||
}: ResolverData = this.route.snapshot.data.instanceData
|
||||
|
||||
this.serverStats = serverStats
|
||||
this.serverConfig = serverConfig
|
||||
|
||||
this.aboutHTML = aboutHTML
|
||||
this.descriptionElement = descriptionElement
|
||||
|
||||
this.languages = languages
|
||||
this.categories = categories
|
||||
this.menuEntries = [
|
||||
{
|
||||
label: $localize`General`,
|
||||
routerLink: '/about/instance/home'
|
||||
}
|
||||
]
|
||||
|
||||
this.shortDescription = about.instance.shortDescription
|
||||
|
||||
this.instanceBannerUrl = about.instance.banners.length !== 0
|
||||
? maxBy(about.instance.banners, 'width').path
|
||||
: undefined
|
||||
|
||||
this.serverConfig = this.serverService.getHTMLConfig()
|
||||
|
||||
this.route.data.subscribe(data => {
|
||||
if (!data?.isContact) return
|
||||
|
||||
const prefill = this.route.snapshot.queryParams
|
||||
|
||||
this.contactAdminModal.show(prefill)
|
||||
if (aboutHTML.administrator || aboutHTML.creationReason || aboutHTML.maintenanceLifetime || aboutHTML.businessModel) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Team`,
|
||||
routerLink: '/about/instance/team'
|
||||
})
|
||||
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
ngAfterViewChecked () {
|
||||
if (this.initialized && window.location.hash && window.location.hash !== this.lastScrollHash) {
|
||||
this.viewportScroller.scrollToAnchor(window.location.hash.replace('#', ''))
|
||||
|
||||
this.lastScrollHash = window.location.hash
|
||||
}
|
||||
if (aboutHTML.moderationInformation || aboutHTML.codeOfConduct) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Moderation and code of conduct`,
|
||||
routerLink: '/about/instance/moderation'
|
||||
})
|
||||
}
|
||||
|
||||
onClickCopyLink (anchor: HTMLAnchorElement) {
|
||||
const link = anchor.href
|
||||
copyToClipboard(link)
|
||||
this.notifier.success(link, $localize`Link copied`)
|
||||
if (aboutHTML.hardwareInformation) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Technical information`,
|
||||
routerLink: '/about/instance/tech'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ import { forkJoin, Observable } from 'rxjs'
|
|||
import { map, switchMap } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ServerService } from '@app/core'
|
||||
import { About, ServerStats } from '@peertube/peertube-models'
|
||||
import { About, ServerConfig, ServerStats } from '@peertube/peertube-models'
|
||||
import { AboutHTML, InstanceService } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||
|
||||
export type ResolverData = {
|
||||
serverConfig: ServerConfig
|
||||
serverStats: ServerStats
|
||||
about: About
|
||||
languages: string[]
|
||||
|
@ -27,14 +28,17 @@ export class AboutInstanceResolver {
|
|||
resolve (): Observable<ResolverData> {
|
||||
return forkJoin([
|
||||
this.buildInstanceAboutObservable(),
|
||||
this.buildInstanceStatsObservable()
|
||||
this.serverService.getServerStats(),
|
||||
this.serverService.getConfig()
|
||||
]).pipe(
|
||||
map(([
|
||||
[ about, languages, categories, aboutHTML, { rootElement } ],
|
||||
serverStats
|
||||
serverStats,
|
||||
serverConfig
|
||||
]) => {
|
||||
return {
|
||||
serverStats,
|
||||
serverConfig,
|
||||
about,
|
||||
languages,
|
||||
categories,
|
||||
|
@ -59,8 +63,4 @@ export class AboutInstanceResolver {
|
|||
})
|
||||
)
|
||||
}
|
||||
|
||||
private buildInstanceStatsObservable () {
|
||||
return this.serverService.getServerStats()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { AboutInstanceComponent } from './about-instance.component'
|
||||
import { AboutInstanceResolver } from './about-instance.resolver'
|
||||
import { AboutInstanceHomeComponent } from './children/about-instance-home.component'
|
||||
import { AboutInstanceModerationComponent } from './children/about-instance-moderation.component'
|
||||
import { AboutInstanceTeamComponent } from './children/about-instance-team.component'
|
||||
import { AboutInstanceTechComponent } from './children/about-instance-tech.component'
|
||||
|
||||
export const aboutInstanceRoutes: Routes = [
|
||||
{
|
||||
path: 'instance',
|
||||
providers: [ AboutInstanceResolver ],
|
||||
component: AboutInstanceComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`About this instance`
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
instanceData: AboutInstanceResolver
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'home',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
component: AboutInstanceHomeComponent
|
||||
},
|
||||
{
|
||||
path: 'support',
|
||||
component: AboutInstanceHomeComponent,
|
||||
data: {
|
||||
isSupport: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'team',
|
||||
component: AboutInstanceTeamComponent
|
||||
},
|
||||
{
|
||||
path: 'tech',
|
||||
component: AboutInstanceTechComponent
|
||||
},
|
||||
{
|
||||
path: 'moderation',
|
||||
component: AboutInstanceModerationComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
h4 {
|
||||
color: pvar(--fg-300);
|
||||
font-size: 18px;
|
||||
font-weight: $font-bold;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
color: pvar(--fg-200);
|
||||
}
|
||||
|
||||
.block {
|
||||
margin-bottom: 1rem;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<div class="block specifics" *ngIf="categories.length !== 0 || languages.length !== 0 || config.instance.isNSFW">
|
||||
<h4 i18n>Specifics</h4>
|
||||
|
||||
<div *ngIf="languages.length !== 0" class="d-inline-block me-2">
|
||||
<span class="text-content top-2px" i18n>Language: </span>
|
||||
<span *ngFor="let language of languages" class="pt-badge badge-primary me-1">{{ language }}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="categories.length !== 0" class="d-inline-block mt-2">
|
||||
<span class="text-content top-2px" i18n>Categories: </span>
|
||||
<span *ngFor="let category of categories" class="pt-badge badge-secondary me-1">{{ category }}</span>
|
||||
</div>
|
||||
|
||||
<div i18n *ngIf="config.instance.isNSFW" class="fw-bold text-content mt-3">{{ config.instance.name }} is dedicated to sensitive/NSFW content.</div>
|
||||
</div>
|
||||
|
||||
<div class="block description">
|
||||
<h4 i18n>Description</h4>
|
||||
|
||||
<my-custom-markup-container class="text-content" [content]="descriptionElement"></my-custom-markup-container>
|
||||
</div>
|
||||
|
||||
<div class="block terms">
|
||||
<h4 i18n class="section-title">Terms</h4>
|
||||
|
||||
<div class="text-content" [innerHTML]="aboutHTML.terms"></div>
|
||||
</div>
|
||||
|
||||
<my-support-modal #supportModal [name]="config.instance.name" [content]="config.instance.support.text"></my-support-modal>
|
|
@ -0,0 +1,65 @@
|
|||
import { NgFor, NgIf } from '@angular/common'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ServerService } from '@app/core'
|
||||
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { SupportModalComponent } from '@app/shared/shared-support-modal/support-modal.component'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { CustomMarkupContainerComponent } from '../../../shared/shared-custom-markup/custom-markup-container.component'
|
||||
import { ResolverData } from '../about-instance.resolver'
|
||||
|
||||
@Component({
|
||||
templateUrl: './about-instance-home.component.html',
|
||||
styleUrls: [ './about-instance-common.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIf,
|
||||
NgFor,
|
||||
CustomMarkupContainerComponent,
|
||||
SupportModalComponent
|
||||
]
|
||||
})
|
||||
export class AboutInstanceHomeComponent implements OnInit {
|
||||
@ViewChild('supportModal') supportModal: SupportModalComponent
|
||||
|
||||
aboutHTML: AboutHTML
|
||||
descriptionElement: HTMLDivElement
|
||||
|
||||
languages: string[] = []
|
||||
categories: string[] = []
|
||||
|
||||
config: HTMLServerConfig
|
||||
|
||||
constructor (
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private serverService: ServerService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.config = this.serverService.getHTMLConfig()
|
||||
|
||||
const {
|
||||
languages,
|
||||
categories,
|
||||
aboutHTML,
|
||||
descriptionElement
|
||||
}: ResolverData = this.route.parent.snapshot.data.instanceData
|
||||
|
||||
this.aboutHTML = aboutHTML
|
||||
this.descriptionElement = descriptionElement
|
||||
|
||||
this.languages = languages
|
||||
this.categories = categories
|
||||
|
||||
this.route.data.subscribe(data => {
|
||||
if (!data?.isSupport) return
|
||||
|
||||
setTimeout(() => {
|
||||
const modal = this.supportModal.show()
|
||||
|
||||
modal.hidden.subscribe(() => this.router.navigateByUrl('/about/instance/home'))
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<div myPluginSelector pluginSelectorId="about-instance-moderation">
|
||||
|
||||
<div class="block moderation-information" *ngIf="aboutHTML.moderationInformation">
|
||||
<h4 i18n class="section-title">Moderation information</h4>
|
||||
<div [innerHTML]="aboutHTML.moderationInformation"></div>
|
||||
</div>
|
||||
|
||||
<div class="block code-of-conduct" *ngIf="aboutHTML.codeOfConduct">
|
||||
<h4 i18n class="section-title">Code of conduct</h4>
|
||||
<div [innerHTML]="aboutHTML.codeOfConduct"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,32 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ServerService } from '@app/core'
|
||||
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { ResolverData } from '../about-instance.resolver'
|
||||
import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-selector.directive'
|
||||
|
||||
@Component({
|
||||
templateUrl: './about-instance-moderation.component.html',
|
||||
styleUrls: [ './about-instance-common.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ CommonModule, PluginSelectorDirective ]
|
||||
})
|
||||
export class AboutInstanceModerationComponent implements OnInit {
|
||||
aboutHTML: AboutHTML
|
||||
|
||||
constructor (
|
||||
private route: ActivatedRoute,
|
||||
private serverService: ServerService
|
||||
) {}
|
||||
|
||||
get instanceName () {
|
||||
return this.serverService.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
const { aboutHTML }: ResolverData = this.route.parent.snapshot.data.instanceData
|
||||
|
||||
this.aboutHTML = aboutHTML
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<div class="block administrator" *ngIf="aboutHTML.administrator">
|
||||
<h4 i18n>Who we are</h4>
|
||||
<div class="text-content" [innerHTML]="aboutHTML.administrator"></div>
|
||||
</div>
|
||||
|
||||
<div class="block creation-reason" *ngIf="aboutHTML.creationReason">
|
||||
<h4 i18n>Why we created {{ instanceName }}</h4>
|
||||
<div class="text-content" [innerHTML]="aboutHTML.creationReason"></div>
|
||||
</div>
|
||||
|
||||
<div class="block maintenance-lifetime" *ngIf="aboutHTML.maintenanceLifetime">
|
||||
<h4 i18n>How long we plan to maintain {{ instanceName }}</h4>
|
||||
<div [innerHTML]="aboutHTML.maintenanceLifetime"></div>
|
||||
</div>
|
||||
|
||||
<div class="block business-model" *ngIf="aboutHTML.businessModel">
|
||||
<h4 i18n>How we will pay for keeping {{ instanceName }} running</h4>
|
||||
<div class="text-content" [innerHTML]="aboutHTML.businessModel"></div>
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ServerService } from '@app/core'
|
||||
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { ResolverData } from '../about-instance.resolver'
|
||||
|
||||
@Component({
|
||||
templateUrl: './about-instance-team.component.html',
|
||||
styleUrls: [ './about-instance-common.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ CommonModule ]
|
||||
})
|
||||
export class AboutInstanceTeamComponent implements OnInit {
|
||||
aboutHTML: AboutHTML
|
||||
|
||||
constructor (
|
||||
private route: ActivatedRoute,
|
||||
private serverService: ServerService
|
||||
) {}
|
||||
|
||||
get instanceName () {
|
||||
return this.serverService.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
const { aboutHTML }: ResolverData = this.route.parent.snapshot.data.instanceData
|
||||
|
||||
this.aboutHTML = aboutHTML
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<div myPluginSelector pluginSelectorId="about-instance-other-information">
|
||||
<h4 i18n class="section-title">Hardware information</h4>
|
||||
<div [innerHTML]="aboutHTML.hardwareInformation"></div>
|
||||
</div>
|
||||
|
||||
<div myPluginSelector pluginSelectorId="about-instance-features">
|
||||
<h4 class="visually-hidden" i18n>FEATURES</h4>
|
||||
<my-instance-features-table></my-instance-features-table>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ServerService } from '@app/core'
|
||||
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-selector.directive'
|
||||
import { ResolverData } from '../about-instance.resolver'
|
||||
import { InstanceFeaturesTableComponent } from '@app/shared/shared-instance/instance-features-table.component'
|
||||
|
||||
@Component({
|
||||
templateUrl: './about-instance-tech.component.html',
|
||||
styleUrls: [ './about-instance-common.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ CommonModule, PluginSelectorDirective, InstanceFeaturesTableComponent ]
|
||||
})
|
||||
export class AboutInstanceTechComponent implements OnInit {
|
||||
aboutHTML: AboutHTML
|
||||
|
||||
constructor (
|
||||
private route: ActivatedRoute,
|
||||
private serverService: ServerService
|
||||
) {}
|
||||
|
||||
get instanceName () {
|
||||
return this.serverService.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
const { aboutHTML }: ResolverData = this.route.parent.snapshot.data.instanceData
|
||||
|
||||
this.aboutHTML = aboutHTML
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<h1 i18n class="modal-title">Contact the administrator(s)<p class="modal-subtitle">{{ instanceName }}</p></h1>
|
||||
|
||||
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
|
||||
<my-global-icon iconName="cross"></my-global-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<form *ngIf="isContactFormEnabled()" novalidate [formGroup]="form" (ngSubmit)="sendForm()">
|
||||
<div class="form-group">
|
||||
<label i18n for="fromName">Your name</label>
|
||||
<input
|
||||
type="text" id="fromName" class="form-control"
|
||||
formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
|
||||
autocomplete="name"
|
||||
>
|
||||
<div *ngIf="formErrors.fromName" class="form-error" role="alert">{{ formErrors.fromName }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="fromEmail">Your email</label>
|
||||
<input
|
||||
type="text" id="fromEmail" class="form-control"
|
||||
formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
|
||||
i18n-placeholder placeholder="Example: john@example.com" autocomplete="email"
|
||||
>
|
||||
<div *ngIf="formErrors.fromEmail" class="form-error" role="alert">{{ formErrors.fromEmail }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="subject">Subject</label>
|
||||
<input
|
||||
type="text" id="subject" class="form-control"
|
||||
formControlName="subject" [ngClass]="{ 'input-error': formErrors['subject'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.subject" class="form-error" role="alert">{{ formErrors.subject }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="body">Your message</label>
|
||||
<textarea id="body" formControlName="body" class="form-control" [ngClass]="{ 'input-error': formErrors['body'] }">
|
||||
</textarea>
|
||||
<div *ngIf="formErrors.body" class="form-error" role="alert">{{ formErrors.body }}</div>
|
||||
</div>
|
||||
|
||||
<my-alert *ngIf="error" type="danger">{{ error }}</my-alert>
|
||||
|
||||
<div class="form-group inputs">
|
||||
<input
|
||||
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
|
||||
(click)="hide()" (key.enter)="hide()"
|
||||
>
|
||||
|
||||
<input type="submit" i18n-value value="Submit" class="peertube-button orange-button" [disabled]="!form.valid" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<my-alert *ngIf="!isContactFormEnabled()" type="danger" i18n>The contact form is not enabled on this instance.</my-alert>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -1,19 +0,0 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.modal-subtitle {
|
||||
line-height: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(340px);
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(100%, 200px);
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
<div class="root">
|
||||
|
||||
<div class="stats-block">
|
||||
<h4 i18n>Our platform in figures</h4>
|
||||
|
||||
<div class="blocks" myPluginSelector pluginSelectorId="about-instance-statistics">
|
||||
<div class="stat">
|
||||
<strong>{{ stats.totalModerators + stats.totalAdmins | number }}</strong>
|
||||
<div i18n>moderators</div>
|
||||
<my-global-icon iconName="moderation"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<strong>{{ stats.totalUsers | number }}</strong>
|
||||
<div i18n>users</div>
|
||||
<my-global-icon iconName="user"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<strong>{{ stats.totalLocalVideos | number }}</strong>
|
||||
<a routerLink="/videos/browse" [queryParams]="{ scope: 'local' }" i18n>videos</a>
|
||||
<my-global-icon iconName="videos"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<strong>{{ stats.totalLocalVideoViews | number }}</strong>
|
||||
<div i18n>views</div>
|
||||
<my-global-icon iconName="eye-open"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<strong>{{ stats.totalLocalVideoComments | number }}</strong>
|
||||
<div i18n>comments</div>
|
||||
<my-global-icon iconName="message-circle"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<strong>{{ stats.totalLocalVideoFilesSize | bytes:1 }}</strong>
|
||||
<div i18n>hosted videos</div>
|
||||
<my-global-icon iconName="film"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usage-rules-block">
|
||||
<h4 i18n>Usage rules</h4>
|
||||
|
||||
<div class="blocks">
|
||||
|
||||
<div class="usage-rule" *ngIf="config.instance.serverCountry">
|
||||
<div class="icon-container">
|
||||
<my-global-icon iconName="message-circle"></my-global-icon>
|
||||
|
||||
<div class="icon-status">
|
||||
<div class="icon-info"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong i18n>This platform has been created in {{ config.instance.serverCountry }}</strong>
|
||||
<div class="rule-content">
|
||||
<ng-container i18n>Your content (comments, videos...) must comply with the legislation in force in this country.</ng-container>
|
||||
<ng-container *ngIf="aboutHTML.codeOfConduct" i18n> You must also follow our <a routerLink="/about/instance/moderation">code of conduct</a>.</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usage-rule">
|
||||
<div class="icon-container">
|
||||
<my-global-icon iconName="user"></my-global-icon>
|
||||
|
||||
@if (config.signup.allowed && config.signup.allowedForCurrentIP) {
|
||||
<div class="icon-status">
|
||||
<my-global-icon iconName="tick"></my-global-icon>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="icon-status">
|
||||
<my-global-icon iconName="cross"></my-global-icon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@if (config.signup.allowed && config.signup.allowedForCurrentIP) {
|
||||
@if (config.signup.requiresApproval) {
|
||||
<strong i18n>You can <a routerLink="/signup">request an account</a> on our platform</strong>
|
||||
|
||||
@if (stats.averageRegistrationRequestResponseTimeMs) {
|
||||
<div class="rule-content" i18n>Our moderator will validate it within a {{ stats.averageRegistrationRequestResponseTimeMs | myDaysDurationFormatter }}.</div>
|
||||
} @else {
|
||||
<div class="rule-content" i18n>Our moderator will validate it within a few days.</div>
|
||||
}
|
||||
} @else {
|
||||
<strong i18n>You can <a routerLink="/signup">create an account</a> on our platform</strong>
|
||||
}
|
||||
} @else {
|
||||
<strong i18n>Public registration on our platform is not allowed</strong>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usage-rule" *ngIf="config.federation.enabled">
|
||||
<div class="icon-container">
|
||||
<my-global-icon iconName="fediverse"></my-global-icon>
|
||||
|
||||
<div class="icon-status">
|
||||
<my-global-icon iconName="tick"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong i18n>This platform is compatible with Mastodon, Lemmy, Misskey and other services from the Fediverse</strong>
|
||||
|
||||
<div class="rule-content" i18n>You can use these services to interact with our videos</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usage-rule">
|
||||
@if (canUpload()) {
|
||||
<div class="icon-container">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
|
||||
<div class="icon-status">
|
||||
<my-global-icon iconName="tick"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong i18n>Vous pouvez publier des vidéos</strong>
|
||||
|
||||
<div class="rule-content">
|
||||
<ng-container i18n>By default, your account allows you to publish videos.</ng-container>
|
||||
|
||||
<ng-container *ngIf="canPublishLive()" i18n> You can also stream lives.</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="icon-container">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
|
||||
<div class="icon-status">
|
||||
<my-global-icon iconName="cross"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@if (isContactFormEnabled()) {
|
||||
<strong i18n>Contact us to publish videos</strong>
|
||||
} @else {
|
||||
<strong i18n>You can't publish videos</strong>
|
||||
}
|
||||
|
||||
<div class="rule-content">
|
||||
<ng-container i18n>By default, your account does not allow to publish videos.</ng-container>
|
||||
|
||||
<ng-container *ngIf="isContactFormEnabled()" i18n> If you want to publish videos, <a routerLink="/about/contact">contact us</a>.</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,104 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_components' as *;
|
||||
|
||||
.root {
|
||||
padding: 1.5rem;
|
||||
border-radius: 14px;
|
||||
background-color: pvar(--bg-secondary-400);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
color: pvar(--fg-300);
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
.stats-block {
|
||||
.blocks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.stat {
|
||||
@include stats-card;
|
||||
}
|
||||
|
||||
.usage-rules-block {
|
||||
@include rfs(1.5rem, margin-top);
|
||||
|
||||
.blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.usage-rule {
|
||||
color: pvar(--fg-300);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.usage-rule:nth-child(2n + 1) {
|
||||
background-color: pvar(--bg-secondary-450);
|
||||
}
|
||||
|
||||
.usage-rule:nth-child(2n) {
|
||||
border: 1px solid pvar(--border-secondary);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $font-bold;
|
||||
color: pvar(--fg-400);
|
||||
}
|
||||
|
||||
.rule-content {
|
||||
@include font-size(14px);
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: relative;
|
||||
|
||||
> my-global-icon:first-child {
|
||||
color: pvar(--secondary-icon-color);
|
||||
|
||||
@include global-icon-size(42px);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-status {
|
||||
background-color: pvar(--bg);
|
||||
border-radius: 100%;
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
text-align: center;
|
||||
|
||||
@include global-icon-size(18px);
|
||||
|
||||
my-global-icon {
|
||||
@include global-icon-size(14px);
|
||||
}
|
||||
}
|
||||
|
||||
my-global-icon[iconName=tick] {
|
||||
color: pvar(--green);
|
||||
}
|
||||
|
||||
my-global-icon[iconName=cross] {
|
||||
color: pvar(--red);
|
||||
}
|
||||
|
||||
.icon-info::after {
|
||||
content: '!';
|
||||
display: block;
|
||||
color: pvar(--fg-200);
|
||||
font-size: 14px;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { CommonModule, DecimalPipe, NgIf } from '@angular/common'
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { BytesPipe } from '@app/shared/shared-main/common/bytes.pipe'
|
||||
import { DaysDurationFormatterPipe } from '@app/shared/shared-main/date/days-duration-formatter.pipe'
|
||||
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
|
||||
import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-selector.directive'
|
||||
import { ServerConfig, ServerStats } from '@peertube/peertube-models'
|
||||
import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
|
||||
import { AuthService } from '@app/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-stat-rules',
|
||||
templateUrl: './instance-stat-rules.component.html',
|
||||
styleUrls: [ './instance-stat-rules.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIf,
|
||||
GlobalIconComponent,
|
||||
DecimalPipe,
|
||||
DaysDurationFormatterPipe,
|
||||
BytesPipe,
|
||||
PluginSelectorDirective,
|
||||
RouterLink
|
||||
]
|
||||
})
|
||||
export class InstanceStatRulesComponent {
|
||||
@Input({ required: true }) stats: ServerStats
|
||||
@Input({ required: true }) config: ServerConfig
|
||||
@Input({ required: true }) aboutHTML: AboutHTML
|
||||
|
||||
constructor (private auth: AuthService) {
|
||||
|
||||
}
|
||||
|
||||
canUpload () {
|
||||
const user = this.auth.getUser()
|
||||
|
||||
if (user) {
|
||||
if (user.videoQuota === 0 || user.videoQuotaDaily === 0) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return this.config.user.videoQuota !== 0 && this.config.user.videoQuotaDaily !== 0
|
||||
}
|
||||
|
||||
canPublishLive () {
|
||||
return this.config.live.enabled
|
||||
}
|
||||
|
||||
isContactFormEnabled () {
|
||||
return this.config.email.enabled && this.config.contactForm.enabled
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
<p i18n *ngIf="null === serverStats">Loading instance statistics...</p>
|
||||
|
||||
<section *ngIf="null !== serverStats">
|
||||
<h3 i18n>By users on this instance</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalUsers | number }}</p>
|
||||
<p class="stat-label" i18n>users</p>
|
||||
</div>
|
||||
<my-global-icon iconName="user"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalLocalVideos | number }}</p>
|
||||
<p class="stat-label" i18n>videos</p>
|
||||
</div>
|
||||
<my-global-icon iconName="film"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalLocalVideoViews | number }}</p>
|
||||
<p class="stat-label" i18n>views</p>
|
||||
</div>
|
||||
<my-global-icon iconName="eye-open"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalLocalVideoComments | number }}</p>
|
||||
<p class="stat-label" i18n>comments</p>
|
||||
</div>
|
||||
<my-global-icon iconName="message-circle"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalLocalVideoFilesSize | bytes:1 }}</p>
|
||||
<p class="stat-label" i18n>hosted video</p>
|
||||
</div>
|
||||
<my-global-icon iconName="home"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 i18n>In this instance federation</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalVideos | number }}</p>
|
||||
<p class="stat-label" i18n>videos</p>
|
||||
</div>
|
||||
<my-global-icon iconName="film"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalVideoComments | number }}</p>
|
||||
<p class="stat-label" i18n>comments</p>
|
||||
</div>
|
||||
<my-global-icon iconName="message-circle"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalInstanceFollowers | number }}</p>
|
||||
<p class="stat-label" i18n>followers</p>
|
||||
</div>
|
||||
<my-global-icon iconName="share"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-4 col-xl-3">
|
||||
<div class="card stat">
|
||||
<div class="card-body">
|
||||
<p class="stat-value">{{ serverStats.totalInstanceFollowing | number }}</p>
|
||||
<p class="stat-label" i18n>following</p>
|
||||
</div>
|
||||
<my-global-icon iconName="globe"></my-global-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,39 +0,0 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
overflow: hidden;
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.25em;
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 1.15em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
my-global-icon {
|
||||
opacity: 0.12;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: -24px;
|
||||
width: 110px;
|
||||
|
||||
&.icon-bottom {
|
||||
top: 4px;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { ServerStats } from '@peertube/peertube-models'
|
||||
import { BytesPipe } from '../../shared/shared-main/common/bytes.pipe'
|
||||
import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
|
||||
import { NgIf, DecimalPipe } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-statistics',
|
||||
templateUrl: './instance-statistics.component.html',
|
||||
styleUrls: [ './instance-statistics.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ NgIf, GlobalIconComponent, DecimalPipe, BytesPipe ]
|
||||
})
|
||||
export class InstanceStatisticsComponent {
|
||||
@Input() serverStats: ServerStats
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<div class="margin-content mt-4">
|
||||
<h1 i18n class="fs-3 text-center fw-semibold mb-3">
|
||||
This website is powered by PeerTube
|
||||
</h1>
|
||||
<div class="margin-content mt-5">
|
||||
<h3 i18n class="fs-3 text-center fw-semibold mb-3">
|
||||
This platform is powered by PeerTube
|
||||
</h3>
|
||||
|
||||
<img class="d-block my-4 mx-auto" width="121px" height="147px" src="/client/assets/images/mascot/default.svg" alt="mascot"/>
|
||||
|
||||
|
@ -11,12 +11,12 @@
|
|||
</p>
|
||||
|
||||
<p i18n>
|
||||
It is free and open-source software, under <a class="link-orange" href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE">AGPLv3
|
||||
It is free and open-source software, under <a class="link-primary" href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE">AGPLv3
|
||||
licence</a>.
|
||||
</p>
|
||||
|
||||
<p i18n>
|
||||
For more information, please visit <a class="link-orange" target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">joinpeertube.org</a>.
|
||||
For more information, please visit <a class="link-primary" target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">joinpeertube.org</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
|||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use/setup-account">Use PeerTube documentation</a>
|
||||
<a i18n class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use/setup-account">Use PeerTube documentation</a>
|
||||
</div>
|
||||
|
||||
<div i18n class="card-text">
|
||||
|
@ -37,7 +37,7 @@
|
|||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use/third-party-application">PeerTube Applications</a>
|
||||
<a i18n class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/use/third-party-application">PeerTube Applications</a>
|
||||
</div>
|
||||
|
||||
<div i18n class="card-text">
|
||||
|
@ -49,7 +49,7 @@
|
|||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<a i18n class="link-orange" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/contribute/getting-started">Contribute on PeerTube</a>
|
||||
<a i18n class="link-primary" target="_blank" rel="noopener noreferrer" href="https://docs.joinpeertube.org/contribute/getting-started">Contribute on PeerTube</a>
|
||||
</div>
|
||||
|
||||
<div i18n class="card-text">
|
||||
|
@ -58,102 +58,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<h2 class="mb-4 mt-5 text-center fs-5 fw-semibold">
|
||||
<div class="anchor" id="privacy"></div> <!-- privacy anchor -->
|
||||
<ng-container i18n>P2P & Privacy</ng-container>
|
||||
</h2>
|
||||
|
||||
<p i18n>
|
||||
PeerTube uses the BitTorrent protocol to share bandwidth between users by default to help lower the load on the server,
|
||||
but ultimately leaves you the choice to switch back to regular streaming exclusively from the server of the video. What
|
||||
follows applies only if you want to keep using the P2P mode of PeerTube.
|
||||
</p>
|
||||
|
||||
<p i18n>
|
||||
The main threat to your privacy induced by BitTorrent lies in your IP address being stored in the instance's BitTorrent
|
||||
tracker as long as you download or watch the video.
|
||||
</p>
|
||||
|
||||
<h3 i18n class="fs-5">What are the consequences?</h3>
|
||||
|
||||
<p i18n>
|
||||
In theory, someone with enough technical skills could create a script that tracks which IP is downloading which video.
|
||||
In practice, this is much more difficult because:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li i18n>
|
||||
An HTTP request has to be sent on each tracker for each video to spy.
|
||||
If we want to spy all PeerTube's videos, we have to send as many requests as there are videos (so potentially a lot)
|
||||
</li>
|
||||
|
||||
<li i18n>
|
||||
For each request sent, the tracker returns random peers at a limited number.
|
||||
For instance, if there are 1000 peers in the swarm and the tracker sends only 20 peers for each request, there must be at least 50
|
||||
requests sent to know every peer in the swarm
|
||||
</li>
|
||||
|
||||
<li i18n>
|
||||
Those requests have to be sent regularly to know who starts/stops watching a video. It is easy to detect that kind of behaviour
|
||||
</li>
|
||||
|
||||
<li i18n>
|
||||
If an IP address is stored in the tracker, it doesn't mean that the person behind the IP (if this person exists) has watched the
|
||||
video
|
||||
</li>
|
||||
|
||||
<li i18n>
|
||||
The IP address is a vague information: usually, it regularly changes and can represent many persons or entities
|
||||
</li>
|
||||
|
||||
<li i18n>
|
||||
Web peers are not publicly accessible: because we use the websocket transport, the protocol is different from classic BitTorrent tracker.
|
||||
When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers
|
||||
to forward the information to.
|
||||
See <a class="link-orange" href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p i18n>
|
||||
The worst-case scenario of an average person spying on their friends is quite unlikely.
|
||||
There are much more effective ways to get that kind of information.
|
||||
</p>
|
||||
|
||||
<h3 i18n class="p2p-privacy-title">How does PeerTube compare with YouTube?</h3>
|
||||
|
||||
<p i18n>
|
||||
The threats to privacy with YouTube are different from PeerTube's.
|
||||
In YouTube's case, the platform gathers a huge amount of your personal information (not only your IP) to analyze them and track you.
|
||||
Moreover, YouTube is owned by Google/Alphabet, a company that tracks you across many websites (via AdSense or Google Analytics).
|
||||
</p>
|
||||
|
||||
<h3 i18n class="p2p-privacy-title">What can I do to limit the exposure of my IP address?</h3>
|
||||
|
||||
<p i18n>
|
||||
Your IP address is public so every time you consult a website, there is a number of actors (in addition to the final website) seeing
|
||||
your IP in their connection logs: ISP/routers/trackers/CDN and more.
|
||||
PeerTube is transparent about it: we warn you that if you want to keep your IP private, you must use a VPN or Tor Browser.
|
||||
Thinking that removing P2P from PeerTube will give you back anonymity doesn't make sense.
|
||||
</p>
|
||||
|
||||
<h3 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h3>
|
||||
|
||||
<p i18n>
|
||||
PeerTube wants to deliver the best countermeasures possible, to give you more choice
|
||||
and render attacks less likely. Here is what we put in place so far:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li i18n>We set a limit to the number of peers sent by the tracker</li>
|
||||
<li i18n>We set a limit on the request frequency received by the tracker</li>
|
||||
<li i18n>Allow instance admins to disable P2P from the administration interface</li>
|
||||
</ul>
|
||||
|
||||
<p i18n>
|
||||
Ultimately, remember you can always disable P2P by toggling it in the video player, or just by disabling
|
||||
WebRTC in your browser.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,3 +18,7 @@
|
|||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,65 @@
|
|||
<div>
|
||||
<div class="sub-menu mb-0" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }">
|
||||
<a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="sub-menu-entry">Instance</a>
|
||||
<div class="margin-content">
|
||||
<h1>
|
||||
<my-global-icon iconName="help"></my-global-icon>
|
||||
|
||||
<a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="sub-menu-entry">PeerTube</a>
|
||||
<ng-container i18n>About</ng-container>
|
||||
</h1>
|
||||
|
||||
<a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="sub-menu-entry">Network</a>
|
||||
<div class="instance-info-container">
|
||||
<div class="banner" *ngIf="bannerUrl">
|
||||
<img [src]="bannerUrl" alt="">
|
||||
</div>
|
||||
|
||||
<div class="instance-info">
|
||||
<div class="avatar" *ngIf="avatarUrl">
|
||||
<img [src]="avatarUrl" alt="">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="instance-name">{{ config.instance.name }}</div>
|
||||
<div class="instance-description">{{ config.instance.shortDescription }}</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<div class="social-buttons d-flex flex-wrap justify-content-end">
|
||||
<a
|
||||
*ngIf="config.instance.social.mastodonLink"
|
||||
class="media peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the Mastodon profile"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.mastodonLink"
|
||||
>
|
||||
<my-global-icon iconName="mastodon"></my-global-icon>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="config.instance.social.blueskyLink"
|
||||
class="media peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the Bluesky profile"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.blueskyLink"
|
||||
>
|
||||
<my-global-icon iconName="bluesky"></my-global-icon>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="config.instance.social.externalLink"
|
||||
class="external-link peertube-button-link rounded-icon-button mb-3" i18n-title title="Go to the external website"
|
||||
target="_blank" rel="noopener noreferrer" [href]="config.instance.social.externalLink"
|
||||
>
|
||||
<my-global-icon iconName="link"></my-global-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap justify-content-end">
|
||||
<my-button *ngIf="isContactFormEnabled()" class="ms-3" theme="primary" ptRouterLink="/about/contact" i18n>Contact us</my-button>
|
||||
|
||||
<my-button *ngIf="config.instance.support.text" class="ms-3" theme="secondary" ptRouterLink="/about/instance/support" i18n>Support</my-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-horizontal-menu [menuEntries]="menuEntries" withMarginBottom="false"></my-horizontal-menu>
|
||||
</div>
|
||||
|
||||
<div [ngClass]="{ 'sub-menu-offset-content': !isBroadcastMessageDisplayed }">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
$container-radius: 14px;
|
||||
|
||||
h1 {
|
||||
font-weight: $font-bold;
|
||||
|
||||
@include font-size(2rem);
|
||||
@include rfs(1.5rem, margin-bottom);
|
||||
|
||||
my-global-icon {
|
||||
@include margin-right(0.5rem);
|
||||
@include global-icon-size(24px);
|
||||
}
|
||||
}
|
||||
|
||||
.instance-info-container {
|
||||
background: pvar(--bg-secondary-400);
|
||||
border-radius: $container-radius;
|
||||
|
||||
@include rfs(2rem, margin-bottom);
|
||||
}
|
||||
|
||||
.instance-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@include rfs(1.25rem, gap);
|
||||
@include rfs(1.75rem, padding);
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
border-radius: $instance-img-radius;;
|
||||
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
@include fade-text(73%, #{pvar(--bg-secondary-400)});
|
||||
|
||||
img {
|
||||
border-start-start-radius: $container-radius;
|
||||
border-start-end-radius: $container-radius;
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.instance-name {
|
||||
color: pvar(--fg-350);
|
||||
font-weight: $font-bold;
|
||||
line-height: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include font-size(2.25rem);
|
||||
}
|
||||
|
||||
.instance-description {
|
||||
color: pvar(--fg-300);
|
||||
|
||||
@include font-size(1.25rem);
|
||||
}
|
||||
|
||||
.social-buttons {
|
||||
|
||||
.peertube-button-link {
|
||||
@include margin-left(0.5rem);
|
||||
}
|
||||
|
||||
.media {
|
||||
color: pvar(--fg-300);
|
||||
background-color: pvar(--bg-secondary-450);
|
||||
border: 1px solid pvar(--bg-secondary-450);
|
||||
|
||||
&:hover {
|
||||
color: pvar(--fg-300);
|
||||
background-color: pvar(--bg-secondary-400);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: pvar(--bg-secondary-350);
|
||||
}
|
||||
}
|
||||
|
||||
.external-link {
|
||||
color: pvar(--bg-secondary-350);
|
||||
background-color: pvar(--fg-350);
|
||||
border: 1px solid pvar(--fg-350);
|
||||
|
||||
&:hover {
|
||||
background-color: pvar(--fg-400);
|
||||
color: pvar(--bg-secondary-400);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: pvar(--fg-450);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,68 @@
|
|||
import { Component } from '@angular/core'
|
||||
import { ScreenService } from '@app/core'
|
||||
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'
|
||||
import { PluginSelectorDirective } from '../shared/shared-main/plugins/plugin-selector.directive'
|
||||
import { NgClass } from '@angular/common'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { ServerService } from '@app/core'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { Actor } from '@app/shared/shared-main/account/actor.model'
|
||||
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
|
||||
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
|
||||
import { SupportModalComponent } from '@app/shared/shared-support-modal/support-modal.component'
|
||||
import { maxBy } from '@peertube/peertube-core-utils'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-about',
|
||||
templateUrl: './about.component.html',
|
||||
styleUrls: [ './about.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ NgClass, PluginSelectorDirective, RouterLink, RouterLinkActive, RouterOutlet ]
|
||||
imports: [ CommonModule, RouterOutlet, HorizontalMenuComponent, GlobalIconComponent, ButtonComponent, SupportModalComponent ]
|
||||
})
|
||||
|
||||
export class AboutComponent {
|
||||
constructor (
|
||||
private screenService: ScreenService
|
||||
) { }
|
||||
export class AboutComponent implements OnInit {
|
||||
@ViewChild('supportModal') supportModal: SupportModalComponent
|
||||
|
||||
get isBroadcastMessageDisplayed () {
|
||||
return this.screenService.isBroadcastMessageDisplayed
|
||||
bannerUrl: string
|
||||
avatarUrl: string
|
||||
|
||||
menuEntries: HorizontalMenuEntry[] = []
|
||||
|
||||
config: HTMLServerConfig
|
||||
|
||||
constructor (
|
||||
private server: ServerService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.config = this.server.getHTMLConfig()
|
||||
|
||||
this.bannerUrl = this.config.instance.banners.length !== 0
|
||||
? maxBy(this.config.instance.banners, 'width').path
|
||||
: undefined
|
||||
|
||||
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.config.instance, 110)
|
||||
|
||||
this.menuEntries = [
|
||||
{
|
||||
label: $localize`Platform`,
|
||||
routerLink: '/about/instance/home',
|
||||
pluginSelectorId: 'about-menu-instance'
|
||||
},
|
||||
{
|
||||
label: $localize`PeerTube`,
|
||||
routerLink: '/about/peertube',
|
||||
pluginSelectorId: 'about-menu-peertube'
|
||||
},
|
||||
{
|
||||
label: $localize`Network`,
|
||||
routerLink: '/about/follows',
|
||||
pluginSelectorId: 'about-menu-network'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
isContactFormEnabled () {
|
||||
return this.config.email.enabled && this.config.contactForm.enabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import { Routes } from '@angular/router'
|
||||
import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
|
||||
import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
|
||||
import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver'
|
||||
import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
|
||||
import { AboutComponent } from './about.component'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||
import { DynamicElementService } from '@app/shared/shared-custom-markup/dynamic-element.service'
|
||||
import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service'
|
||||
import { AboutContactComponent } from './about-contact/about-contact.component'
|
||||
import { aboutInstanceRoutes } from './about-instance/about-instance.routes'
|
||||
import { AboutComponent } from './about.component'
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
component: AboutComponent,
|
||||
providers: [
|
||||
AboutInstanceResolver,
|
||||
InstanceFollowService,
|
||||
CustomMarkupService,
|
||||
DynamicElementService
|
||||
|
@ -24,31 +23,9 @@ export default [
|
|||
redirectTo: 'instance',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'instance',
|
||||
component: AboutInstanceComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`About this instance`
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
instanceData: AboutInstanceResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'contact',
|
||||
component: AboutInstanceComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Contact`
|
||||
},
|
||||
isContact: true
|
||||
},
|
||||
resolve: {
|
||||
instanceData: AboutInstanceResolver
|
||||
}
|
||||
},
|
||||
|
||||
...aboutInstanceRoutes,
|
||||
|
||||
{
|
||||
path: 'peertube',
|
||||
component: AboutPeertubeComponent,
|
||||
|
@ -58,6 +35,7 @@ export default [
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'follows',
|
||||
component: AboutFollowsComponent,
|
||||
|
@ -66,6 +44,16 @@ export default [
|
|||
title: $localize`About this instance's network`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'contact',
|
||||
component: AboutContactComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Contact`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
></my-actor-avatar>
|
||||
|
||||
<h2 class="fs-5 lh-1 fw-bold m-0">
|
||||
<a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
|
||||
<a class="text-decoration-none" [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
|
||||
{{ videoChannel.displayName }}
|
||||
</a>
|
||||
</h2>
|
||||
|
@ -35,7 +35,7 @@
|
|||
|
||||
<my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button>
|
||||
|
||||
<a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a>
|
||||
<a i18n class="button-show-channel peertube-button-link primary-button" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a>
|
||||
|
||||
<div class="videos-overflow-workaround">
|
||||
<div class="videos">
|
||||
|
@ -47,7 +47,7 @@
|
|||
></my-video-miniature>
|
||||
|
||||
<div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel">
|
||||
<a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a>
|
||||
<a class="link-primary" i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
.channel {
|
||||
max-width: $max-channels-width;
|
||||
background-color: pvar(--channelBackgroundColor);
|
||||
background-color: pvar(--bg-secondary-350);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
@ -36,7 +36,7 @@
|
|||
}
|
||||
|
||||
a {
|
||||
color: pvar(--mainForegroundColor);
|
||||
color: pvar(--fg);
|
||||
|
||||
@include peertube-word-wrap;
|
||||
}
|
||||
|
@ -60,7 +60,7 @@
|
|||
|
||||
max-height: 80px;
|
||||
|
||||
@include fade-text(50px, pvar(--channelBackgroundColor));
|
||||
@include fade-text(50px, pvar(--bg-secondary-350));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,14 +95,9 @@ my-subscribe-button {
|
|||
height: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background: linear-gradient(90deg, transparent 0, pvar(--channelBackgroundColor) 45px);
|
||||
background: linear-gradient(90deg, transparent 0, pvar(--bg-secondary-350) 45px);
|
||||
padding: (math.div($video-thumbnail-medium-height, 2) - 10px) 15px 0 60px;
|
||||
z-index: z(miniature) + 1;
|
||||
|
||||
a {
|
||||
color: pvar(--mainColor);
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.button-show-channel {
|
||||
|
@ -130,11 +125,6 @@ my-subscribe-button {
|
|||
}
|
||||
}
|
||||
|
||||
.show-channel a {
|
||||
@include peertube-button-link;
|
||||
@include orange-button-inverted;
|
||||
}
|
||||
|
||||
.videos {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
<h1 class="visually-hidden" i18n>Videos</h1>
|
||||
|
||||
<my-videos-list
|
||||
#videosList
|
||||
|
||||
*ngIf="account"
|
||||
|
||||
[title]="title"
|
||||
displayTitle="false"
|
||||
|
||||
[getVideosObservableFunction]="getVideosObservableFunction"
|
||||
[getSyndicationItemsFunction]="getSyndicationItemsFunction"
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ export class AccountVideosComponent implements OnInit, OnDestroy, DisableForReus
|
|||
getVideosObservableFunction = this.getVideosObservable.bind(this)
|
||||
getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
|
||||
|
||||
title = $localize`Videos`
|
||||
defaultSort = '-publishedAt' as VideoSortField
|
||||
|
||||
account: Account
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div *ngIf="account" class="root">
|
||||
<div class="account-info d-md-grid d-block">
|
||||
<div class="margin-content account-info d-md-grid d-block">
|
||||
|
||||
<div class="account-avatar-row">
|
||||
<my-actor-avatar [size]="getAccountAvatarSize()" actorType="account" [actor]="account"></my-actor-avatar>
|
||||
|
@ -10,7 +10,7 @@
|
|||
<div class="actor-info">
|
||||
<div>
|
||||
<div class="actor-display-name align-items-center">
|
||||
<h1 i18n-title [title]="'Created on ' + (account.createdAt | date)">{{ account.displayName }}</h1>
|
||||
<h1 i18n-title [title]="'Created on ' + (account.createdAt | ptDate)">{{ account.displayName }}</h1>
|
||||
|
||||
<my-user-moderation-dropdown
|
||||
class="mx-3" [prependActions]="prependModerationActions"
|
||||
|
@ -48,7 +48,7 @@
|
|||
<div class="description-html" [innerHTML]="accountDescriptionHTML"></div>
|
||||
</div>
|
||||
|
||||
<button *ngIf="hasShowMoreDescription()" class="show-more d-md-none d-block button-unstyle"
|
||||
<button *ngIf="hasShowMoreDescription()" class="show-more peertube-button-like-link d-md-none d-block"
|
||||
(click)="accountDescriptionExpanded = !accountDescriptionExpanded"
|
||||
title="Show the complete description" i18n-title i18n
|
||||
>
|
||||
|
@ -56,7 +56,7 @@
|
|||
</button>
|
||||
|
||||
<div class="buttons">
|
||||
<a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
|
||||
<a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link primary-button" i18n>
|
||||
Manage account
|
||||
</a>
|
||||
|
||||
|
@ -64,14 +64,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links" [ngClass]="{ 'on-channel-page': isOnChannelPage() }">
|
||||
<div class="margin-content horizontal-menu mb-3">
|
||||
<ng-template #linkTemplate let-item="item">
|
||||
<a [routerLink]="item.routerLink" routerLinkActive="active" class="sub-menu-entry">{{ item.label }}</a>
|
||||
</ng-template>
|
||||
|
||||
<my-list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></my-list-overflow>
|
||||
<my-horizontal-menu [hidden]="hideMenu" [menuEntries]="links"></my-horizontal-menu>
|
||||
|
||||
<my-simple-search-input
|
||||
class="ms-auto"
|
||||
[alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
|
||||
(inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
|
||||
i18n-iconTitle icon-title="Search account videos"
|
||||
|
|
|
@ -4,28 +4,21 @@
|
|||
@use '_miniature' as *;
|
||||
|
||||
.root {
|
||||
--myFontSize: 1rem;
|
||||
--myGreyFontSize: 1rem;
|
||||
--co-font-size: 1rem;
|
||||
--co-secondary-font-size: 1rem;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
@include section-label-responsive;
|
||||
my-horizontal-menu {
|
||||
flex-grow: 1;
|
||||
|
||||
@include margin-right(3rem);
|
||||
}
|
||||
|
||||
.links {
|
||||
.horizontal-menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@include grid-videos-miniature-margins;
|
||||
|
||||
&.on-channel-page {
|
||||
max-width: $max-channels-width;
|
||||
}
|
||||
|
||||
simple-search-input {
|
||||
@include margin-left(auto);
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
my-copy-button {
|
||||
|
@ -36,17 +29,12 @@ my-copy-button {
|
|||
grid-template-columns: 1fr min-content;
|
||||
grid-template-rows: auto auto;
|
||||
|
||||
background-color: pvar(--submenuBackgroundColor);
|
||||
|
||||
@include grid-videos-miniature-margins(false, 15px);
|
||||
@include padding-top(3.75rem);
|
||||
@include padding-bottom(3.75rem);
|
||||
@include margin-bottom(3rem);
|
||||
@include font-size(1rem);
|
||||
}
|
||||
|
||||
.account-avatar-row {
|
||||
@include avatar-row-responsive(2rem, var(--myGreyFontSize));
|
||||
@include avatar-row-responsive(2rem, var(--co-secondary-font-size));
|
||||
}
|
||||
|
||||
.actor-display-name {
|
||||
|
@ -93,7 +81,7 @@ my-copy-button {
|
|||
.description:not(.expanded) {
|
||||
max-height: 70px;
|
||||
|
||||
@include fade-text(30px, pvar(--submenuBackgroundColor));
|
||||
@include fade-text(30px, pvar(--bg));
|
||||
}
|
||||
|
||||
.buttons {
|
||||
|
@ -103,8 +91,8 @@ my-copy-button {
|
|||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
.root {
|
||||
--myFontSize: 14px;
|
||||
--myGreyFontSize: 13px;
|
||||
--co-font-size: 14px;
|
||||
--co-secondary-font-size: 13px;
|
||||
}
|
||||
|
||||
.links {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DatePipe, NgClass, NgIf } from '@angular/common'
|
||||
import { NgClass, NgIf } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'
|
||||
import { AuthService, MarkdownService, MetaService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
|
||||
|
@ -7,6 +7,8 @@ import { AccountService } from '@app/shared/shared-main/account/account.service'
|
|||
import { DropdownAction } from '@app/shared/shared-main/buttons/action-dropdown.component'
|
||||
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
|
||||
import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service'
|
||||
import { AccountReportComponent } from '@app/shared/shared-moderation/report-modals'
|
||||
|
@ -16,7 +18,6 @@ import { Subscription } from 'rxjs'
|
|||
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
|
||||
import { ActorAvatarComponent } from '../shared/shared-actor-image/actor-avatar.component'
|
||||
import { CopyButtonComponent } from '../shared/shared-main/buttons/copy-button.component'
|
||||
import { ListOverflowComponent, ListOverflowItem } from '../shared/shared-main/menu/list-overflow.component'
|
||||
import { SimpleSearchInputComponent } from '../shared/shared-main/search/simple-search-input.component'
|
||||
import { AccountBlockBadgesComponent } from '../shared/shared-moderation/account-block-badges.component'
|
||||
import { UserModerationDropdownComponent } from '../shared/shared-moderation/user-moderation-dropdown.component'
|
||||
|
@ -37,11 +38,12 @@ import { SubscribeButtonComponent } from '../shared/shared-user-subscription/sub
|
|||
RouterLink,
|
||||
SubscribeButtonComponent,
|
||||
RouterLinkActive,
|
||||
ListOverflowComponent,
|
||||
HorizontalMenuComponent,
|
||||
SimpleSearchInputComponent,
|
||||
RouterOutlet,
|
||||
AccountReportComponent,
|
||||
DatePipe
|
||||
PTDatePipe,
|
||||
HorizontalMenuComponent
|
||||
]
|
||||
})
|
||||
export class AccountsComponent implements OnInit, OnDestroy {
|
||||
|
@ -52,7 +54,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
|
||||
videoChannels: VideoChannel[] = []
|
||||
|
||||
links: ListOverflowItem[] = []
|
||||
links: HorizontalMenuEntry[] = []
|
||||
hideMenu = false
|
||||
|
||||
accountVideosCount: number
|
||||
|
@ -103,8 +105,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
})
|
||||
|
||||
this.links = [
|
||||
{ label: $localize`CHANNELS`, routerLink: 'video-channels' },
|
||||
{ label: $localize`VIDEOS`, routerLink: 'videos' }
|
||||
{ label: $localize`Channels`, routerLink: 'video-channels' },
|
||||
{ label: $localize`Videos`, routerLink: 'videos' }
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<div class="root">
|
||||
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#admin-moderation-content" (click)="$event.preventDefault(); adminContent.focus()">Skip the sub menu</a>
|
||||
|
||||
<div class="margin-content">
|
||||
<my-horizontal-menu i18n-h1 h1="Moderation" h1Icon="moderation" [menuEntries]="menuEntries"></my-horizontal-menu>
|
||||
</div>
|
||||
|
||||
<div #adminContent tabindex="-1" id="admin-moderation-content" class="margin-content outline-0">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,87 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { AuthService, ServerService } from '@app/core'
|
||||
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
|
||||
import { UserRight, UserRightType } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
templateUrl: './admin-moderation.component.html',
|
||||
standalone: true,
|
||||
imports: [ HorizontalMenuComponent, RouterOutlet ]
|
||||
})
|
||||
export class AdminModerationComponent implements OnInit {
|
||||
menuEntries: HorizontalMenuEntry[] = []
|
||||
|
||||
constructor (
|
||||
private auth: AuthService,
|
||||
private server: ServerService
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.server.configReloaded.subscribe(() => this.buildMenu())
|
||||
|
||||
this.buildMenu()
|
||||
}
|
||||
|
||||
private buildMenu () {
|
||||
this.menuEntries = []
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_ABUSES)) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Reports`,
|
||||
routerLink: '/admin/moderation/abuses/list'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_REGISTRATIONS)) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Registrations`,
|
||||
routerLink: '/admin/moderation/registrations/list'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Video blocks`,
|
||||
routerLink: '/admin/moderation/video-blocks/list'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST) || this.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
|
||||
const item: HorizontalMenuEntry = {
|
||||
label: $localize`Mutes`,
|
||||
routerLink: '',
|
||||
children: []
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
|
||||
item.children.push({
|
||||
label: $localize`Muted accounts`,
|
||||
routerLink: '/admin/moderation/blocklist/accounts'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
|
||||
item.children.push({
|
||||
label: $localize`Muted servers`,
|
||||
routerLink: '/admin/moderation/blocklist/servers'
|
||||
})
|
||||
}
|
||||
|
||||
item.routerLink = item.children[0].routerLink
|
||||
|
||||
this.menuEntries.push(item)
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_INSTANCE_WATCHED_WORDS)) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Watched words`,
|
||||
routerLink: '/admin/moderation/watched-words/list'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private hasRight (right: UserRightType) {
|
||||
return this.auth.getUser().hasRight(right)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<div class="root">
|
||||
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#admin-overview-content" (click)="$event.preventDefault(); adminContent.focus()">Skip the sub menu</a>
|
||||
|
||||
<div class="margin-content">
|
||||
<my-horizontal-menu i18n-h1 h1="Overview" h1Icon="overview" [menuEntries]="menuEntries"></my-horizontal-menu>
|
||||
</div>
|
||||
|
||||
<div #adminContent tabindex="-1" id="admin-overview-content" class="margin-content outline-0">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,54 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { AuthService } from '@app/core'
|
||||
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
|
||||
import { UserRight, UserRightType } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
templateUrl: './admin-overview.component.html',
|
||||
standalone: true,
|
||||
imports: [ HorizontalMenuComponent, RouterOutlet ]
|
||||
})
|
||||
export class AdminOverviewComponent implements OnInit {
|
||||
menuEntries: HorizontalMenuEntry[] = []
|
||||
|
||||
constructor (
|
||||
private auth: AuthService
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.buildMenu()
|
||||
}
|
||||
|
||||
private buildMenu () {
|
||||
this.menuEntries = []
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_USERS)) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Users`,
|
||||
routerLink: '/admin/overview/users'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.SEE_ALL_VIDEOS)) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Videos`,
|
||||
routerLink: '/admin/overview/videos',
|
||||
queryParams: {
|
||||
search: 'isLocal:true'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.SEE_ALL_COMMENTS)) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Comments`,
|
||||
routerLink: '/admin/overview/comments'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private hasRight (right: UserRightType) {
|
||||
return this.auth.getUser().hasRight(right)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<div class="root">
|
||||
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#admin-settings-content" (click)="$event.preventDefault(); adminContent.focus()">Skip the sub menu</a>
|
||||
|
||||
<div class="margin-content">
|
||||
<my-horizontal-menu i18n-h1 h1="Settings" h1Icon="config" [menuEntries]="menuEntries"></my-horizontal-menu>
|
||||
</div>
|
||||
|
||||
<div #adminContent tabindex="-1" id="admin-settings-content" class="margin-content outline-0">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,179 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { AuthService, ServerService } from '@app/core'
|
||||
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
|
||||
import { PluginType, UserRight, UserRightType } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
templateUrl: './admin-settings.component.html',
|
||||
standalone: true,
|
||||
imports: [ HorizontalMenuComponent, RouterOutlet ]
|
||||
})
|
||||
export class AdminSettingsComponent implements OnInit {
|
||||
menuEntries: HorizontalMenuEntry[] = []
|
||||
|
||||
constructor (
|
||||
private auth: AuthService,
|
||||
private server: ServerService
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.server.configReloaded.subscribe(() => this.buildMenu())
|
||||
|
||||
this.buildMenu()
|
||||
}
|
||||
|
||||
private buildMenu () {
|
||||
this.menuEntries = []
|
||||
|
||||
this.buildConfigurationItems()
|
||||
this.buildFederationItems()
|
||||
this.buildPluginItems()
|
||||
this.buildRunnerItems()
|
||||
this.buildSystemItems()
|
||||
}
|
||||
|
||||
private buildFederationItems () {
|
||||
if (!this.hasRight(UserRight.MANAGE_SERVER_FOLLOW)) return
|
||||
|
||||
this.menuEntries.push({
|
||||
label: $localize`Federation`,
|
||||
routerLink: '/admin/settings/follows/following-list',
|
||||
children: [
|
||||
{
|
||||
label: $localize`Following`,
|
||||
routerLink: '/admin/settings/follows/following-list'
|
||||
},
|
||||
{
|
||||
label: $localize`Followers`,
|
||||
routerLink: '/admin/settings/follows/followers-list'
|
||||
},
|
||||
{
|
||||
label: $localize`Video redundancies`,
|
||||
routerLink: '/admin/settings/follows/video-redundancies-list'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
private buildConfigurationItems () {
|
||||
if (this.hasRight(UserRight.MANAGE_CONFIGURATION)) {
|
||||
this.menuEntries.push({ label: $localize`Configuration`, routerLink: '/admin/settings/config' })
|
||||
}
|
||||
}
|
||||
|
||||
private buildPluginItems () {
|
||||
if (this.hasRight(UserRight.MANAGE_PLUGINS)) {
|
||||
this.menuEntries.push({
|
||||
label: $localize`Plugins/Themes`,
|
||||
routerLink: '/admin/settings/plugins',
|
||||
queryParams: {
|
||||
pluginType: PluginType.PLUGIN
|
||||
},
|
||||
children: [
|
||||
{
|
||||
label: $localize`Installed plugins`,
|
||||
routerLink: '/admin/settings/plugins/list-installed',
|
||||
queryParams: {
|
||||
pluginType: PluginType.PLUGIN
|
||||
}
|
||||
},
|
||||
{
|
||||
label: $localize`Search plugins`,
|
||||
routerLink: '/admin/settings/plugins/search',
|
||||
queryParams: {
|
||||
pluginType: PluginType.PLUGIN
|
||||
}
|
||||
},
|
||||
{
|
||||
label: $localize`Installed themes`,
|
||||
routerLink: '/admin/settings/plugins/list-installed',
|
||||
queryParams: {
|
||||
pluginType: PluginType.THEME
|
||||
}
|
||||
},
|
||||
{
|
||||
label: $localize`Search themes`,
|
||||
routerLink: '/admin/settings/plugins/search',
|
||||
queryParams: {
|
||||
pluginType: PluginType.THEME
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private buildRunnerItems () {
|
||||
if (!this.isRemoteRunnersEnabled() || !this.hasRight(UserRight.MANAGE_RUNNERS)) return
|
||||
|
||||
this.menuEntries.push({
|
||||
label: $localize`Runners`,
|
||||
routerLink: '/admin/settings/system/runners/runners-list',
|
||||
children: [
|
||||
{
|
||||
label: $localize`Remote runners`,
|
||||
routerLink: '/admin/settings/system/runners/runners-list'
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Runner jobs`,
|
||||
routerLink: '/admin/settings/system/runners/jobs-list'
|
||||
},
|
||||
|
||||
{
|
||||
label: $localize`Registration tokens`,
|
||||
routerLink: '/admin/settings/system/runners/registration-tokens-list'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
private buildSystemItems () {
|
||||
const systemItems: HorizontalMenuEntry = {
|
||||
label: $localize`System`,
|
||||
routerLink: '',
|
||||
children: []
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_JOBS)) {
|
||||
systemItems.children.push({
|
||||
label: $localize`Local jobs`,
|
||||
routerLink: '/admin/settings/system/jobs'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_LOGS)) {
|
||||
systemItems.children.push({
|
||||
label: $localize`Logs`,
|
||||
routerLink: '/admin/settings/system/logs'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_DEBUG)) {
|
||||
systemItems.children.push({
|
||||
label: $localize`Debug`,
|
||||
routerLink: '/admin/settings/system/debug'
|
||||
})
|
||||
}
|
||||
|
||||
if (systemItems.children.length === 0) return
|
||||
|
||||
systemItems.routerLink = systemItems.children[0].routerLink
|
||||
|
||||
this.menuEntries.push(systemItems)
|
||||
}
|
||||
|
||||
private hasRight (right: UserRightType) {
|
||||
return this.auth.getUser().hasRight(right)
|
||||
}
|
||||
|
||||
private isRemoteRunnersEnabled () {
|
||||
const config = this.server.getHTMLConfig()
|
||||
|
||||
return config.transcoding.remoteRunners.enabled ||
|
||||
config.live.transcoding.remoteRunners.enabled ||
|
||||
config.videoStudio.remoteRunners.enabled ||
|
||||
config.videoTranscription.remoteRunners.enabled
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<div class="root">
|
||||
<a i18n class="visually-hidden-focusable skip-to-content-sub-menu" href="#admin-content" (click)="$event.preventDefault(); adminContent.focus()">Skip the sub menu</a>
|
||||
|
||||
<my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
|
||||
|
||||
<div #adminContent tabindex="-1" id="admin-content" class="margin-content outline-0" [ngClass]="{ 'sub-menu-offset-content': !isBroadcastMessageDisplayed }">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
my-top-menu-dropdown {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.root {
|
||||
@include sub-menu-h1;
|
||||
}
|
|
@ -1,240 +0,0 @@
|
|||
import { NgClass } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { AuthService, ScreenService, ServerService } from '@app/core'
|
||||
import { ListOverflowItem } from '@app/shared/shared-main/menu/list-overflow.component'
|
||||
import { TopMenuDropdownParam } from '@app/shared/shared-main/menu/top-menu-dropdown.component'
|
||||
import { UserRight, UserRightType } from '@peertube/peertube-models'
|
||||
import { TopMenuDropdownComponent } from '../shared/shared-main/menu/top-menu-dropdown.component'
|
||||
|
||||
@Component({
|
||||
templateUrl: './admin.component.html',
|
||||
styleUrls: [ './admin.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ TopMenuDropdownComponent, NgClass, RouterOutlet ]
|
||||
})
|
||||
export class AdminComponent implements OnInit {
|
||||
items: ListOverflowItem[] = []
|
||||
menuEntries: TopMenuDropdownParam[] = []
|
||||
|
||||
constructor (
|
||||
private auth: AuthService,
|
||||
private screen: ScreenService,
|
||||
private server: ServerService
|
||||
) { }
|
||||
|
||||
get isBroadcastMessageDisplayed () {
|
||||
return this.screen.isBroadcastMessageDisplayed
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.server.configReloaded.subscribe(() => this.buildMenu())
|
||||
|
||||
this.buildMenu()
|
||||
}
|
||||
|
||||
private buildMenu () {
|
||||
this.menuEntries = []
|
||||
|
||||
this.buildOverviewItems()
|
||||
this.buildFederationItems()
|
||||
this.buildModerationItems()
|
||||
this.buildConfigurationItems()
|
||||
this.buildPluginItems()
|
||||
this.buildSystemItems()
|
||||
}
|
||||
|
||||
private buildOverviewItems () {
|
||||
const overviewItems: TopMenuDropdownParam = {
|
||||
label: $localize`Overview`,
|
||||
children: []
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_USERS)) {
|
||||
overviewItems.children.push({
|
||||
label: $localize`Users`,
|
||||
routerLink: '/admin/users',
|
||||
iconName: 'user'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.SEE_ALL_VIDEOS)) {
|
||||
overviewItems.children.push({
|
||||
label: $localize`Videos`,
|
||||
routerLink: '/admin/videos',
|
||||
queryParams: {
|
||||
search: 'isLocal:true'
|
||||
},
|
||||
iconName: 'videos'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.SEE_ALL_COMMENTS)) {
|
||||
overviewItems.children.push({
|
||||
label: $localize`Comments`,
|
||||
routerLink: '/admin/comments',
|
||||
iconName: 'message-circle'
|
||||
})
|
||||
}
|
||||
|
||||
if (overviewItems.children.length !== 0) {
|
||||
this.menuEntries.push(overviewItems)
|
||||
}
|
||||
}
|
||||
|
||||
private buildFederationItems () {
|
||||
if (!this.hasRight(UserRight.MANAGE_SERVER_FOLLOW)) return
|
||||
|
||||
this.menuEntries.push({
|
||||
label: $localize`Federation`,
|
||||
children: [
|
||||
{
|
||||
label: $localize`Following`,
|
||||
routerLink: '/admin/follows/following-list',
|
||||
iconName: 'following'
|
||||
},
|
||||
{
|
||||
label: $localize`Followers`,
|
||||
routerLink: '/admin/follows/followers-list',
|
||||
iconName: 'follower'
|
||||
},
|
||||
{
|
||||
label: $localize`Video redundancies`,
|
||||
routerLink: '/admin/follows/video-redundancies-list',
|
||||
iconName: 'videos'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
private buildModerationItems () {
|
||||
const moderationItems: TopMenuDropdownParam = {
|
||||
label: $localize`Moderation`,
|
||||
children: []
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_REGISTRATIONS)) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Registrations`,
|
||||
routerLink: '/admin/moderation/registrations/list',
|
||||
iconName: 'user'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_ABUSES)) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Reports`,
|
||||
routerLink: '/admin/moderation/abuses/list',
|
||||
iconName: 'flag'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Video blocks`,
|
||||
routerLink: '/admin/moderation/video-blocks/list',
|
||||
iconName: 'cross'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Muted accounts`,
|
||||
routerLink: '/admin/moderation/blocklist/accounts',
|
||||
iconName: 'user-x'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Muted servers`,
|
||||
routerLink: '/admin/moderation/blocklist/servers',
|
||||
iconName: 'peertube-x'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_INSTANCE_WATCHED_WORDS)) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Watched words`,
|
||||
routerLink: '/admin/moderation/watched-words/list',
|
||||
iconName: 'eye-open'
|
||||
})
|
||||
}
|
||||
|
||||
if (moderationItems.children.length !== 0) this.menuEntries.push(moderationItems)
|
||||
}
|
||||
|
||||
private buildConfigurationItems () {
|
||||
if (this.hasRight(UserRight.MANAGE_CONFIGURATION)) {
|
||||
this.menuEntries.push({ label: $localize`Configuration`, routerLink: '/admin/config' })
|
||||
}
|
||||
}
|
||||
|
||||
private buildPluginItems () {
|
||||
if (this.hasRight(UserRight.MANAGE_PLUGINS)) {
|
||||
this.menuEntries.push({ label: $localize`Plugins/Themes`, routerLink: '/admin/plugins' })
|
||||
}
|
||||
}
|
||||
|
||||
private buildSystemItems () {
|
||||
const systemItems: TopMenuDropdownParam = {
|
||||
label: $localize`System`,
|
||||
children: []
|
||||
}
|
||||
|
||||
if (this.isRemoteRunnersEnabled() && this.hasRight(UserRight.MANAGE_RUNNERS)) {
|
||||
systemItems.children.push({
|
||||
label: $localize`Remote runners`,
|
||||
iconName: 'codesandbox',
|
||||
routerLink: '/admin/system/runners/runners-list'
|
||||
})
|
||||
|
||||
systemItems.children.push({
|
||||
label: $localize`Runner jobs`,
|
||||
iconName: 'globe',
|
||||
routerLink: '/admin/system/runners/jobs-list'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_JOBS)) {
|
||||
systemItems.children.push({
|
||||
label: $localize`Local jobs`,
|
||||
iconName: 'circle-tick',
|
||||
routerLink: '/admin/system/jobs'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_LOGS)) {
|
||||
systemItems.children.push({
|
||||
label: $localize`Logs`,
|
||||
iconName: 'playlists',
|
||||
routerLink: '/admin/system/logs'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasRight(UserRight.MANAGE_DEBUG)) {
|
||||
systemItems.children.push({
|
||||
label: $localize`Debug`,
|
||||
iconName: 'cog',
|
||||
routerLink: '/admin/system/debug'
|
||||
})
|
||||
}
|
||||
|
||||
if (systemItems.children.length !== 0) {
|
||||
this.menuEntries.push(systemItems)
|
||||
}
|
||||
}
|
||||
|
||||
private hasRight (right: UserRightType) {
|
||||
return this.auth.getUser().hasRight(right)
|
||||
}
|
||||
|
||||
private isRemoteRunnersEnabled () {
|
||||
const config = this.server.getHTMLConfig()
|
||||
|
||||
return config.transcoding.remoteRunners.enabled ||
|
||||
config.live.transcoding.remoteRunners.enabled ||
|
||||
config.videoStudio.remoteRunners.enabled ||
|
||||
config.videoTranscription.remoteRunners.enabled
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config
|
|||
import { UserRightGuard } from '@app/core'
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
|
||||
export const ConfigRoutes: Routes = [
|
||||
export const configRoutes: Routes = [
|
||||
{
|
||||
path: 'config',
|
||||
canActivate: [ UserRightGuard ],
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<h2 i18n>APPEARANCE</h2>
|
||||
|
||||
<div i18n class="inner-form-description">
|
||||
Use <a class="link-orange" routerLink="/admin/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-orange" routerLink="/admin/config/edit-custom" fragment="advanced-configuration">customizations</a>.
|
||||
Use <a class="link-primary" routerLink="/admin/settings/plugins">plugins & themes</a> for more involved changes, or add slight <a class="link-primary" routerLink="/admin/settings/config/edit-custom" fragment="advanced-configuration">customizations</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -14,13 +14,7 @@
|
|||
<div class="form-group">
|
||||
<label i18n for="themeDefault">Theme</label>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select formControlName="default" id="themeDefault" class="form-control">
|
||||
<option i18n value="default">{{ getDefaultThemeLabel() }}</option>
|
||||
|
||||
<option *ngFor="let theme of availableThemes" [value]="theme.id">{{ theme.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<my-select-options formControlName="default" inputId="themeDefault" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
@ -42,7 +36,7 @@
|
|||
<div class="form-group" formGroupName="trending">
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="algorithms">
|
||||
<label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
|
||||
<label i18n for="trendingVideosAlgorithmsDefault">Default trending algorithm</label>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
|
||||
|
@ -151,7 +145,7 @@
|
|||
<div class="title-col">
|
||||
<h2 i18n>NEW USERS</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-orange" routerLink="/admin/users">users</a> to set their quota individually.
|
||||
Manage <a class="link-primary" routerLink="/admin/overview/users">users</a> to set their quota individually.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -186,6 +180,7 @@
|
|||
|
||||
<div [ngClass]="getDisabledSignupClass()">
|
||||
<label i18n for="signupLimit">Signup limit</label>
|
||||
<span i18n class="small muted ms-1">When the total number of users in your instance reaches this limit, registrations are disabled. -1 == unlimited</span>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
|
@ -295,7 +290,7 @@
|
|||
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>⚠️ If enabled, we recommend to use <a class="link-orange" href="https://docs.joinpeertube.org/maintain/configuration#security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||
<span i18n>⚠️ If enabled, we recommend to use <a class="link-primary" href="https://docs.joinpeertube.org/maintain/configuration#security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
@ -326,6 +321,20 @@
|
|||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="videoChannelSynchronizationMaxPerUser">Max channel synchronization per user</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
<input
|
||||
type="number" min="1" id="videoChannelSynchronizationMaxPerUser" class="form-control"
|
||||
formControlName="maxPerUser" [ngClass]="{ 'input-error': formErrors['import']['videoChannelSynchronization']['maxPerUser'] }"
|
||||
>
|
||||
<span i18n>{form.value['import']['videoChannelSynchronization']['maxPerUser'], plural, =1 {sync} other {syncs}}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="formErrors.import.videoChannelSynchronization.maxPerUser" class="form-error" role="alert">{{ formErrors.import.videoChannelSynchronization.maxPerUser }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
@ -381,7 +390,7 @@
|
|||
i18n-labelText labelText="Enable video transcription"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Automatically create a subtitle file of uploaded/imported VOD videos</span>
|
||||
<span i18n><a href="https://docs.joinpeertube.org/admin/configuration#automatic-transcription" target="_blank">Automatically create subtitles</a> for uploaded/imported VOD videos</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
|
@ -392,7 +401,7 @@
|
|||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>
|
||||
Use <a routerLink="/admin/system/runners/runners-list">remote runners</a> to process transcription tasks.
|
||||
Use <a routerLink="/admin/settings/system/runners/runners-list">remote runners</a> to process transcription tasks.
|
||||
Remote runners has to register on your instance first.
|
||||
</span>
|
||||
</ng-container>
|
||||
|
@ -471,7 +480,7 @@
|
|||
<div i18n>⚠️ This functionality depends heavily on the moderation of instances followed by the search index you select.</div>
|
||||
|
||||
<div i18n>
|
||||
You should only use moderated search indexes in production, or <a class="link-orange" href="https://framagit.org/framasoft/peertube/search-index">host your own</a>.
|
||||
You should only use moderated search indexes in production, or <a class="link-primary" href="https://framagit.org/framasoft/peertube/search-index">host your own</a>.
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
@ -594,7 +603,7 @@
|
|||
<div class="title-col">
|
||||
<h2 i18n>FEDERATION</h2>
|
||||
<div i18n class="inner-form-description">
|
||||
Manage <a class="link-orange" routerLink="/admin/follows">relations</a> with other instances.
|
||||
Manage <a class="link-primary" routerLink="/admin/settings/follows">relations</a> with other instances.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -645,7 +654,7 @@
|
|||
<div i18n>⚠️ This functionality requires a lot of attention and extra moderation.</div>
|
||||
|
||||
<span i18n>
|
||||
See <a class="link-orange" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
|
||||
See <a class="link-primary" href="https://docs.joinpeertube.org/admin/following-instances#automatically-follow-other-instances" rel="noopener noreferrer" target="_blank">the documentation</a> for more information about the expected URL
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { NgClass, NgFor, NgIf } from '@angular/common'
|
|||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { MenuService, ThemeService } from '@app/core'
|
||||
import { ThemeService } from '@app/core'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
|
@ -12,7 +12,6 @@ import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube
|
|||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
||||
import { ConfigService } from '../shared/config.service'
|
||||
|
||||
|
@ -34,7 +33,6 @@ import { ConfigService } from '../shared/config.service'
|
|||
NgClass,
|
||||
UserRealQuotaInfoComponent,
|
||||
SelectOptionsComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
|
@ -53,7 +51,6 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
|
||||
constructor (
|
||||
private configService: ConfigService,
|
||||
private menuService: MenuService,
|
||||
private themeService: ThemeService
|
||||
) {}
|
||||
|
||||
|
@ -62,7 +59,11 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
this.checkSignupField()
|
||||
this.checkImportSyncField()
|
||||
|
||||
this.availableThemes = this.themeService.buildAvailableThemes()
|
||||
this.availableThemes = [
|
||||
this.themeService.getDefaultThemeItem(),
|
||||
|
||||
...this.themeService.buildAvailableThemes()
|
||||
]
|
||||
|
||||
this.exportExpirationOptions = [
|
||||
{ id: 1000 * 3600 * 24, label: $localize`1 day` },
|
||||
|
@ -156,19 +157,25 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
buildLandingPageOptions () {
|
||||
this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig)
|
||||
.links
|
||||
.map(o => ({
|
||||
let links: { label: string, path: string }[] = []
|
||||
|
||||
if (this.serverConfig.homepage.enabled) {
|
||||
links.push({ label: $localize`Home`, path: '/home' })
|
||||
}
|
||||
|
||||
links = links.concat([
|
||||
{ label: $localize`Discover`, path: '/videos/overview' },
|
||||
{ label: $localize`Browse all videos`, path: '/videos/browse' },
|
||||
{ label: $localize`Browse local videos`, path: '/videos/browse?scope=local' }
|
||||
])
|
||||
|
||||
this.defaultLandingPageOptions = links.map(o => ({
|
||||
id: o.path,
|
||||
label: o.label,
|
||||
description: o.path
|
||||
}))
|
||||
}
|
||||
|
||||
getDefaultThemeLabel () {
|
||||
return this.themeService.getDefaultThemeLabel()
|
||||
}
|
||||
|
||||
private checkImportSyncField () {
|
||||
const importSyncControl = this.form.get('import.videoChannelSynchronization.enabled')
|
||||
const importVideosHttpControl = this.form.get('import.videos.http.enabled')
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue