From 2539932e16129992a2c0889b4ff527c265a8e2c7 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 27 May 2021 15:59:55 +0200 Subject: Instance homepage support (#4007) * Prepare homepage parsers * Add ability to update instance hompage * Add ability to set homepage as landing page * Add homepage preview in admin * Dynamically update left menu for homepage * Inject home content in homepage * Add videos list and channel miniature custom markup * Remove unused elements in markup service --- .../about-peertube-contributors.component.ts | 2 +- client/src/app/+admin/admin.module.ts | 8 +- .../edit-basic-configuration.component.html | 23 ++-- .../edit-basic-configuration.component.ts | 26 +++- .../edit-custom-config.component.html | 14 ++- .../edit-custom-config.component.ts | 48 ++++++-- .../edit-homepage.component.html | 28 +++++ .../edit-custom-config/edit-homepage.component.ts | 25 ++++ .../app/+admin/config/edit-custom-config/index.ts | 1 + client/src/app/+home/home-routing.module.ts | 18 +++ client/src/app/+home/home.component.html | 4 + client/src/app/+home/home.component.scss | 3 + client/src/app/+home/home.component.ts | 26 ++++ client/src/app/+home/home.module.ts | 25 ++++ client/src/app/+home/index.ts | 3 + .../comment/video-comment.component.ts | 2 +- .../+videos/+video-watch/video-watch.component.ts | 2 +- client/src/app/app-routing.module.ts | 4 + client/src/app/app.component.ts | 2 +- client/src/app/core/menu/menu.service.ts | 58 +++++++++ .../src/app/core/renderer/html-renderer.service.ts | 10 +- client/src/app/core/renderer/markdown.service.ts | 53 +++++--- client/src/app/core/server/server.service.ts | 7 +- client/src/app/menu/menu.component.html | 21 +--- client/src/app/menu/menu.component.ts | 23 +++- .../channel-miniature-markup.component.html | 8 ++ .../channel-miniature-markup.component.scss | 9 ++ .../channel-miniature-markup.component.ts | 26 ++++ .../shared-custom-markup/custom-markup.service.ts | 136 +++++++++++++++++++++ .../dynamic-element.service.ts | 57 +++++++++ .../shared-custom-markup/embed-markup.component.ts | 22 ++++ .../src/app/shared/shared-custom-markup/index.ts | 3 + .../playlist-miniature-markup.component.html | 2 + .../playlist-miniature-markup.component.scss | 7 ++ .../playlist-miniature-markup.component.ts | 38 ++++++ .../shared-custom-markup.module.ts | 49 ++++++++ .../video-miniature-markup.component.html | 6 + .../video-miniature-markup.component.scss | 7 ++ .../video-miniature-markup.component.ts | 44 +++++++ .../videos-list-markup.component.html | 13 ++ .../videos-list-markup.component.scss | 9 ++ .../videos-list-markup.component.ts | 60 +++++++++ .../shared-forms/markdown-textarea.component.html | 1 + .../shared-forms/markdown-textarea.component.ts | 45 +++++-- .../shared/shared-icons/global-icon.component.ts | 1 + .../shared-main/custom-page/custom-page.service.ts | 38 ++++++ .../app/shared/shared-main/custom-page/index.ts | 1 + .../app/shared/shared-main/shared-main.module.ts | 5 +- client/src/assets/images/feather/octagon.svg | 3 + client/src/assets/player/utils.ts | 2 +- 50 files changed, 935 insertions(+), 93 deletions(-) create mode 100644 client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html create mode 100644 client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts create mode 100644 client/src/app/+home/home-routing.module.ts create mode 100644 client/src/app/+home/home.component.html create mode 100644 client/src/app/+home/home.component.scss create mode 100644 client/src/app/+home/home.component.ts create mode 100644 client/src/app/+home/home.module.ts create mode 100644 client/src/app/+home/index.ts create mode 100644 client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html create mode 100644 client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss create mode 100644 client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts create mode 100644 client/src/app/shared/shared-custom-markup/custom-markup.service.ts create mode 100644 client/src/app/shared/shared-custom-markup/dynamic-element.service.ts create mode 100644 client/src/app/shared/shared-custom-markup/embed-markup.component.ts create mode 100644 client/src/app/shared/shared-custom-markup/index.ts create mode 100644 client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html create mode 100644 client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss create mode 100644 client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts create mode 100644 client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts create mode 100644 client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html create mode 100644 client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss create mode 100644 client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts create mode 100644 client/src/app/shared/shared-custom-markup/videos-list-markup.component.html create mode 100644 client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss create mode 100644 client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts create mode 100644 client/src/app/shared/shared-main/custom-page/custom-page.service.ts create mode 100644 client/src/app/shared/shared-main/custom-page/index.ts create mode 100644 client/src/assets/images/feather/octagon.svg (limited to 'client/src') diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts index c45269be4..dd774a4ef 100644 --- a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts +++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts @@ -14,6 +14,6 @@ export class AboutPeertubeContributorsComponent implements OnInit { constructor (private markdownService: MarkdownService) { } async ngOnInit () { - this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown) + this.creditsHtml = await this.markdownService.unsafeMarkdownToHTML(this.markdown, true) } } diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 45366f9ec..a7fe20b07 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -4,12 +4,13 @@ import { TableModule } from 'primeng/table' import { NgModule } from '@angular/core' import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit' +import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module' +import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup' import { SharedFormModule } from '@app/shared/shared-forms' import { SharedGlobalIconModule } from '@app/shared/shared-icons' import { SharedMainModule } from '@app/shared/shared-main' import { SharedModerationModule } from '@app/shared/shared-moderation' import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' -import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' import { AdminRoutingModule } from './admin-routing.module' import { AdminComponent } from './admin.component' import { @@ -18,6 +19,7 @@ import { EditBasicConfigurationComponent, EditConfigurationService, EditCustomConfigComponent, + EditHomepageComponent, EditInstanceInformationComponent, EditLiveConfigurationComponent, EditVODTranscodingComponent @@ -53,6 +55,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom SharedVideoCommentModule, SharedActorImageModule, SharedActorImageEditModule, + SharedCustomMarkupModule, TableModule, SelectButtonModule, @@ -100,7 +103,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom EditVODTranscodingComponent, EditLiveConfigurationComponent, EditAdvancedConfigurationComponent, - EditInstanceInformationComponent + EditInstanceInformationComponent, + EditHomepageComponent ], exports: [ diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html index 84a793ae4..451e6a34a 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html @@ -26,22 +26,13 @@
-
- -
+
{{ formErrors.instance.defaultClientRoute }}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts index 34d05f9f3..d50148e7a 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts @@ -1,7 +1,9 @@ import { pairwise } from 'rxjs/operators' -import { Component, Input, OnInit } from '@angular/core' +import { SelectOptionsItem } from 'src/types/select-options-item.model' +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core' import { FormGroup } from '@angular/forms' +import { MenuService } from '@app/core' import { ServerConfig } from '@shared/models' import { ConfigService } from '../shared/config.service' @@ -10,22 +12,31 @@ import { ConfigService } from '../shared/config.service' templateUrl: './edit-basic-configuration.component.html', styleUrls: [ './edit-custom-config.component.scss' ] }) -export class EditBasicConfigurationComponent implements OnInit { +export class EditBasicConfigurationComponent implements OnInit, OnChanges { @Input() form: FormGroup @Input() formErrors: any @Input() serverConfig: ServerConfig signupAlertMessage: string + defaultLandingPageOptions: SelectOptionsItem[] = [] constructor ( - private configService: ConfigService + private configService: ConfigService, + private menuService: MenuService ) { } ngOnInit () { + this.buildLandingPageOptions() this.checkSignupField() } + ngOnChanges (changes: SimpleChanges) { + if (changes['serverConfig']) { + this.buildLandingPageOptions() + } + } + getVideoQuotaOptions () { return this.configService.videoQuotaOptions } @@ -70,6 +81,15 @@ export class EditBasicConfigurationComponent implements OnInit { return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true } + buildLandingPageOptions () { + this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig) + .map(o => ({ + id: o.path, + label: o.label, + description: o.path + })) + } + private checkSignupField () { const signupControl = this.form.get('signup.enabled') diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index b6365614d..3ceea02ca 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -3,8 +3,16 @@ diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 8fa1de326..2f7e0cf07 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts @@ -4,7 +4,17 @@ import { switchMap } from 'rxjs/operators' import { ViewportScroller } from '@angular/common' import { Component, OnInit, ViewChild } from '@angular/core' import { Router } from '@angular/router' -import { AuthService, AuthStatus, AuthUser, MenuService, RedirectService, ScreenService, ServerService, UserService } from '@app/core' +import { + AuthService, + AuthStatus, + AuthUser, + MenuLink, + MenuService, + RedirectService, + ScreenService, + ServerService, + UserService +} from '@app/core' import { scrollToTop } from '@app/helpers' import { LanguageChooserComponent } from '@app/menu/language-chooser.component' import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' @@ -35,6 +45,8 @@ export class MenuComponent implements OnInit { currentInterfaceLanguage: string + commonMenuLinks: MenuLink[] = [] + private languages: VideoConstant[] = [] private serverConfig: ServerConfig private routesPerRight: { [role in UserRight]?: string } = { @@ -80,7 +92,10 @@ export class MenuComponent implements OnInit { ngOnInit () { this.serverConfig = this.serverService.getTmpConfig() this.serverService.getConfig() - .subscribe(config => this.serverConfig = config) + .subscribe(config => { + this.serverConfig = config + this.buildMenuLinks() + }) this.isLoggedIn = this.authService.isLoggedIn() if (this.isLoggedIn === true) { @@ -241,6 +256,10 @@ export class MenuComponent implements OnInit { } } + private buildMenuLinks () { + this.commonMenuLinks = this.menuService.buildCommonLinks(this.serverConfig) + } + private buildUserLanguages () { if (!this.user) { this.videoLanguages = [] diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html new file mode 100644 index 000000000..da81006b9 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html @@ -0,0 +1,8 @@ +
+ + +
{{ channel.displayName }}
+
{{ channel.name }}
+ +
{{ channel.description }}
+
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss new file mode 100644 index 000000000..85018afe2 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss @@ -0,0 +1,9 @@ +@import '_variables'; +@import '_mixins'; + +.channel { + border-radius: 15px; + padding: 10px; + width: min-content; + border: 1px solid pvar(--mainColor); +} diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts new file mode 100644 index 000000000..97bb5567e --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts @@ -0,0 +1,26 @@ +import { Component, Input, OnInit } from '@angular/core' +import { VideoChannel, VideoChannelService } from '../shared-main' + +/* + * Markup component that creates a channel miniature only +*/ + +@Component({ + selector: 'my-channel-miniature-markup', + templateUrl: 'channel-miniature-markup.component.html', + styleUrls: [ 'channel-miniature-markup.component.scss' ] +}) +export class ChannelMiniatureMarkupComponent implements OnInit { + @Input() name: string + + channel: VideoChannel + + constructor ( + private channelService: VideoChannelService + ) { } + + ngOnInit () { + this.channelService.getVideoChannel(this.name) + .subscribe(channel => this.channel = channel) + } +} diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts new file mode 100644 index 000000000..ffaf15710 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts @@ -0,0 +1,136 @@ +import { ComponentRef, Injectable } from '@angular/core' +import { MarkdownService } from '@app/core' +import { + ChannelMiniatureMarkupData, + EmbedMarkupData, + PlaylistMiniatureMarkupData, + VideoMiniatureMarkupData, + VideosListMarkupData +} from '@shared/models' +import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component' +import { DynamicElementService } from './dynamic-element.service' +import { EmbedMarkupComponent } from './embed-markup.component' +import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component' +import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component' +import { VideosListMarkupComponent } from './videos-list-markup.component' + +type BuilderFunction = (el: HTMLElement) => ComponentRef + +@Injectable() +export class CustomMarkupService { + private builders: { [ selector: string ]: BuilderFunction } = { + 'peertube-video-embed': el => this.embedBuilder(el, 'video'), + 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'), + 'peertube-video-miniature': el => this.videoMiniatureBuilder(el), + 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el), + 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el), + 'peertube-videos-list': el => this.videosListBuilder(el) + } + + constructor ( + private dynamicElementService: DynamicElementService, + private markdown: MarkdownService + ) { } + + async buildElement (text: string) { + const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags()) + + const rootElement = document.createElement('div') + rootElement.innerHTML = html + + for (const selector of this.getSupportedTags()) { + rootElement.querySelectorAll(selector) + .forEach((e: HTMLElement) => { + try { + const component = this.execBuilder(selector, e) + + this.dynamicElementService.injectElement(e, component) + } catch (err) { + console.error('Cannot inject component %s.', selector, err) + } + }) + } + + return rootElement + } + + private getSupportedTags () { + return Object.keys(this.builders) + } + + private execBuilder (selector: string, el: HTMLElement) { + return this.builders[selector](el) + } + + private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') { + const data = el.dataset as EmbedMarkupData + const component = this.dynamicElementService.createElement(EmbedMarkupComponent) + + this.dynamicElementService.setModel(component, { uuid: data.uuid, type }) + + return component + } + + private videoMiniatureBuilder (el: HTMLElement) { + const data = el.dataset as VideoMiniatureMarkupData + const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent) + + this.dynamicElementService.setModel(component, { uuid: data.uuid }) + + return component + } + + private playlistMiniatureBuilder (el: HTMLElement) { + const data = el.dataset as PlaylistMiniatureMarkupData + const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent) + + this.dynamicElementService.setModel(component, { uuid: data.uuid }) + + return component + } + + private channelMiniatureBuilder (el: HTMLElement) { + const data = el.dataset as ChannelMiniatureMarkupData + const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent) + + this.dynamicElementService.setModel(component, { name: data.name }) + + return component + } + + private videosListBuilder (el: HTMLElement) { + const data = el.dataset as VideosListMarkupData + const component = this.dynamicElementService.createElement(VideosListMarkupComponent) + + const model = { + title: data.title, + description: data.description, + sort: data.sort, + categoryOneOf: this.buildArrayNumber(data.categoryOneOf), + languageOneOf: this.buildArrayString(data.languageOneOf), + count: this.buildNumber(data.count) || 10 + } + + this.dynamicElementService.setModel(component, model) + + return component + } + + private buildNumber (value: string) { + if (!value) return undefined + + return parseInt(value, 10) + } + + private buildArrayNumber (value: string) { + if (!value) return undefined + + return value.split(',').map(v => parseInt(v, 10)) + } + + private buildArrayString (value: string) { + if (!value) return undefined + + return value.split(',') + } +} diff --git a/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts b/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts new file mode 100644 index 000000000..e967e30ac --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts @@ -0,0 +1,57 @@ +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + EmbeddedViewRef, + Injectable, + Injector, + OnChanges, + SimpleChange, + SimpleChanges, + Type +} from '@angular/core' + +@Injectable() +export class DynamicElementService { + + constructor ( + private injector: Injector, + private applicationRef: ApplicationRef, + private componentFactoryResolver: ComponentFactoryResolver + ) { } + + createElement (ofComponent: Type) { + const div = document.createElement('div') + + const component = this.componentFactoryResolver.resolveComponentFactory(ofComponent) + .create(this.injector, [], div) + + return component + } + + injectElement (wrapper: HTMLElement, componentRef: ComponentRef) { + const hostView = componentRef.hostView as EmbeddedViewRef + + this.applicationRef.attachView(hostView) + wrapper.appendChild(hostView.rootNodes[0]) + } + + setModel (componentRef: ComponentRef, attributes: Partial) { + const changes: SimpleChanges = {} + + for (const key of Object.keys(attributes)) { + const previousValue = componentRef.instance[key] + const newValue = attributes[key] + + componentRef.instance[key] = newValue + changes[key] = new SimpleChange(previousValue, newValue, previousValue === undefined) + } + + const component = componentRef.instance + if (typeof (component as unknown as OnChanges).ngOnChanges === 'function') { + (component as unknown as OnChanges).ngOnChanges(changes) + } + + componentRef.changeDetectorRef.detectChanges() + } +} diff --git a/client/src/app/shared/shared-custom-markup/embed-markup.component.ts b/client/src/app/shared/shared-custom-markup/embed-markup.component.ts new file mode 100644 index 000000000..a854d89f6 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/embed-markup.component.ts @@ -0,0 +1,22 @@ +import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' +import { environment } from 'src/environments/environment' +import { Component, ElementRef, Input, OnInit } from '@angular/core' + +@Component({ + selector: 'my-embed-markup', + template: '' +}) +export class EmbedMarkupComponent implements OnInit { + @Input() uuid: string + @Input() type: 'video' | 'playlist' = 'video' + + constructor (private el: ElementRef) { } + + ngOnInit () { + const link = this.type === 'video' + ? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` }) + : buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` }) + + this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid) + } +} diff --git a/client/src/app/shared/shared-custom-markup/index.ts b/client/src/app/shared/shared-custom-markup/index.ts new file mode 100644 index 000000000..14bde3ea9 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/index.ts @@ -0,0 +1,3 @@ +export * from './custom-markup.service' +export * from './dynamic-element.service' +export * from './shared-custom-markup.module' diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html new file mode 100644 index 000000000..4e1d1a13f --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html @@ -0,0 +1,2 @@ + + diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss new file mode 100644 index 000000000..281cef726 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +my-video-playlist-miniature { + display: inline-block; + width: $video-thumbnail-width; +} diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts new file mode 100644 index 000000000..7aee450f1 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, OnInit } from '@angular/core' +import { MiniatureDisplayOptions } from '../shared-video-miniature' +import { VideoPlaylist, VideoPlaylistService } from '../shared-video-playlist' + +/* + * Markup component that creates a playlist miniature only +*/ + +@Component({ + selector: 'my-playlist-miniature-markup', + templateUrl: 'playlist-miniature-markup.component.html', + styleUrls: [ 'playlist-miniature-markup.component.scss' ] +}) +export class PlaylistMiniatureMarkupComponent implements OnInit { + @Input() uuid: string + + playlist: VideoPlaylist + + displayOptions: MiniatureDisplayOptions = { + date: true, + views: true, + by: true, + avatar: false, + privacyLabel: false, + privacyText: false, + state: false, + blacklistInfo: false + } + + constructor ( + private playlistService: VideoPlaylistService + ) { } + + ngOnInit () { + this.playlistService.getVideoPlaylist(this.uuid) + .subscribe(playlist => this.playlist = playlist) + } +} diff --git a/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts new file mode 100644 index 000000000..4bbb71588 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts @@ -0,0 +1,49 @@ + +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' +import { SharedGlobalIconModule } from '../shared-icons' +import { SharedMainModule } from '../shared-main' +import { SharedVideoMiniatureModule } from '../shared-video-miniature' +import { SharedVideoPlaylistModule } from '../shared-video-playlist' +import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component' +import { CustomMarkupService } from './custom-markup.service' +import { DynamicElementService } from './dynamic-element.service' +import { EmbedMarkupComponent } from './embed-markup.component' +import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component' +import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component' +import { VideosListMarkupComponent } from './videos-list-markup.component' + +@NgModule({ + imports: [ + CommonModule, + + SharedMainModule, + SharedGlobalIconModule, + SharedVideoMiniatureModule, + SharedVideoPlaylistModule, + SharedActorImageModule + ], + + declarations: [ + VideoMiniatureMarkupComponent, + PlaylistMiniatureMarkupComponent, + ChannelMiniatureMarkupComponent, + EmbedMarkupComponent, + VideosListMarkupComponent + ], + + exports: [ + VideoMiniatureMarkupComponent, + PlaylistMiniatureMarkupComponent, + ChannelMiniatureMarkupComponent, + VideosListMarkupComponent, + EmbedMarkupComponent + ], + + providers: [ + CustomMarkupService, + DynamicElementService + ] +}) +export class SharedCustomMarkupModule { } diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html new file mode 100644 index 000000000..9b4930b6d --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html @@ -0,0 +1,6 @@ + + diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss new file mode 100644 index 000000000..81e265f29 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +my-video-miniature { + display: inline-block; + width: $video-thumbnail-width; +} diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts new file mode 100644 index 000000000..79add0c3b --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts @@ -0,0 +1,44 @@ +import { Component, Input, OnInit } from '@angular/core' +import { AuthService } from '@app/core' +import { Video, VideoService } from '../shared-main' +import { MiniatureDisplayOptions } from '../shared-video-miniature' + +/* + * Markup component that creates a video miniature only +*/ + +@Component({ + selector: 'my-video-miniature-markup', + templateUrl: 'video-miniature-markup.component.html', + styleUrls: [ 'video-miniature-markup.component.scss' ] +}) +export class VideoMiniatureMarkupComponent implements OnInit { + @Input() uuid: string + + video: Video + + displayOptions: MiniatureDisplayOptions = { + date: true, + views: true, + by: true, + avatar: false, + privacyLabel: false, + privacyText: false, + state: false, + blacklistInfo: false + } + + constructor ( + private auth: AuthService, + private videoService: VideoService + ) { } + + getUser () { + return this.auth.getUser() + } + + ngOnInit () { + this.videoService.getVideo({ videoId: this.uuid }) + .subscribe(video => this.video = video) + } +} diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html new file mode 100644 index 000000000..501f35e04 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html @@ -0,0 +1,13 @@ +
+

{{ title }}

+
{{ description }}
+ +
+ + +
+
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss new file mode 100644 index 000000000..dcd931090 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss @@ -0,0 +1,9 @@ +@import '_variables'; +@import '_mixins'; + +my-video-miniature { + margin-right: 15px; + display: inline-block; + min-width: $video-thumbnail-width; + max-width: $video-thumbnail-width; +} diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts new file mode 100644 index 000000000..cc25d0a51 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts @@ -0,0 +1,60 @@ +import { Component, Input, OnInit } from '@angular/core' +import { AuthService } from '@app/core' +import { VideoSortField } from '@shared/models' +import { Video, VideoService } from '../shared-main' +import { MiniatureDisplayOptions } from '../shared-video-miniature' + +/* + * Markup component list videos depending on criterias +*/ + +@Component({ + selector: 'my-videos-list-markup', + templateUrl: 'videos-list-markup.component.html', + styleUrls: [ 'videos-list-markup.component.scss' ] +}) +export class VideosListMarkupComponent implements OnInit { + @Input() title: string + @Input() description: string + @Input() sort = '-publishedAt' + @Input() categoryOneOf: number[] + @Input() languageOneOf: string[] + @Input() count = 10 + + videos: Video[] + + displayOptions: MiniatureDisplayOptions = { + date: true, + views: true, + by: true, + avatar: false, + privacyLabel: false, + privacyText: false, + state: false, + blacklistInfo: false + } + + constructor ( + private auth: AuthService, + private videoService: VideoService + ) { } + + getUser () { + return this.auth.getUser() + } + + ngOnInit () { + const options = { + videoPagination: { + currentPage: 1, + itemsPerPage: this.count + }, + categoryOneOf: this.categoryOneOf, + languageOneOf: this.languageOneOf, + sort: this.sort as VideoSortField + } + + this.videoService.getVideos(options) + .subscribe(({ data }) => this.videos = data) + } +} diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.html b/client/src/app/shared/shared-forms/markdown-textarea.component.html index 513b543cd..6e70e2f37 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.html +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.html @@ -19,6 +19,7 @@ Complete preview +
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts index 9b3ab9cf3..a233a4205 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts @@ -1,9 +1,10 @@ -import { ViewportScroller } from '@angular/common' import truncate from 'lodash-es/truncate' import { Subject } from 'rxjs' import { debounceTime, distinctUntilChanged } from 'rxjs/operators' +import { ViewportScroller } from '@angular/common' import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core' import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' +import { SafeHtml } from '@angular/platform-browser' import { MarkdownService, ScreenService } from '@app/core' @Component({ @@ -21,18 +22,27 @@ import { MarkdownService, ScreenService } from '@app/core' export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { @Input() content = '' + @Input() classes: string[] | { [klass: string]: any[] | any } = [] + @Input() textareaMaxWidth = '100%' @Input() textareaHeight = '150px' + @Input() truncate: number + @Input() markdownType: 'text' | 'enhanced' = 'text' + @Input() customMarkdownRenderer?: (text: string) => Promise + @Input() markdownVideo = false + @Input() name = 'description' @ViewChild('textarea') textareaElement: ElementRef + @ViewChild('previewElement') previewElement: ElementRef + + truncatedPreviewHTML: SafeHtml | string = '' + previewHTML: SafeHtml | string = '' - truncatedPreviewHTML = '' - previewHTML = '' isMaximized = false maximizeInText = $localize`Maximize editor` @@ -115,10 +125,31 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { } private async markdownRender (text: string) { - const html = this.markdownType === 'text' ? - await this.markdownService.textMarkdownToHTML(text) : - await this.markdownService.enhancedMarkdownToHTML(text) + let html: string + + if (this.customMarkdownRenderer) { + const result = await this.customMarkdownRenderer(text) + + if (result instanceof HTMLElement) { + html = '' + + const wrapperElement = this.previewElement.nativeElement as HTMLElement + wrapperElement.innerHTML = '' + wrapperElement.appendChild(result) + return + } + + html = result + } else if (this.markdownType === 'text') { + html = await this.markdownService.textMarkdownToHTML(text) + } else { + html = await this.markdownService.enhancedMarkdownToHTML(text) + } + + if (this.markdownVideo) { + html = this.markdownService.processVideoTimestamps(html) + } - return this.markdownVideo ? this.markdownService.processVideoTimestamps(html) : html + return html } } diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts index 3af517927..a4dd72db6 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts @@ -72,6 +72,7 @@ const icons = { 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default, + 'octagon': require('!!raw-loader?!../../../assets/images/feather/octagon.svg').default, 'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default } diff --git a/client/src/app/shared/shared-main/custom-page/custom-page.service.ts b/client/src/app/shared/shared-main/custom-page/custom-page.service.ts new file mode 100644 index 000000000..e5c2b3cd4 --- /dev/null +++ b/client/src/app/shared/shared-main/custom-page/custom-page.service.ts @@ -0,0 +1,38 @@ +import { of } from 'rxjs' +import { catchError, map } from 'rxjs/operators' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor } from '@app/core' +import { CustomPage } from '@shared/models' +import { environment } from '../../../../environments/environment' + +@Injectable() +export class CustomPageService { + static BASE_INSTANCE_HOMEPAGE_URL = environment.apiUrl + '/api/v1/custom-pages/homepage/instance' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor + ) { } + + getInstanceHomepage () { + return this.authHttp.get(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL) + .pipe( + catchError(err => { + if (err.status === 404) { + return of({ content: '' }) + } + + this.restExtractor.handleError(err) + }) + ) + } + + updateInstanceHomepage (content: string) { + return this.authHttp.put(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL, { content }) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => this.restExtractor.handleError(err)) + ) + } +} diff --git a/client/src/app/shared/shared-main/custom-page/index.ts b/client/src/app/shared/shared-main/custom-page/index.ts new file mode 100644 index 000000000..7269ece95 --- /dev/null +++ b/client/src/app/shared/shared-main/custom-page/index.ts @@ -0,0 +1 @@ +export * from './custom-page.service' diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 772198cb2..f9b6085cf 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts @@ -29,6 +29,7 @@ import { } from './angular' import { AUTH_INTERCEPTOR_PROVIDER } from './auth' import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' +import { CustomPageService } from './custom-page' import { DateToggleComponent } from './date' import { FeedComponent } from './feeds' import { LoaderComponent, SmallLoaderComponent } from './loaders' @@ -171,7 +172,9 @@ import { VideoChannelService } from './video-channel' VideoCaptionService, - VideoChannelService + VideoChannelService, + + CustomPageService ] }) export class SharedMainModule { } diff --git a/client/src/assets/images/feather/octagon.svg b/client/src/assets/images/feather/octagon.svg new file mode 100644 index 000000000..1ed9bacbf --- /dev/null +++ b/client/src/assets/images/feather/octagon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index d7451fa1d..1243526d2 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts @@ -95,7 +95,7 @@ function buildVideoLink (options: { function buildPlaylistLink (options: { baseUrl?: string - playlistPosition: number + playlistPosition?: number }) { const { baseUrl } = options -- cgit v1.2.3