diff options
Diffstat (limited to 'client/src/app/shared')
46 files changed, 750 insertions, 41 deletions
diff --git a/client/src/app/shared/form-validators/custom-config-validators.ts b/client/src/app/shared/form-validators/custom-config-validators.ts index ef6e9b456..1ed5700ff 100644 --- a/client/src/app/shared/form-validators/custom-config-validators.ts +++ b/client/src/app/shared/form-validators/custom-config-validators.ts | |||
@@ -49,6 +49,15 @@ export const SIGNUP_LIMIT_VALIDATOR: BuildFormValidator = { | |||
49 | } | 49 | } |
50 | } | 50 | } |
51 | 51 | ||
52 | export const SIGNUP_MINIMUM_AGE_VALIDATOR: BuildFormValidator = { | ||
53 | VALIDATORS: [Validators.required, Validators.min(1), Validators.pattern('[0-9]+')], | ||
54 | MESSAGES: { | ||
55 | 'required': $localize`Signup minimum age is required.`, | ||
56 | 'min': $localize`Signup minimum age must be greater than 1.`, | ||
57 | 'pattern': $localize`Signup minimum age must be a number.` | ||
58 | } | ||
59 | } | ||
60 | |||
52 | export const ADMIN_EMAIL_VALIDATOR: BuildFormValidator = { | 61 | export const ADMIN_EMAIL_VALIDATOR: BuildFormValidator = { |
53 | VALIDATORS: [Validators.required, Validators.email], | 62 | VALIDATORS: [Validators.required, Validators.email], |
54 | MESSAGES: { | 63 | MESSAGES: { |
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts index fee37e95f..976c97b87 100644 --- a/client/src/app/shared/form-validators/user-validators.ts +++ b/client/src/app/shared/form-validators/user-validators.ts | |||
@@ -1,12 +1,14 @@ | |||
1 | import { Validators } from '@angular/forms' | 1 | import { Validators } from '@angular/forms' |
2 | import { BuildFormValidator } from './form-validator.model' | 2 | import { BuildFormValidator } from './form-validator.model' |
3 | 3 | ||
4 | export const USER_USERNAME_REGEX_CHARACTERS = '[a-z0-9][a-z0-9._]' | ||
5 | |||
4 | export const USER_USERNAME_VALIDATOR: BuildFormValidator = { | 6 | export const USER_USERNAME_VALIDATOR: BuildFormValidator = { |
5 | VALIDATORS: [ | 7 | VALIDATORS: [ |
6 | Validators.required, | 8 | Validators.required, |
7 | Validators.minLength(1), | 9 | Validators.minLength(1), |
8 | Validators.maxLength(50), | 10 | Validators.maxLength(50), |
9 | Validators.pattern(/^[a-z0-9][a-z0-9._]*$/) | 11 | Validators.pattern(new RegExp(`^${USER_USERNAME_REGEX_CHARACTERS}*$`)) |
10 | ], | 12 | ], |
11 | MESSAGES: { | 13 | MESSAGES: { |
12 | 'required': $localize`Username is required.`, | 14 | 'required': $localize`Username is required.`, |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 4dc2b4f10..07b9dddba 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -124,7 +124,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit { | |||
124 | } | 124 | } |
125 | 125 | ||
126 | getAccountUrl (abuse: ProcessedAbuse) { | 126 | getAccountUrl (abuse: ProcessedAbuse) { |
127 | return '/accounts/' + abuse.flaggedAccount.nameWithHost | 127 | return '/a/' + abuse.flaggedAccount.nameWithHost |
128 | } | 128 | } |
129 | 129 | ||
130 | getVideoEmbed (abuse: AdminAbuse) { | 130 | getVideoEmbed (abuse: AdminAbuse) { |
diff --git a/client/src/app/shared/shared-custom-markup/button-markup.component.html b/client/src/app/shared/shared-custom-markup/button-markup.component.html new file mode 100644 index 000000000..619bb9d8c --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/button-markup.component.html | |||
@@ -0,0 +1 @@ | |||
<a [href]="href" [ngClass]="getClasses()" [target]="getTarget()">{{ label }}</a> | |||
diff --git a/client/src/app/shared/shared-custom-markup/button-markup.component.scss b/client/src/app/shared/shared-custom-markup/button-markup.component.scss new file mode 100644 index 000000000..f43d6b400 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/button-markup.component.scss | |||
@@ -0,0 +1,3 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
diff --git a/client/src/app/shared/shared-custom-markup/button-markup.component.ts b/client/src/app/shared/shared-custom-markup/button-markup.component.ts new file mode 100644 index 000000000..c0aab2edd --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/button-markup.component.ts | |||
@@ -0,0 +1,34 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { VideoChannel } from '../shared-main' | ||
3 | |||
4 | /* | ||
5 | * Markup component that creates a button | ||
6 | */ | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-button-markup', | ||
10 | templateUrl: 'button-markup.component.html', | ||
11 | styleUrls: [ 'button-markup.component.scss' ] | ||
12 | }) | ||
13 | export class ButtonMarkupComponent { | ||
14 | @Input() theme: 'primary' | 'secondary' | ||
15 | @Input() href: string | ||
16 | @Input() label: string | ||
17 | @Input() blankTarget?: boolean | ||
18 | |||
19 | channel: VideoChannel | ||
20 | |||
21 | getTarget () { | ||
22 | if (this.blankTarget === true) return '_blank' | ||
23 | |||
24 | return '' | ||
25 | } | ||
26 | |||
27 | getClasses () { | ||
28 | const additionalClass = this.theme === 'primary' | ||
29 | ? 'orange-button' | ||
30 | : 'grey-button' | ||
31 | |||
32 | return [ 'peertube-button-link', additionalClass ] | ||
33 | } | ||
34 | } | ||
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 @@ | |||
1 | <div *ngIf="channel" class="channel"> | ||
2 | <my-actor-avatar [channel]="channel" size="34"></my-actor-avatar> | ||
3 | |||
4 | <div class="display-name">{{ channel.displayName }}</div> | ||
5 | <div class="username">{{ channel.name }}</div> | ||
6 | |||
7 | <div class="description">{{ channel.description }}</div> | ||
8 | </div> | ||
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 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .channel { | ||
5 | border-radius: 15px; | ||
6 | padding: 10px; | ||
7 | width: min-content; | ||
8 | border: 1px solid pvar(--mainColor); | ||
9 | } | ||
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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { VideoChannel, VideoChannelService } from '../shared-main' | ||
3 | |||
4 | /* | ||
5 | * Markup component that creates a channel miniature only | ||
6 | */ | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-channel-miniature-markup', | ||
10 | templateUrl: 'channel-miniature-markup.component.html', | ||
11 | styleUrls: [ 'channel-miniature-markup.component.scss' ] | ||
12 | }) | ||
13 | export class ChannelMiniatureMarkupComponent implements OnInit { | ||
14 | @Input() name: string | ||
15 | |||
16 | channel: VideoChannel | ||
17 | |||
18 | constructor ( | ||
19 | private channelService: VideoChannelService | ||
20 | ) { } | ||
21 | |||
22 | ngOnInit () { | ||
23 | this.channelService.getVideoChannel(this.name) | ||
24 | .subscribe(channel => this.channel = channel) | ||
25 | } | ||
26 | } | ||
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..09414da95 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts | |||
@@ -0,0 +1,161 @@ | |||
1 | import { ComponentRef, Injectable } from '@angular/core' | ||
2 | import { MarkdownService } from '@app/core' | ||
3 | import { | ||
4 | ButtonMarkupData, | ||
5 | ChannelMiniatureMarkupData, | ||
6 | EmbedMarkupData, | ||
7 | PlaylistMiniatureMarkupData, | ||
8 | VideoMiniatureMarkupData, | ||
9 | VideosListMarkupData | ||
10 | } from '@shared/models' | ||
11 | import { ButtonMarkupComponent } from './button-markup.component' | ||
12 | import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component' | ||
13 | import { DynamicElementService } from './dynamic-element.service' | ||
14 | import { EmbedMarkupComponent } from './embed-markup.component' | ||
15 | import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component' | ||
16 | import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component' | ||
17 | import { VideosListMarkupComponent } from './videos-list-markup.component' | ||
18 | |||
19 | type BuilderFunction = (el: HTMLElement) => ComponentRef<any> | ||
20 | |||
21 | @Injectable() | ||
22 | export class CustomMarkupService { | ||
23 | private builders: { [ selector: string ]: BuilderFunction } = { | ||
24 | 'peertube-button': el => this.buttonBuilder(el), | ||
25 | 'peertube-video-embed': el => this.embedBuilder(el, 'video'), | ||
26 | 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'), | ||
27 | 'peertube-video-miniature': el => this.videoMiniatureBuilder(el), | ||
28 | 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el), | ||
29 | 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el), | ||
30 | 'peertube-videos-list': el => this.videosListBuilder(el) | ||
31 | } | ||
32 | |||
33 | constructor ( | ||
34 | private dynamicElementService: DynamicElementService, | ||
35 | private markdown: MarkdownService | ||
36 | ) { } | ||
37 | |||
38 | async buildElement (text: string) { | ||
39 | const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags()) | ||
40 | |||
41 | const rootElement = document.createElement('div') | ||
42 | rootElement.innerHTML = html | ||
43 | |||
44 | for (const selector of this.getSupportedTags()) { | ||
45 | rootElement.querySelectorAll(selector) | ||
46 | .forEach((e: HTMLElement) => { | ||
47 | try { | ||
48 | const component = this.execBuilder(selector, e) | ||
49 | |||
50 | this.dynamicElementService.injectElement(e, component) | ||
51 | } catch (err) { | ||
52 | console.error('Cannot inject component %s.', selector, err) | ||
53 | } | ||
54 | }) | ||
55 | } | ||
56 | |||
57 | return rootElement | ||
58 | } | ||
59 | |||
60 | private getSupportedTags () { | ||
61 | return Object.keys(this.builders) | ||
62 | } | ||
63 | |||
64 | private execBuilder (selector: string, el: HTMLElement) { | ||
65 | return this.builders[selector](el) | ||
66 | } | ||
67 | |||
68 | private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') { | ||
69 | const data = el.dataset as EmbedMarkupData | ||
70 | const component = this.dynamicElementService.createElement(EmbedMarkupComponent) | ||
71 | |||
72 | this.dynamicElementService.setModel(component, { uuid: data.uuid, type }) | ||
73 | |||
74 | return component | ||
75 | } | ||
76 | |||
77 | private videoMiniatureBuilder (el: HTMLElement) { | ||
78 | const data = el.dataset as VideoMiniatureMarkupData | ||
79 | const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent) | ||
80 | |||
81 | this.dynamicElementService.setModel(component, { uuid: data.uuid }) | ||
82 | |||
83 | return component | ||
84 | } | ||
85 | |||
86 | private playlistMiniatureBuilder (el: HTMLElement) { | ||
87 | const data = el.dataset as PlaylistMiniatureMarkupData | ||
88 | const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent) | ||
89 | |||
90 | this.dynamicElementService.setModel(component, { uuid: data.uuid }) | ||
91 | |||
92 | return component | ||
93 | } | ||
94 | |||
95 | private channelMiniatureBuilder (el: HTMLElement) { | ||
96 | const data = el.dataset as ChannelMiniatureMarkupData | ||
97 | const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent) | ||
98 | |||
99 | this.dynamicElementService.setModel(component, { name: data.name }) | ||
100 | |||
101 | return component | ||
102 | } | ||
103 | |||
104 | private buttonBuilder (el: HTMLElement) { | ||
105 | const data = el.dataset as ButtonMarkupData | ||
106 | const component = this.dynamicElementService.createElement(ButtonMarkupComponent) | ||
107 | |||
108 | const model = { | ||
109 | theme: data.theme, | ||
110 | href: data.href, | ||
111 | label: data.label, | ||
112 | blankTarget: this.buildBoolean(data.blankTarget) | ||
113 | } | ||
114 | this.dynamicElementService.setModel(component, model) | ||
115 | |||
116 | return component | ||
117 | } | ||
118 | |||
119 | private videosListBuilder (el: HTMLElement) { | ||
120 | const data = el.dataset as VideosListMarkupData | ||
121 | const component = this.dynamicElementService.createElement(VideosListMarkupComponent) | ||
122 | |||
123 | const model = { | ||
124 | title: data.title, | ||
125 | description: data.description, | ||
126 | sort: data.sort, | ||
127 | categoryOneOf: this.buildArrayNumber(data.categoryOneOf), | ||
128 | languageOneOf: this.buildArrayString(data.languageOneOf), | ||
129 | count: this.buildNumber(data.count) || 10 | ||
130 | } | ||
131 | |||
132 | this.dynamicElementService.setModel(component, model) | ||
133 | |||
134 | return component | ||
135 | } | ||
136 | |||
137 | private buildNumber (value: string) { | ||
138 | if (!value) return undefined | ||
139 | |||
140 | return parseInt(value, 10) | ||
141 | } | ||
142 | |||
143 | private buildBoolean (value: string) { | ||
144 | if (value === 'true') return true | ||
145 | if (value === 'false') return false | ||
146 | |||
147 | return undefined | ||
148 | } | ||
149 | |||
150 | private buildArrayNumber (value: string) { | ||
151 | if (!value) return undefined | ||
152 | |||
153 | return value.split(',').map(v => parseInt(v, 10)) | ||
154 | } | ||
155 | |||
156 | private buildArrayString (value: string) { | ||
157 | if (!value) return undefined | ||
158 | |||
159 | return value.split(',') | ||
160 | } | ||
161 | } | ||
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 @@ | |||
1 | import { | ||
2 | ApplicationRef, | ||
3 | ComponentFactoryResolver, | ||
4 | ComponentRef, | ||
5 | EmbeddedViewRef, | ||
6 | Injectable, | ||
7 | Injector, | ||
8 | OnChanges, | ||
9 | SimpleChange, | ||
10 | SimpleChanges, | ||
11 | Type | ||
12 | } from '@angular/core' | ||
13 | |||
14 | @Injectable() | ||
15 | export class DynamicElementService { | ||
16 | |||
17 | constructor ( | ||
18 | private injector: Injector, | ||
19 | private applicationRef: ApplicationRef, | ||
20 | private componentFactoryResolver: ComponentFactoryResolver | ||
21 | ) { } | ||
22 | |||
23 | createElement <T> (ofComponent: Type<T>) { | ||
24 | const div = document.createElement('div') | ||
25 | |||
26 | const component = this.componentFactoryResolver.resolveComponentFactory(ofComponent) | ||
27 | .create(this.injector, [], div) | ||
28 | |||
29 | return component | ||
30 | } | ||
31 | |||
32 | injectElement <T> (wrapper: HTMLElement, componentRef: ComponentRef<T>) { | ||
33 | const hostView = componentRef.hostView as EmbeddedViewRef<any> | ||
34 | |||
35 | this.applicationRef.attachView(hostView) | ||
36 | wrapper.appendChild(hostView.rootNodes[0]) | ||
37 | } | ||
38 | |||
39 | setModel <T> (componentRef: ComponentRef<T>, attributes: Partial<T>) { | ||
40 | const changes: SimpleChanges = {} | ||
41 | |||
42 | for (const key of Object.keys(attributes)) { | ||
43 | const previousValue = componentRef.instance[key] | ||
44 | const newValue = attributes[key] | ||
45 | |||
46 | componentRef.instance[key] = newValue | ||
47 | changes[key] = new SimpleChange(previousValue, newValue, previousValue === undefined) | ||
48 | } | ||
49 | |||
50 | const component = componentRef.instance | ||
51 | if (typeof (component as unknown as OnChanges).ngOnChanges === 'function') { | ||
52 | (component as unknown as OnChanges).ngOnChanges(changes) | ||
53 | } | ||
54 | |||
55 | componentRef.changeDetectorRef.detectChanges() | ||
56 | } | ||
57 | } | ||
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 @@ | |||
1 | import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' | ||
2 | import { environment } from 'src/environments/environment' | ||
3 | import { Component, ElementRef, Input, OnInit } from '@angular/core' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-embed-markup', | ||
7 | template: '' | ||
8 | }) | ||
9 | export class EmbedMarkupComponent implements OnInit { | ||
10 | @Input() uuid: string | ||
11 | @Input() type: 'video' | 'playlist' = 'video' | ||
12 | |||
13 | constructor (private el: ElementRef) { } | ||
14 | |||
15 | ngOnInit () { | ||
16 | const link = this.type === 'video' | ||
17 | ? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` }) | ||
18 | : buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` }) | ||
19 | |||
20 | this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid) | ||
21 | } | ||
22 | } | ||
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 @@ | |||
1 | export * from './custom-markup.service' | ||
2 | export * from './dynamic-element.service' | ||
3 | 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 @@ | |||
1 | <my-video-playlist-miniature *ngIf="playlist" [playlist]="playlist"> | ||
2 | </my-video-playlist-miniature> | ||
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 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | my-video-playlist-miniature { | ||
5 | display: inline-block; | ||
6 | width: $video-thumbnail-width; | ||
7 | } | ||
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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { MiniatureDisplayOptions } from '../shared-video-miniature' | ||
3 | import { VideoPlaylist, VideoPlaylistService } from '../shared-video-playlist' | ||
4 | |||
5 | /* | ||
6 | * Markup component that creates a playlist miniature only | ||
7 | */ | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-playlist-miniature-markup', | ||
11 | templateUrl: 'playlist-miniature-markup.component.html', | ||
12 | styleUrls: [ 'playlist-miniature-markup.component.scss' ] | ||
13 | }) | ||
14 | export class PlaylistMiniatureMarkupComponent implements OnInit { | ||
15 | @Input() uuid: string | ||
16 | |||
17 | playlist: VideoPlaylist | ||
18 | |||
19 | displayOptions: MiniatureDisplayOptions = { | ||
20 | date: true, | ||
21 | views: true, | ||
22 | by: true, | ||
23 | avatar: false, | ||
24 | privacyLabel: false, | ||
25 | privacyText: false, | ||
26 | state: false, | ||
27 | blacklistInfo: false | ||
28 | } | ||
29 | |||
30 | constructor ( | ||
31 | private playlistService: VideoPlaylistService | ||
32 | ) { } | ||
33 | |||
34 | ngOnInit () { | ||
35 | this.playlistService.getVideoPlaylist(this.uuid) | ||
36 | .subscribe(playlist => this.playlist = playlist) | ||
37 | } | ||
38 | } | ||
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..d03aa856f --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | |||
2 | import { CommonModule } from '@angular/common' | ||
3 | import { NgModule } from '@angular/core' | ||
4 | import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' | ||
5 | import { SharedGlobalIconModule } from '../shared-icons' | ||
6 | import { SharedMainModule } from '../shared-main' | ||
7 | import { SharedVideoMiniatureModule } from '../shared-video-miniature' | ||
8 | import { SharedVideoPlaylistModule } from '../shared-video-playlist' | ||
9 | import { ButtonMarkupComponent } from './button-markup.component' | ||
10 | import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component' | ||
11 | import { CustomMarkupService } from './custom-markup.service' | ||
12 | import { DynamicElementService } from './dynamic-element.service' | ||
13 | import { EmbedMarkupComponent } from './embed-markup.component' | ||
14 | import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component' | ||
15 | import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component' | ||
16 | import { VideosListMarkupComponent } from './videos-list-markup.component' | ||
17 | |||
18 | @NgModule({ | ||
19 | imports: [ | ||
20 | CommonModule, | ||
21 | |||
22 | SharedMainModule, | ||
23 | SharedGlobalIconModule, | ||
24 | SharedVideoMiniatureModule, | ||
25 | SharedVideoPlaylistModule, | ||
26 | SharedActorImageModule | ||
27 | ], | ||
28 | |||
29 | declarations: [ | ||
30 | VideoMiniatureMarkupComponent, | ||
31 | PlaylistMiniatureMarkupComponent, | ||
32 | ChannelMiniatureMarkupComponent, | ||
33 | EmbedMarkupComponent, | ||
34 | VideosListMarkupComponent, | ||
35 | ButtonMarkupComponent | ||
36 | ], | ||
37 | |||
38 | exports: [ | ||
39 | VideoMiniatureMarkupComponent, | ||
40 | PlaylistMiniatureMarkupComponent, | ||
41 | ChannelMiniatureMarkupComponent, | ||
42 | VideosListMarkupComponent, | ||
43 | EmbedMarkupComponent, | ||
44 | ButtonMarkupComponent | ||
45 | ], | ||
46 | |||
47 | providers: [ | ||
48 | CustomMarkupService, | ||
49 | DynamicElementService | ||
50 | ] | ||
51 | }) | ||
52 | 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 @@ | |||
1 | <my-video-miniature | ||
2 | *ngIf="video" | ||
3 | [video]="video" [user]="getUser()" [displayAsRow]="false" | ||
4 | [displayVideoActions]="false" [displayOptions]="displayOptions" | ||
5 | > | ||
6 | </my-video-miniature> | ||
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 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | my-video-miniature { | ||
5 | display: inline-block; | ||
6 | width: $video-thumbnail-width; | ||
7 | } | ||
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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { Video, VideoService } from '../shared-main' | ||
4 | import { MiniatureDisplayOptions } from '../shared-video-miniature' | ||
5 | |||
6 | /* | ||
7 | * Markup component that creates a video miniature only | ||
8 | */ | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-video-miniature-markup', | ||
12 | templateUrl: 'video-miniature-markup.component.html', | ||
13 | styleUrls: [ 'video-miniature-markup.component.scss' ] | ||
14 | }) | ||
15 | export class VideoMiniatureMarkupComponent implements OnInit { | ||
16 | @Input() uuid: string | ||
17 | |||
18 | video: Video | ||
19 | |||
20 | displayOptions: MiniatureDisplayOptions = { | ||
21 | date: true, | ||
22 | views: true, | ||
23 | by: true, | ||
24 | avatar: false, | ||
25 | privacyLabel: false, | ||
26 | privacyText: false, | ||
27 | state: false, | ||
28 | blacklistInfo: false | ||
29 | } | ||
30 | |||
31 | constructor ( | ||
32 | private auth: AuthService, | ||
33 | private videoService: VideoService | ||
34 | ) { } | ||
35 | |||
36 | getUser () { | ||
37 | return this.auth.getUser() | ||
38 | } | ||
39 | |||
40 | ngOnInit () { | ||
41 | this.videoService.getVideo({ videoId: this.uuid }) | ||
42 | .subscribe(video => this.video = video) | ||
43 | } | ||
44 | } | ||
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 @@ | |||
1 | <div class="root"> | ||
2 | <h4 *ngIf="title">{{ title }}</h4> | ||
3 | <div *ngIf="description" class="description">{{ description }}</div> | ||
4 | |||
5 | <div class="videos"> | ||
6 | <my-video-miniature | ||
7 | *ngFor="let video of videos" | ||
8 | [video]="video" [user]="getUser()" [displayAsRow]="false" | ||
9 | [displayVideoActions]="false" [displayOptions]="displayOptions" | ||
10 | > | ||
11 | </my-video-miniature> | ||
12 | </div> | ||
13 | </div> | ||
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 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | my-video-miniature { | ||
5 | margin-right: 15px; | ||
6 | display: inline-block; | ||
7 | min-width: $video-thumbnail-width; | ||
8 | max-width: $video-thumbnail-width; | ||
9 | } | ||
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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { VideoSortField } from '@shared/models' | ||
4 | import { Video, VideoService } from '../shared-main' | ||
5 | import { MiniatureDisplayOptions } from '../shared-video-miniature' | ||
6 | |||
7 | /* | ||
8 | * Markup component list videos depending on criterias | ||
9 | */ | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-videos-list-markup', | ||
13 | templateUrl: 'videos-list-markup.component.html', | ||
14 | styleUrls: [ 'videos-list-markup.component.scss' ] | ||
15 | }) | ||
16 | export class VideosListMarkupComponent implements OnInit { | ||
17 | @Input() title: string | ||
18 | @Input() description: string | ||
19 | @Input() sort = '-publishedAt' | ||
20 | @Input() categoryOneOf: number[] | ||
21 | @Input() languageOneOf: string[] | ||
22 | @Input() count = 10 | ||
23 | |||
24 | videos: Video[] | ||
25 | |||
26 | displayOptions: MiniatureDisplayOptions = { | ||
27 | date: true, | ||
28 | views: true, | ||
29 | by: true, | ||
30 | avatar: false, | ||
31 | privacyLabel: false, | ||
32 | privacyText: false, | ||
33 | state: false, | ||
34 | blacklistInfo: false | ||
35 | } | ||
36 | |||
37 | constructor ( | ||
38 | private auth: AuthService, | ||
39 | private videoService: VideoService | ||
40 | ) { } | ||
41 | |||
42 | getUser () { | ||
43 | return this.auth.getUser() | ||
44 | } | ||
45 | |||
46 | ngOnInit () { | ||
47 | const options = { | ||
48 | videoPagination: { | ||
49 | currentPage: 1, | ||
50 | itemsPerPage: this.count | ||
51 | }, | ||
52 | categoryOneOf: this.categoryOneOf, | ||
53 | languageOneOf: this.languageOneOf, | ||
54 | sort: this.sort as VideoSortField | ||
55 | } | ||
56 | |||
57 | this.videoService.getVideos(options) | ||
58 | .subscribe(({ data }) => this.videos = data) | ||
59 | } | ||
60 | } | ||
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 @@ | |||
19 | <a ngbNavLink i18n>Complete preview</a> | 19 | <a ngbNavLink i18n>Complete preview</a> |
20 | 20 | ||
21 | <ng-template ngbNavContent> | 21 | <ng-template ngbNavContent> |
22 | <div #previewElement></div> | ||
22 | <div [innerHTML]="previewHTML"></div> | 23 | <div [innerHTML]="previewHTML"></div> |
23 | </ng-template> | 24 | </ng-template> |
24 | </ng-container> | 25 | </ng-container> |
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 @@ | |||
1 | import { ViewportScroller } from '@angular/common' | ||
2 | import truncate from 'lodash-es/truncate' | 1 | import truncate from 'lodash-es/truncate' |
3 | import { Subject } from 'rxjs' | 2 | import { Subject } from 'rxjs' |
4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 3 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
4 | import { ViewportScroller } from '@angular/common' | ||
5 | import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core' | 5 | import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core' |
6 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 6 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
7 | import { SafeHtml } from '@angular/platform-browser' | ||
7 | import { MarkdownService, ScreenService } from '@app/core' | 8 | import { MarkdownService, ScreenService } from '@app/core' |
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
@@ -21,18 +22,27 @@ import { MarkdownService, ScreenService } from '@app/core' | |||
21 | 22 | ||
22 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | 23 | export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { |
23 | @Input() content = '' | 24 | @Input() content = '' |
25 | |||
24 | @Input() classes: string[] | { [klass: string]: any[] | any } = [] | 26 | @Input() classes: string[] | { [klass: string]: any[] | any } = [] |
27 | |||
25 | @Input() textareaMaxWidth = '100%' | 28 | @Input() textareaMaxWidth = '100%' |
26 | @Input() textareaHeight = '150px' | 29 | @Input() textareaHeight = '150px' |
30 | |||
27 | @Input() truncate: number | 31 | @Input() truncate: number |
32 | |||
28 | @Input() markdownType: 'text' | 'enhanced' = 'text' | 33 | @Input() markdownType: 'text' | 'enhanced' = 'text' |
34 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> | ||
35 | |||
29 | @Input() markdownVideo = false | 36 | @Input() markdownVideo = false |
37 | |||
30 | @Input() name = 'description' | 38 | @Input() name = 'description' |
31 | 39 | ||
32 | @ViewChild('textarea') textareaElement: ElementRef | 40 | @ViewChild('textarea') textareaElement: ElementRef |
41 | @ViewChild('previewElement') previewElement: ElementRef | ||
42 | |||
43 | truncatedPreviewHTML: SafeHtml | string = '' | ||
44 | previewHTML: SafeHtml | string = '' | ||
33 | 45 | ||
34 | truncatedPreviewHTML = '' | ||
35 | previewHTML = '' | ||
36 | isMaximized = false | 46 | isMaximized = false |
37 | 47 | ||
38 | maximizeInText = $localize`Maximize editor` | 48 | maximizeInText = $localize`Maximize editor` |
@@ -115,10 +125,31 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
115 | } | 125 | } |
116 | 126 | ||
117 | private async markdownRender (text: string) { | 127 | private async markdownRender (text: string) { |
118 | const html = this.markdownType === 'text' ? | 128 | let html: string |
119 | await this.markdownService.textMarkdownToHTML(text) : | 129 | |
120 | await this.markdownService.enhancedMarkdownToHTML(text) | 130 | if (this.customMarkdownRenderer) { |
131 | const result = await this.customMarkdownRenderer(text) | ||
132 | |||
133 | if (result instanceof HTMLElement) { | ||
134 | html = '' | ||
135 | |||
136 | const wrapperElement = this.previewElement.nativeElement as HTMLElement | ||
137 | wrapperElement.innerHTML = '' | ||
138 | wrapperElement.appendChild(result) | ||
139 | return | ||
140 | } | ||
141 | |||
142 | html = result | ||
143 | } else if (this.markdownType === 'text') { | ||
144 | html = await this.markdownService.textMarkdownToHTML(text) | ||
145 | } else { | ||
146 | html = await this.markdownService.enhancedMarkdownToHTML(text) | ||
147 | } | ||
148 | |||
149 | if (this.markdownVideo) { | ||
150 | html = this.markdownService.processVideoTimestamps(html) | ||
151 | } | ||
121 | 152 | ||
122 | return this.markdownVideo ? this.markdownService.processVideoTimestamps(html) : html | 153 | return html |
123 | } | 154 | } |
124 | } | 155 | } |
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..a47f07fc3 100644 --- a/client/src/app/shared/shared-icons/global-icon.component.ts +++ b/client/src/app/shared/shared-icons/global-icon.component.ts | |||
@@ -17,6 +17,7 @@ const icons = { | |||
17 | 'follower': require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui | 17 | 'follower': require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui |
18 | 'following': require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui | 18 | 'following': require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui |
19 | 'flame': require('!!raw-loader?!../../../assets/images/misc/flame.svg').default, | 19 | 'flame': require('!!raw-loader?!../../../assets/images/misc/flame.svg').default, |
20 | 'local': require('!!raw-loader?!../../../assets/images/misc/local.svg').default, | ||
20 | 21 | ||
21 | // feather icons | 22 | // feather icons |
22 | 'flag': require('!!raw-loader?!../../../assets/images/feather/flag.svg').default, | 23 | 'flag': require('!!raw-loader?!../../../assets/images/feather/flag.svg').default, |
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts index 5e7832807..d62c1f88e 100644 --- a/client/src/app/shared/shared-main/angular/from-now.pipe.ts +++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts | |||
@@ -3,32 +3,37 @@ import { Pipe, PipeTransform } from '@angular/core' | |||
3 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site | 3 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site |
4 | @Pipe({ name: 'myFromNow' }) | 4 | @Pipe({ name: 'myFromNow' }) |
5 | export class FromNowPipe implements PipeTransform { | 5 | export class FromNowPipe implements PipeTransform { |
6 | |||
7 | transform (arg: number | Date | string) { | 6 | transform (arg: number | Date | string) { |
8 | const argDate = new Date(arg) | 7 | const argDate = new Date(arg) |
9 | const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) | 8 | const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) |
10 | 9 | ||
11 | let interval = Math.round(seconds / 31536000) | 10 | let interval = Math.floor(seconds / 31536000) |
12 | if (interval > 1) return $localize`${interval} years ago` | 11 | if (interval > 1) return $localize`${interval} years ago` |
13 | if (interval === 1) return $localize`${interval} year ago` | 12 | if (interval === 1) return $localize`1 year ago` |
14 | 13 | ||
15 | interval = Math.round(seconds / 2592000) | 14 | interval = Math.floor(seconds / 2419200) |
15 | // 12 months = 360 days, but a year ~ 365 days | ||
16 | // Display "1 year ago" rather than "12 months ago" | ||
17 | if (interval >= 12) return $localize`1 year ago` | ||
16 | if (interval > 1) return $localize`${interval} months ago` | 18 | if (interval > 1) return $localize`${interval} months ago` |
17 | if (interval === 1) return $localize`${interval} month ago` | 19 | if (interval === 1) return $localize`1 month ago` |
18 | 20 | ||
19 | interval = Math.round(seconds / 604800) | 21 | interval = Math.floor(seconds / 604800) |
22 | // 4 weeks ~ 28 days, but our month is 30 days | ||
23 | // Display "1 month ago" rather than "4 weeks ago" | ||
24 | if (interval >= 4) return $localize`1 month ago` | ||
20 | if (interval > 1) return $localize`${interval} weeks ago` | 25 | if (interval > 1) return $localize`${interval} weeks ago` |
21 | if (interval === 1) return $localize`${interval} week ago` | 26 | if (interval === 1) return $localize`1 week ago` |
22 | 27 | ||
23 | interval = Math.round(seconds / 86400) | 28 | interval = Math.floor(seconds / 86400) |
24 | if (interval > 1) return $localize`${interval} days ago` | 29 | if (interval > 1) return $localize`${interval} days ago` |
25 | if (interval === 1) return $localize`${interval} day ago` | 30 | if (interval === 1) return $localize`1 day ago` |
26 | 31 | ||
27 | interval = Math.round(seconds / 3600) | 32 | interval = Math.floor(seconds / 3600) |
28 | if (interval > 1) return $localize`${interval} hours ago` | 33 | if (interval > 1) return $localize`${interval} hours ago` |
29 | if (interval === 1) return $localize`${interval} hour ago` | 34 | if (interval === 1) return $localize`1 hour ago` |
30 | 35 | ||
31 | interval = Math.round(seconds / 60) | 36 | interval = Math.floor(seconds / 60) |
32 | if (interval >= 1) return $localize`${interval} min ago` | 37 | if (interval >= 1) return $localize`${interval} min ago` |
33 | 38 | ||
34 | return $localize`just now` | 39 | return $localize`just now` |
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 @@ | |||
1 | import { of } from 'rxjs' | ||
2 | import { catchError, map } from 'rxjs/operators' | ||
3 | import { HttpClient } from '@angular/common/http' | ||
4 | import { Injectable } from '@angular/core' | ||
5 | import { RestExtractor } from '@app/core' | ||
6 | import { CustomPage } from '@shared/models' | ||
7 | import { environment } from '../../../../environments/environment' | ||
8 | |||
9 | @Injectable() | ||
10 | export class CustomPageService { | ||
11 | static BASE_INSTANCE_HOMEPAGE_URL = environment.apiUrl + '/api/v1/custom-pages/homepage/instance' | ||
12 | |||
13 | constructor ( | ||
14 | private authHttp: HttpClient, | ||
15 | private restExtractor: RestExtractor | ||
16 | ) { } | ||
17 | |||
18 | getInstanceHomepage () { | ||
19 | return this.authHttp.get<CustomPage>(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL) | ||
20 | .pipe( | ||
21 | catchError(err => { | ||
22 | if (err.status === 404) { | ||
23 | return of({ content: '' }) | ||
24 | } | ||
25 | |||
26 | this.restExtractor.handleError(err) | ||
27 | }) | ||
28 | ) | ||
29 | } | ||
30 | |||
31 | updateInstanceHomepage (content: string) { | ||
32 | return this.authHttp.put(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL, { content }) | ||
33 | .pipe( | ||
34 | map(this.restExtractor.extractDataBool), | ||
35 | catchError(err => this.restExtractor.handleError(err)) | ||
36 | ) | ||
37 | } | ||
38 | } | ||
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/index.ts b/client/src/app/shared/shared-main/index.ts index a4d813c06..3a7fd4c34 100644 --- a/client/src/app/shared/shared-main/index.ts +++ b/client/src/app/shared/shared-main/index.ts | |||
@@ -5,6 +5,9 @@ export * from './date' | |||
5 | export * from './feeds' | 5 | export * from './feeds' |
6 | export * from './loaders' | 6 | export * from './loaders' |
7 | export * from './misc' | 7 | export * from './misc' |
8 | export * from './peertube-modal' | ||
9 | export * from './plugins' | ||
10 | export * from './router' | ||
8 | export * from './users' | 11 | export * from './users' |
9 | export * from './video' | 12 | export * from './video' |
10 | export * from './video-caption' | 13 | export * from './video-caption' |
diff --git a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.scss b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.scss new file mode 100644 index 000000000..4e37c5e61 --- /dev/null +++ b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.scss | |||
@@ -0,0 +1,3 @@ | |||
1 | div { | ||
2 | height: 100%; | ||
3 | } | ||
diff --git a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts index 4d5381e8d..858eff9ba 100644 --- a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts +++ b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts | |||
@@ -4,7 +4,7 @@ import { PluginElementPlaceholder } from '@shared/models' | |||
4 | @Component({ | 4 | @Component({ |
5 | selector: 'my-plugin-placeholder', | 5 | selector: 'my-plugin-placeholder', |
6 | template: '<div [id]="getId()"></div>', | 6 | template: '<div [id]="getId()"></div>', |
7 | styles: [ 'div { height: 100%; }' ] | 7 | styleUrls: [ './plugin-placeholder.component.scss' ] |
8 | }) | 8 | }) |
9 | 9 | ||
10 | export class PluginPlaceholderComponent { | 10 | export class PluginPlaceholderComponent { |
diff --git a/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts b/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts new file mode 100644 index 000000000..49d61f945 --- /dev/null +++ b/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import { forkJoin, of } from 'rxjs' | ||
2 | import { catchError, map } from 'rxjs/operators' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router' | ||
5 | import { AccountService } from '../account' | ||
6 | import { VideoChannelService } from '../video-channel' | ||
7 | |||
8 | @Injectable() | ||
9 | export class ActorRedirectGuard implements CanActivate { | ||
10 | |||
11 | constructor ( | ||
12 | private router: Router, | ||
13 | private accountService: AccountService, | ||
14 | private channelService: VideoChannelService | ||
15 | ) {} | ||
16 | |||
17 | canActivate (route: ActivatedRouteSnapshot) { | ||
18 | const actorName = route.params.actorName | ||
19 | |||
20 | return forkJoin([ | ||
21 | this.accountService.getAccount(actorName).pipe(this.orUndefined()), | ||
22 | this.channelService.getVideoChannel(actorName).pipe(this.orUndefined()) | ||
23 | ]).pipe( | ||
24 | map(([ account, channel ]) => { | ||
25 | if (!account && !channel) { | ||
26 | this.router.navigate([ '/404' ]) | ||
27 | return false | ||
28 | } | ||
29 | |||
30 | if (account) { | ||
31 | this.router.navigate([ `/a/${actorName}` ], { skipLocationChange: true }) | ||
32 | } | ||
33 | |||
34 | if (channel) { | ||
35 | this.router.navigate([ `/c/${actorName}` ], { skipLocationChange: true }) | ||
36 | } | ||
37 | |||
38 | return true | ||
39 | }) | ||
40 | ) | ||
41 | } | ||
42 | |||
43 | private orUndefined () { | ||
44 | return catchError(() => of(undefined)) | ||
45 | } | ||
46 | } | ||
diff --git a/client/src/app/shared/shared-main/router/index.ts b/client/src/app/shared/shared-main/router/index.ts new file mode 100644 index 000000000..f4000b674 --- /dev/null +++ b/client/src/app/shared/shared-main/router/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './actor-redirect-guard.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..c8dd01429 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -4,7 +4,7 @@ import { CommonModule, DatePipe } from '@angular/common' | |||
4 | import { HttpClientModule } from '@angular/common/http' | 4 | import { HttpClientModule } from '@angular/common/http' |
5 | import { NgModule } from '@angular/core' | 5 | import { NgModule } from '@angular/core' |
6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' | 6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' |
7 | import { RouterModule } from '@angular/router' | 7 | import { ActivatedRouteSnapshot, RouterModule } from '@angular/router' |
8 | import { | 8 | import { |
9 | NgbButtonsModule, | 9 | NgbButtonsModule, |
10 | NgbCollapseModule, | 10 | NgbCollapseModule, |
@@ -29,6 +29,7 @@ import { | |||
29 | } from './angular' | 29 | } from './angular' |
30 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | 30 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' |
31 | import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' | 31 | import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' |
32 | import { CustomPageService } from './custom-page' | ||
32 | import { DateToggleComponent } from './date' | 33 | import { DateToggleComponent } from './date' |
33 | import { FeedComponent } from './feeds' | 34 | import { FeedComponent } from './feeds' |
34 | import { LoaderComponent, SmallLoaderComponent } from './loaders' | 35 | import { LoaderComponent, SmallLoaderComponent } from './loaders' |
@@ -38,6 +39,7 @@ import { UserHistoryService, UserNotificationsComponent, UserNotificationService | |||
38 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' | 39 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' |
39 | import { VideoCaptionService } from './video-caption' | 40 | import { VideoCaptionService } from './video-caption' |
40 | import { VideoChannelService } from './video-channel' | 41 | import { VideoChannelService } from './video-channel' |
42 | import { ActorRedirectGuard } from './router' | ||
41 | 43 | ||
42 | @NgModule({ | 44 | @NgModule({ |
43 | imports: [ | 45 | imports: [ |
@@ -171,7 +173,11 @@ import { VideoChannelService } from './video-channel' | |||
171 | 173 | ||
172 | VideoCaptionService, | 174 | VideoCaptionService, |
173 | 175 | ||
174 | VideoChannelService | 176 | VideoChannelService, |
177 | |||
178 | CustomPageService, | ||
179 | |||
180 | ActorRedirectGuard | ||
175 | ] | 181 | ] |
176 | }) | 182 | }) |
177 | export class SharedMainModule { } | 183 | export class SharedMainModule { } |
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index ed5791794..c80bc13b0 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts | |||
@@ -238,11 +238,11 @@ export class UserNotification implements UserNotificationServer { | |||
238 | } | 238 | } |
239 | 239 | ||
240 | private buildVideoUrl (video: { uuid: string }) { | 240 | private buildVideoUrl (video: { uuid: string }) { |
241 | return '/videos/watch/' + video.uuid | 241 | return '/w/' + video.uuid |
242 | } | 242 | } |
243 | 243 | ||
244 | private buildAccountUrl (account: { name: string, host: string }) { | 244 | private buildAccountUrl (account: { name: string, host: string }) { |
245 | return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host) | 245 | return '/a/' + Actor.CREATE_BY_STRING(account.name, account.host) |
246 | } | 246 | } |
247 | 247 | ||
248 | private buildVideoImportUrl () { | 248 | private buildVideoImportUrl () { |
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index 526d10e32..e7f739bfe 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -88,7 +88,7 @@ export class Video implements VideoServerModel { | |||
88 | pluginData?: any | 88 | pluginData?: any |
89 | 89 | ||
90 | static buildClientUrl (videoUUID: string) { | 90 | static buildClientUrl (videoUUID: string) { |
91 | return '/videos/watch/' + videoUUID | 91 | return '/w/' + videoUUID |
92 | } | 92 | } |
93 | 93 | ||
94 | constructor (hash: VideoServerModel, translations = {}) { | 94 | constructor (hash: VideoServerModel, translations = {}) { |
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts index e8760bfcc..2a73e6166 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.ts +++ b/client/src/app/shared/shared-share-modal/video-share.component.ts | |||
@@ -98,14 +98,14 @@ export class VideoShareComponent { | |||
98 | 98 | ||
99 | getVideoUrl () { | 99 | getVideoUrl () { |
100 | let baseUrl = this.customizations.originUrl ? this.video.originInstanceUrl : window.location.origin | 100 | let baseUrl = this.customizations.originUrl ? this.video.originInstanceUrl : window.location.origin |
101 | baseUrl += '/videos/watch/' + this.video.uuid | 101 | baseUrl += '/w/' + this.video.uuid |
102 | const options = this.getVideoOptions(baseUrl) | 102 | const options = this.getVideoOptions(baseUrl) |
103 | 103 | ||
104 | return buildVideoLink(options) | 104 | return buildVideoLink(options) |
105 | } | 105 | } |
106 | 106 | ||
107 | getPlaylistUrl () { | 107 | getPlaylistUrl () { |
108 | const base = window.location.origin + '/videos/watch/playlist/' + this.playlist.uuid | 108 | const base = window.location.origin + '/w/p/' + this.playlist.uuid |
109 | 109 | ||
110 | if (!this.includeVideoInPlaylist) return base | 110 | if (!this.includeVideoInPlaylist) return base |
111 | 111 | ||
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts index bdede17a3..d5583c29f 100644 --- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts +++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts | |||
@@ -57,7 +57,7 @@ export class VideoThumbnailComponent { | |||
57 | getVideoRouterLink () { | 57 | getVideoRouterLink () { |
58 | if (this.videoRouterLink) return this.videoRouterLink | 58 | if (this.videoRouterLink) return this.videoRouterLink |
59 | 59 | ||
60 | return [ '/videos/watch', this.video.uuid ] | 60 | return [ '/w', this.video.uuid ] |
61 | } | 61 | } |
62 | 62 | ||
63 | onWatchLaterClick (event: Event) { | 63 | onWatchLaterClick (event: Event) { |
diff --git a/client/src/app/shared/shared-video-comment/video-comment.model.ts b/client/src/app/shared/shared-video-comment/video-comment.model.ts index 9a4e3954e..94d6c5fa8 100644 --- a/client/src/app/shared/shared-video-comment/video-comment.model.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.model.ts | |||
@@ -85,7 +85,7 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel { | |||
85 | id: hash.video.id, | 85 | id: hash.video.id, |
86 | uuid: hash.video.uuid, | 86 | uuid: hash.video.uuid, |
87 | name: hash.video.name, | 87 | name: hash.video.name, |
88 | localUrl: '/videos/watch/' + hash.video.uuid | 88 | localUrl: '/w/' + hash.video.uuid |
89 | } | 89 | } |
90 | 90 | ||
91 | this.localUrl = this.video.localUrl + ';threadId=' + this.threadId | 91 | this.localUrl = this.video.localUrl + ';threadId=' + this.threadId |
@@ -95,7 +95,7 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel { | |||
95 | if (this.account) { | 95 | if (this.account) { |
96 | this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) | 96 | this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) |
97 | 97 | ||
98 | this.account.localUrl = '/accounts/' + this.by | 98 | this.account.localUrl = '/a/' + this.by |
99 | } | 99 | } |
100 | } | 100 | } |
101 | } | 101 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index 645be92bd..6c34123ed 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html | |||
@@ -12,12 +12,12 @@ | |||
12 | <div class="d-flex video-miniature-meta"> | 12 | <div class="d-flex video-miniature-meta"> |
13 | <my-actor-avatar | 13 | <my-actor-avatar |
14 | *ngIf="displayOptions.avatar && displayOwnerVideoChannel()" [title]="channelLinkTitle" | 14 | *ngIf="displayOptions.avatar && displayOwnerVideoChannel()" [title]="channelLinkTitle" |
15 | [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]" | 15 | [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" |
16 | ></my-actor-avatar> | 16 | ></my-actor-avatar> |
17 | 17 | ||
18 | <my-actor-avatar | 18 | <my-actor-avatar |
19 | *ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle" | 19 | *ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle" |
20 | [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]" | 20 | [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]" |
21 | ></my-actor-avatar> | 21 | ></my-actor-avatar> |
22 | 22 | ||
23 | <div class="w-100 d-flex flex-column"> | 23 | <div class="w-100 d-flex flex-column"> |
@@ -39,10 +39,10 @@ | |||
39 | </span> | 39 | </span> |
40 | </span> | 40 | </span> |
41 | 41 | ||
42 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> | 42 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/c', video.byVideoChannel ]"> |
43 | {{ video.byAccount }} | 43 | {{ video.byAccount }} |
44 | </a> | 44 | </a> |
45 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> | 45 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/c', video.byVideoChannel ]"> |
46 | {{ video.byVideoChannel }} | 46 | {{ video.byVideoChannel }} |
47 | </a> | 47 | </a> |
48 | 48 | ||
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index b58c118be..aac55a6e9 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -125,7 +125,7 @@ export class VideoMiniatureComponent implements OnInit { | |||
125 | 125 | ||
126 | buildVideoLink () { | 126 | buildVideoLink () { |
127 | if (this.videoLinkType === 'internal' || !this.video.url) { | 127 | if (this.videoLinkType === 'internal' || !this.video.url) { |
128 | this.videoRouterLink = [ '/videos/watch', this.video.uuid ] | 128 | this.videoRouterLink = [ '/w', this.video.uuid ] |
129 | return | 129 | return |
130 | } | 130 | } |
131 | 131 | ||
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html index ec004a407..e74f58f47 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html +++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html | |||
@@ -20,7 +20,7 @@ | |||
20 | [attr.title]="playlistElement.video.name" | 20 | [attr.title]="playlistElement.video.name" |
21 | >{{ playlistElement.video.name }}</a> | 21 | >{{ playlistElement.video.name }}</a> |
22 | 22 | ||
23 | <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', playlistElement.video.byAccount ]"> | 23 | <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/a', playlistElement.video.byAccount ]"> |
24 | {{ playlistElement.video.byAccount }} | 24 | {{ playlistElement.video.byAccount }} |
25 | </a> | 25 | </a> |
26 | <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span> | 26 | <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span> |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts index 7c083ae26..86c281a1e 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts | |||
@@ -71,7 +71,7 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit { | |||
71 | buildRouterLink () { | 71 | buildRouterLink () { |
72 | if (!this.playlist) return null | 72 | if (!this.playlist) return null |
73 | 73 | ||
74 | return [ '/videos/watch/playlist', this.playlist.uuid ] | 74 | return [ '/w/p', this.playlist.uuid ] |
75 | } | 75 | } |
76 | 76 | ||
77 | buildRouterQuery () { | 77 | buildRouterQuery () { |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html index f50f95003..81c36e6fe 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html +++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html | |||
@@ -19,7 +19,7 @@ | |||
19 | {{ playlist.displayName }} | 19 | {{ playlist.displayName }} |
20 | </a> | 20 | </a> |
21 | 21 | ||
22 | <a i18n [routerLink]="[ '/video-channels', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy"> | 22 | <a i18n [routerLink]="[ '/c', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy"> |
23 | {{ playlist.videoChannelBy }} | 23 | {{ playlist.videoChannelBy }} |
24 | </a> | 24 | </a> |
25 | 25 | ||
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts index 6b0b1056f..9bbec6038 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts | |||
@@ -18,6 +18,6 @@ export class VideoPlaylistMiniatureComponent { | |||
18 | if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] | 18 | if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] |
19 | if (this.playlist.videosLength === 0) return null | 19 | if (this.playlist.videosLength === 0) return null |
20 | 20 | ||
21 | return [ '/videos/watch/playlist', this.playlist.uuid ] | 21 | return [ '/w/p', this.playlist.uuid ] |
22 | } | 22 | } |
23 | } | 23 | } |