From: Chocobozzz Date: Tue, 20 Feb 2018 15:13:05 +0000 (+0100) Subject: Add support to video support on client X-Git-Tag: v0.0.26-alpha~9 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=07fa4c97ca50b83b0bee9230da97d02401b4e05f;p=oweals%2Fpeertube.git Add support to video support on client --- diff --git a/client/src/app/about/about.component.ts b/client/src/app/about/about.component.ts index 6a2e59be1..adad32b26 100644 --- a/client/src/app/about/about.component.ts +++ b/client/src/app/about/about.component.ts @@ -27,8 +27,8 @@ export class AboutComponent implements OnInit { this.serverService.getAbout() .subscribe( res => { - this.descriptionHTML = this.markdownService.markdownToHTML(res.instance.description) - this.termsHTML = this.markdownService.markdownToHTML(res.instance.terms) + this.descriptionHTML = this.markdownService.textMarkdownToHTML(res.instance.description) + this.termsHTML = this.markdownService.textMarkdownToHTML(res.instance.terms) }, err => this.notificationsService.error('Error', err) diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 24c4f66c7..8e88bceff 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -49,7 +49,6 @@ #peertube-title { @include disable-default-a-behaviour; - width: 100%; font-size: 20px; font-weight: $font-bold; color: inherit !important; @@ -79,6 +78,10 @@ display: none; } } + + @media screen and (max-width: 350px) { + flex: auto; + } } .header-right { diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 220b104b7..3af33ba2b 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core' -import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router' +import { GuardsCheckStart, Router } from '@angular/router' import { AuthService, ServerService } from '@app/core' import { isInSmallView } from '@app/shared/misc/utils' diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index 63b724cba..d79e6274b 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss @@ -14,7 +14,7 @@ width: calc(100% - 150px); } - @media screen and (max-width: 400px) { + @media screen and (max-width: 600px) { width: calc(100% - 70px); } } @@ -50,7 +50,7 @@ margin-right: 6px; } - @media screen and (max-width: 400px) { + @media screen and (max-width: 600px) { margin-right: 10px; padding: 0 10px; diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts index 34a237a12..9ecbbbd60 100644 --- a/client/src/app/shared/forms/form-validators/video.ts +++ b/client/src/app/shared/forms/form-validators/video.ts @@ -44,10 +44,10 @@ export const VIDEO_CHANNEL = { } export const VIDEO_DESCRIPTION = { - VALIDATORS: [ Validators.minLength(3), Validators.maxLength(3000) ], + VALIDATORS: [ Validators.minLength(3), Validators.maxLength(10000) ], MESSAGES: { 'minlength': 'Video description must be at least 3 characters long.', - 'maxlength': 'Video description cannot be more than 3000 characters long.' + 'maxlength': 'Video description cannot be more than 10000 characters long.' } } @@ -58,3 +58,11 @@ export const VIDEO_TAGS = { 'maxlength': 'A tag should be less than 30 characters long.' } } + +export const VIDEO_SUPPORT = { + VALIDATORS: [ Validators.minLength(3), Validators.maxLength(300) ], + MESSAGES: { + 'minlength': 'Video support must be at least 3 characters long.', + 'maxlength': 'Video support cannot be more than 300 characters long.' + } +} diff --git a/client/src/app/shared/forms/markdown-textarea.component.html b/client/src/app/shared/forms/markdown-textarea.component.html index e8c5ded5b..46a97b163 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.html +++ b/client/src/app/shared/forms/markdown-textarea.component.html @@ -1,12 +1,12 @@
- - + +
diff --git a/client/src/app/shared/forms/markdown-textarea.component.scss b/client/src/app/shared/forms/markdown-textarea.component.scss index 82aff541d..6f7494621 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.scss +++ b/client/src/app/shared/forms/markdown-textarea.component.scss @@ -22,6 +22,7 @@ min-height: 75px; padding: 15px; font-size: 15px; + word-wrap: break-word; } } } diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts index 2eae1b27e..928a63b28 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.ts +++ b/client/src/app/shared/forms/markdown-textarea.component.ts @@ -21,29 +21,30 @@ import truncate from 'lodash-es/truncate' }) export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { - @Input() description = '' + @Input() content = '' @Input() classes: string[] = [] @Input() textareaWidth = '100%' @Input() textareaHeight = '150px' @Input() previewColumn = false @Input() truncate: number + @Input() markdownType: 'text' | 'enhanced' = 'text' textareaMarginRight = '0' flexDirection = 'column' - truncatedDescriptionHTML = '' - descriptionHTML = '' + truncatedPreviewHTML = '' + previewHTML = '' - private descriptionChanged = new Subject() + private contentChanged = new Subject() constructor (private markdownService: MarkdownService) {} ngOnInit () { - this.descriptionChanged + this.contentChanged .debounceTime(150) .distinctUntilChanged() - .subscribe(() => this.updateDescriptionPreviews()) + .subscribe(() => this.updatePreviews()) - this.descriptionChanged.next(this.description) + this.contentChanged.next(this.content) if (this.previewColumn) { this.flexDirection = 'row' @@ -54,9 +55,9 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { propagateChange = (_: any) => { /* empty */ } writeValue (description: string) { - this.description = description + this.content = description - this.descriptionChanged.next(this.description) + this.contentChanged.next(this.content) } registerOnChange (fn: (_: any) => void) { @@ -68,19 +69,25 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { } onModelChange () { - this.propagateChange(this.description) + this.propagateChange(this.content) - this.descriptionChanged.next(this.description) + this.contentChanged.next(this.content) } arePreviewsDisplayed () { return isInSmallView() === false } - private updateDescriptionPreviews () { - if (this.description === null || this.description === undefined) return + private updatePreviews () { + if (this.content === null || this.content === undefined) return - this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: this.truncate })) - this.descriptionHTML = this.markdownService.markdownToHTML(this.description) + this.truncatedPreviewHTML = this.markdownRender(truncate(this.content, { length: this.truncate })) + this.previewHTML = this.markdownRender(this.content) + } + + private markdownRender (text: string) { + if (this.markdownType === 'text') return this.markdownService.textMarkdownToHTML(text) + + return this.markdownService.enhancedMarkdownToHTML(text) } } diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index c746bfd66..4e4f64c7b 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -57,6 +57,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { this.channel = hash.channel this.account = hash.account this.tags = hash.tags + this.support = hash.support this.commentsEnabled = hash.commentsEnabled this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index c39252f46..a8bbb63eb 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts @@ -12,6 +12,7 @@ export class VideoEdit { commentsEnabled: boolean channel: number privacy: VideoPrivacy + support: string thumbnailfile?: any previewfile?: any thumbnailUrl: string @@ -33,6 +34,7 @@ export class VideoEdit { this.commentsEnabled = videoDetails.commentsEnabled this.channel = videoDetails.channel.id this.privacy = videoDetails.privacy + this.support = videoDetails.support this.thumbnailUrl = videoDetails.thumbnailUrl this.previewUrl = videoDetails.previewUrl } @@ -50,6 +52,7 @@ export class VideoEdit { licence: this.licence, language: this.language, description: this.description, + support: this.support, name: this.name, tags: this.tags, nsfw: this.nsfw, diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 2e7138cd1..99da14779 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -62,6 +62,7 @@ export class VideoService { tags: video.tags, nsfw: video.nsfw, commentsEnabled: video.commentsEnabled, + support: video.support, thumbnailfile: video.thumbnailfile, previewfile: video.previewfile } diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index 899249778..bf2204013 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html @@ -111,7 +111,7 @@ -
+
+ +
+ + +
+ {{ formErrors.support }} +
+
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index f78336fa8..1317f7426 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss @@ -59,6 +59,10 @@ padding: 0 15px !important; } } + + .advanced-settings .form-group { + margin-bottom: 20px; + } } .submit-container { diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts index 85e5cc3f5..c26906551 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit } from '@angular/core' import { FormBuilder, FormControl, FormGroup } from '@angular/forms' import { ActivatedRoute, Router } from '@angular/router' -import { VIDEO_IMAGE } from '@app/shared' +import { VIDEO_IMAGE, VIDEO_SUPPORT } from '@app/shared' import { NotificationsService } from 'angular2-notifications' import 'rxjs/add/observable/forkJoin' import { ServerService } from '../../../core/server' @@ -60,6 +60,7 @@ export class VideoEditComponent implements OnInit { this.formErrors['description'] = '' this.formErrors['thumbnailfile'] = '' this.formErrors['previewfile'] = '' + this.formErrors['support'] = '' this.validationMessages['name'] = VIDEO_NAME.MESSAGES this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES @@ -70,6 +71,7 @@ export class VideoEditComponent implements OnInit { this.validationMessages['description'] = VIDEO_DESCRIPTION.MESSAGES this.validationMessages['thumbnailfile'] = VIDEO_IMAGE.MESSAGES this.validationMessages['previewfile'] = VIDEO_IMAGE.MESSAGES + this.validationMessages['support'] = VIDEO_SUPPORT.MESSAGES this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS)) this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS)) @@ -83,6 +85,7 @@ export class VideoEditComponent implements OnInit { this.form.addControl('tags', new FormControl('')) this.form.addControl('thumbnailfile', new FormControl('')) this.form.addControl('previewfile', new FormControl('')) + this.form.addControl('support', new FormControl('')) } ngOnInit () { diff --git a/client/src/app/videos/+video-edit/shared/video-image.component.scss b/client/src/app/videos/+video-edit/shared/video-image.component.scss index c44d00b3c..98313536e 100644 --- a/client/src/app/videos/+video-edit/shared/video-image.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-image.component.scss @@ -2,7 +2,7 @@ @import '_mixins'; .root { - height: 150px; + height: auto; display: flex; align-items: center; diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index de34555ab..0ef3c0259 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts @@ -61,8 +61,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { .switchMap(video => { return this.videoService .loadCompleteDescription(video.descriptionPath) - .do(description => video.description = description) - .map(() => video) + .map(description => Object.assign(video, { description })) }) .subscribe( video => { diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.html b/client/src/app/videos/+video-watch/modal/video-support.component.html new file mode 100644 index 000000000..16ad9502a --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-support.component.html @@ -0,0 +1,22 @@ + diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.scss b/client/src/app/videos/+video-watch/modal/video-support.component.scss new file mode 100644 index 000000000..184e09027 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-support.component.scss @@ -0,0 +1,3 @@ +.action-button-cancel { + margin-right: 0 !important; +} diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts new file mode 100644 index 000000000..f805215b9 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts @@ -0,0 +1,36 @@ +import { Component, Input, ViewChild } from '@angular/core' +import { MarkdownService } from '@app/videos/shared' + +import { ModalDirective } from 'ngx-bootstrap/modal' +import { VideoDetails } from '../../../shared/video/video-details.model' + +@Component({ + selector: 'my-video-support', + templateUrl: './video-support.component.html', + styleUrls: [ './video-support.component.scss' ] +}) +export class VideoSupportComponent { + @Input() video: VideoDetails = null + + @ViewChild('modal') modal: ModalDirective + + videoHTMLSupport = '' + + constructor (private markdownService: MarkdownService) { + // empty + } + + show () { + this.modal.show() + + if (this.video.support) { + this.videoHTMLSupport = this.markdownService.enhancedMarkdownToHTML(this.video.support) + } else { + this.videoHTMLSupport = '' + } + } + + hide () { + this.modal.hide() + } +} diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index 8c173d6b3..b8ec048b2 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html @@ -44,9 +44,14 @@
+
+ + Support +
+
- Share + Share
@@ -175,6 +180,7 @@
+ diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index bc737ccd5..eb701e0ab 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss @@ -99,6 +99,7 @@ font-weight: $font-semibold; display: inline-block; padding: 0 10px 0 10px; + white-space: nowrap; .icon { @include icon(21px); @@ -114,6 +115,10 @@ background-image: url('../../../assets/images/video/dislike-grey.svg'); } + &.icon-support { + background-image: url('../../../assets/images/video/heart.svg'); + } + &.icon-share { background-image: url('../../../assets/images/video/share.svg'); } @@ -249,11 +254,7 @@ } -@media screen and (max-width: 1300px) { - .other-videos { - display: none; - } - +@media screen and (max-width: 1600px) { .video-bottom { .video-info { margin-right: 0; @@ -288,6 +289,12 @@ } } +@media screen and (max-width: 1200px) { + .other-videos { + display: none; + } +} + @media screen and (max-width: 600px) { .video-bottom { margin: 20px 0 0 0; @@ -304,3 +311,9 @@ } } } + +@media screen and (max-width: 450px) { + .video-bottom .action-button .icon-text { + display: none !important; + } +} diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 553eed341..6b118b1de 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -1,5 +1,6 @@ import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' +import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' import { MetaService } from '@ngx-meta/core' import { NotificationsService } from 'angular2-notifications' import { Observable } from 'rxjs/Observable' @@ -28,6 +29,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent @ViewChild('videoShareModal') videoShareModal: VideoShareComponent @ViewChild('videoReportModal') videoReportModal: VideoReportComponent + @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent otherVideosDisplayed: Video[] = [] @@ -189,6 +191,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.videoReportModal.show() } + showSupportModal () { + this.videoSupportModal.show() + } + showShareModal () { this.videoShareModal.show() } @@ -264,7 +270,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { return } - this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description) + this.videoHTMLDescription = this.markdownService.textMarkdownToHTML(this.video.description) } private setVideoLikesBarTooltipText () { diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts index 085a9ec5a..6a22c36d9 100644 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ b/client/src/app/videos/+video-watch/video-watch.module.ts @@ -1,4 +1,5 @@ import { NgModule } from '@angular/core' +import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' import { TooltipModule } from 'ngx-bootstrap/tooltip' import { ClipboardModule } from 'ngx-clipboard' import { SharedModule } from '../../shared' @@ -29,6 +30,7 @@ import { VideoWatchComponent } from './video-watch.component' VideoDownloadComponent, VideoShareComponent, VideoReportComponent, + VideoSupportComponent, VideoCommentsComponent, VideoCommentAddComponent, VideoCommentComponent diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/videos/shared/markdown.service.ts index a275446eb..bd100f092 100644 --- a/client/src/app/videos/shared/markdown.service.ts +++ b/client/src/app/videos/shared/markdown.service.ts @@ -4,33 +4,51 @@ import * as MarkdownIt from 'markdown-it' @Injectable() export class MarkdownService { - private markdownIt: MarkdownIt.MarkdownIt + private textMarkdownIt: MarkdownIt.MarkdownIt private linkifier: MarkdownIt.MarkdownIt + private enhancedMarkdownIt: MarkdownIt.MarkdownIt constructor () { - this.markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true }) + this.textMarkdownIt = new MarkdownIt('zero', { linkify: true, breaks: true }) .enable('linkify') .enable('autolink') .enable('emphasis') .enable('link') .enable('newline') .enable('list') - this.setTargetToLinks(this.markdownIt) + this.setTargetToLinks(this.textMarkdownIt) + + this.enhancedMarkdownIt = new MarkdownIt('zero', { linkify: true, breaks: true }) + .enable('linkify') + .enable('autolink') + .enable('emphasis') + .enable('link') + .enable('newline') + .enable('list') + .enable('image') + this.setTargetToLinks(this.enhancedMarkdownIt) this.linkifier = new MarkdownIt('zero', { linkify: true }) .enable('linkify') this.setTargetToLinks(this.linkifier) } - markdownToHTML (markdown: string) { - const html = this.markdownIt.render(markdown) + textMarkdownToHTML (markdown: string) { + const html = this.textMarkdownIt.render(markdown) - // Avoid linkify truncated links - return html.replace(/]+>([^<]+)<\/a>\s*...(<\/p>)?$/mi, '$1...') + return this.avoidTruncatedLinks(html) + } + + enhancedMarkdownToHTML (markdown: string) { + const html = this.enhancedMarkdownIt.render(markdown) + + return this.avoidTruncatedLinks(html) } linkify (text: string) { - return this.linkifier.render(text) + const html = this.linkifier.render(text) + + return this.avoidTruncatedLinks(html) } private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) { @@ -53,4 +71,8 @@ export class MarkdownService { return defaultRender(tokens, idx, options, env, self) } } + + private avoidTruncatedLinks (html) { + return html.replace(/]+>([^<]+)<\/a>\s*...(<\/p>)?$/mi, '$1...') + } } diff --git a/client/src/assets/images/video/heart.svg b/client/src/assets/images/video/heart.svg new file mode 100644 index 000000000..5d64aee0f --- /dev/null +++ b/client/src/assets/images/video/heart.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 80dd3408f..f04aef85d 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss @@ -24,6 +24,10 @@ body { color: #000; } +strong { + font-weight: $font-semibold; +} + input.readonly { /* Force blank on readonly inputs */ background-color: #fff !important; diff --git a/client/src/sass/video-js-custom.scss b/client/src/sass/video-js-custom.scss index 209fb1683..ee6b9219b 100644 --- a/client/src/sass/video-js-custom.scss +++ b/client/src/sass/video-js-custom.scss @@ -24,7 +24,7 @@ $control-bar-height: 34px; .vjs-big-play-button { outline: 0; - font-size: 7em; + font-size: 6em; $big-play-width: 1.5em; $big-play-height: 1em; @@ -340,7 +340,7 @@ $control-bar-height: 34px; @media screen and (max-width: 550px) { .vjs-big-play-button { - font-size: 6.5em; + font-size: 5em; } .vjs-webtorrent { @@ -358,7 +358,7 @@ $control-bar-height: 34px; } .vjs-big-play-button { - font-size: 5em; + font-size: 4em; } .vjs-volume-control { diff --git a/support/doc/dependencies.md b/support/doc/dependencies.md index 4ced42b8b..b223b1368 100644 --- a/support/doc/dependencies.md +++ b/support/doc/dependencies.md @@ -10,7 +10,7 @@ ``` $ sudo apt update -$ sudo apt install nginx ffmpeg postgresql openssl g++ make redis-server +$ sudo apt install nginx ffmpeg postgresql openssl g++ make redis-server git ``` ## Arch Linux @@ -18,7 +18,7 @@ $ sudo apt install nginx ffmpeg postgresql openssl g++ make redis-server 1. Run: ``` -$ sudo pacman -S nodejs yarn ffmpeg postgresql openssl redis +$ sudo pacman -S nodejs yarn ffmpeg postgresql openssl redis git ``` ## CentOS 7 @@ -36,7 +36,7 @@ $ sudo pacman -S nodejs yarn ffmpeg postgresql openssl redis $ sudo yum update $ sudo yum install epel-release $ sudo yum update -$ sudo yum install nginx postgresql postgresql-server openssl gcc make redis +$ sudo yum install nginx postgresql postgresql-server openssl gcc make redis git ``` ## Other distributions