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)
#peertube-title {
@include disable-default-a-behaviour;
- width: 100%;
font-size: 20px;
font-weight: $font-bold;
color: inherit !important;
display: none;
}
}
+
+ @media screen and (max-width: 350px) {
+ flex: auto;
+ }
}
.header-right {
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'
width: calc(100% - 150px);
}
- @media screen and (max-width: 400px) {
+ @media screen and (max-width: 600px) {
width: calc(100% - 70px);
}
}
margin-right: 6px;
}
- @media screen and (max-width: 400px) {
+ @media screen and (max-width: 600px) {
margin-right: 10px;
padding: 0 10px;
}
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.'
}
}
'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.'
+ }
+}
<div class="root" [ngStyle]="{ 'flex-direction': flexDirection }">
<textarea
- [(ngModel)]="description" (ngModelChange)="onModelChange()"
- [ngClass]="classes" [ngStyle]="{ width: textareaWidth, height: textareaHeight, 'margin-right': textareaMarginRight }"
- id="description" name="description">
+ [(ngModel)]="content" (ngModelChange)="onModelChange()"
+ [ngClass]="classes" [ngStyle]="{ width: textareaWidth, height: textareaHeight, 'margin-right': textareaMarginRight }"
+ id="description" name="description">
</textarea>
<tabset *ngIf="arePreviewsDisplayed()" class="previews">
- <tab *ngIf="truncate !== undefined" heading="Truncated description preview" [innerHTML]="truncatedDescriptionHTML"></tab>
- <tab heading="Complete description preview" [innerHTML]="descriptionHTML"></tab>
+ <tab *ngIf="truncate !== undefined" heading="Truncated preview" [innerHTML]="truncatedPreviewHTML"></tab>
+ <tab heading="Complete preview" [innerHTML]="previewHTML"></tab>
</tabset>
</div>
min-height: 75px;
padding: 15px;
font-size: 15px;
+ word-wrap: break-word;
}
}
}
})
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<string>()
+ private contentChanged = new Subject<string>()
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'
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) {
}
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)
}
}
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
commentsEnabled: boolean
channel: number
privacy: VideoPrivacy
+ support: string
thumbnailfile?: any
previewfile?: any
thumbnailUrl: string
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
}
licence: this.licence,
language: this.language,
description: this.description,
+ support: this.support,
name: this.name,
tags: this.tags,
nsfw: this.nsfw,
tags: video.tags,
nsfw: video.nsfw,
commentsEnabled: video.commentsEnabled,
+ support: video.support,
thumbnailfile: video.thumbnailfile,
previewfile: video.previewfile
}
</tab>
<tab heading="Advanced settings">
- <div class="col-md-12">
+ <div class="col-md-12 advanced-settings">
<div class="form-group">
<my-video-image
inputLabel="Upload thumbnail" inputName="thumbnailfile" formControlName="thumbnailfile"
previewWidth="360px" previewHeight="200px"
></my-video-image>
</div>
+
+ <div class="form-group">
+ <label for="support">Support (markdown)</label>
+ <my-markdown-textarea
+ id="support" formControlName="support" textareaWidth="500px" [previewColumn]="true" markdownType="enhanced"
+ [classes]="{ 'input-error': formErrors['support'] }"
+ ></my-markdown-textarea>
+ <div *ngIf="formErrors.support" class="form-error">
+ {{ formErrors.support }}
+ </div>
+ </div>
</div>
</tab>
padding: 0 15px !important;
}
}
+
+ .advanced-settings .form-group {
+ margin-bottom: 20px;
+ }
}
.submit-container {
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'
this.formErrors['description'] = ''
this.formErrors['thumbnailfile'] = ''
this.formErrors['previewfile'] = ''
+ this.formErrors['support'] = ''
this.validationMessages['name'] = VIDEO_NAME.MESSAGES
this.validationMessages['privacy'] = VIDEO_PRIVACY.MESSAGES
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))
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 () {
@import '_mixins';
.root {
- height: 150px;
+ height: auto;
display: flex;
align-items: center;
.switchMap(video => {
return this.videoService
.loadCompleteDescription(video.descriptionPath)
- .do(description => video.description = description)
- .map(() => video)
+ .map(description => Object.assign(video, { description }))
})
.subscribe(
video => {
--- /dev/null
+<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
+ <div class="modal-dialog">
+ <div class="modal-content">
+
+ <div class="modal-header">
+ <span class="close" aria-hidden="true" (click)="hide()"></span>
+ <h4 class="modal-title">Support</h4>
+ </div>
+
+ <div class="modal-body">
+
+ <div [innerHTML]="videoHTMLSupport"></div>
+
+ <div class="form-group inputs">
+ <span class="action-button action-button-cancel" (click)="hide()">
+ Cancel
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+.action-button-cancel {
+ margin-right: 0 !important;
+}
--- /dev/null
+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()
+ }
+}
<span class="icon icon-dislike" title="Dislike this video"></span>
</div>
+ <div (click)="showSupportModal()" class="action-button action-button-support">
+ <span class="icon icon-support"></span>
+ <span class="icon-text">Support</span>
+ </div>
+
<div (click)="showShareModal()" class="action-button action-button-share">
<span class="icon icon-share"></span>
- Share
+ <span class="icon-text">Share</span>
</div>
<div class="action-more" dropdown dropup="true" placement="right">
</div>
<ng-template [ngIf]="video !== null">
+ <my-video-support #videoSupportModal [video]="video"></my-video-support>
<my-video-share #videoShareModal [video]="video"></my-video-share>
<my-video-download #videoDownloadModal [video]="video"></my-video-download>
<my-video-report #videoReportModal [video]="video"></my-video-report>
font-weight: $font-semibold;
display: inline-block;
padding: 0 10px 0 10px;
+ white-space: nowrap;
.icon {
@include icon(21px);
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');
}
}
-@media screen and (max-width: 1300px) {
- .other-videos {
- display: none;
- }
-
+@media screen and (max-width: 1600px) {
.video-bottom {
.video-info {
margin-right: 0;
}
}
+@media screen and (max-width: 1200px) {
+ .other-videos {
+ display: none;
+ }
+}
+
@media screen and (max-width: 600px) {
.video-bottom {
margin: 20px 0 0 0;
}
}
}
+
+@media screen and (max-width: 450px) {
+ .video-bottom .action-button .icon-text {
+ display: none !important;
+ }
+}
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'
@ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
@ViewChild('videoShareModal') videoShareModal: VideoShareComponent
@ViewChild('videoReportModal') videoReportModal: VideoReportComponent
+ @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
otherVideosDisplayed: Video[] = []
this.videoReportModal.show()
}
+ showSupportModal () {
+ this.videoSupportModal.show()
+ }
+
showShareModal () {
this.videoShareModal.show()
}
return
}
- this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
+ this.videoHTMLDescription = this.markdownService.textMarkdownToHTML(this.video.description)
}
private setVideoLikesBarTooltipText () {
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'
VideoDownloadComponent,
VideoShareComponent,
VideoReportComponent,
+ VideoSupportComponent,
VideoCommentsComponent,
VideoCommentAddComponent,
VideoCommentComponent
@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[^>]+>([^<]+)<\/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) {
return defaultRender(tokens, idx, options, env, self)
}
}
+
+ private avoidTruncatedLinks (html) {
+ return html.replace(/<a[^>]+>([^<]+)<\/a>\s*...(<\/p>)?$/mi, '$1...')
+ }
}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Artboard-4" transform="translate(-48.000000, -1046.000000)" fill-rule="nonzero" fill="#585858">
+ <g id="Extras" transform="translate(48.000000, 1046.000000)">
+ <g id="heart">
+ <path d="M12.0174466,21 L20.9041801,11.3556763 C22.6291961,9.13778099 22.2795957,5.90145416 20.1233257,4.12713796 C17.9670557,2.35282175 14.8206518,2.71241362 13.0956358,4.93030888 L12.0174465,6.5 L10.9043642,4.93030888 C9.17934824,2.71241362 6.0329443,2.35282175 3.87667432,4.12713796 C1.72040435,5.90145416 1.37080391,9.13778099 3.09581989,11.3556763 L12.0174466,21 Z"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
color: #000;
}
+strong {
+ font-weight: $font-semibold;
+}
+
input.readonly {
/* Force blank on readonly inputs */
background-color: #fff !important;
.vjs-big-play-button {
outline: 0;
- font-size: 7em;
+ font-size: 6em;
$big-play-width: 1.5em;
$big-play-height: 1em;
@media screen and (max-width: 550px) {
.vjs-big-play-button {
- font-size: 6.5em;
+ font-size: 5em;
}
.vjs-webtorrent {
}
.vjs-big-play-button {
- font-size: 5em;
+ font-size: 4em;
}
.vjs-volume-control {
```
$ 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
1. Run:
```
-$ sudo pacman -S nodejs yarn ffmpeg postgresql openssl redis
+$ sudo pacman -S nodejs yarn ffmpeg postgresql openssl redis git
```
## CentOS 7
$ 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