diff options
Diffstat (limited to 'client/src/app/shared')
34 files changed, 637 insertions, 37 deletions
diff --git a/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts b/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts index 8c12d3c4c..08372d8ad 100644 --- a/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts +++ b/client/src/app/shared/shared-actor-image-edit/actor-banner-edit.component.ts | |||
@@ -42,7 +42,7 @@ export class ActorBannerEditComponent implements OnInit { | |||
42 | this.bannerExtensions = config.banner.file.extensions.join(', ') | 42 | this.bannerExtensions = config.banner.file.extensions.join(', ') |
43 | 43 | ||
44 | // tslint:disable:max-line-length | 44 | // tslint:disable:max-line-length |
45 | this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}` | 45 | this.bannerFormat = $localize`ratio 6/1, recommended size: 1920x317, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}` |
46 | }) | 46 | }) |
47 | } | 47 | } |
48 | 48 | ||
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..ffaf15710 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts | |||
@@ -0,0 +1,136 @@ | |||
1 | import { ComponentRef, Injectable } from '@angular/core' | ||
2 | import { MarkdownService } from '@app/core' | ||
3 | import { | ||
4 | ChannelMiniatureMarkupData, | ||
5 | EmbedMarkupData, | ||
6 | PlaylistMiniatureMarkupData, | ||
7 | VideoMiniatureMarkupData, | ||
8 | VideosListMarkupData | ||
9 | } from '@shared/models' | ||
10 | import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component' | ||
11 | import { DynamicElementService } from './dynamic-element.service' | ||
12 | import { EmbedMarkupComponent } from './embed-markup.component' | ||
13 | import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component' | ||
14 | import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component' | ||
15 | import { VideosListMarkupComponent } from './videos-list-markup.component' | ||
16 | |||
17 | type BuilderFunction = (el: HTMLElement) => ComponentRef<any> | ||
18 | |||
19 | @Injectable() | ||
20 | export class CustomMarkupService { | ||
21 | private builders: { [ selector: string ]: BuilderFunction } = { | ||
22 | 'peertube-video-embed': el => this.embedBuilder(el, 'video'), | ||
23 | 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'), | ||
24 | 'peertube-video-miniature': el => this.videoMiniatureBuilder(el), | ||
25 | 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el), | ||
26 | 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el), | ||
27 | 'peertube-videos-list': el => this.videosListBuilder(el) | ||
28 | } | ||
29 | |||
30 | constructor ( | ||
31 | private dynamicElementService: DynamicElementService, | ||
32 | private markdown: MarkdownService | ||
33 | ) { } | ||
34 | |||
35 | async buildElement (text: string) { | ||
36 | const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags()) | ||
37 | |||
38 | const rootElement = document.createElement('div') | ||
39 | rootElement.innerHTML = html | ||
40 | |||
41 | for (const selector of this.getSupportedTags()) { | ||
42 | rootElement.querySelectorAll(selector) | ||
43 | .forEach((e: HTMLElement) => { | ||
44 | try { | ||
45 | const component = this.execBuilder(selector, e) | ||
46 | |||
47 | this.dynamicElementService.injectElement(e, component) | ||
48 | } catch (err) { | ||
49 | console.error('Cannot inject component %s.', selector, err) | ||
50 | } | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | return rootElement | ||
55 | } | ||
56 | |||
57 | private getSupportedTags () { | ||
58 | return Object.keys(this.builders) | ||
59 | } | ||
60 | |||
61 | private execBuilder (selector: string, el: HTMLElement) { | ||
62 | return this.builders[selector](el) | ||
63 | } | ||
64 | |||
65 | private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') { | ||
66 | const data = el.dataset as EmbedMarkupData | ||
67 | const component = this.dynamicElementService.createElement(EmbedMarkupComponent) | ||
68 | |||
69 | this.dynamicElementService.setModel(component, { uuid: data.uuid, type }) | ||
70 | |||
71 | return component | ||
72 | } | ||
73 | |||
74 | private videoMiniatureBuilder (el: HTMLElement) { | ||
75 | const data = el.dataset as VideoMiniatureMarkupData | ||
76 | const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent) | ||
77 | |||
78 | this.dynamicElementService.setModel(component, { uuid: data.uuid }) | ||
79 | |||
80 | return component | ||
81 | } | ||
82 | |||
83 | private playlistMiniatureBuilder (el: HTMLElement) { | ||
84 | const data = el.dataset as PlaylistMiniatureMarkupData | ||
85 | const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent) | ||
86 | |||
87 | this.dynamicElementService.setModel(component, { uuid: data.uuid }) | ||
88 | |||
89 | return component | ||
90 | } | ||
91 | |||
92 | private channelMiniatureBuilder (el: HTMLElement) { | ||
93 | const data = el.dataset as ChannelMiniatureMarkupData | ||
94 | const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent) | ||
95 | |||
96 | this.dynamicElementService.setModel(component, { name: data.name }) | ||
97 | |||
98 | return component | ||
99 | } | ||
100 | |||
101 | private videosListBuilder (el: HTMLElement) { | ||
102 | const data = el.dataset as VideosListMarkupData | ||
103 | const component = this.dynamicElementService.createElement(VideosListMarkupComponent) | ||
104 | |||
105 | const model = { | ||
106 | title: data.title, | ||
107 | description: data.description, | ||
108 | sort: data.sort, | ||
109 | categoryOneOf: this.buildArrayNumber(data.categoryOneOf), | ||
110 | languageOneOf: this.buildArrayString(data.languageOneOf), | ||
111 | count: this.buildNumber(data.count) || 10 | ||
112 | } | ||
113 | |||
114 | this.dynamicElementService.setModel(component, model) | ||
115 | |||
116 | return component | ||
117 | } | ||
118 | |||
119 | private buildNumber (value: string) { | ||
120 | if (!value) return undefined | ||
121 | |||
122 | return parseInt(value, 10) | ||
123 | } | ||
124 | |||
125 | private buildArrayNumber (value: string) { | ||
126 | if (!value) return undefined | ||
127 | |||
128 | return value.split(',').map(v => parseInt(v, 10)) | ||
129 | } | ||
130 | |||
131 | private buildArrayString (value: string) { | ||
132 | if (!value) return undefined | ||
133 | |||
134 | return value.split(',') | ||
135 | } | ||
136 | } | ||
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..4bbb71588 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts | |||
@@ -0,0 +1,49 @@ | |||
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 { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component' | ||
10 | import { CustomMarkupService } from './custom-markup.service' | ||
11 | import { DynamicElementService } from './dynamic-element.service' | ||
12 | import { EmbedMarkupComponent } from './embed-markup.component' | ||
13 | import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component' | ||
14 | import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component' | ||
15 | import { VideosListMarkupComponent } from './videos-list-markup.component' | ||
16 | |||
17 | @NgModule({ | ||
18 | imports: [ | ||
19 | CommonModule, | ||
20 | |||
21 | SharedMainModule, | ||
22 | SharedGlobalIconModule, | ||
23 | SharedVideoMiniatureModule, | ||
24 | SharedVideoPlaylistModule, | ||
25 | SharedActorImageModule | ||
26 | ], | ||
27 | |||
28 | declarations: [ | ||
29 | VideoMiniatureMarkupComponent, | ||
30 | PlaylistMiniatureMarkupComponent, | ||
31 | ChannelMiniatureMarkupComponent, | ||
32 | EmbedMarkupComponent, | ||
33 | VideosListMarkupComponent | ||
34 | ], | ||
35 | |||
36 | exports: [ | ||
37 | VideoMiniatureMarkupComponent, | ||
38 | PlaylistMiniatureMarkupComponent, | ||
39 | ChannelMiniatureMarkupComponent, | ||
40 | VideosListMarkupComponent, | ||
41 | EmbedMarkupComponent | ||
42 | ], | ||
43 | |||
44 | providers: [ | ||
45 | CustomMarkupService, | ||
46 | DynamicElementService | ||
47 | ] | ||
48 | }) | ||
49 | 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..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 = { | |||
72 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, | 72 | 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, |
73 | 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, | 73 | 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, |
74 | 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default, | 74 | 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default, |
75 | 'octagon': require('!!raw-loader?!../../../assets/images/feather/octagon.svg').default, | ||
75 | 'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default | 76 | 'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default |
76 | } | 77 | } |
77 | 78 | ||
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts index 6d9f0ee65..7b5611f35 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts | |||
@@ -4,8 +4,12 @@ import { Actor } from './actor.model' | |||
4 | export class Account extends Actor implements ServerAccount { | 4 | export class Account extends Actor implements ServerAccount { |
5 | displayName: string | 5 | displayName: string |
6 | description: string | 6 | description: string |
7 | |||
8 | updatedAt: Date | string | ||
9 | |||
7 | nameWithHost: string | 10 | nameWithHost: string |
8 | nameWithHostForced: string | 11 | nameWithHostForced: string |
12 | |||
9 | mutedByUser: boolean | 13 | mutedByUser: boolean |
10 | mutedByInstance: boolean | 14 | mutedByInstance: boolean |
11 | mutedServerByUser: boolean | 15 | mutedServerByUser: boolean |
@@ -30,6 +34,8 @@ export class Account extends Actor implements ServerAccount { | |||
30 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) | 34 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) |
31 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) | 35 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) |
32 | 36 | ||
37 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
38 | |||
33 | this.mutedByUser = false | 39 | this.mutedByUser = false |
34 | this.mutedByInstance = false | 40 | this.mutedByInstance = false |
35 | this.mutedServerByUser = false | 41 | this.mutedServerByUser = false |
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 6ba0bb09e..2fccc472a 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -12,7 +12,6 @@ export abstract class Actor implements ServerActor { | |||
12 | followersCount: number | 12 | followersCount: number |
13 | 13 | ||
14 | createdAt: Date | string | 14 | createdAt: Date | string |
15 | updatedAt: Date | string | ||
16 | 15 | ||
17 | avatar: ActorImage | 16 | avatar: ActorImage |
18 | 17 | ||
@@ -55,7 +54,6 @@ export abstract class Actor implements ServerActor { | |||
55 | this.followersCount = hash.followersCount | 54 | this.followersCount = hash.followersCount |
56 | 55 | ||
57 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) | 56 | if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) |
58 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
59 | 57 | ||
60 | this.avatar = hash.avatar | 58 | this.avatar = hash.avatar |
61 | this.isLocal = Actor.IS_LOCAL(this.host) | 59 | this.isLocal = Actor.IS_LOCAL(this.host) |
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/buttons/button.component.scss b/client/src/app/shared/shared-main/buttons/button.component.scss index 09b5f95d7..22b24c853 100644 --- a/client/src/app/shared/shared-main/buttons/button.component.scss +++ b/client/src/app/shared/shared-main/buttons/button.component.scss | |||
@@ -30,7 +30,7 @@ span[class$=-button] { | |||
30 | 30 | ||
31 | .action-button { | 31 | .action-button { |
32 | @include peertube-button-link; | 32 | @include peertube-button-link; |
33 | @include button-with-icon(21px, 0, -1px); | 33 | @include button-with-icon(21px); |
34 | } | 34 | } |
35 | 35 | ||
36 | .orange-button { | 36 | .orange-button { |
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/plugins/plugin-placeholder.component.ts b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts index 93ba9fb9b..4d5381e8d 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 | |||
@@ -3,7 +3,8 @@ import { PluginElementPlaceholder } from '@shared/models' | |||
3 | 3 | ||
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 | }) | 8 | }) |
8 | 9 | ||
9 | export class PluginPlaceholderComponent { | 10 | export class PluginPlaceholderComponent { |
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 05a5d77c7..f06f25ca5 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 { | |||
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' |
@@ -172,7 +173,9 @@ import { VideoChannelService } from './video-channel' | |||
172 | 173 | ||
173 | VideoCaptionService, | 174 | VideoCaptionService, |
174 | 175 | ||
175 | VideoChannelService | 176 | VideoChannelService, |
177 | |||
178 | CustomPageService | ||
176 | ] | 179 | ] |
177 | }) | 180 | }) |
178 | export class SharedMainModule { } | 181 | export class SharedMainModule { } |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts index c40dd5311..a9dcf2fa2 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts | |||
@@ -16,6 +16,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
16 | banner: ActorImage | 16 | banner: ActorImage |
17 | bannerUrl: string | 17 | bannerUrl: string |
18 | 18 | ||
19 | updatedAt: Date | string | ||
20 | |||
19 | ownerAccount?: ServerAccount | 21 | ownerAccount?: ServerAccount |
20 | ownerBy?: string | 22 | ownerBy?: string |
21 | 23 | ||
@@ -59,6 +61,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
59 | 61 | ||
60 | this.videosCount = hash.videosCount | 62 | this.videosCount = hash.videosCount |
61 | 63 | ||
64 | if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) | ||
65 | |||
62 | if (hash.viewsPerDay) { | 66 | if (hash.viewsPerDay) { |
63 | this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) | 67 | this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) |
64 | } | 68 | } |
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index e65261763..a89f1065a 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts | |||
@@ -40,23 +40,24 @@ export class VideoChannelService { | |||
40 | ) | 40 | ) |
41 | } | 41 | } |
42 | 42 | ||
43 | listAccountVideoChannels ( | 43 | listAccountVideoChannels (options: { |
44 | account: Account, | 44 | account: Account |
45 | componentPagination?: ComponentPaginationLight, | 45 | componentPagination?: ComponentPaginationLight |
46 | withStats = false, | 46 | withStats?: boolean |
47 | sort?: string | ||
47 | search?: string | 48 | search?: string |
48 | ): Observable<ResultList<VideoChannel>> { | 49 | }): Observable<ResultList<VideoChannel>> { |
50 | const { account, componentPagination, withStats = false, sort, search } = options | ||
51 | |||
49 | const pagination = componentPagination | 52 | const pagination = componentPagination |
50 | ? this.restService.componentPaginationToRestPagination(componentPagination) | 53 | ? this.restService.componentPaginationToRestPagination(componentPagination) |
51 | : { start: 0, count: 20 } | 54 | : { start: 0, count: 20 } |
52 | 55 | ||
53 | let params = new HttpParams() | 56 | let params = new HttpParams() |
54 | params = this.restService.addRestGetParams(params, pagination) | 57 | params = this.restService.addRestGetParams(params, pagination, sort) |
55 | params = params.set('withStats', withStats + '') | 58 | params = params.set('withStats', withStats + '') |
56 | 59 | ||
57 | if (search) { | 60 | if (search) params = params.set('search', search) |
58 | params = params.set('search', search) | ||
59 | } | ||
60 | 61 | ||
61 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' | 62 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' |
62 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) | 63 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) |
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts index 0e3924841..2c83f53b6 100644 --- a/client/src/app/shared/shared-search/advanced-search.model.ts +++ b/client/src/app/shared/shared-search/advanced-search.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { BooleanBothQuery, SearchTargetType } from '@shared/models' | 1 | import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models' |
2 | 2 | ||
3 | export class AdvancedSearch { | 3 | export class AdvancedSearch { |
4 | startDate: string // ISO 8601 | 4 | startDate: string // ISO 8601 |
@@ -21,6 +21,8 @@ export class AdvancedSearch { | |||
21 | durationMin: number // seconds | 21 | durationMin: number // seconds |
22 | durationMax: number // seconds | 22 | durationMax: number // seconds |
23 | 23 | ||
24 | isLive: BooleanQuery | ||
25 | |||
24 | sort: string | 26 | sort: string |
25 | 27 | ||
26 | searchTarget: SearchTargetType | 28 | searchTarget: SearchTargetType |
@@ -41,6 +43,8 @@ export class AdvancedSearch { | |||
41 | tagsOneOf?: any | 43 | tagsOneOf?: any |
42 | tagsAllOf?: any | 44 | tagsAllOf?: any |
43 | 45 | ||
46 | isLive?: BooleanQuery | ||
47 | |||
44 | durationMin?: string | 48 | durationMin?: string |
45 | durationMax?: string | 49 | durationMax?: string |
46 | sort?: string | 50 | sort?: string |
@@ -54,6 +58,8 @@ export class AdvancedSearch { | |||
54 | this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined | 58 | this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined |
55 | 59 | ||
56 | this.nsfw = options.nsfw || undefined | 60 | this.nsfw = options.nsfw || undefined |
61 | this.isLive = options.isLive || undefined | ||
62 | |||
57 | this.categoryOneOf = options.categoryOneOf || undefined | 63 | this.categoryOneOf = options.categoryOneOf || undefined |
58 | this.licenceOneOf = options.licenceOneOf || undefined | 64 | this.licenceOneOf = options.licenceOneOf || undefined |
59 | this.languageOneOf = options.languageOneOf || undefined | 65 | this.languageOneOf = options.languageOneOf || undefined |
@@ -94,6 +100,7 @@ export class AdvancedSearch { | |||
94 | this.tagsAllOf = undefined | 100 | this.tagsAllOf = undefined |
95 | this.durationMin = undefined | 101 | this.durationMin = undefined |
96 | this.durationMax = undefined | 102 | this.durationMax = undefined |
103 | this.isLive = undefined | ||
97 | 104 | ||
98 | this.sort = '-match' | 105 | this.sort = '-match' |
99 | } | 106 | } |
@@ -112,12 +119,16 @@ export class AdvancedSearch { | |||
112 | tagsAllOf: this.tagsAllOf, | 119 | tagsAllOf: this.tagsAllOf, |
113 | durationMin: this.durationMin, | 120 | durationMin: this.durationMin, |
114 | durationMax: this.durationMax, | 121 | durationMax: this.durationMax, |
122 | isLive: this.isLive, | ||
115 | sort: this.sort, | 123 | sort: this.sort, |
116 | searchTarget: this.searchTarget | 124 | searchTarget: this.searchTarget |
117 | } | 125 | } |
118 | } | 126 | } |
119 | 127 | ||
120 | toAPIObject () { | 128 | toAPIObject (): VideosSearchQuery { |
129 | let isLive: boolean | ||
130 | if (this.isLive) isLive = this.isLive === 'true' | ||
131 | |||
121 | return { | 132 | return { |
122 | startDate: this.startDate, | 133 | startDate: this.startDate, |
123 | endDate: this.endDate, | 134 | endDate: this.endDate, |
@@ -131,6 +142,7 @@ export class AdvancedSearch { | |||
131 | tagsAllOf: this.tagsAllOf, | 142 | tagsAllOf: this.tagsAllOf, |
132 | durationMin: this.durationMin, | 143 | durationMin: this.durationMin, |
133 | durationMax: this.durationMax, | 144 | durationMax: this.durationMax, |
145 | isLive, | ||
134 | sort: this.sort, | 146 | sort: this.sort, |
135 | searchTarget: this.searchTarget | 147 | searchTarget: this.searchTarget |
136 | } | 148 | } |
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html index 75cfc918b..d8699ff69 100644 --- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html +++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html | |||
@@ -40,7 +40,7 @@ | |||
40 | </ng-container> | 40 | </ng-container> |
41 | 41 | ||
42 | <div | 42 | <div |
43 | class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right" | 43 | class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right bottom-left" |
44 | role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label | 44 | role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label |
45 | > | 45 | > |
46 | <button class="btn btn-sm dropdown-toggle-split" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label> | 46 | <button class="btn btn-sm dropdown-toggle-split" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index 5df89d019..0bbdff1e6 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss | |||
@@ -95,6 +95,7 @@ my-actor-avatar { | |||
95 | .video-bottom { | 95 | .video-bottom { |
96 | display: flex; | 96 | display: flex; |
97 | width: 100%; | 97 | width: 100%; |
98 | min-width: 1px; | ||
98 | } | 99 | } |
99 | 100 | ||
100 | .video-miniature-name { | 101 | .video-miniature-name { |
@@ -145,6 +146,7 @@ my-actor-avatar { | |||
145 | 146 | ||
146 | .video-bottom { | 147 | .video-bottom { |
147 | display: flex; | 148 | display: flex; |
149 | min-width: 1px; | ||
148 | } | 150 | } |
149 | 151 | ||
150 | // We don't display avatar in row mode | 152 | // We don't display avatar in row mode |