diff options
Diffstat (limited to 'client/src/app')
154 files changed, 2963 insertions, 1850 deletions
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html index 2cf890acf..e9139b503 100644 --- a/client/src/app/+about/about-follows/about-follows.component.html +++ b/client/src/app/+about/about-follows/about-follows.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <div class="row"> | 1 | <div class="row"> |
2 | <h1 class="sr-only" i18n>Follows</h1> | 2 | <h1 class="sr-only" i18n>Follows</h1> |
3 | <div class="col-xl-6 col-md-12"> | 3 | <div class="col-xl-6 col-md-12"> |
4 | <h2 i18n class="subtitle">Followers instances ({{ followersPagination.totalItems }})</h2> | 4 | <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2> |
5 | 5 | ||
6 | <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div> | 6 | <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div> |
7 | 7 | ||
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html index d8794d602..1f372090e 100644 --- a/client/src/app/+about/about-instance/about-instance.component.html +++ b/client/src/app/+about/about-instance/about-instance.component.html | |||
@@ -83,7 +83,7 @@ | |||
83 | fragment="business-model" | 83 | fragment="business-model" |
84 | #anchorLink | 84 | #anchorLink |
85 | (click)="onClickCopyLink(anchorLink)"> | 85 | (click)="onClickCopyLink(anchorLink)"> |
86 | <h3 i18n class="section-title">How we will pay for this instance</h3> | 86 | <h3 i18n class="section-title">How we will pay for keeping our instance running</h3> |
87 | </a> | 87 | </a> |
88 | 88 | ||
89 | <div [innerHTML]="html.businessModel"></div> | 89 | <div [innerHTML]="html.businessModel"></div> |
diff --git a/client/src/app/+accounts/account-about/account-about.component.html b/client/src/app/+accounts/account-about/account-about.component.html deleted file mode 100644 index e9e0e4079..000000000 --- a/client/src/app/+accounts/account-about/account-about.component.html +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <h1 class="sr-only" i18n>About</h1> | ||
2 | <div class="margin-content"> | ||
3 | <div *ngIf="account" class="row no-gutters"> | ||
4 | <div class="block col-md-6 col-sm-12 pr-2"> | ||
5 | <h2 i18n class="small-title">DESCRIPTION</h2> | ||
6 | <div class="content" [innerHtml]="getAccountDescription()"></div> | ||
7 | </div> | ||
8 | |||
9 | <div class="block col-md-6 col-sm-12"> | ||
10 | <h2 i18n class="small-title">STATS</h2> | ||
11 | |||
12 | <div i18n class="content">Joined {{ account.createdAt | date }}</div> | ||
13 | </div> | ||
14 | </div> | ||
15 | </div> | ||
diff --git a/client/src/app/+accounts/account-about/account-about.component.scss b/client/src/app/+accounts/account-about/account-about.component.scss deleted file mode 100644 index 5bcd4b561..000000000 --- a/client/src/app/+accounts/account-about/account-about.component.scss +++ /dev/null | |||
@@ -1,12 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .block { | ||
5 | margin-bottom: 40px; | ||
6 | |||
7 | .small-title { | ||
8 | @include in-content-small-title; | ||
9 | |||
10 | margin-bottom: 20px; | ||
11 | } | ||
12 | } | ||
diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts deleted file mode 100644 index 6cf846d72..000000000 --- a/client/src/app/+accounts/account-about/account-about.component.ts +++ /dev/null | |||
@@ -1,40 +0,0 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
3 | import { MarkdownService } from '@app/core' | ||
4 | import { Account, AccountService } from '@app/shared/shared-main' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-account-about', | ||
8 | templateUrl: './account-about.component.html', | ||
9 | styleUrls: [ './account-about.component.scss' ] | ||
10 | }) | ||
11 | export class AccountAboutComponent implements OnInit, OnDestroy { | ||
12 | account: Account | ||
13 | descriptionHTML = '' | ||
14 | |||
15 | private accountSub: Subscription | ||
16 | |||
17 | constructor ( | ||
18 | private accountService: AccountService, | ||
19 | private markdownService: MarkdownService | ||
20 | ) { } | ||
21 | |||
22 | ngOnInit () { | ||
23 | // Parent get the account for us | ||
24 | this.accountSub = this.accountService.accountLoaded | ||
25 | .subscribe(async account => { | ||
26 | this.account = account | ||
27 | this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.account.description, true) | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | ngOnDestroy () { | ||
32 | if (this.accountSub) this.accountSub.unsubscribe() | ||
33 | } | ||
34 | |||
35 | getAccountDescription () { | ||
36 | if (this.descriptionHTML) return this.descriptionHTML | ||
37 | |||
38 | return $localize`No description` | ||
39 | } | ||
40 | } | ||
diff --git a/client/src/app/+accounts/account-search/account-search.component.ts b/client/src/app/+accounts/account-search/account-search.component.ts index dda4bf0c7..f54ab846a 100644 --- a/client/src/app/+accounts/account-search/account-search.component.ts +++ b/client/src/app/+accounts/account-search/account-search.component.ts | |||
@@ -64,9 +64,14 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit, | |||
64 | } | 64 | } |
65 | 65 | ||
66 | updateSearch (value: string) { | 66 | updateSearch (value: string) { |
67 | if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route }) | ||
68 | this.search = value | 67 | this.search = value |
69 | 68 | ||
69 | if (!this.search) { | ||
70 | this.router.navigate([ '../videos' ], { relativeTo: this.route }) | ||
71 | return | ||
72 | } | ||
73 | |||
74 | this.videos = [] | ||
70 | this.reloadVideos() | 75 | this.reloadVideos() |
71 | } | 76 | } |
72 | 77 | ||
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index 5dbb341d2..19a4b3c9c 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html | |||
@@ -1,33 +1,50 @@ | |||
1 | <h1 class="sr-only" i18n>Video channels</h1> | 1 | <h1 class="sr-only" i18n>Video channels</h1> |
2 | |||
2 | <div class="margin-content"> | 3 | <div class="margin-content"> |
3 | 4 | ||
4 | <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> | 5 | <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> |
5 | 6 | ||
6 | <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()"> | 7 | <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()"> |
7 | <div class="section channel" *ngFor="let videoChannel of videoChannels"> | 8 | <div class="channel" *ngFor="let videoChannel of videoChannels"> |
8 | <div class="section-title"> | 9 | |
9 | <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> | 10 | <div class="channel-avatar-row"> |
11 | <a class="avatar-link" [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> | ||
10 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | 12 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> |
13 | </a> | ||
11 | 14 | ||
12 | <h2 class="section-title">{{ videoChannel.displayName }}</h2> | 15 | <h2> |
16 | <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> | ||
17 | {{ videoChannel.displayName }} | ||
18 | </a> | ||
19 | </h2> | ||
20 | |||
21 | <div class="actor-counters"> | ||
13 | <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | 22 | <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> |
14 | </a> | ||
15 | 23 | ||
16 | <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> | 24 | <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n> |
25 | {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}} | ||
26 | </span> | ||
27 | </div> | ||
28 | |||
29 | <div class="description-html" [innerHTML]="getChannelDescription(videoChannel)"></div> | ||
17 | </div> | 30 | </div> |
18 | 31 | ||
19 | <div *ngIf="getVideosOf(videoChannel)" class="videos"> | 32 | <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> |
20 | <div class="no-results my-5" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel doesn't have any videos.</div> | 33 | |
34 | <a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a> | ||
35 | |||
36 | <div class="videos"> | ||
37 | <div class="no-results" i18n *ngIf="getTotalVideosOf(videoChannel) === 0">This channel doesn't have any videos.</div> | ||
21 | 38 | ||
22 | <my-video-miniature | 39 | <my-video-miniature |
23 | *ngFor="let video of getVideosOf(videoChannel)" | 40 | *ngFor="let video of getVideosOf(videoChannel)" |
24 | [video]="video" [user]="userMiniature" [displayVideoActions]="true" | 41 | [video]="video" [user]="userMiniature" [displayVideoActions]="true" [displayOptions]="miniatureDisplayOptions" |
25 | ></my-video-miniature> | 42 | ></my-video-miniature> |
26 | </div> | ||
27 | 43 | ||
28 | <a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> | 44 | <div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel"> |
29 | SHOW THIS CHANNEL | 45 | <a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a> |
30 | </a> | 46 | </div> |
47 | </div> | ||
31 | </div> | 48 | </div> |
32 | </div> | 49 | </div> |
33 | </div> | 50 | </div> |
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index 4957e91d7..7e88802f3 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss | |||
@@ -3,37 +3,175 @@ | |||
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .margin-content { | 5 | .margin-content { |
6 | @include fluid-videos-miniature-layout; | 6 | @include grid-videos-miniature-margins; |
7 | } | 7 | } |
8 | 8 | ||
9 | .section { | 9 | .channel { |
10 | @include miniature-rows; | 10 | max-width: $max-channels-width; |
11 | background-color: pvar(--channelBackgroundColor); | ||
12 | padding: 30px; | ||
11 | 13 | ||
12 | padding-top: 0 !important; | 14 | margin: 30px 0; |
13 | 15 | ||
14 | .section-title { | 16 | display: grid; |
17 | grid-template-columns: 1fr auto; | ||
18 | grid-template-rows: auto auto; | ||
19 | column-gap: 15px; | ||
20 | } | ||
21 | |||
22 | .channel-avatar-row { | ||
23 | grid-column: 1; | ||
24 | grid-row: 1; | ||
25 | |||
26 | display: grid; | ||
27 | grid-template-columns: auto auto 1fr; | ||
28 | grid-template-rows: auto 1fr; | ||
29 | |||
30 | .avatar-link { | ||
31 | grid-column: 1; | ||
32 | grid-row: 1 / 3; | ||
33 | margin-right: 30px; | ||
34 | } | ||
35 | |||
36 | img { | ||
37 | @include channel-avatar(75px); | ||
38 | } | ||
39 | |||
40 | a { | ||
41 | color: pvar(--mainForegroundColor); | ||
42 | } | ||
43 | |||
44 | h2 { | ||
45 | grid-row: 1; | ||
46 | grid-column: 2; | ||
47 | font-size: 20px; | ||
48 | line-height: 1; | ||
49 | font-weight: $font-bold; | ||
50 | margin: 0; | ||
51 | } | ||
52 | |||
53 | .actor-counters { | ||
54 | grid-row: 1; | ||
55 | grid-column: 3; | ||
56 | color: pvar(--greyForegroundColor); | ||
57 | font-size: 16px; | ||
58 | display: flex; | ||
15 | align-items: center; | 59 | align-items: center; |
60 | margin-left: 15px; | ||
16 | } | 61 | } |
17 | 62 | ||
18 | .videos { | 63 | .actor-counters > *:not(:last-child)::after { |
19 | overflow: hidden; | 64 | content: '•'; |
65 | margin: 0 10px; | ||
66 | color: pvar(--mainColor); | ||
67 | } | ||
20 | 68 | ||
21 | .no-results { | 69 | .description-html { |
22 | height: 50px; | 70 | grid-column: 2 / 4; |
23 | } | 71 | grid-row: 2; |
72 | |||
73 | max-height: 80px; | ||
74 | font-size: 16px; | ||
75 | |||
76 | @include fade-text(30px, pvar(--channelBackgroundColor)); | ||
77 | } | ||
78 | } | ||
79 | |||
80 | my-subscribe-button { | ||
81 | grid-row: 1; | ||
82 | grid-column: 2; | ||
83 | } | ||
84 | |||
85 | .videos { | ||
86 | display: flex; | ||
87 | grid-column: 1 / 3; | ||
88 | grid-row: 2; | ||
89 | margin-top: 30px; | ||
90 | |||
91 | position: relative; | ||
92 | overflow: hidden; | ||
93 | |||
94 | my-video-miniature { | ||
95 | margin-right: 15px; | ||
96 | min-width: $video-thumbnail-medium-width; | ||
97 | max-width: $video-thumbnail-medium-width; | ||
98 | } | ||
99 | |||
100 | .no-results { | ||
101 | height: auto; | ||
24 | } | 102 | } |
103 | } | ||
25 | 104 | ||
26 | my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown { | 105 | .miniature-show-channel { |
27 | // Fix our overflow | 106 | height: 100%; |
28 | position: absolute; | 107 | position: absolute; |
108 | right: 0; | ||
109 | background: linear-gradient(90deg, transparent 0, pvar(--channelBackgroundColor) 45px); | ||
110 | padding: ($video-thumbnail-medium-height / 2 - 10px) 15px 0 60px; | ||
111 | z-index: z(miniature) + 1; | ||
112 | |||
113 | a { | ||
114 | color: pvar(--mainColor); | ||
115 | font-size: 16px; | ||
116 | font-weight: $font-semibold; | ||
29 | } | 117 | } |
30 | } | 118 | } |
31 | 119 | ||
120 | .button-show-channel { | ||
121 | display: none; | ||
122 | } | ||
123 | |||
32 | @media screen and (max-width: $mobile-view) { | 124 | @media screen and (max-width: $mobile-view) { |
33 | .section { | 125 | .channel { |
34 | .section-title { | 126 | padding: 15px; |
35 | flex-direction: column; | 127 | } |
36 | align-items: normal; | 128 | |
129 | .channel-avatar-row { | ||
130 | grid-template-columns: auto auto auto 1fr; | ||
131 | |||
132 | .avatar-link { | ||
133 | grid-row: 1 / 4; | ||
134 | } | ||
135 | |||
136 | h2 { | ||
137 | font-size: 16px; | ||
37 | } | 138 | } |
139 | |||
140 | .actor-counters { | ||
141 | margin: 0; | ||
142 | font-size: 13px; | ||
143 | grid-row: 2; | ||
144 | grid-column: 2 / 4; | ||
145 | } | ||
146 | |||
147 | .description-html { | ||
148 | grid-row: 3; | ||
149 | font-size: 14px; | ||
150 | } | ||
151 | } | ||
152 | |||
153 | .show-channel a { | ||
154 | @include peertube-button-link; | ||
155 | @include orange-button-inverted; | ||
156 | } | ||
157 | |||
158 | .videos { | ||
159 | display: none; | ||
160 | } | ||
161 | |||
162 | my-subscribe-button, | ||
163 | .button-show-channel { | ||
164 | grid-column: 1 / 4; | ||
165 | grid-row: 3; | ||
166 | margin-top: 15px; | ||
167 | } | ||
168 | |||
169 | my-subscribe-button { | ||
170 | justify-self: start; | ||
171 | } | ||
172 | |||
173 | .button-show-channel { | ||
174 | display: block; | ||
175 | justify-self: end; | ||
38 | } | 176 | } |
39 | } | 177 | } |
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index f2beb6689..0628c7a96 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { from, Subject, Subscription } from 'rxjs' | 1 | import { from, Subject, Subscription } from 'rxjs' |
2 | import { concatMap, map, switchMap, tap } from 'rxjs/operators' | 2 | import { concatMap, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
4 | import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core' | 4 | import { ComponentPagination, hasMoreItems, MarkdownService, ScreenService, User, UserService } from '@app/core' |
5 | import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' | 5 | import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' |
6 | import { NSFWPolicyType, VideoSortField } from '@shared/models' | 6 | import { NSFWPolicyType, VideoSortField } from '@shared/models' |
7 | import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' | ||
7 | 8 | ||
8 | @Component({ | 9 | @Component({ |
9 | selector: 'my-account-video-channels', | 10 | selector: 'my-account-video-channels', |
@@ -13,7 +14,10 @@ import { NSFWPolicyType, VideoSortField } from '@shared/models' | |||
13 | export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | 14 | export class AccountVideoChannelsComponent implements OnInit, OnDestroy { |
14 | account: Account | 15 | account: Account |
15 | videoChannels: VideoChannel[] = [] | 16 | videoChannels: VideoChannel[] = [] |
16 | videos: { [id: number]: Video[] } = {} | 17 | |
18 | videos: { [id: number]: { total: number, videos: Video[] } } = {} | ||
19 | |||
20 | channelsDescriptionHTML: { [ id: number ]: string } = {} | ||
17 | 21 | ||
18 | channelPagination: ComponentPagination = { | 22 | channelPagination: ComponentPagination = { |
19 | currentPage: 1, | 23 | currentPage: 1, |
@@ -23,7 +27,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
23 | 27 | ||
24 | videosPagination: ComponentPagination = { | 28 | videosPagination: ComponentPagination = { |
25 | currentPage: 1, | 29 | currentPage: 1, |
26 | itemsPerPage: 12, | 30 | itemsPerPage: 5, |
27 | totalItems: null | 31 | totalItems: null |
28 | } | 32 | } |
29 | videosSort: VideoSortField = '-publishedAt' | 33 | videosSort: VideoSortField = '-publishedAt' |
@@ -32,6 +36,16 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
32 | 36 | ||
33 | userMiniature: User | 37 | userMiniature: User |
34 | nsfwPolicy: NSFWPolicyType | 38 | nsfwPolicy: NSFWPolicyType |
39 | miniatureDisplayOptions: MiniatureDisplayOptions = { | ||
40 | date: true, | ||
41 | views: true, | ||
42 | by: false, | ||
43 | avatar: false, | ||
44 | privacyLabel: false, | ||
45 | privacyText: false, | ||
46 | state: false, | ||
47 | blacklistInfo: false | ||
48 | } | ||
35 | 49 | ||
36 | private accountSub: Subscription | 50 | private accountSub: Subscription |
37 | 51 | ||
@@ -39,7 +53,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
39 | private accountService: AccountService, | 53 | private accountService: AccountService, |
40 | private videoChannelService: VideoChannelService, | 54 | private videoChannelService: VideoChannelService, |
41 | private videoService: VideoService, | 55 | private videoService: VideoService, |
42 | private screenService: ScreenService, | 56 | private markdown: MarkdownService, |
43 | private userService: UserService | 57 | private userService: UserService |
44 | ) { } | 58 | ) { } |
45 | 59 | ||
@@ -78,23 +92,36 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { | |||
78 | } | 92 | } |
79 | 93 | ||
80 | return this.videoService.getVideoChannelVideos(options) | 94 | return this.videoService.getVideoChannelVideos(options) |
81 | .pipe(map(data => ({ videoChannel, videos: data.data }))) | 95 | .pipe(map(data => ({ videoChannel, videos: data.data, total: data.total }))) |
82 | }) | 96 | }) |
83 | ) | 97 | ) |
84 | .subscribe(({ videoChannel, videos }) => { | 98 | .subscribe(async ({ videoChannel, videos, total }) => { |
99 | this.channelsDescriptionHTML[videoChannel.id] = await this.markdown.textMarkdownToHTML(videoChannel.description) | ||
100 | |||
85 | this.videoChannels.push(videoChannel) | 101 | this.videoChannels.push(videoChannel) |
86 | 102 | ||
87 | this.videos[videoChannel.id] = videos | 103 | this.videos[videoChannel.id] = { videos, total } |
88 | 104 | ||
89 | this.onChannelDataSubject.next([ videoChannel ]) | 105 | this.onChannelDataSubject.next([ videoChannel ]) |
90 | }) | 106 | }) |
91 | } | 107 | } |
92 | 108 | ||
93 | getVideosOf (videoChannel: VideoChannel) { | 109 | getVideosOf (videoChannel: VideoChannel) { |
94 | const numberOfVideos = this.screenService.getNumberOfAvailableMiniatures() | 110 | const obj = this.videos[ videoChannel.id ] |
111 | if (!obj) return [] | ||
112 | |||
113 | return obj.videos | ||
114 | } | ||
115 | |||
116 | getTotalVideosOf (videoChannel: VideoChannel) { | ||
117 | const obj = this.videos[ videoChannel.id ] | ||
118 | if (!obj) return undefined | ||
119 | |||
120 | return obj.total | ||
121 | } | ||
95 | 122 | ||
96 | // 2 rows | 123 | getChannelDescription (videoChannel: VideoChannel) { |
97 | return this.videos[ videoChannel.id ].slice(0, numberOfVideos * 2) | 124 | return this.channelsDescriptionHTML[videoChannel.id] |
98 | } | 125 | } |
99 | 126 | ||
100 | onNearOfBottom () { | 127 | onNearOfBottom () { |
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index 484d60e25..75af45e90 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts | |||
@@ -16,6 +16,7 @@ import { VideoFilter } from '@shared/models' | |||
16 | ] | 16 | ] |
17 | }) | 17 | }) |
18 | export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | 18 | export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { |
19 | // No value because we don't want a page title | ||
19 | titlePage: string | 20 | titlePage: string |
20 | loadOnInit = false | 21 | loadOnInit = false |
21 | loadUserVideoPreferences = true | 22 | loadUserVideoPreferences = true |
@@ -77,11 +78,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
77 | 78 | ||
78 | return this.videoService | 79 | return this.videoService |
79 | .getAccountVideos(options) | 80 | .getAccountVideos(options) |
80 | .pipe( | ||
81 | tap(({ total }) => { | ||
82 | this.titlePage = $localize`Published ${total} videos` | ||
83 | }) | ||
84 | ) | ||
85 | } | 81 | } |
86 | 82 | ||
87 | toggleModerationDisplay () { | 83 | toggleModerationDisplay () { |
@@ -93,4 +89,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
93 | generateSyndicationList () { | 89 | generateSyndicationList () { |
94 | this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) | 90 | this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) |
95 | } | 91 | } |
92 | |||
93 | displayAsRow () { | ||
94 | return this.screenService.isInMobileView() | ||
95 | } | ||
96 | } | 96 | } |
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts index 15937a67b..3bf0f7185 100644 --- a/client/src/app/+accounts/accounts-routing.module.ts +++ b/client/src/app/+accounts/accounts-routing.module.ts | |||
@@ -1,11 +1,10 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
4 | import { AccountsComponent } from './accounts.component' | ||
5 | import { AccountVideosComponent } from './account-videos/account-videos.component' | ||
6 | import { AccountAboutComponent } from './account-about/account-about.component' | ||
7 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | ||
8 | import { AccountSearchComponent } from './account-search/account-search.component' | 4 | import { AccountSearchComponent } from './account-search/account-search.component' |
5 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | ||
6 | import { AccountVideosComponent } from './account-videos/account-videos.component' | ||
7 | import { AccountsComponent } from './accounts.component' | ||
9 | 8 | ||
10 | const accountsRoutes: Routes = [ | 9 | const accountsRoutes: Routes = [ |
11 | { | 10 | { |
@@ -32,15 +31,6 @@ const accountsRoutes: Routes = [ | |||
32 | } | 31 | } |
33 | }, | 32 | }, |
34 | { | 33 | { |
35 | path: 'about', | ||
36 | component: AccountAboutComponent, | ||
37 | data: { | ||
38 | meta: { | ||
39 | title: $localize`About account` | ||
40 | } | ||
41 | } | ||
42 | }, | ||
43 | { | ||
44 | path: 'videos', | 34 | path: 'videos', |
45 | component: AccountVideosComponent, | 35 | component: AccountVideosComponent, |
46 | data: { | 36 | data: { |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 5bd7b0824..03d083bb6 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -1,57 +1,89 @@ | |||
1 | <div *ngIf="account" class="row"> | 1 | <div *ngIf="account" class="root"> |
2 | <div class="sub-menu"> | 2 | <div class="account-info"> |
3 | 3 | ||
4 | <div class="actor"> | 4 | <div class="account-avatar-row"> |
5 | <img [src]="account.avatarUrl" alt="Avatar" /> | 5 | <img class="account-avatar" [src]="account.avatarUrl" alt="Avatar" /> |
6 | 6 | ||
7 | <div class="actor-info"> | 7 | <div> |
8 | <div class="actor-names"> | 8 | <div class="section-label" i18n>PEERTUBE ACCOUNT</div> |
9 | <div class="actor-display-name">{{ account.displayName }}</div> | 9 | |
10 | <div class="actor-name"> | 10 | <div class="actor-info"> |
11 | <span>{{ account.nameWithHost }}</span> | 11 | <div> |
12 | <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" | 12 | <div class="actor-display-name"> |
13 | class="btn btn-outline-secondary btn-sm copy-button" | 13 | <h1>{{ account.displayName }}</h1> |
14 | > | 14 | |
15 | <span class="glyphicon glyphicon-copy"></span> | 15 | <my-user-moderation-dropdown |
16 | </button> | 16 | [prependActions]="prependModerationActions" |
17 | buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto" | ||
18 | (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" | ||
19 | ></my-user-moderation-dropdown> | ||
20 | |||
21 | <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> | ||
22 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> | ||
23 | <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> | ||
24 | <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span> | ||
25 | <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> | ||
26 | </div> | ||
27 | |||
28 | <div class="actor-handle"> | ||
29 | <span>@{{ account.nameWithHost }}</span> | ||
30 | <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" | ||
31 | class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title | ||
32 | > | ||
33 | <span class="glyphicon glyphicon-duplicate"></span> | ||
34 | </button> | ||
35 | </div> | ||
36 | |||
37 | <div class="actor-counters"> | ||
38 | <span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span> | ||
39 | |||
40 | <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n> | ||
41 | {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}} | ||
42 | </span> | ||
43 | </div> | ||
17 | </div> | 44 | </div> |
18 | <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> | ||
19 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> | ||
20 | <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> | ||
21 | <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span> | ||
22 | <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> | ||
23 | |||
24 | <my-user-moderation-dropdown | ||
25 | [prependActions]="prependModerationActions" | ||
26 | buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto" | ||
27 | (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" | ||
28 | ></my-user-moderation-dropdown> | ||
29 | </div> | ||
30 | <div class="actor-followers" [title]="accountFollowerTitle"> | ||
31 | {{ subscribersDisplayFor(naiveAggregatedSubscribers) }} | ||
32 | </div> | 45 | </div> |
33 | </div> | 46 | </div> |
47 | </div> | ||
34 | 48 | ||
35 | <div class="right-buttons"> | 49 | <div class="description" [ngClass]="{ expanded: accountDescriptionExpanded }"> |
36 | <a *ngIf="isAccountManageable && !isInSmallView" routerLink="/my-account" class="btn btn-outline-tertiary mr-2" i18n>Manage account</a> | 50 | <div class="description-html" [innerHTML]="accountDescriptionHTML"></div> |
37 | <my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button> | 51 | |
38 | </div> | 52 | <div class="created-at" i18n>Account created on {{ account.createdAt | date }}</div> |
39 | </div> | 53 | </div> |
40 | 54 | ||
41 | <div class="links w-100"> | 55 | <div *ngIf="hasShowMoreDescription()" class="show-more" role="button" |
42 | <ng-template #linkTemplate let-item="item"> | 56 | (click)="accountDescriptionExpanded = !accountDescriptionExpanded" |
43 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | 57 | title="Show the complete description" i18n-title i18n |
44 | </ng-template> | 58 | > |
59 | Show more... | ||
60 | </div> | ||
45 | 61 | ||
46 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | 62 | <div class="buttons"> |
63 | <a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link orange-button" i18n> | ||
64 | Manage account | ||
65 | </a> | ||
47 | 66 | ||
48 | <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> | 67 | <my-subscribe-button *ngIf="hasVideoChannels() && !isManageable()" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button> |
49 | </div> | 68 | </div> |
50 | </div> | 69 | </div> |
51 | 70 | ||
52 | <div class="margin-content"> | 71 | <div class="links"> |
53 | <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> | 72 | <ng-template #linkTemplate let-item="item"> |
73 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | ||
74 | </ng-template> | ||
75 | |||
76 | <list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | ||
77 | |||
78 | <simple-search-input | ||
79 | [alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)" | ||
80 | (inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos" | ||
81 | i18n-iconTitle icon-title="Search account videos" | ||
82 | i18n-placeholder placeholder="Search account videos" | ||
83 | ></simple-search-input> | ||
54 | </div> | 84 | </div> |
85 | |||
86 | <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> | ||
55 | </div> | 87 | </div> |
56 | 88 | ||
57 | <ng-container *ngIf="prependModerationActions"> | 89 | <ng-container *ngIf="prependModerationActions"> |
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss index 40c6b6493..a836e84ce 100644 --- a/client/src/app/+accounts/accounts.component.scss +++ b/client/src/app/+accounts/accounts.component.scss | |||
@@ -1,48 +1,29 @@ | |||
1 | // Bootstrap grid utilities require functions, variables and mixins | ||
2 | @import 'node_modules/bootstrap/scss/functions'; | ||
3 | @import 'node_modules/bootstrap/scss/variables'; | ||
4 | @import 'node_modules/bootstrap/scss/mixins'; | ||
5 | @import 'node_modules/bootstrap/scss/grid'; | ||
6 | |||
7 | @import '_variables'; | 1 | @import '_variables'; |
8 | @import '_mixins'; | 2 | @import '_mixins'; |
9 | 3 | @import '_actor'; | |
10 | .sub-menu { | 4 | @import '_miniature'; |
11 | @include sub-menu-with-actor; | 5 | |
12 | 6 | .root { | |
13 | .actor { | 7 | --myGlobalTopPadding: 60px; |
14 | width: 100%; | 8 | --myImgMargin: 30px; |
15 | } | 9 | --myFontSize: 16px; |
10 | --myGreyFontSize: 16px; | ||
16 | } | 11 | } |
17 | 12 | ||
18 | .margin-content { | 13 | .section-label { |
19 | // margin-content is required, but child views have their own margins | 14 | @include section-label-responsive; |
20 | // that match views outside the scope of accounts, so we only align | ||
21 | // them with the margins of .sub-menu when required. | ||
22 | margin: 0; | ||
23 | } | 15 | } |
24 | 16 | ||
25 | .right-buttons { | 17 | .links { |
26 | display: flex; | 18 | @include grid-videos-miniature-margins; |
27 | height: max-content; | ||
28 | margin-left: auto; | ||
29 | margin-top: 10px; | ||
30 | |||
31 | @include media-breakpoint-down(lg) { | ||
32 | flex-flow: column-reverse; | ||
33 | 19 | ||
34 | a { | 20 | display: flex; |
35 | margin-top: 0.25rem; | 21 | justify-content: space-between; |
36 | margin-right: 0 !important; | 22 | align-items: center; |
37 | } | 23 | max-width: $max-channels-width; |
38 | } | ||
39 | |||
40 | a { | ||
41 | @include peertube-button-outline; | ||
42 | } | ||
43 | 24 | ||
44 | my-subscribe-button { | 25 | simple-search-input { |
45 | min-height: 30px; | 26 | margin-left: auto; |
46 | } | 27 | } |
47 | } | 28 | } |
48 | 29 | ||
@@ -60,39 +41,106 @@ my-user-moderation-dropdown, | |||
60 | 41 | ||
61 | .copy-button { | 42 | .copy-button { |
62 | border: none; | 43 | border: none; |
63 | padding: 5px; | 44 | } |
64 | margin-top: -2px; | 45 | |
46 | .account-info { | ||
47 | @include grid-videos-miniature-margins(false, 15px); | ||
48 | |||
49 | display: grid; | ||
50 | grid-template-columns: 1fr min-content; | ||
51 | grid-template-rows: auto auto; | ||
52 | |||
53 | background-color: pvar(--submenuBackgroundColor); | ||
54 | margin-bottom: 45px; | ||
55 | padding-top: var(--myGlobalTopPadding); | ||
56 | font-size: var(--myFontSize); | ||
57 | } | ||
58 | |||
59 | .account-avatar-row { | ||
60 | @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize)); | ||
61 | } | ||
62 | |||
63 | .description { | ||
64 | grid-column: 1 / 3; | ||
65 | max-width: 1000px; | ||
66 | word-break: break-word; | ||
67 | } | ||
68 | |||
69 | .created-at { | ||
70 | margin-top: 15px; | ||
71 | color: pvar(--greyForegroundColor); | ||
72 | padding-bottom: 60px; | ||
73 | } | ||
74 | |||
75 | .show-more { | ||
76 | @include show-more-description; | ||
77 | |||
78 | display: none; | ||
79 | text-align: center; | ||
80 | } | ||
81 | |||
82 | .buttons { | ||
83 | grid-column: 2; | ||
84 | grid-row: 1; | ||
85 | |||
86 | display: flex; | ||
87 | flex-wrap: wrap; | ||
88 | justify-content: flex-end; | ||
89 | align-content: flex-start; | ||
90 | |||
91 | > *:not(:last-child) { | ||
92 | margin-bottom: 15px; | ||
93 | } | ||
94 | |||
95 | > a { | ||
96 | white-space: nowrap; | ||
97 | } | ||
98 | } | ||
99 | |||
100 | @media screen and (max-width: $small-view) { | ||
101 | .root { | ||
102 | --myGlobalTopPadding: 45px; | ||
103 | --myChannelImgMargin: 15px; | ||
104 | } | ||
105 | |||
106 | .account-info { | ||
107 | display: block; | ||
108 | padding-bottom: 60px; | ||
109 | } | ||
110 | |||
111 | .description:not(.expanded) { | ||
112 | max-height: 70px; | ||
113 | |||
114 | @include fade-text(30px, pvar(--submenuBackgroundColor)); | ||
115 | } | ||
116 | |||
117 | .show-more { | ||
118 | display: block; | ||
119 | } | ||
120 | |||
121 | .buttons { | ||
122 | justify-content: center; | ||
123 | } | ||
65 | } | 124 | } |
66 | 125 | ||
67 | @media screen and (max-width: $mobile-view) { | 126 | @media screen and (max-width: $mobile-view) { |
68 | .sub-menu { | 127 | .root { |
69 | .actor { | 128 | --myGlobalTopPadding: 15px; |
70 | flex-direction: column; | 129 | --myFontSize: 14px; |
71 | align-items: center; | 130 | --myGreyFontSize: 13px; |
72 | 131 | } | |
73 | img, | 132 | |
74 | .actor-info .actor-names .actor-display-name { | 133 | .account-info { |
75 | margin-right: 0; | 134 | display: block; |
76 | } | 135 | padding-bottom: 30px; |
77 | 136 | } | |
78 | .actor-info { | 137 | |
79 | .actor-names { | 138 | .links { |
80 | flex-direction: column; | 139 | margin: auto !important; |
81 | align-items: center; | 140 | width: min-content; |
82 | } | 141 | } |
83 | 142 | ||
84 | my-user-moderation-dropdown { | 143 | .show-more { |
85 | margin-left: 0; | 144 | margin-bottom: 30px; |
86 | } | ||
87 | |||
88 | .actor-followers { | ||
89 | text-align: center; | ||
90 | } | ||
91 | } | ||
92 | |||
93 | .right-buttons { | ||
94 | margin-left: 0; | ||
95 | } | ||
96 | } | ||
97 | } | 145 | } |
98 | } | 146 | } |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index e6a5a5d5e..fbd7380a9 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -2,11 +2,19 @@ import { Subscription } from 'rxjs' | |||
2 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' | 2 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute } from '@angular/router' | 4 | import { ActivatedRoute } from '@angular/router' |
5 | import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' | 5 | import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' |
6 | import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 6 | import { |
7 | Account, | ||
8 | AccountService, | ||
9 | DropdownAction, | ||
10 | ListOverflowItem, | ||
11 | VideoChannel, | ||
12 | VideoChannelService, | ||
13 | VideoService | ||
14 | } from '@app/shared/shared-main' | ||
7 | import { AccountReportComponent } from '@app/shared/shared-moderation' | 15 | import { AccountReportComponent } from '@app/shared/shared-moderation' |
8 | import { User, UserRight } from '@shared/models' | ||
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
17 | import { User, UserRight } from '@shared/models' | ||
10 | import { AccountSearchComponent } from './account-search/account-search.component' | 18 | import { AccountSearchComponent } from './account-search/account-search.component' |
11 | 19 | ||
12 | @Component({ | 20 | @Component({ |
@@ -15,16 +23,23 @@ import { AccountSearchComponent } from './account-search/account-search.componen | |||
15 | }) | 23 | }) |
16 | export class AccountsComponent implements OnInit, OnDestroy { | 24 | export class AccountsComponent implements OnInit, OnDestroy { |
17 | @ViewChild('accountReportModal') accountReportModal: AccountReportComponent | 25 | @ViewChild('accountReportModal') accountReportModal: AccountReportComponent |
26 | |||
18 | accountSearch: AccountSearchComponent | 27 | accountSearch: AccountSearchComponent |
19 | 28 | ||
20 | account: Account | 29 | account: Account |
21 | accountUser: User | 30 | accountUser: User |
31 | |||
22 | videoChannels: VideoChannel[] = [] | 32 | videoChannels: VideoChannel[] = [] |
33 | |||
23 | links: ListOverflowItem[] = [] | 34 | links: ListOverflowItem[] = [] |
35 | hideMenu = false | ||
24 | 36 | ||
25 | isAccountManageable = false | ||
26 | accountFollowerTitle = '' | 37 | accountFollowerTitle = '' |
27 | 38 | ||
39 | accountVideosCount: number | ||
40 | accountDescriptionHTML = '' | ||
41 | accountDescriptionExpanded = false | ||
42 | |||
28 | prependModerationActions: DropdownAction<any>[] | 43 | prependModerationActions: DropdownAction<any>[] |
29 | 44 | ||
30 | private routeSub: Subscription | 45 | private routeSub: Subscription |
@@ -38,6 +53,8 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
38 | private restExtractor: RestExtractor, | 53 | private restExtractor: RestExtractor, |
39 | private redirectService: RedirectService, | 54 | private redirectService: RedirectService, |
40 | private authService: AuthService, | 55 | private authService: AuthService, |
56 | private videoService: VideoService, | ||
57 | private markdown: MarkdownService, | ||
41 | private screenService: ScreenService | 58 | private screenService: ScreenService |
42 | ) { | 59 | ) { |
43 | } | 60 | } |
@@ -62,9 +79,8 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
62 | ) | 79 | ) |
63 | 80 | ||
64 | this.links = [ | 81 | this.links = [ |
65 | { label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' }, | 82 | { label: $localize`CHANNELS`, routerLink: 'video-channels' }, |
66 | { label: $localize`VIDEOS`, routerLink: 'videos' }, | 83 | { label: $localize`VIDEOS`, routerLink: 'videos' } |
67 | { label: $localize`ABOUT`, routerLink: 'about' } | ||
68 | ] | 84 | ] |
69 | } | 85 | } |
70 | 86 | ||
@@ -72,19 +88,29 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
72 | if (this.routeSub) this.routeSub.unsubscribe() | 88 | if (this.routeSub) this.routeSub.unsubscribe() |
73 | } | 89 | } |
74 | 90 | ||
75 | get naiveAggregatedSubscribers () { | 91 | naiveAggregatedSubscribers () { |
76 | return this.videoChannels.reduce( | 92 | return this.videoChannels.reduce( |
77 | (acc, val) => acc + val.followersCount, | 93 | (acc, val) => acc + val.followersCount, |
78 | this.account.followersCount // accumulator starts with the base number of subscribers the account has | 94 | this.account.followersCount // accumulator starts with the base number of subscribers the account has |
79 | ) | 95 | ) |
80 | } | 96 | } |
81 | 97 | ||
82 | get isInSmallView () { | 98 | isUserLoggedIn () { |
99 | return this.authService.isLoggedIn() | ||
100 | } | ||
101 | |||
102 | isInSmallView () { | ||
83 | return this.screenService.isInSmallView() | 103 | return this.screenService.isInSmallView() |
84 | } | 104 | } |
85 | 105 | ||
106 | isManageable () { | ||
107 | if (!this.isUserLoggedIn()) return false | ||
108 | |||
109 | return this.account?.userId === this.authService.getUser().id | ||
110 | } | ||
111 | |||
86 | onUserChanged () { | 112 | onUserChanged () { |
87 | this.getUserIfNeeded(this.account) | 113 | this.loadUserIfNeeded(this.account) |
88 | } | 114 | } |
89 | 115 | ||
90 | onUserDeleted () { | 116 | onUserDeleted () { |
@@ -113,40 +139,38 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
113 | if (this.accountSearch) this.accountSearch.updateSearch(search) | 139 | if (this.accountSearch) this.accountSearch.updateSearch(search) |
114 | } | 140 | } |
115 | 141 | ||
116 | private onAccount (account: Account) { | 142 | onSearchInputDisplayChanged (displayed: boolean) { |
143 | this.hideMenu = this.isInSmallView() && displayed | ||
144 | } | ||
145 | |||
146 | hasVideoChannels () { | ||
147 | return this.videoChannels.length !== 0 | ||
148 | } | ||
149 | |||
150 | hasShowMoreDescription () { | ||
151 | return !this.accountDescriptionExpanded && this.accountDescriptionHTML.length > 100 | ||
152 | } | ||
153 | |||
154 | private async onAccount (account: Account) { | ||
155 | this.accountFollowerTitle = $localize`${account.followersCount} direct account followers` | ||
156 | |||
117 | this.prependModerationActions = undefined | 157 | this.prependModerationActions = undefined |
118 | 158 | ||
119 | this.account = account | 159 | this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description) |
120 | 160 | ||
121 | if (this.authService.isLoggedIn()) { | 161 | // After the markdown renderer to avoid layout changes |
122 | this.authService.userInformationLoaded.subscribe( | 162 | this.account = account |
123 | () => { | ||
124 | this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id | ||
125 | |||
126 | const followers = this.subscribersDisplayFor(account.followersCount) | ||
127 | this.accountFollowerTitle = $localize`${followers} direct account followers` | ||
128 | |||
129 | // It's not our account, we can report it | ||
130 | if (!this.isAccountManageable) { | ||
131 | this.prependModerationActions = [ | ||
132 | { | ||
133 | label: $localize`Report this account`, | ||
134 | handler: () => this.showReportModal() | ||
135 | } | ||
136 | ] | ||
137 | } | ||
138 | } | ||
139 | ) | ||
140 | } | ||
141 | 163 | ||
142 | this.getUserIfNeeded(account) | 164 | this.updateModerationActions() |
165 | this.loadUserIfNeeded(account) | ||
166 | this.loadAccountVideosCount() | ||
143 | } | 167 | } |
144 | 168 | ||
145 | private showReportModal () { | 169 | private showReportModal () { |
146 | this.accountReportModal.show() | 170 | this.accountReportModal.show() |
147 | } | 171 | } |
148 | 172 | ||
149 | private getUserIfNeeded (account: Account) { | 173 | private loadUserIfNeeded (account: Account) { |
150 | if (!account.userId || !this.authService.isLoggedIn()) return | 174 | if (!account.userId || !this.authService.isLoggedIn()) return |
151 | 175 | ||
152 | const user = this.authService.getUser() | 176 | const user = this.authService.getUser() |
@@ -158,4 +182,33 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
158 | ) | 182 | ) |
159 | } | 183 | } |
160 | } | 184 | } |
185 | |||
186 | private updateModerationActions () { | ||
187 | if (!this.authService.isLoggedIn()) return | ||
188 | |||
189 | this.authService.userInformationLoaded.subscribe( | ||
190 | () => { | ||
191 | if (this.isManageable()) return | ||
192 | |||
193 | // It's not our account, we can report it | ||
194 | this.prependModerationActions = [ | ||
195 | { | ||
196 | label: $localize`Report this account`, | ||
197 | handler: () => this.showReportModal() | ||
198 | } | ||
199 | ] | ||
200 | } | ||
201 | ) | ||
202 | } | ||
203 | |||
204 | private loadAccountVideosCount () { | ||
205 | this.videoService.getAccountVideos({ | ||
206 | account: this.account, | ||
207 | videoPagination: { | ||
208 | currentPage: 1, | ||
209 | itemsPerPage: 0 | ||
210 | }, | ||
211 | sort: '-publishedAt' | ||
212 | }).subscribe(res => this.accountVideosCount = res.total) | ||
213 | } | ||
161 | } | 214 | } |
diff --git a/client/src/app/+accounts/accounts.module.ts b/client/src/app/+accounts/accounts.module.ts index 6da65cbc1..3354b4189 100644 --- a/client/src/app/+accounts/accounts.module.ts +++ b/client/src/app/+accounts/accounts.module.ts | |||
@@ -5,10 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main' | |||
5 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 5 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
6 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 6 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
7 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | 7 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' |
8 | import { AccountAboutComponent } from './account-about/account-about.component' | 8 | import { AccountSearchComponent } from './account-search/account-search.component' |
9 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' | 9 | import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' |
10 | import { AccountVideosComponent } from './account-videos/account-videos.component' | 10 | import { AccountVideosComponent } from './account-videos/account-videos.component' |
11 | import { AccountSearchComponent } from './account-search/account-search.component' | ||
12 | import { AccountsRoutingModule } from './accounts-routing.module' | 11 | import { AccountsRoutingModule } from './accounts-routing.module' |
13 | import { AccountsComponent } from './accounts.component' | 12 | import { AccountsComponent } from './accounts.component' |
14 | 13 | ||
@@ -28,7 +27,6 @@ import { AccountsComponent } from './accounts.component' | |||
28 | AccountsComponent, | 27 | AccountsComponent, |
29 | AccountVideosComponent, | 28 | AccountVideosComponent, |
30 | AccountVideoChannelsComponent, | 29 | AccountVideoChannelsComponent, |
31 | AccountAboutComponent, | ||
32 | AccountSearchComponent | 30 | AccountSearchComponent |
33 | ], | 31 | ], |
34 | 32 | ||
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index fd648a425..bac65c88e 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -3,6 +3,7 @@ import { SelectButtonModule } from 'primeng/selectbutton' | |||
3 | import { TableModule } from 'primeng/table' | 3 | import { TableModule } from 'primeng/table' |
4 | import { NgModule } from '@angular/core' | 4 | import { NgModule } from '@angular/core' |
5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' | 5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' |
6 | import { SharedActorImageModule } from '@app/shared/shared-actor-image' | ||
6 | import { SharedFormModule } from '@app/shared/shared-forms' | 7 | import { SharedFormModule } from '@app/shared/shared-forms' |
7 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 8 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
8 | import { SharedMainModule } from '@app/shared/shared-main' | 9 | import { SharedMainModule } from '@app/shared/shared-main' |
@@ -49,6 +50,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom | |||
49 | SharedGlobalIconModule, | 50 | SharedGlobalIconModule, |
50 | SharedAbuseListModule, | 51 | SharedAbuseListModule, |
51 | SharedVideoCommentModule, | 52 | SharedVideoCommentModule, |
53 | SharedActorImageModule, | ||
52 | 54 | ||
53 | TableModule, | 55 | TableModule, |
54 | SelectButtonModule, | 56 | SelectButtonModule, |
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index 82c371f4d..d6aca10e7 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts | |||
@@ -164,7 +164,8 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV | |||
164 | baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`, | 164 | baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`, |
165 | title: false, | 165 | title: false, |
166 | warningTitle: false | 166 | warningTitle: false |
167 | }) | 167 | }), |
168 | entry.video.name | ||
168 | ) | 169 | ) |
169 | } | 170 | } |
170 | 171 | ||
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html index 1b5fe45c6..8edf03a89 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | </div> | 3 | </div> |
4 | 4 | ||
5 | <div class="search-bar"> | 5 | <div class="search-bar"> |
6 | <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..."/> | 6 | <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..." autofocus /> |
7 | </div> | 7 | </div> |
8 | 8 | ||
9 | <div class="alert alert-info" i18n *ngIf="pluginInstalled"> | 9 | <div class="alert alert-info" i18n *ngIf="pluginInstalled"> |
@@ -20,8 +20,8 @@ | |||
20 | <my-global-icon iconName="search"></my-global-icon> | 20 | <my-global-icon iconName="search"></my-global-icon> |
21 | 21 | ||
22 | <ng-container i18n> | 22 | <ng-container i18n> |
23 | {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" | 23 | {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}" |
24 | </ng-container> | 24 | </ng-container> |
25 | </ng-container> | 25 | </ng-container> |
26 | </div> | 26 | </div> |
27 | 27 | ||
diff --git a/client/src/app/+admin/plugins/shared/plugin-list.component.scss b/client/src/app/+admin/plugins/shared/plugin-list.component.scss index 83030b7e0..f59a01b74 100644 --- a/client/src/app/+admin/plugins/shared/plugin-list.component.scss +++ b/client/src/app/+admin/plugins/shared/plugin-list.component.scss | |||
@@ -3,7 +3,7 @@ | |||
3 | 3 | ||
4 | .plugin { | 4 | .plugin { |
5 | margin: 15px 0; | 5 | margin: 15px 0; |
6 | background-color: pvar(--submenuColor); | 6 | background-color: pvar(--submenuBackgroundColor); |
7 | } | 7 | } |
8 | 8 | ||
9 | .first-row { | 9 | .first-row { |
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 243c6556a..5e92c0f36 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -72,7 +72,7 @@ | |||
72 | <div class="anchor" id="user"></div> <!-- user anchor --> | 72 | <div class="anchor" id="user"></div> <!-- user anchor --> |
73 | <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> | 73 | <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> |
74 | <div *ngIf="!isCreation() && user" class="account-title"> | 74 | <div *ngIf="!isCreation() && user" class="account-title"> |
75 | <my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info> | 75 | <my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit> |
76 | </div> | 76 | </div> |
77 | </div> | 77 | </div> |
78 | 78 | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index aa87b8d6d..8b0ac8783 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss | |||
@@ -72,11 +72,3 @@ input[type=submit], button { | |||
72 | @include dashboard; | 72 | @include dashboard; |
73 | max-width: 900px; | 73 | max-width: 900px; |
74 | } | 74 | } |
75 | |||
76 | my-actor-avatar-info ::ng-deep { | ||
77 | .actor-img-edit-container, | ||
78 | .actor-info-followers, | ||
79 | .actor-info-username { | ||
80 | display: none; | ||
81 | } | ||
82 | } | ||
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html index 3171e5b0f..0167066a0 100644 --- a/client/src/app/+login/login.component.html +++ b/client/src/app/+login/login.component.html | |||
@@ -21,7 +21,7 @@ | |||
21 | <label i18n for="username">User</label> | 21 | <label i18n for="username">User</label> |
22 | <input | 22 | <input |
23 | type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1" | 23 | type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1" |
24 | formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #usernameInput | 24 | formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" autofocus |
25 | > | 25 | > |
26 | </div> | 26 | </div> |
27 | 27 | ||
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts index af747b7fa..d8ad49081 100644 --- a/client/src/app/+login/login.component.ts +++ b/client/src/app/+login/login.component.ts | |||
@@ -3,9 +3,9 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angula | |||
3 | import { ActivatedRoute } from '@angular/router' | 3 | import { ActivatedRoute } from '@angular/router' |
4 | import { AuthService, Notifier, RedirectService, UserService } from '@app/core' | 4 | import { AuthService, Notifier, RedirectService, UserService } from '@app/core' |
5 | import { HooksService } from '@app/core/plugins/hooks.service' | 5 | import { HooksService } from '@app/core/plugins/hooks.service' |
6 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | ||
7 | import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' | 6 | import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' |
8 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' |
8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | ||
9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
10 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | 10 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' |
11 | 11 | ||
@@ -16,7 +16,6 @@ import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | |||
16 | }) | 16 | }) |
17 | 17 | ||
18 | export class LoginComponent extends FormReactive implements OnInit, AfterViewInit { | 18 | export class LoginComponent extends FormReactive implements OnInit, AfterViewInit { |
19 | @ViewChild('usernameInput', { static: false }) usernameInput: ElementRef | ||
20 | @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef | 19 | @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef |
21 | 20 | ||
22 | accordion: NgbAccordion | 21 | accordion: NgbAccordion |
@@ -91,10 +90,6 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni | |||
91 | } | 90 | } |
92 | 91 | ||
93 | ngAfterViewInit () { | 92 | ngAfterViewInit () { |
94 | if (this.usernameInput) { | ||
95 | this.usernameInput.nativeElement.focus() | ||
96 | } | ||
97 | |||
98 | this.hooks.runAction('action:login.init', 'login') | 93 | this.hooks.runAction('action:login.init', 'login') |
99 | } | 94 | } |
100 | 95 | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts index ad7497f45..c7e173038 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts | |||
@@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { | |||
42 | newInstanceFollower: $localize`Your instance has a new follower`, | 42 | newInstanceFollower: $localize`Your instance has a new follower`, |
43 | autoInstanceFollowing: $localize`Your instance automatically followed another instance`, | 43 | autoInstanceFollowing: $localize`Your instance automatically followed another instance`, |
44 | abuseNewMessage: $localize`An abuse report received a new message`, | 44 | abuseNewMessage: $localize`An abuse report received a new message`, |
45 | abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators` | 45 | abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`, |
46 | newPeerTubeVersion: $localize`A new PeerTube version is available`, | ||
47 | newPluginVersion: $localize`One of your plugin/theme has a new available version` | ||
46 | } | 48 | } |
47 | this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] | 49 | this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] |
48 | 50 | ||
@@ -51,7 +53,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { | |||
51 | videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, | 53 | videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, |
52 | newUserRegistration: UserRight.MANAGE_USERS, | 54 | newUserRegistration: UserRight.MANAGE_USERS, |
53 | newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, | 55 | newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, |
54 | autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION | 56 | autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION, |
57 | newPeerTubeVersion: UserRight.MANAGE_DEBUG, | ||
58 | newPluginVersion: UserRight.MANAGE_DEBUG | ||
55 | } | 59 | } |
56 | } | 60 | } |
57 | 61 | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index b0d2ec58d..48d06280b 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | <div class="form-group col-12 col-lg-4 col-xl-3"></div> | 3 | <div class="form-group col-12 col-lg-4 col-xl-3"></div> |
4 | 4 | ||
5 | <div class="form-group col-12 col-lg-8 col-xl-9"> | 5 | <div class="form-group col-12 col-lg-8 col-xl-9"> |
6 | <my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info> | 6 | <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit> |
7 | </div> | 7 | </div> |
8 | </div> | 8 | </div> |
9 | 9 | ||
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 076864563..3df48d0aa 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -3,6 +3,7 @@ import { TableModule } from 'primeng/table' | |||
3 | import { DragDropModule } from '@angular/cdk/drag-drop' | 3 | import { DragDropModule } from '@angular/cdk/drag-drop' |
4 | import { NgModule } from '@angular/core' | 4 | import { NgModule } from '@angular/core' |
5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' | 5 | import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' |
6 | import { SharedActorImageModule } from '@app/shared/shared-actor-image' | ||
6 | import { SharedFormModule } from '@app/shared/shared-forms' | 7 | import { SharedFormModule } from '@app/shared/shared-forms' |
7 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 8 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
8 | import { SharedMainModule } from '@app/shared/shared-main' | 9 | import { SharedMainModule } from '@app/shared/shared-main' |
@@ -10,6 +11,7 @@ import { SharedModerationModule } from '@app/shared/shared-moderation' | |||
10 | import { SharedShareModal } from '@app/shared/shared-share-modal' | 11 | import { SharedShareModal } from '@app/shared/shared-share-modal' |
11 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' | 12 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' |
12 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' | 13 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' |
14 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | ||
13 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' | 15 | import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' |
14 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' | 16 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' |
15 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' | 17 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' |
@@ -20,7 +22,6 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d | |||
20 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' | 22 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' |
21 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' | 23 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' |
22 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 24 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
23 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | ||
24 | import { MyAccountComponent } from './my-account.component' | 25 | import { MyAccountComponent } from './my-account.component' |
25 | 26 | ||
26 | @NgModule({ | 27 | @NgModule({ |
@@ -37,7 +38,8 @@ import { MyAccountComponent } from './my-account.component' | |||
37 | SharedUserInterfaceSettingsModule, | 38 | SharedUserInterfaceSettingsModule, |
38 | SharedGlobalIconModule, | 39 | SharedGlobalIconModule, |
39 | SharedAbuseListModule, | 40 | SharedAbuseListModule, |
40 | SharedShareModal | 41 | SharedShareModal, |
42 | SharedActorImageModule | ||
41 | ], | 43 | ], |
42 | 44 | ||
43 | declarations: [ | 45 | declarations: [ |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts index a625493de..b3265210f 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts | |||
@@ -8,10 +8,12 @@ import { | |||
8 | VIDEO_CHANNEL_SUPPORT_VALIDATOR | 8 | VIDEO_CHANNEL_SUPPORT_VALIDATOR |
9 | } from '@app/shared/form-validators/video-channel-validators' | 9 | } from '@app/shared/form-validators/video-channel-validators' |
10 | import { FormValidatorService } from '@app/shared/shared-forms' | 10 | import { FormValidatorService } from '@app/shared/shared-forms' |
11 | import { VideoChannelService } from '@app/shared/shared-main' | 11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
12 | import { VideoChannelCreate } from '@shared/models' | 12 | import { VideoChannelCreate } from '@shared/models' |
13 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 13 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
14 | import { MyVideoChannelEdit } from './my-video-channel-edit' | 14 | import { MyVideoChannelEdit } from './my-video-channel-edit' |
15 | import { switchMap } from 'rxjs/operators' | ||
16 | import { of } from 'rxjs' | ||
15 | 17 | ||
16 | @Component({ | 18 | @Component({ |
17 | templateUrl: './my-video-channel-edit.component.html', | 19 | templateUrl: './my-video-channel-edit.component.html', |
@@ -19,6 +21,10 @@ import { MyVideoChannelEdit } from './my-video-channel-edit' | |||
19 | }) | 21 | }) |
20 | export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { | 22 | export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { |
21 | error: string | 23 | error: string |
24 | videoChannel = new VideoChannel({}) | ||
25 | |||
26 | private avatar: FormData | ||
27 | private banner: FormData | ||
22 | 28 | ||
23 | constructor ( | 29 | constructor ( |
24 | protected formValidatorService: FormValidatorService, | 30 | protected formValidatorService: FormValidatorService, |
@@ -50,23 +56,43 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements | |||
50 | support: body.support || null | 56 | support: body.support || null |
51 | } | 57 | } |
52 | 58 | ||
53 | this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( | 59 | this.videoChannelService.createVideoChannel(videoChannelCreate) |
54 | () => { | 60 | .pipe( |
55 | this.authService.refreshUserInformation() | 61 | switchMap(() => this.uploadAvatar()), |
62 | switchMap(() => this.uploadBanner()) | ||
63 | ).subscribe( | ||
64 | () => { | ||
65 | this.authService.refreshUserInformation() | ||
66 | |||
67 | this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) | ||
68 | this.router.navigate(['/my-library', 'video-channels']) | ||
69 | }, | ||
56 | 70 | ||
57 | this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) | 71 | err => { |
58 | this.router.navigate([ '/my-library', 'video-channels' ]) | 72 | if (err.status === HttpStatusCode.CONFLICT_409) { |
59 | }, | 73 | this.error = $localize`This name already exists on this instance.` |
74 | return | ||
75 | } | ||
60 | 76 | ||
61 | err => { | 77 | this.error = err.message |
62 | if (err.status === HttpStatusCode.CONFLICT_409) { | ||
63 | this.error = $localize`This name already exists on this instance.` | ||
64 | return | ||
65 | } | 78 | } |
79 | ) | ||
80 | } | ||
81 | |||
82 | onAvatarChange (formData: FormData) { | ||
83 | this.avatar = formData | ||
84 | } | ||
85 | |||
86 | onAvatarDelete () { | ||
87 | this.avatar = null | ||
88 | } | ||
89 | |||
90 | onBannerChange (formData: FormData) { | ||
91 | this.banner = formData | ||
92 | } | ||
66 | 93 | ||
67 | this.error = err.message | 94 | onBannerDelete () { |
68 | } | 95 | this.banner = null |
69 | ) | ||
70 | } | 96 | } |
71 | 97 | ||
72 | isCreation () { | 98 | isCreation () { |
@@ -76,4 +102,20 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements | |||
76 | getFormButtonTitle () { | 102 | getFormButtonTitle () { |
77 | return $localize`Create` | 103 | return $localize`Create` |
78 | } | 104 | } |
105 | |||
106 | getUsername () { | ||
107 | return this.form.value.name | ||
108 | } | ||
109 | |||
110 | private uploadAvatar () { | ||
111 | if (!this.avatar) return of(undefined) | ||
112 | |||
113 | return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.avatar, 'avatar') | ||
114 | } | ||
115 | |||
116 | private uploadBanner () { | ||
117 | if (!this.banner) return of(undefined) | ||
118 | |||
119 | return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.banner, 'banner') | ||
120 | } | ||
79 | } | 121 | } |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html index 735f9e3ba..2910dffad 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html | |||
@@ -10,7 +10,7 @@ | |||
10 | <ng-container *ngIf="!isCreation()"> | 10 | <ng-container *ngIf="!isCreation()"> |
11 | <li class="breadcrumb-item active" i18n>Edit</li> | 11 | <li class="breadcrumb-item active" i18n>Edit</li> |
12 | <li class="breadcrumb-item active" aria-current="page"> | 12 | <li class="breadcrumb-item active" aria-current="page"> |
13 | <a *ngIf="videoChannelToUpdate" [routerLink]="[ '/my-library/video-channels/update', videoChannelToUpdate?.nameWithHost ]">{{ videoChannelToUpdate?.displayName }}</a> | 13 | <a *ngIf="videoChannel" [routerLink]="[ '/my-library/video-channels/update', videoChannel?.nameWithHost ]">{{ videoChannel?.displayName }}</a> |
14 | </li> | 14 | </li> |
15 | </ng-container> | 15 | </ng-container> |
16 | </ol> | 16 | </ol> |
@@ -23,10 +23,22 @@ | |||
23 | <div class="form-row"> <!-- channel grid --> | 23 | <div class="form-row"> <!-- channel grid --> |
24 | <div class="form-group col-12 col-lg-4 col-xl-3"> | 24 | <div class="form-group col-12 col-lg-4 col-xl-3"> |
25 | <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div> | 25 | <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div> |
26 | <div *ngIf="!isCreation() && videoChannelToUpdate" class="video-channel-title" i18n>CHANNEL</div> | 26 | <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div> |
27 | </div> | 27 | </div> |
28 | 28 | ||
29 | <div class="form-group col-12 col-lg-8 col-xl-9"> | 29 | <div class="form-group col-12 col-lg-8 col-xl-9"> |
30 | <h6 i18n>Banner image of your channel</h6> | ||
31 | |||
32 | <my-actor-banner-edit | ||
33 | *ngIf="videoChannel" [previewImage]="isCreation()" | ||
34 | [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()" | ||
35 | ></my-actor-banner-edit> | ||
36 | |||
37 | <my-actor-avatar-edit | ||
38 | *ngIf="videoChannel" [previewImage]="isCreation()" | ||
39 | [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()" | ||
40 | [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()" | ||
41 | ></my-actor-avatar-edit> | ||
30 | 42 | ||
31 | <div class="form-group" *ngIf="isCreation()"> | 43 | <div class="form-group" *ngIf="isCreation()"> |
32 | <label i18n for="name">Name</label> | 44 | <label i18n for="name">Name</label> |
@@ -44,11 +56,6 @@ | |||
44 | </div> | 56 | </div> |
45 | </div> | 57 | </div> |
46 | 58 | ||
47 | <my-actor-avatar-info | ||
48 | *ngIf="!isCreation() && videoChannelToUpdate" | ||
49 | [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()" | ||
50 | ></my-actor-avatar-info> | ||
51 | |||
52 | <div class="form-group"> | 59 | <div class="form-group"> |
53 | <label i18n for="display-name">Display name</label> | 60 | <label i18n for="display-name">Display name</label> |
54 | <input | 61 | <input |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss index 8f8af655c..22de103d1 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss | |||
@@ -10,11 +10,16 @@ label { | |||
10 | @include settings-big-title; | 10 | @include settings-big-title; |
11 | } | 11 | } |
12 | 12 | ||
13 | my-actor-avatar-info { | 13 | my-actor-avatar-edit, |
14 | my-actor-banner-edit { | ||
14 | display: block; | 15 | display: block; |
15 | margin-bottom: 20px; | 16 | margin-bottom: 20px; |
16 | } | 17 | } |
17 | 18 | ||
19 | my-actor-banner-edit { | ||
20 | max-width: 500px; | ||
21 | } | ||
22 | |||
18 | .input-group { | 23 | .input-group { |
19 | @include peertube-input-group(fit-content); | 24 | @include peertube-input-group(fit-content); |
20 | } | 25 | } |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts index 3e20a27ee..33bb90f14 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts | |||
@@ -2,8 +2,7 @@ import { FormReactive } from '@app/shared/shared-forms' | |||
2 | import { VideoChannel } from '@app/shared/shared-main' | 2 | import { VideoChannel } from '@app/shared/shared-main' |
3 | 3 | ||
4 | export abstract class MyVideoChannelEdit extends FormReactive { | 4 | export abstract class MyVideoChannelEdit extends FormReactive { |
5 | // We need it even in the create component because it's used in the edit template | 5 | videoChannel: VideoChannel |
6 | videoChannelToUpdate: VideoChannel | ||
7 | 6 | ||
8 | abstract isCreation (): boolean | 7 | abstract isCreation (): boolean |
9 | abstract getFormButtonTitle (): string | 8 | abstract getFormButtonTitle (): string |
@@ -12,10 +11,6 @@ export abstract class MyVideoChannelEdit extends FormReactive { | |||
12 | return window.location.host | 11 | return window.location.host |
13 | } | 12 | } |
14 | 13 | ||
15 | // We need this method so angular does not complain in child template that doesn't need this | ||
16 | onAvatarChange (formData: FormData) { /* empty */ } | ||
17 | onAvatarDelete () { /* empty */ } | ||
18 | |||
19 | // Should be implemented by the child | 14 | // Should be implemented by the child |
20 | isBulkUpdateVideosDisplayed () { | 15 | isBulkUpdateVideosDisplayed () { |
21 | return false | 16 | return false |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts index 6cd1ff503..a29af176c 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { Subscription } from 'rxjs' | 1 | import { Subscription } from 'rxjs' |
2 | import { HttpErrorResponse } from '@angular/common/http' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | 3 | import { Component, OnDestroy, OnInit } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, Notifier, ServerService } from '@app/core' |
6 | import { uploadErrorHandler } from '@app/helpers' | ||
5 | import { | 7 | import { |
6 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, | 8 | VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, |
7 | VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, | 9 | VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, |
@@ -11,8 +13,6 @@ import { FormValidatorService } from '@app/shared/shared-forms' | |||
11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
12 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' | 14 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' |
13 | import { MyVideoChannelEdit } from './my-video-channel-edit' | 15 | import { MyVideoChannelEdit } from './my-video-channel-edit' |
14 | import { HttpErrorResponse } from '@angular/common/http' | ||
15 | import { uploadErrorHandler } from '@app/helpers' | ||
16 | 16 | ||
17 | @Component({ | 17 | @Component({ |
18 | selector: 'my-video-channel-update', | 18 | selector: 'my-video-channel-update', |
@@ -21,7 +21,7 @@ import { uploadErrorHandler } from '@app/helpers' | |||
21 | }) | 21 | }) |
22 | export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { | 22 | export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { |
23 | error: string | 23 | error: string |
24 | videoChannelToUpdate: VideoChannel | 24 | videoChannel: VideoChannel |
25 | 25 | ||
26 | private paramsSub: Subscription | 26 | private paramsSub: Subscription |
27 | private oldSupportField: string | 27 | private oldSupportField: string |
@@ -56,7 +56,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
56 | 56 | ||
57 | this.videoChannelService.getVideoChannel(videoChannelId).subscribe( | 57 | this.videoChannelService.getVideoChannel(videoChannelId).subscribe( |
58 | videoChannelToUpdate => { | 58 | videoChannelToUpdate => { |
59 | this.videoChannelToUpdate = videoChannelToUpdate | 59 | this.videoChannel = videoChannelToUpdate |
60 | 60 | ||
61 | this.oldSupportField = videoChannelToUpdate.support | 61 | this.oldSupportField = videoChannelToUpdate.support |
62 | 62 | ||
@@ -87,7 +87,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
87 | bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false | 87 | bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false |
88 | } | 88 | } |
89 | 89 | ||
90 | this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( | 90 | this.videoChannelService.updateVideoChannel(this.videoChannel.name, videoChannelUpdate).subscribe( |
91 | () => { | 91 | () => { |
92 | this.authService.refreshUserInformation() | 92 | this.authService.refreshUserInformation() |
93 | 93 | ||
@@ -101,12 +101,12 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
101 | } | 101 | } |
102 | 102 | ||
103 | onAvatarChange (formData: FormData) { | 103 | onAvatarChange (formData: FormData) { |
104 | this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) | 104 | this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'avatar') |
105 | .subscribe( | 105 | .subscribe( |
106 | data => { | 106 | data => { |
107 | this.notifier.success($localize`Avatar changed.`) | 107 | this.notifier.success($localize`Avatar changed.`) |
108 | 108 | ||
109 | this.videoChannelToUpdate.updateAvatar(data.avatar) | 109 | this.videoChannel.updateAvatar(data.avatar) |
110 | }, | 110 | }, |
111 | 111 | ||
112 | (err: HttpErrorResponse) => uploadErrorHandler({ | 112 | (err: HttpErrorResponse) => uploadErrorHandler({ |
@@ -118,12 +118,42 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
118 | } | 118 | } |
119 | 119 | ||
120 | onAvatarDelete () { | 120 | onAvatarDelete () { |
121 | this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name) | 121 | this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'avatar') |
122 | .subscribe( | 122 | .subscribe( |
123 | data => { | 123 | data => { |
124 | this.notifier.success($localize`Avatar deleted.`) | 124 | this.notifier.success($localize`Avatar deleted.`) |
125 | 125 | ||
126 | this.videoChannelToUpdate.resetAvatar() | 126 | this.videoChannel.resetAvatar() |
127 | }, | ||
128 | |||
129 | err => this.notifier.error(err.message) | ||
130 | ) | ||
131 | } | ||
132 | |||
133 | onBannerChange (formData: FormData) { | ||
134 | this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'banner') | ||
135 | .subscribe( | ||
136 | data => { | ||
137 | this.notifier.success($localize`Banner changed.`) | ||
138 | |||
139 | this.videoChannel.updateBanner(data.banner) | ||
140 | }, | ||
141 | |||
142 | (err: HttpErrorResponse) => uploadErrorHandler({ | ||
143 | err, | ||
144 | name: $localize`banner`, | ||
145 | notifier: this.notifier | ||
146 | }) | ||
147 | ) | ||
148 | } | ||
149 | |||
150 | onBannerDelete () { | ||
151 | this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'banner') | ||
152 | .subscribe( | ||
153 | data => { | ||
154 | this.notifier.success($localize`Banner deleted.`) | ||
155 | |||
156 | this.videoChannel.resetBanner() | ||
127 | }, | 157 | }, |
128 | 158 | ||
129 | err => this.notifier.error(err.message) | 159 | err => this.notifier.error(err.message) |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss index f2f42459f..8804fa95c 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss | |||
@@ -17,10 +17,11 @@ input[type=text] { | |||
17 | 17 | ||
18 | .video-channel { | 18 | .video-channel { |
19 | @include row-blocks; | 19 | @include row-blocks; |
20 | |||
20 | padding-bottom: 0; | 21 | padding-bottom: 0; |
21 | 22 | ||
22 | img { | 23 | img { |
23 | @include avatar(80px); | 24 | @include channel-avatar(80px); |
24 | 25 | ||
25 | margin-right: 10px; | 26 | margin-right: 10px; |
26 | } | 27 | } |
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts index 92b56db49..53557ca02 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { ChartModule } from 'primeng/chart' | 1 | import { ChartModule } from 'primeng/chart' |
2 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
3 | import { SharedActorImageModule } from '@app/shared/shared-actor-image' | ||
3 | import { SharedFormModule } from '@app/shared/shared-forms' | 4 | import { SharedFormModule } from '@app/shared/shared-forms' |
4 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 5 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
5 | import { SharedMainModule } from '@app/shared/shared-main' | 6 | import { SharedMainModule } from '@app/shared/shared-main' |
@@ -16,7 +17,8 @@ import { MyVideoChannelsComponent } from './my-video-channels.component' | |||
16 | 17 | ||
17 | SharedMainModule, | 18 | SharedMainModule, |
18 | SharedFormModule, | 19 | SharedFormModule, |
19 | SharedGlobalIconModule | 20 | SharedGlobalIconModule, |
21 | SharedActorImageModule | ||
20 | ], | 22 | ], |
21 | 23 | ||
22 | declarations: [ | 24 | declarations: [ |
diff --git a/client/src/app/+my-library/my-history/my-history.component.html b/client/src/app/+my-library/my-history/my-history.component.html index c180161e7..9dec64645 100644 --- a/client/src/app/+my-library/my-history/my-history.component.html +++ b/client/src/app/+my-library/my-history/my-history.component.html | |||
@@ -4,7 +4,7 @@ | |||
4 | </h1> | 4 | </h1> |
5 | 5 | ||
6 | <div class="top-buttons"> | 6 | <div class="top-buttons"> |
7 | <div> | 7 | <div class="search-wrapper"> |
8 | <div class="input-group has-feedback has-clear"> | 8 | <div class="input-group has-feedback has-clear"> |
9 | <input | 9 | <input |
10 | type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history" | 10 | type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history" |
@@ -15,7 +15,7 @@ | |||
15 | </div> | 15 | </div> |
16 | </div> | 16 | </div> |
17 | 17 | ||
18 | <div class="history-switch ml-auto mr-3"> | 18 | <div class="history-switch"> |
19 | <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch> | 19 | <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch> |
20 | <label i18n>Track watch history</label> | 20 | <label i18n>Track watch history</label> |
21 | </div> | 21 | </div> |
diff --git a/client/src/app/+my-library/my-history/my-history.component.scss b/client/src/app/+my-library/my-history/my-history.component.scss index 928a8a3da..af4a34b4b 100644 --- a/client/src/app/+my-library/my-history/my-history.component.scss +++ b/client/src/app/+my-library/my-history/my-history.component.scss | |||
@@ -11,16 +11,24 @@ | |||
11 | 11 | ||
12 | .top-buttons { | 12 | .top-buttons { |
13 | margin-bottom: 30px; | 13 | margin-bottom: 30px; |
14 | display: flex; | 14 | display: grid; |
15 | grid-template-columns: 250px 1fr auto auto; | ||
15 | align-items: center; | 16 | align-items: center; |
16 | flex-wrap: wrap; | ||
17 | 17 | ||
18 | #history-search { | 18 | .search-wrapper { |
19 | @include peertube-input-text(250px); | 19 | grid-column: 1; |
20 | |||
21 | input { | ||
22 | @include peertube-input-text(250px); | ||
23 | } | ||
20 | } | 24 | } |
21 | 25 | ||
22 | .history-switch { | 26 | .history-switch { |
27 | grid-column: 3; | ||
28 | |||
23 | display: flex; | 29 | display: flex; |
30 | margin-left: auto; | ||
31 | margin-right: 15px; | ||
24 | 32 | ||
25 | label { | 33 | label { |
26 | margin: 0 0 0 5px; | 34 | margin: 0 0 0 5px; |
@@ -31,6 +39,8 @@ | |||
31 | } | 39 | } |
32 | 40 | ||
33 | .delete-history { | 41 | .delete-history { |
42 | grid-column: 4; | ||
43 | |||
34 | @include peertube-button; | 44 | @include peertube-button; |
35 | @include grey-button; | 45 | @include grey-button; |
36 | @include button-with-icon; | 46 | @include button-with-icon; |
@@ -40,26 +50,27 @@ | |||
40 | } | 50 | } |
41 | 51 | ||
42 | .video { | 52 | .video { |
43 | @include row-blocks; | 53 | @include row-blocks($column-responsive: false); |
44 | |||
45 | .my-video-miniature { | ||
46 | flex-grow: 1; | ||
47 | } | ||
48 | } | 54 | } |
49 | 55 | ||
50 | @media screen and (max-width: $mobile-view) { | 56 | @media screen and (max-width: $small-view) { |
51 | .top-buttons { | 57 | .top-buttons { |
52 | .history-switch label, .delete-history { | 58 | grid-template-columns: auto 1fr auto; |
53 | @include ellipsis; | 59 | row-gap: 20px; |
54 | } | ||
55 | 60 | ||
56 | .history-switch label { | 61 | .history-switch { |
57 | width: 60%; | 62 | grid-row: 1; |
63 | grid-column: 1; | ||
64 | margin: 0; | ||
58 | } | 65 | } |
59 | 66 | ||
60 | .delete-history { | 67 | .delete-history { |
61 | margin-left: auto; | 68 | grid-row: 1; |
62 | max-width: 32%; | 69 | grid-column: 3; |
70 | } | ||
71 | |||
72 | .search-wrapper { | ||
73 | grid-column: 1 / 4; | ||
63 | } | 74 | } |
64 | } | 75 | } |
65 | } | 76 | } |
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html index 510b400c0..ff448ad87 100644 --- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html +++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html | |||
@@ -6,7 +6,7 @@ | |||
6 | </span> | 6 | </span> |
7 | </h1> | 7 | </h1> |
8 | 8 | ||
9 | <div class="video-subscriptions-header d-flex justify-content-between"> | 9 | <div class="video-subscriptions-header"> |
10 | <div class="has-feedback has-clear"> | 10 | <div class="has-feedback has-clear"> |
11 | <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" | 11 | <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" |
12 | (ngModelChange)="onSubscriptionsSearchChanged()" /> | 12 | (ngModelChange)="onSubscriptionsSearchChanged()" /> |
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss index 5ead45dd8..3c1a4d2ad 100644 --- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss +++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss | |||
@@ -9,40 +9,40 @@ input[type=text] { | |||
9 | @include row-blocks; | 9 | @include row-blocks; |
10 | 10 | ||
11 | img { | 11 | img { |
12 | @include avatar(80px); | 12 | @include channel-avatar(80px); |
13 | 13 | ||
14 | margin-right: 10px; | 14 | margin-right: 10px; |
15 | } | 15 | } |
16 | } | ||
16 | 17 | ||
17 | .video-channel-info { | 18 | .video-channel-info { |
18 | flex-grow: 1; | 19 | flex-grow: 1; |
19 | 20 | ||
20 | a.video-channel-names { | 21 | a.video-channel-names { |
21 | @include disable-default-a-behaviour; | 22 | @include disable-default-a-behaviour; |
22 | 23 | ||
23 | width: fit-content; | 24 | width: fit-content; |
24 | display: flex; | 25 | display: flex; |
25 | align-items: baseline; | 26 | align-items: baseline; |
26 | color: pvar(--mainForegroundColor); | 27 | color: pvar(--mainForegroundColor); |
27 | 28 | ||
28 | .video-channel-display-name { | 29 | .video-channel-display-name { |
29 | font-weight: $font-semibold; | 30 | font-weight: $font-semibold; |
30 | font-size: 18px; | 31 | font-size: 18px; |
31 | } | 32 | } |
32 | 33 | ||
33 | .video-channel-name { | 34 | .video-channel-name { |
34 | font-size: 14px; | 35 | font-size: 14px; |
35 | color: $grey-actor-name; | 36 | color: $grey-actor-name; |
36 | margin-left: 5px; | 37 | margin-left: 5px; |
37 | } | ||
38 | } | 38 | } |
39 | } | 39 | } |
40 | } | ||
40 | 41 | ||
41 | .actor-owner { | 42 | .actor-owner { |
42 | @include actor-owner; | 43 | @include actor-owner; |
43 | 44 | ||
44 | margin-top: 0; | 45 | margin-top: 0; |
45 | } | ||
46 | } | 46 | } |
47 | 47 | ||
48 | .video-subscriptions-header { | 48 | .video-subscriptions-header { |
@@ -50,32 +50,22 @@ input[type=text] { | |||
50 | } | 50 | } |
51 | 51 | ||
52 | @media screen and (max-width: $small-view) { | 52 | @media screen and (max-width: $small-view) { |
53 | .video-channel { | 53 | .video-subscriptions-header input[type=text] { |
54 | .video-channel-info { | 54 | width: 100% !important; |
55 | padding-bottom: 10px; | ||
56 | text-align: center; | ||
57 | |||
58 | .video-channel-names { | ||
59 | flex-direction: column; | ||
60 | align-items: center !important; | ||
61 | margin: auto; | ||
62 | } | ||
63 | } | ||
64 | |||
65 | img { | ||
66 | margin-right: 0; | ||
67 | } | ||
68 | } | 55 | } |
69 | } | ||
70 | 56 | ||
71 | @media screen and (max-width: $mobile-view) { | 57 | .video-channel-info { |
72 | .video-subscriptions-header { | 58 | padding-bottom: 10px; |
73 | flex-direction: column; | 59 | text-align: center; |
74 | 60 | ||
75 | input[type=text] { | 61 | .video-channel-names { |
76 | width: 100% !important; | 62 | flex-direction: column; |
63 | align-items: center !important; | ||
64 | margin: auto; | ||
77 | } | 65 | } |
78 | } | 66 | } |
79 | } | ||
80 | |||
81 | 67 | ||
68 | img { | ||
69 | margin-right: 0; | ||
70 | } | ||
71 | } | ||
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html index a97b2b4fb..e7e3c17b3 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div class="row"> | 1 | <div class="root"> |
2 | 2 | ||
3 | <div class="playlist-info col-xs-12 col-md-5 col-xl-3"> | 3 | <div class="playlist-info"> |
4 | <my-video-playlist-miniature | 4 | <my-video-playlist-miniature |
5 | *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true" | 5 | *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true" |
6 | [displayDescription]="true" [displayPrivacy]="true" | 6 | [displayDescription]="true" [displayPrivacy]="true" |
@@ -20,7 +20,7 @@ | |||
20 | 20 | ||
21 | </div> | 21 | </div> |
22 | 22 | ||
23 | <div class="playlist-elements col-xs-12 col-md-7 col-xl-9"> | 23 | <div class="playlist-elements"> |
24 | <div class="no-results" *ngIf="pagination.totalItems === 0"> | 24 | <div class="no-results" *ngIf="pagination.totalItems === 0"> |
25 | <div i18n>No videos in this playlist.</div> | 25 | <div i18n>No videos in this playlist.</div> |
26 | 26 | ||
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss index de7e1993f..0c68dedf6 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss | |||
@@ -2,21 +2,25 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .root { | ||
6 | display: grid; | ||
7 | grid-template-columns: auto 1fr; | ||
8 | } | ||
9 | |||
5 | .playlist-info { | 10 | .playlist-info { |
6 | background-color: pvar(--submenuColor); | 11 | grid-column: 1; |
7 | margin-left: -$not-expanded-horizontal-margins; | 12 | background-color: pvar(--submenuBackgroundColor); |
13 | margin-left: calc(#{pvar(--horizontalMarginContent)} * -1); | ||
8 | margin-top: -$sub-menu-margin-bottom; | 14 | margin-top: -$sub-menu-margin-bottom; |
9 | 15 | ||
10 | padding: 10px; | 16 | padding: 15px; |
11 | 17 | ||
12 | display: flex; | 18 | display: flex; |
13 | flex-direction: column; | 19 | flex-direction: column; |
14 | justify-content: flex-start; | ||
15 | align-items: center; | ||
16 | 20 | ||
17 | /* fix ellipsis dots background color */ | 21 | /* fix ellipsis dots background color */ |
18 | ::ng-deep .miniature-name::after { | 22 | ::ng-deep .miniature-name::after { |
19 | background-color: pvar(--submenuColor) !important; | 23 | background-color: pvar(--submenuBackgroundColor) !important; |
20 | } | 24 | } |
21 | } | 25 | } |
22 | 26 | ||
@@ -59,15 +63,35 @@ | |||
59 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); | 63 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); |
60 | } | 64 | } |
61 | 65 | ||
62 | @media screen and (max-width: $small-view) { | 66 | .playlist-elements { |
67 | grid-column: 2; | ||
68 | } | ||
69 | |||
70 | my-video-playlist-miniature { | ||
71 | width: $video-thumbnail-width; | ||
72 | } | ||
73 | |||
74 | @include on-small-main-col { | ||
75 | my-video-playlist-miniature { | ||
76 | width: $video-thumbnail-medium-width; | ||
77 | } | ||
78 | } | ||
79 | |||
80 | @include on-mobile-main-col { | ||
81 | .root { | ||
82 | display: block; | ||
83 | } | ||
84 | |||
63 | .playlist-info { | 85 | .playlist-info { |
64 | width: 100vw; | 86 | width: calc(100% + (2 * var(--horizontalMarginContent))); |
65 | padding-top: 20px; | 87 | padding-top: 20px; |
66 | margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1); | 88 | margin-bottom: 10px; |
67 | } | 89 | } |
68 | 90 | ||
69 | .playlist-elements { | 91 | my-video-playlist-miniature, |
70 | padding: 0 !important; | 92 | .playlist-buttons { |
93 | margin-left: auto; | ||
94 | margin-right: auto; | ||
71 | } | 95 | } |
72 | 96 | ||
73 | ::ng-deep my-video-playlist-element-miniature { | 97 | ::ng-deep my-video-playlist-element-miniature { |
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html index afcf6a084..b88ea3db7 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <h1> | 1 | <h1> |
2 | <span> | 2 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> |
3 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> | 3 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> |
4 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> | ||
5 | </span> | ||
6 | </h1> | 4 | </h1> |
7 | 5 | ||
8 | <div class="video-playlists-header d-flex justify-content-between"> | 6 | <div class="video-playlists-header d-flex justify-content-between"> |
@@ -21,10 +19,10 @@ | |||
21 | 19 | ||
22 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 20 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
23 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> | 21 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> |
24 | <div class="miniature-wrapper"> | 22 | <my-video-playlist-miniature |
25 | <my-video-playlist-miniature [playlist]="playlist" [toManage]="true" [displayChannel]="true" [displayDescription]="true" [displayPrivacy]="true" | 23 | [playlist]="playlist" [toManage]="true" [displayChannel]="true" |
26 | ></my-video-playlist-miniature> | 24 | [displayDescription]="true" [displayPrivacy]="true" [displayAsRow]="true" |
27 | </div> | 25 | ></my-video-playlist-miniature> |
28 | 26 | ||
29 | <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons"> | 27 | <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons"> |
30 | <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button> | 28 | <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button> |
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss index 2b7c88246..94187efd4 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss | |||
@@ -1,6 +1,10 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | h1 { | ||
5 | display: flex; | ||
6 | } | ||
7 | |||
4 | .create-button { | 8 | .create-button { |
5 | @include create-button; | 9 | @include create-button; |
6 | } | 10 | } |
@@ -9,64 +13,45 @@ input[type=text] { | |||
9 | @include peertube-input-text(300px); | 13 | @include peertube-input-text(300px); |
10 | } | 14 | } |
11 | 15 | ||
12 | ::ng-deep .action-button { | ||
13 | &.action-button-delete { | ||
14 | margin-right: 10px; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .video-playlist { | 16 | .video-playlist { |
19 | @include row-blocks; | 17 | @include row-blocks($column-responsive: false); |
20 | 18 | } | |
21 | .miniature-wrapper { | ||
22 | flex-grow: 1; | ||
23 | |||
24 | ::ng-deep .miniature { | ||
25 | display: flex; | ||
26 | |||
27 | .miniature-info { | ||
28 | margin-left: 10px; | ||
29 | width: auto; | ||
30 | } | ||
31 | } | ||
32 | } | ||
33 | 19 | ||
34 | .video-playlist-buttons { | 20 | .video-playlist-buttons { |
35 | min-width: 190px; | 21 | display: flex; |
36 | height: max-content; | 22 | margin-left: 10px; |
37 | } | 23 | align-self: flex-end; |
38 | } | 24 | } |
39 | 25 | ||
40 | .video-playlists-header { | 26 | .video-playlists-header { |
41 | margin-bottom: 30px; | 27 | margin-bottom: 30px; |
42 | } | 28 | } |
43 | 29 | ||
44 | @media screen and (max-width: $small-view) { | 30 | my-video-playlist-miniature { |
31 | display: block; | ||
32 | flex-grow: 1; | ||
33 | } | ||
34 | |||
35 | my-delete-button { | ||
36 | margin-right: 10px; | ||
37 | } | ||
38 | |||
39 | @include on-small-main-col { | ||
45 | .video-playlists-header { | 40 | .video-playlists-header { |
46 | text-align: center; | 41 | text-align: center; |
47 | } | 42 | } |
48 | 43 | ||
49 | .video-playlist { | 44 | .video-playlist { |
50 | 45 | flex-wrap: wrap; | |
51 | .video-playlist-buttons { | ||
52 | margin-top: 10px; | ||
53 | } | ||
54 | } | 46 | } |
55 | 47 | ||
56 | my-video-playlist-miniature ::ng-deep .miniature { | 48 | .video-playlist-buttons { |
57 | flex-direction: column; | 49 | margin-top: 10px; |
58 | 50 | margin-left: auto; | |
59 | .miniature-info { | ||
60 | margin-left: 0 !important; | ||
61 | } | ||
62 | |||
63 | .miniature-name { | ||
64 | max-width: $video-thumbnail-width; | ||
65 | } | ||
66 | } | 51 | } |
67 | } | 52 | } |
68 | 53 | ||
69 | @media screen and (max-width: $mobile-view) { | 54 | @include on-mobile-main-col { |
70 | .video-playlists-header { | 55 | .video-playlists-header { |
71 | flex-direction: column; | 56 | flex-direction: column; |
72 | 57 | ||
@@ -75,4 +60,8 @@ input[type=text] { | |||
75 | margin-bottom: 12px; | 60 | margin-bottom: 12px; |
76 | } | 61 | } |
77 | } | 62 | } |
63 | |||
64 | .action-button { | ||
65 | margin-left: 0; | ||
66 | } | ||
78 | } | 67 | } |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html index 5fa4c02ec..e9f436378 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.html +++ b/client/src/app/+my-library/my-videos/my-videos.component.html | |||
@@ -25,6 +25,17 @@ | |||
25 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | 25 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> |
26 | <span class="sr-only" i18n>Clear filters</span> | 26 | <span class="sr-only" i18n>Clear filters</span> |
27 | </div> | 27 | </div> |
28 | |||
29 | <div class="peertube-select-container peertube-select-button"> | ||
30 | <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control"> | ||
31 | <option value="undefined" disabled>Sort by</option> | ||
32 | <option value="-publishedAt" i18n>Last published first</option> | ||
33 | <option value="-createdAt" i18n>Last created first</option> | ||
34 | <option value="-views" i18n>Most viewed first</option> | ||
35 | <option value="-likes" i18n>Most liked first</option> | ||
36 | <option value="-duration" i18n>Longest first</option> | ||
37 | </select> | ||
38 | </div> | ||
28 | </div> | 39 | </div> |
29 | 40 | ||
30 | <my-videos-selection | 41 | <my-videos-selection |
@@ -34,7 +45,6 @@ | |||
34 | [miniatureDisplayOptions]="miniatureDisplayOptions" | 45 | [miniatureDisplayOptions]="miniatureDisplayOptions" |
35 | [titlePage]="titlePage" | 46 | [titlePage]="titlePage" |
36 | [getVideosObservableFunction]="getVideosObservableFunction" | 47 | [getVideosObservableFunction]="getVideosObservableFunction" |
37 | [ownerDisplayType]="ownerDisplayType" | ||
38 | [user]="user" | 48 | [user]="user" |
39 | #videosSelection | 49 | #videosSelection |
40 | > | 50 | > |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.scss b/client/src/app/+my-library/my-videos/my-videos.component.scss index 59fc5fe80..aaf21126b 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.scss +++ b/client/src/app/+my-library/my-videos/my-videos.component.scss | |||
@@ -5,6 +5,11 @@ input[type=text] { | |||
5 | @include peertube-input-text(300px); | 5 | @include peertube-input-text(300px); |
6 | } | 6 | } |
7 | 7 | ||
8 | .peertube-select-container { | ||
9 | @include peertube-select-container(auto); | ||
10 | margin-left: 0.5rem; | ||
11 | } | ||
12 | |||
8 | h1 { | 13 | h1 { |
9 | display: flex; | 14 | display: flex; |
10 | justify-content: space-between; | 15 | justify-content: space-between; |
@@ -32,36 +37,9 @@ h1 { | |||
32 | } | 37 | } |
33 | } | 38 | } |
34 | 39 | ||
35 | ::ng-deep { | ||
36 | .video { | ||
37 | flex-wrap: wrap; | ||
38 | } | ||
39 | |||
40 | .action-button span { | ||
41 | white-space: nowrap; | ||
42 | } | ||
43 | |||
44 | .video-miniature { | ||
45 | &.display-as-row { | ||
46 | // width: min-content !important; | ||
47 | width: 100% !important; | ||
48 | |||
49 | .video-bottom .video-miniature-information { | ||
50 | width: max-content !important; | ||
51 | min-width: unset !important; | ||
52 | } | ||
53 | } | ||
54 | |||
55 | .video-bottom { | ||
56 | max-width: 350px; | ||
57 | } | ||
58 | } | ||
59 | } | ||
60 | |||
61 | .action-button { | 40 | .action-button { |
62 | display: flex; | 41 | display: flex; |
63 | margin-left: 55px; | 42 | margin-left: 10px; |
64 | margin-top: 10px; | ||
65 | align-self: flex-end; | 43 | align-self: flex-end; |
66 | } | 44 | } |
67 | 45 | ||
@@ -69,7 +47,7 @@ my-edit-button { | |||
69 | margin-right: 10px; | 47 | margin-right: 10px; |
70 | } | 48 | } |
71 | 49 | ||
72 | @media screen and (max-width: $small-view) { | 50 | @include on-small-main-col { |
73 | h1 { | 51 | h1 { |
74 | flex-direction: column; | 52 | flex-direction: column; |
75 | 53 | ||
@@ -80,59 +58,25 @@ my-edit-button { | |||
80 | } | 58 | } |
81 | 59 | ||
82 | .action-button { | 60 | .action-button { |
83 | flex-direction: column; | 61 | margin-top: 10px; |
84 | align-self: center; | 62 | margin-left: auto; |
85 | align-items: center; | ||
86 | margin-left: 0px; | ||
87 | } | ||
88 | |||
89 | my-edit-button { | ||
90 | margin: 15px 0 5px 0; | ||
91 | width: 100%; | ||
92 | text-align: center; | ||
93 | |||
94 | ::ng-deep { | ||
95 | .action-button { | ||
96 | /* same width than a.video-thumbnail */ | ||
97 | width: $video-thumbnail-width; | ||
98 | } | ||
99 | } | ||
100 | } | ||
101 | |||
102 | ::ng-deep { | ||
103 | .video-miniature { | ||
104 | align-items: center; | ||
105 | |||
106 | .video-bottom, | ||
107 | .video-bottom .video-miniature-information { | ||
108 | /* same width than a.video-thumbnail */ | ||
109 | max-width: $video-thumbnail-width !important; | ||
110 | } | ||
111 | } | ||
112 | } | 63 | } |
113 | } | 64 | } |
114 | 65 | ||
115 | // Adapt my-video-miniature on small screens with menu | 66 | @include on-mobile-main-col { |
116 | @media screen and (min-width: $small-view) and (max-width: #{breakpoint(lg) + ($not-expanded-horizontal-margins / 3) * 2}) { | ||
117 | :host-context(.main-col:not(.expanded)) { | ||
118 | ::ng-deep { | ||
119 | .video-miniature { | ||
120 | flex-direction: column; | ||
121 | |||
122 | .video-miniature-name { | ||
123 | max-width: $video-thumbnail-width; | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | @media screen and (max-width: $mobile-view) { | ||
131 | .videos-header { | 67 | .videos-header { |
132 | flex-direction: column; | 68 | flex-direction: column; |
133 | 69 | ||
134 | input[type=text] { | 70 | input[type=text] { |
135 | width: 100% !important; | 71 | width: 100%; |
72 | margin-bottom: 12px; | ||
73 | } | ||
74 | .peertube-select-container { | ||
75 | margin-left: 0; | ||
136 | } | 76 | } |
137 | } | 77 | } |
78 | |||
79 | .action-button { | ||
80 | margin-left: 0; | ||
81 | } | ||
138 | } | 82 | } |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index 6a2a62608..356e158d6 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts | |||
@@ -2,12 +2,12 @@ import { concat, Observable, Subject } from 'rxjs' | |||
2 | import { debounceTime, tap, toArray } from 'rxjs/operators' | 2 | import { debounceTime, tap, toArray } from 'rxjs/operators' |
3 | import { Component, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core' | 5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' |
6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
7 | import { immutableAssign } from '@app/helpers' | 7 | import { immutableAssign } from '@app/helpers' |
8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
9 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | 9 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' |
10 | import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' | 10 | import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' |
11 | import { VideoSortField } from '@shared/models' | 11 | import { VideoSortField } from '@shared/models' |
12 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' | 12 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' |
13 | 13 | ||
@@ -36,7 +36,6 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
36 | state: true, | 36 | state: true, |
37 | blacklistInfo: true | 37 | blacklistInfo: true |
38 | } | 38 | } |
39 | ownerDisplayType: OwnerDisplayType = 'videoChannel' | ||
40 | 39 | ||
41 | videoActions: DropdownAction<{ video: Video }>[] = [] | 40 | videoActions: DropdownAction<{ video: Video }>[] = [] |
42 | 41 | ||
@@ -44,6 +43,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
44 | videosSearch: string | 43 | videosSearch: string |
45 | videosSearchChanged = new Subject<string>() | 44 | videosSearchChanged = new Subject<string>() |
46 | getVideosObservableFunction = this.getVideosObservable.bind(this) | 45 | getVideosObservableFunction = this.getVideosObservable.bind(this) |
46 | sort: VideoSortField = '-publishedAt' | ||
47 | 47 | ||
48 | user: User | 48 | user: User |
49 | 49 | ||
@@ -81,6 +81,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
81 | this.videosSearchChanged.next() | 81 | this.videosSearchChanged.next() |
82 | } | 82 | } |
83 | 83 | ||
84 | onChangeSortColumn () { | ||
85 | this.videosSelection.reloadVideos() | ||
86 | } | ||
87 | |||
84 | disableForReuse () { | 88 | disableForReuse () { |
85 | this.videosSelection.disableForReuse() | 89 | this.videosSelection.disableForReuse() |
86 | } | 90 | } |
@@ -89,10 +93,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
89 | this.videosSelection.enabledForReuse() | 93 | this.videosSelection.enabledForReuse() |
90 | } | 94 | } |
91 | 95 | ||
92 | getVideosObservable (page: number, sort: VideoSortField) { | 96 | getVideosObservable (page: number) { |
93 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) | 97 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) |
94 | 98 | ||
95 | return this.videoService.getMyVideos(newPagination, sort, this.videosSearch) | 99 | return this.videoService.getMyVideos(newPagination, this.sort, this.videosSearch) |
96 | .pipe( | 100 | .pipe( |
97 | tap(res => this.pagination.totalItems = res.total) | 101 | tap(res => this.pagination.totalItems = res.total) |
98 | ) | 102 | ) |
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html index 84be4fb14..65d4b6ecd 100644 --- a/client/src/app/+search/search.component.html +++ b/client/src/app/+search/search.component.html | |||
@@ -2,14 +2,12 @@ | |||
2 | <div class="results-header"> | 2 | <div class="results-header"> |
3 | <div class="first-line"> | 3 | <div class="first-line"> |
4 | <div class="results-counter" *ngIf="pagination.totalItems"> | 4 | <div class="results-counter" *ngIf="pagination.totalItems"> |
5 | <span i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} </span> | 5 | <span class="mr-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span> |
6 | 6 | ||
7 | <span i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span> | 7 | <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span> |
8 | <span i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span> | 8 | <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span> |
9 | 9 | ||
10 | <span *ngIf="currentSearch" i18n> | 10 | <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span> |
11 | for <span class="search-value">{{ currentSearch }}</span> | ||
12 | </span> | ||
13 | </div> | 11 | </div> |
14 | 12 | ||
15 | <div | 13 | <div |
@@ -35,11 +33,11 @@ | |||
35 | 33 | ||
36 | <ng-container *ngFor="let result of results"> | 34 | <ng-container *ngFor="let result of results"> |
37 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> | 35 | <div *ngIf="isVideoChannel(result)" class="entry video-channel"> |
38 | <a *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)"> | 36 | <a class="link-avatar" *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)"> |
39 | <img [src]="result.avatarUrl" alt="Avatar" /> | 37 | <img [src]="result.avatarUrl" alt="Avatar" /> |
40 | </a> | 38 | </a> |
41 | 39 | ||
42 | <a *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank"> | 40 | <a class="link-avatar" *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank"> |
43 | <img [src]="result.avatarUrl" alt="Avatar" /> | 41 | <img [src]="result.avatarUrl" alt="Avatar" /> |
44 | </a> | 42 | </a> |
45 | 43 | ||
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss index 64927fa4b..91c8272d7 100644 --- a/client/src/app/+search/search.component.scss +++ b/client/src/app/+search/search.component.scss | |||
@@ -1,159 +1,122 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | @mixin build-channel-img-size ($video-img-width) { | ||
5 | $image-size: min(130px, $video-img-width); | ||
6 | $margin-size: ($video-img-width - $image-size) / 2; // So we have the same width than the video miniature | ||
7 | |||
8 | @include channel-avatar($image-size); | ||
9 | |||
10 | margin: 0 $margin-size 0 $margin-size; | ||
11 | } | ||
12 | |||
4 | .search-result { | 13 | .search-result { |
5 | padding: 40px; | 14 | padding: 40px; |
15 | } | ||
6 | 16 | ||
7 | .results-header { | 17 | .results-header { |
8 | font-size: 16px; | 18 | font-size: 16px; |
9 | padding-bottom: 20px; | 19 | padding-bottom: 20px; |
10 | margin-bottom: 30px; | 20 | margin-bottom: 30px; |
11 | border-bottom: 1px solid #DADADA; | 21 | border-bottom: 1px solid #DADADA; |
12 | 22 | ||
13 | .first-line { | 23 | .first-line { |
14 | display: flex; | 24 | display: flex; |
15 | flex-direction: row; | 25 | flex-direction: row; |
16 | 26 | ||
17 | .results-counter { | 27 | .results-counter { |
18 | flex-grow: 1; | 28 | flex-grow: 1; |
19 | 29 | ||
20 | .search-value { | 30 | .search-value { |
21 | font-weight: $font-semibold; | 31 | font-weight: $font-semibold; |
22 | } | ||
23 | } | 32 | } |
33 | } | ||
24 | 34 | ||
25 | .results-filter-button { | 35 | .results-filter-button { |
26 | cursor: pointer; | 36 | cursor: pointer; |
27 | 37 | ||
28 | .icon.icon-filter { | 38 | .icon.icon-filter { |
29 | @include icon(20px); | 39 | @include icon(20px); |
30 | 40 | ||
31 | position: relative; | 41 | position: relative; |
32 | top: -1px; | 42 | top: -1px; |
33 | margin-right: 5px; | 43 | margin-right: 5px; |
34 | background-image: url('../../assets/images/feather/filter.svg'); | 44 | background-image: url('../../assets/images/feather/filter.svg'); |
35 | } | ||
36 | } | 45 | } |
37 | } | 46 | } |
38 | } | 47 | } |
48 | } | ||
39 | 49 | ||
40 | .entry { | 50 | .entry { |
41 | display: flex; | 51 | display: flex; |
42 | min-height: 130px; | 52 | margin-bottom: 40px; |
43 | padding-bottom: 20px; | 53 | max-width: 800px; |
44 | margin-bottom: 20px; | 54 | } |
45 | 55 | ||
46 | &.video-channel { | 56 | .video-channel { |
47 | img { | 57 | img { |
48 | $image-size: 130px; | 58 | @include build-channel-img-size($video-thumbnail-width); |
49 | $margin-size: ($video-thumbnail-width - $image-size) / 2; // So we have the same width than the video miniature | 59 | } |
60 | } | ||
50 | 61 | ||
51 | @include avatar($image-size); | 62 | .video-channel-info { |
63 | flex-grow: 1; | ||
64 | margin: 0 10px; | ||
65 | width: fit-content; | ||
66 | } | ||
52 | 67 | ||
53 | margin: 0 ($margin-size + 10) 0 $margin-size; | 68 | .video-channel-names { |
54 | } | 69 | @include disable-default-a-behaviour; |
55 | 70 | ||
56 | .video-channel-info { | 71 | display: flex; |
57 | flex-grow: 1; | 72 | align-items: baseline; |
58 | width: fit-content; | 73 | color: pvar(--mainForegroundColor); |
59 | 74 | width: fit-content; | |
60 | .video-channel-names { | ||
61 | @include disable-default-a-behaviour; | ||
62 | |||
63 | display: flex; | ||
64 | align-items: baseline; | ||
65 | color: pvar(--mainForegroundColor); | ||
66 | width: fit-content; | ||
67 | |||
68 | .video-channel-display-name { | ||
69 | font-weight: $font-semibold; | ||
70 | font-size: 18px; | ||
71 | } | ||
72 | |||
73 | .video-channel-name { | ||
74 | font-size: 14px; | ||
75 | color: $grey-actor-name; | ||
76 | margin-left: 5px; | ||
77 | } | ||
78 | } | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | } | 75 | } |
83 | 76 | ||
84 | @media screen and (min-width: $small-view) and (max-width: breakpoint(xl)) { | 77 | .video-channel-display-name { |
85 | .video-channel-info .video-channel-names { | 78 | font-weight: $font-semibold; |
86 | flex-direction: column !important; | 79 | font-size: $video-miniature-row-name-font-size; |
80 | } | ||
87 | 81 | ||
88 | .video-channel-name { | 82 | .video-channel-name { |
89 | @include ellipsis; // Ellipsis and max-width on channel-name to not break screen | 83 | font-size: $video-miniature-row-info-font-size; |
84 | color: pvar(--greyForegroundColor); | ||
85 | margin-left: 5px; | ||
86 | } | ||
90 | 87 | ||
91 | max-width: 250px; | 88 | // Use the same breakpoints than in video-miniature |
92 | margin-left: 0 !important; | 89 | @include on-small-main-col { |
93 | } | 90 | .video-channel { |
94 | } | 91 | display: grid; |
92 | grid-template-columns: auto 1fr; | ||
93 | grid-template-rows: auto auto; | ||
95 | 94 | ||
96 | :host-context(.main-col:not(.expanded)) { | 95 | .link-avatar { |
97 | // Override the min-width: 500px to not break screen | 96 | grid-column: 1; |
98 | ::ng-deep .video-miniature-information { | 97 | grid-row: 1 / -1; |
99 | min-width: 300px !important; | ||
100 | } | 98 | } |
101 | } | ||
102 | } | ||
103 | 99 | ||
104 | @media screen and (min-width: $small-view) and (max-width: breakpoint(lg)) { | 100 | img { |
105 | :host-context(.main-col:not(.expanded)) { | 101 | @include build-channel-img-size($video-thumbnail-medium-width); |
106 | .video-channel-info .video-channel-names { | ||
107 | .video-channel-name { | ||
108 | max-width: 160px; | ||
109 | } | ||
110 | } | 102 | } |
103 | } | ||
111 | 104 | ||
112 | // Override the min-width: 500px to not break screen | 105 | .video-channel-info { |
113 | ::ng-deep .video-miniature-information { | 106 | grid-column: 2; |
114 | min-width: $video-thumbnail-width !important; | 107 | grid-row: 1; |
115 | } | ||
116 | } | 108 | } |
117 | 109 | ||
118 | :host-context(.expanded) { | 110 | my-subscribe-button { |
119 | // Override the min-width: 500px to not break screen | 111 | grid-column: 2; |
120 | ::ng-deep .video-miniature-information { | 112 | grid-row: 2; |
121 | min-width: 300px !important; | 113 | align-self: end; |
122 | } | ||
123 | } | 114 | } |
124 | } | 115 | } |
125 | 116 | ||
126 | @media screen and (max-width: $small-view) { | 117 | @include on-mobile-main-col { |
127 | .search-result { | 118 | .video-channel img { |
128 | .entry.video-channel, | 119 | @include build-channel-img-size($video-thumbnail-small-width); |
129 | .entry.video { | ||
130 | flex-direction: column; | ||
131 | height: auto; | ||
132 | justify-content: center; | ||
133 | align-items: center; | ||
134 | text-align: center; | ||
135 | |||
136 | img { | ||
137 | margin: 0; | ||
138 | } | ||
139 | |||
140 | img { | ||
141 | margin: 0; | ||
142 | } | ||
143 | |||
144 | .video-channel-info .video-channel-names { | ||
145 | align-items: center; | ||
146 | flex-direction: column !important; | ||
147 | |||
148 | .video-channel-name { | ||
149 | margin-left: 0 !important; | ||
150 | } | ||
151 | } | ||
152 | |||
153 | my-subscribe-button { | ||
154 | margin-top: 5px; | ||
155 | } | ||
156 | } | ||
157 | } | 120 | } |
158 | } | 121 | } |
159 | 122 | ||
@@ -164,28 +127,13 @@ | |||
164 | .results-header { | 127 | .results-header { |
165 | font-size: 15px !important; | 128 | font-size: 15px !important; |
166 | } | 129 | } |
130 | } | ||
167 | 131 | ||
168 | .entry { | 132 | .video-channel-display-name { |
169 | &.video { | 133 | font-size: $video-miniature-row-mobile-name-font-size; |
170 | .video-info-name, | 134 | } |
171 | .video-info-account { | 135 | |
172 | margin: auto; | 136 | .video-channel-name { |
173 | } | 137 | font-size: $video-miniature-row-mobile-info-font-size; |
174 | |||
175 | my-video-thumbnail { | ||
176 | margin-right: 0 !important; | ||
177 | |||
178 | ::ng-deep .video-thumbnail { | ||
179 | width: 100%; | ||
180 | height: auto; | ||
181 | |||
182 | img { | ||
183 | width: 100%; | ||
184 | height: auto; | ||
185 | } | ||
186 | } | ||
187 | } | ||
188 | } | ||
189 | } | ||
190 | } | 138 | } |
191 | } | 139 | } |
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html deleted file mode 100644 index 8dff8ba91..000000000 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | <div class="margin-content"> | ||
2 | <div *ngIf="videoChannel" class="row no-gutters"> | ||
3 | <div class="description col-md-6 col-sm-12 pr-2"> | ||
4 | <div class="block"> | ||
5 | <div i18n class="small-title">DESCRIPTION</div> | ||
6 | <div class="content" [innerHtml]="getVideoChannelDescription()"></div> | ||
7 | </div> | ||
8 | |||
9 | <div class="block" *ngIf="supportHTML"> | ||
10 | <div i18n class="small-title">SUPPORT THIS CHANNEL</div> | ||
11 | <div class="content" [innerHtml]="supportHTML"></div> | ||
12 | </div> | ||
13 | </div> | ||
14 | |||
15 | <div class="stats col-md-6 col-sm-12"> | ||
16 | <div class="block"> | ||
17 | <div i18n class="small-title">STATS</div> | ||
18 | <div i18n class="content">Created {{ videoChannel.createdAt | date }}</div> | ||
19 | </div> | ||
20 | </div> | ||
21 | </div> | ||
22 | </div> | ||
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss deleted file mode 100644 index 5bcd4b561..000000000 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss +++ /dev/null | |||
@@ -1,12 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .block { | ||
5 | margin-bottom: 40px; | ||
6 | |||
7 | .small-title { | ||
8 | @include in-content-small-title; | ||
9 | |||
10 | margin-bottom: 20px; | ||
11 | } | ||
12 | } | ||
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts deleted file mode 100644 index 537c7d08e..000000000 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts +++ /dev/null | |||
@@ -1,43 +0,0 @@ | |||
1 | import { Subscription } from 'rxjs' | ||
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
3 | import { MarkdownService } from '@app/core' | ||
4 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-video-channel-about', | ||
8 | templateUrl: './video-channel-about.component.html', | ||
9 | styleUrls: [ './video-channel-about.component.scss' ] | ||
10 | }) | ||
11 | export class VideoChannelAboutComponent implements OnInit, OnDestroy { | ||
12 | videoChannel: VideoChannel | ||
13 | descriptionHTML = '' | ||
14 | supportHTML = '' | ||
15 | |||
16 | private videoChannelSub: Subscription | ||
17 | |||
18 | constructor ( | ||
19 | private videoChannelService: VideoChannelService, | ||
20 | private markdownService: MarkdownService | ||
21 | ) { } | ||
22 | |||
23 | ngOnInit () { | ||
24 | // Parent get the video channel for us | ||
25 | this.videoChannelSub = this.videoChannelService.videoChannelLoaded | ||
26 | .subscribe(async videoChannel => { | ||
27 | this.videoChannel = videoChannel | ||
28 | |||
29 | this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.videoChannel.description) | ||
30 | this.supportHTML = await this.markdownService.enhancedMarkdownToHTML(this.videoChannel.support) | ||
31 | }) | ||
32 | } | ||
33 | |||
34 | ngOnDestroy () { | ||
35 | if (this.videoChannelSub) this.videoChannelSub.unsubscribe() | ||
36 | } | ||
37 | |||
38 | getVideoChannelDescription () { | ||
39 | if (this.descriptionHTML) return this.descriptionHTML | ||
40 | |||
41 | return $localize`No description` | ||
42 | } | ||
43 | } | ||
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html index 03770ceec..b69d1682a 100644 --- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html +++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html | |||
@@ -1,13 +1,13 @@ | |||
1 | <div class="margin-content"> | 1 | <div class="margin-content"> |
2 | <div i18n class="title-page title-page-single"> | 2 | <div i18n class="title-page title-page-single" *ngIf="pagination.totalItems"> |
3 | Created {{ pagination.totalItems }} playlists | 3 | Created {pagination.totalItems, plural, =1 {1 playlist} other {{{ pagination.totalItems }} playlists}} |
4 | </div> | 4 | </div> |
5 | 5 | ||
6 | <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div> | 6 | <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div> |
7 | 7 | ||
8 | <div class="video-playlist" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"> | 8 | <div class="playlists" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"> |
9 | <div *ngFor="let playlist of videoPlaylists" class="playlist-miniature-container"> | 9 | <div *ngFor="let playlist of videoPlaylists" class="playlist-wrapper"> |
10 | <my-video-playlist-miniature [playlist]="playlist" [toManage]="false"></my-video-playlist-miniature> | 10 | <my-video-playlist-miniature [playlist]="playlist" [toManage]="false" [displayAsRow]="displayAsRow()"></my-video-playlist-miniature> |
11 | </div> | 11 | </div> |
12 | </div> | 12 | </div> |
13 | </div> | 13 | </div> |
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss index cb2931858..acd2e409e 100644 --- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss +++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss | |||
@@ -1,14 +1,32 @@ | |||
1 | .title-page { | 1 | @import '_variables'; |
2 | margin-top: 0; | 2 | @import '_mixins'; |
3 | } | 3 | @import '_miniature'; |
4 | 4 | ||
5 | .video-playlist { | 5 | .playlists { |
6 | display: flex; | 6 | display: flex; |
7 | flex-wrap: wrap; | 7 | flex-wrap: wrap; |
8 | justify-content: center; | 8 | justify-content: center; |
9 | 9 | ||
10 | .playlist-miniature-container { | 10 | .playlist-wrapper { |
11 | margin-right: 15px; | 11 | margin-right: 15px; |
12 | margin-bottom: 30px; | 12 | margin-bottom: 30px; |
13 | } | 13 | } |
14 | } | 14 | } |
15 | |||
16 | .margin-content { | ||
17 | @include grid-videos-miniature-layout; | ||
18 | } | ||
19 | |||
20 | @media screen and (max-width: $mobile-view) { | ||
21 | .title-page { | ||
22 | display: block; | ||
23 | text-align: center; | ||
24 | } | ||
25 | |||
26 | .playlists { | ||
27 | justify-content: left; | ||
28 | |||
29 | margin-left: pvar(--horizontalMarginContent) !important; | ||
30 | margin-right: pvar(--horizontalMarginContent) !important; | ||
31 | } | ||
32 | } | ||
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts index 8b507c626..14465bb8d 100644 --- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts +++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Subject, Subscription } from 'rxjs' | 1 | import { Subject, Subscription } from 'rxjs' |
2 | import { Component, OnDestroy, OnInit } from '@angular/core' | 2 | import { Component, OnDestroy, OnInit } from '@angular/core' |
3 | import { ComponentPagination, hasMoreItems } from '@app/core' | 3 | import { ComponentPagination, hasMoreItems, ScreenService } from '@app/core' |
4 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 4 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
5 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 5 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
6 | 6 | ||
@@ -25,7 +25,8 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy { | |||
25 | 25 | ||
26 | constructor ( | 26 | constructor ( |
27 | private videoPlaylistService: VideoPlaylistService, | 27 | private videoPlaylistService: VideoPlaylistService, |
28 | private videoChannelService: VideoChannelService | 28 | private videoChannelService: VideoChannelService, |
29 | private screenService: ScreenService | ||
29 | ) {} | 30 | ) {} |
30 | 31 | ||
31 | ngOnInit () { | 32 | ngOnInit () { |
@@ -48,6 +49,10 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy { | |||
48 | this.loadVideoPlaylists() | 49 | this.loadVideoPlaylists() |
49 | } | 50 | } |
50 | 51 | ||
52 | displayAsRow () { | ||
53 | return this.screenService.isInMobileView() | ||
54 | } | ||
55 | |||
51 | private loadVideoPlaylists () { | 56 | private loadVideoPlaylists () { |
52 | this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination) | 57 | this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination) |
53 | .subscribe(res => { | 58 | .subscribe(res => { |
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index 803651505..d83fc1324 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts | |||
@@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router' | |||
5 | import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' | 5 | import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' |
6 | import { immutableAssign } from '@app/helpers' | 6 | import { immutableAssign } from '@app/helpers' |
7 | import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' | 7 | import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' |
8 | import { AbstractVideoList } from '@app/shared/shared-video-miniature' | 8 | import { AbstractVideoList, MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' |
9 | import { VideoFilter } from '@shared/models' | 9 | import { VideoFilter } from '@shared/models' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
@@ -16,12 +16,24 @@ import { VideoFilter } from '@shared/models' | |||
16 | ] | 16 | ] |
17 | }) | 17 | }) |
18 | export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | 18 | export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { |
19 | // No value because we don't want a page title | ||
19 | titlePage: string | 20 | titlePage: string |
20 | loadOnInit = false | 21 | loadOnInit = false |
21 | loadUserVideoPreferences = true | 22 | loadUserVideoPreferences = true |
22 | 23 | ||
23 | filter: VideoFilter = null | 24 | filter: VideoFilter = null |
24 | 25 | ||
26 | displayOptions: MiniatureDisplayOptions = { | ||
27 | date: true, | ||
28 | views: true, | ||
29 | by: false, | ||
30 | avatar: false, | ||
31 | privacyLabel: true, | ||
32 | privacyText: false, | ||
33 | state: false, | ||
34 | blacklistInfo: false | ||
35 | } | ||
36 | |||
25 | private videoChannel: VideoChannel | 37 | private videoChannel: VideoChannel |
26 | private videoChannelSub: Subscription | 38 | private videoChannelSub: Subscription |
27 | 39 | ||
@@ -83,13 +95,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
83 | 95 | ||
84 | return this.videoService | 96 | return this.videoService |
85 | .getVideoChannelVideos(options) | 97 | .getVideoChannelVideos(options) |
86 | .pipe( | ||
87 | tap(({ total }) => { | ||
88 | this.titlePage = total === 1 | ||
89 | ? $localize`Published 1 video` | ||
90 | : $localize`Published ${total} videos` | ||
91 | }) | ||
92 | ) | ||
93 | } | 98 | } |
94 | 99 | ||
95 | generateSyndicationList () { | 100 | generateSyndicationList () { |
@@ -101,4 +106,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
101 | 106 | ||
102 | this.reloadVideos() | 107 | this.reloadVideos() |
103 | } | 108 | } |
109 | |||
110 | displayAsRow () { | ||
111 | return this.screenService.isInMobileView() | ||
112 | } | ||
104 | } | 113 | } |
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts index f8c32f14e..fcaad8934 100644 --- a/client/src/app/+video-channels/video-channels-routing.module.ts +++ b/client/src/app/+video-channels/video-channels-routing.module.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouterModule, Routes } from '@angular/router' | 2 | import { RouterModule, Routes } from '@angular/router' |
3 | import { MetaGuard } from '@ngx-meta/core' | 3 | import { MetaGuard } from '@ngx-meta/core' |
4 | import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component' | ||
5 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' | 4 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' |
6 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' | 5 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' |
7 | import { VideoChannelsComponent } from './video-channels.component' | 6 | import { VideoChannelsComponent } from './video-channels.component' |
@@ -38,15 +37,6 @@ const videoChannelsRoutes: Routes = [ | |||
38 | title: $localize`Video channel playlists` | 37 | title: $localize`Video channel playlists` |
39 | } | 38 | } |
40 | } | 39 | } |
41 | }, | ||
42 | { | ||
43 | path: 'about', | ||
44 | component: VideoChannelAboutComponent, | ||
45 | data: { | ||
46 | meta: { | ||
47 | title: $localize`About video channel` | ||
48 | } | ||
49 | } | ||
50 | } | 40 | } |
51 | ] | 41 | ] |
52 | } | 42 | } |
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 4b0d12b6e..1312a1b3c 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html | |||
@@ -1,50 +1,129 @@ | |||
1 | <div *ngIf="videoChannel" class="row"> | 1 | <div class="root" *ngIf="videoChannel"> |
2 | <div class="sub-menu"> | 2 | <div class="banner" *ngIf="videoChannel.bannerUrl"> |
3 | 3 | <img [src]="videoChannel.bannerUrl" alt="Channel banner"> | |
4 | <div class="actor"> | 4 | </div> |
5 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | 5 | |
6 | 6 | <div class="channel-info"> | |
7 | <div class="actor-info"> | 7 | |
8 | <div class="actor-names"> | 8 | <ng-template #buttonsTemplate> |
9 | <div class="actor-display-name">{{ videoChannel.displayName }}</div> | 9 | <a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n> |
10 | <div class="actor-name"> | 10 | Manage channel |
11 | <span>{{ videoChannel.nameWithHost }}</span> | 11 | </a> |
12 | <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" | 12 | |
13 | class="btn btn-outline-secondary btn-sm copy-button" | 13 | <my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> |
14 | > | 14 | |
15 | <span class="glyphicon glyphicon-copy"></span> | 15 | <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted"> |
16 | </button> | 16 | <my-global-icon iconName="support" aria-hidden="true"></my-global-icon> |
17 | <span class="icon-text" i18n>Support</span> | ||
18 | </button> | ||
19 | </ng-template> | ||
20 | |||
21 | <ng-template #ownerTemplate> | ||
22 | <div class="owner-block"> | ||
23 | <div class="avatar-row"> | ||
24 | <a [routerLink]="getAccountUrl()" title="View account" i18n-title> | ||
25 | <img class="account-avatar" [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | ||
26 | </a> | ||
27 | |||
28 | <div class="actor-info"> | ||
29 | <h4> | ||
30 | <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a> | ||
31 | </h4> | ||
32 | |||
33 | <div class="actor-handle">@{{ videoChannel.ownerBy }}</div> | ||
17 | </div> | 34 | </div> |
18 | </div> | 35 | </div> |
19 | 36 | ||
20 | <div class="right-buttons"> | 37 | <div class="owner-description"> |
21 | <a *ngIf="isChannelManageable && !isInSmallView" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n> | 38 | <div class="description-html" [innerHTML]="ownerDescriptionHTML"></div> |
22 | Manage channel | ||
23 | </a> | ||
24 | <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> | ||
25 | </div> | 39 | </div> |
26 | 40 | ||
27 | <div class="actor-lower"> | 41 | <a class="view-account short" [routerLink]="getAccountUrl()" i18n> |
28 | <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | 42 | View account |
43 | </a> | ||
29 | 44 | ||
30 | <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> | 45 | <a class="view-account complete" [routerLink]="getAccountUrl()" i18n> |
31 | <span class="d-inline-flex"><span i18n class="d-none d-sm-block mr-1">Created by</span>{{ videoChannel.ownerBy }}</span> | 46 | View owner account |
32 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | 47 | </a> |
33 | </a> | 48 | </div> |
49 | </ng-template> | ||
50 | |||
51 | <div class="channel-avatar-row"> | ||
52 | <img class="channel-avatar" [src]="videoChannel.avatarUrl" alt="Avatar" /> | ||
53 | |||
54 | <div> | ||
55 | <div class="section-label" i18n>VIDEO CHANNEL</div> | ||
56 | |||
57 | <div class="actor-info"> | ||
58 | <div> | ||
59 | <div class="actor-display-name"> | ||
60 | <h1>{{ videoChannel.displayName }}</h1> | ||
61 | </div> | ||
62 | |||
63 | <div class="actor-handle"> | ||
64 | <span>@{{ videoChannel.nameWithHost }}</span> | ||
65 | <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" | ||
66 | class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title | ||
67 | > | ||
68 | <span class="glyphicon glyphicon-duplicate"></span> | ||
69 | </button> | ||
70 | </div> | ||
71 | |||
72 | <div class="actor-counters"> | ||
73 | <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span> | ||
74 | |||
75 | <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n> | ||
76 | {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}} | ||
77 | </span> | ||
78 | </div> | ||
79 | </div> | ||
80 | |||
81 | <div class="channel-buttons right"> | ||
82 | <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template> | ||
83 | </div> | ||
34 | </div> | 84 | </div> |
35 | </div> | 85 | </div> |
36 | </div> | 86 | </div> |
37 | 87 | ||
38 | <div class="links w-100"> | 88 | <div class="channel-description" [ngClass]="{ expanded: channelDescriptionExpanded }"> |
39 | <ng-template #linkTemplate let-item="item"> | 89 | <div class="description-html" [innerHTML]="channelDescriptionHTML"></div> |
40 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | ||
41 | </ng-template> | ||
42 | 90 | ||
43 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | 91 | <div class="created-at" i18n>Channel created on {{ videoChannel.createdAt | date }}</div> |
44 | </div> | 92 | </div> |
93 | |||
94 | <div *ngIf="hasShowMoreDescription()" class="show-more" role="button" | ||
95 | (click)="channelDescriptionExpanded = !channelDescriptionExpanded" | ||
96 | title="Show the complete description" i18n-title i18n | ||
97 | > | ||
98 | Show more... | ||
99 | </div> | ||
100 | |||
101 | <div class="channel-buttons bottom"> | ||
102 | <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template> | ||
103 | </div> | ||
104 | |||
105 | <div class="owner-card"> | ||
106 | <div class="section-label" i18n>OWNER ACCOUNT</div> | ||
107 | |||
108 | <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template> | ||
109 | </div> | ||
110 | </div> | ||
111 | |||
112 | <div class="bottom-owner"> | ||
113 | <div class="section-label" i18n>OWNER ACCOUNT</div> | ||
114 | |||
115 | <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template> | ||
45 | </div> | 116 | </div> |
46 | 117 | ||
47 | <div class="margin-content"> | 118 | <div class="links"> |
48 | <router-outlet></router-outlet> | 119 | <ng-template #linkTemplate let-item="item"> |
120 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | ||
121 | </ng-template> | ||
122 | |||
123 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | ||
49 | </div> | 124 | </div> |
125 | |||
126 | <router-outlet></router-outlet> | ||
50 | </div> | 127 | </div> |
128 | |||
129 | <my-support-modal #supportModal [videoChannel]="videoChannel"></my-support-modal> | ||
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 22f21dcc6..b19b4c81b 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss | |||
@@ -1,89 +1,308 @@ | |||
1 | // Bootstrap grid utilities require functions, variables and mixins | ||
2 | @import 'node_modules/bootstrap/scss/functions'; | ||
3 | @import 'node_modules/bootstrap/scss/variables'; | ||
4 | @import 'node_modules/bootstrap/scss/mixins'; | ||
5 | @import 'node_modules/bootstrap/scss/grid'; | ||
6 | |||
7 | @import '_variables'; | 1 | @import '_variables'; |
8 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_actor'; | ||
4 | @import '_miniature'; | ||
9 | 5 | ||
10 | .sub-menu { | 6 | .root { |
11 | @include sub-menu-with-actor; | 7 | --myGlobalTopPadding: 60px; |
8 | --myChannelImgMargin: 30px; | ||
9 | --myFontSize: 16px; | ||
10 | --myGreyChannelFontSize: 16px; | ||
11 | --myGreyOwnerFontSize: 14px; | ||
12 | } | ||
12 | 13 | ||
13 | .actor, .actor-info { | 14 | .banner { |
14 | width: 100%; | 15 | @include block-ratio('img', $banner-inverted-ratio); |
15 | } | 16 | } |
16 | 17 | ||
17 | .actor-info { | 18 | .section-label { |
18 | display: grid !important; | 19 | @include section-label-responsive; |
19 | grid-template-columns: 1fr auto; | 20 | } |
20 | grid-template-rows: 1fr auto / 1fr auto; | ||
21 | grid-template-areas: "name buttons" "lower buttons"; | ||
22 | 21 | ||
23 | @include media-breakpoint-down(lg) { | 22 | .links { |
24 | grid-template-areas: "name name" "lower buttons"; | 23 | @include grid-videos-miniature-margins; |
25 | } | 24 | } |
25 | |||
26 | .actor-info { | ||
27 | min-width: 1px; | ||
28 | width: 100%; | ||
29 | |||
30 | > h4, | ||
31 | > .actor-handle { | ||
32 | @include ellipsis; | ||
26 | } | 33 | } |
34 | } | ||
35 | |||
36 | .channel-info { | ||
37 | @include grid-videos-miniature-margins(false, 15px); | ||
38 | |||
39 | display: grid; | ||
40 | grid-template-columns: 1fr auto; | ||
41 | grid-template-rows: auto auto; | ||
42 | |||
43 | background-color: pvar(--channelBackgroundColor); | ||
44 | margin-bottom: 45px; | ||
45 | padding-top: var(--myGlobalTopPadding); | ||
46 | font-size: var(--myFontSize); | ||
47 | } | ||
48 | |||
49 | .channel-avatar-row { | ||
50 | @include avatar-row-responsive(var(--myChannelImgMargin), var(--myGreyChannelFontSize)); | ||
51 | } | ||
52 | |||
53 | .support-button { | ||
54 | @include button-with-icon(21px, 0, -1px); | ||
55 | } | ||
56 | |||
57 | .channel-description { | ||
58 | grid-column: 1; | ||
59 | word-break: break-word; | ||
60 | } | ||
61 | |||
62 | .show-more { | ||
63 | @include show-more-description; | ||
64 | |||
65 | display: none; | ||
66 | } | ||
67 | |||
68 | .channel-buttons { | ||
69 | display: flex; | ||
70 | flex-wrap: wrap; | ||
27 | 71 | ||
28 | .actor-names { | 72 | > *:not(:last-child) { |
29 | grid-area: name; | 73 | margin-right: 15px; |
30 | } | 74 | } |
75 | } | ||
76 | |||
77 | .channel-buttons.right { | ||
78 | margin-left: 45px; | ||
79 | } | ||
80 | |||
81 | // Only used by mobile | ||
82 | .channel-buttons.bottom { | ||
83 | display: none; | ||
84 | } | ||
85 | |||
86 | .created-at { | ||
87 | margin-top: 15px; | ||
88 | color: pvar(--greyForegroundColor); | ||
89 | padding-bottom: 60px; | ||
90 | } | ||
91 | |||
92 | .owner-card { | ||
93 | margin-left: 105px; | ||
94 | grid-column: 2; | ||
95 | // Takes all the column | ||
96 | grid-row: 1 / 3; | ||
97 | place-self: end; | ||
98 | } | ||
99 | |||
100 | // Only used on mobile | ||
101 | .bottom-owner { | ||
102 | display: none; | ||
103 | } | ||
104 | |||
105 | .owner-block { | ||
106 | background-color: pvar(--mainBackgroundColor); | ||
107 | padding: 30px; | ||
108 | width: 300px; | ||
109 | font-size: var(--myFontSize); | ||
31 | 110 | ||
32 | .actor-name { | 111 | .avatar-row { |
33 | flex-grow: 1; | 112 | display: flex; |
113 | margin-bottom: 15px; | ||
34 | 114 | ||
35 | .copy-button { | 115 | img { |
36 | border: none; | 116 | @include avatar(48px); |
37 | padding: 5px; | ||
38 | margin-top: -2px; | ||
39 | } | 117 | } |
118 | |||
119 | .actor-info { | ||
120 | margin-left: 15px; | ||
121 | } | ||
122 | |||
123 | h4 { | ||
124 | font-size: 18px; | ||
125 | margin: 0; | ||
126 | |||
127 | a { | ||
128 | color: pvar(--mainForegroundColor); | ||
129 | } | ||
130 | } | ||
131 | |||
132 | .actor-handle { | ||
133 | font-size: var(--myGreyOwnerFontSize); | ||
134 | color: pvar(--greyForegroundColor); | ||
135 | } | ||
136 | } | ||
137 | |||
138 | .owner-description { | ||
139 | max-height: 140px; | ||
140 | word-break: break-word; | ||
141 | |||
142 | @include fade-text(120px, pvar(--mainBackgroundColor)); | ||
40 | } | 143 | } |
41 | } | 144 | } |
42 | 145 | ||
43 | .margin-content { | 146 | .view-account.short { |
44 | // margin-content is required, but child views have their own margins | 147 | @include peertube-button-link; |
45 | // that match views outside the scope of accounts, so we only align | 148 | @include orange-button-inverted; |
46 | // them with the margins of .sub-menu when required. | 149 | |
47 | margin: 0; | 150 | margin-top: 30px; |
151 | } | ||
152 | |||
153 | .view-account.complete { | ||
154 | display: none; | ||
48 | } | 155 | } |
49 | 156 | ||
50 | .right-buttons { | 157 | .copy-button { |
51 | display: flex; | 158 | border: none; |
52 | height: max-content; | 159 | } |
53 | margin-left: auto; | 160 | |
54 | margin-top: 10px; | 161 | @media screen and (max-width: 1400px) { |
162 | // Takes all the row width | ||
163 | .channel-avatar-row { | ||
164 | grid-column: 1 / 3; | ||
165 | } | ||
166 | |||
167 | .owner-card { | ||
168 | grid-row: 2; | ||
169 | margin-left: 60px; | ||
170 | } | ||
171 | } | ||
172 | |||
173 | @media screen and (max-width: 1100px) { | ||
174 | .root { | ||
175 | --myGlobalTopPadding: 45px; | ||
176 | --myChannelImgMargin: 15px; | ||
177 | } | ||
178 | |||
179 | .channel-info { | ||
180 | display: flex; | ||
181 | flex-direction: column; | ||
182 | margin-bottom: 0; | ||
183 | } | ||
184 | |||
185 | .channel-description:not(.expanded) { | ||
186 | max-height: 70px; | ||
187 | |||
188 | @include fade-text(30px, pvar(--channelBackgroundColor)); | ||
189 | } | ||
190 | |||
191 | .show-more { | ||
192 | display: inline-block; | ||
193 | } | ||
194 | |||
195 | .channel-buttons.bottom { | ||
196 | display: flex; | ||
197 | justify-content: center; | ||
198 | margin-bottom: 30px; | ||
199 | } | ||
200 | |||
201 | .channel-buttons.right { | ||
202 | display: none; | ||
203 | } | ||
204 | |||
205 | .owner-card { | ||
206 | display: none; | ||
207 | } | ||
208 | |||
209 | .bottom-owner { | ||
210 | display: block; | ||
211 | width: 100%; | ||
212 | border-bottom: 2px solid $separator-border-color; | ||
213 | padding: var(--myGlobalTopPadding) 45px; | ||
214 | margin-bottom: 60px; | ||
215 | } | ||
55 | 216 | ||
56 | grid-row: buttons-start / span buttons-end; | 217 | .owner-block { |
57 | grid-column: buttons-start; | 218 | display: grid; |
219 | width: 100%; | ||
220 | padding: 0; | ||
58 | 221 | ||
59 | @include media-breakpoint-down(lg) { | 222 | .avatar-row { |
60 | flex-flow: column-reverse; | 223 | grid-column: 1; |
224 | margin-right: 30px; | ||
225 | } | ||
226 | |||
227 | .owner-description { | ||
228 | grid-column: 2; | ||
229 | max-height: 70px; | ||
230 | |||
231 | @include fade-text(30px, pvar(--mainBackgroundColor)); | ||
232 | } | ||
61 | 233 | ||
62 | a { | 234 | .view-account { |
63 | margin-top: 0.25rem; | 235 | grid-column: 2; |
64 | margin-right: 0 !important; | ||
65 | } | 236 | } |
66 | } | 237 | } |
67 | 238 | ||
68 | a { | 239 | .view-account.complete { |
69 | @include peertube-button-outline; | 240 | display: block; |
70 | line-height: 1.8; | 241 | text-align: right; |
242 | margin-top: 10px; | ||
243 | color: pvar(--mainColor); | ||
71 | } | 244 | } |
72 | 245 | ||
73 | my-subscribe-button { | 246 | .view-account.short { |
74 | height: min-content; | 247 | display: none; |
75 | } | 248 | } |
76 | } | 249 | } |
77 | 250 | ||
78 | @media screen and (max-width: $mobile-view) { | 251 | @media screen and (max-width: $mobile-view) { |
79 | .sub-menu { | 252 | .root { |
80 | .actor { | 253 | --myGlobalTopPadding: 15px; |
81 | flex-direction: column; | 254 | --myFontSize: 14px; |
255 | --myGreyChannelFontSize: 13px; | ||
256 | --myGreyOwnerFontSize: 13px; | ||
257 | } | ||
258 | |||
259 | .links { | ||
260 | margin: auto !important; | ||
261 | width: min-content; | ||
262 | } | ||
263 | |||
264 | .show-more { | ||
265 | margin-bottom: 30px; | ||
266 | } | ||
267 | |||
268 | .bottom-owner { | ||
269 | padding: 15px; | ||
270 | margin-bottom: 30px; | ||
82 | 271 | ||
83 | .actor-info .actor-names { | 272 | .section-label { |
273 | display: none; | ||
274 | } | ||
275 | } | ||
276 | |||
277 | .owner-block { | ||
278 | display: block; | ||
279 | |||
280 | .avatar-row { | ||
281 | display: flex; | ||
282 | flex-direction: row-reverse; | ||
283 | margin: 0; | ||
284 | |||
285 | h4 { | ||
286 | font-size: 16px; | ||
287 | } | ||
288 | |||
289 | .actor-info { | ||
290 | display: flex; | ||
84 | flex-direction: column; | 291 | flex-direction: column; |
85 | align-items: normal; | 292 | align-items: flex-end; |
293 | justify-content: flex-end; | ||
294 | margin-top: -5px; | ||
295 | } | ||
296 | |||
297 | img { | ||
298 | @include channel-avatar(64px); | ||
299 | |||
300 | margin: -30px 0 0 15px; | ||
86 | } | 301 | } |
87 | } | 302 | } |
303 | |||
304 | .owner-description { | ||
305 | display: none; | ||
306 | } | ||
88 | } | 307 | } |
89 | } | 308 | } |
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts index bb601e227..41fdb5e79 100644 --- a/client/src/app/+video-channels/video-channels.component.ts +++ b/client/src/app/+video-channels/video-channels.component.ts | |||
@@ -3,8 +3,9 @@ import { Subscription } from 'rxjs' | |||
3 | import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators' | 3 | import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators' |
4 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { AuthService, Notifier, RestExtractor, ScreenService } from '@app/core' | 6 | import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core' |
7 | import { ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 7 | import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' |
8 | import { SupportModalComponent } from '@app/shared/shared-support-modal' | ||
8 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 9 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
10 | 11 | ||
@@ -14,12 +15,18 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | |||
14 | }) | 15 | }) |
15 | export class VideoChannelsComponent implements OnInit, OnDestroy { | 16 | export class VideoChannelsComponent implements OnInit, OnDestroy { |
16 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 17 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
18 | @ViewChild('supportModal') supportModal: SupportModalComponent | ||
17 | 19 | ||
18 | videoChannel: VideoChannel | 20 | videoChannel: VideoChannel |
19 | hotkeys: Hotkey[] | 21 | hotkeys: Hotkey[] |
20 | links: ListOverflowItem[] = [] | 22 | links: ListOverflowItem[] = [] |
21 | isChannelManageable = false | 23 | isChannelManageable = false |
22 | 24 | ||
25 | channelVideosCount: number | ||
26 | ownerDescriptionHTML = '' | ||
27 | channelDescriptionHTML = '' | ||
28 | channelDescriptionExpanded = false | ||
29 | |||
23 | private routeSub: Subscription | 30 | private routeSub: Subscription |
24 | 31 | ||
25 | constructor ( | 32 | constructor ( |
@@ -27,9 +34,11 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
27 | private notifier: Notifier, | 34 | private notifier: Notifier, |
28 | private authService: AuthService, | 35 | private authService: AuthService, |
29 | private videoChannelService: VideoChannelService, | 36 | private videoChannelService: VideoChannelService, |
37 | private videoService: VideoService, | ||
30 | private restExtractor: RestExtractor, | 38 | private restExtractor: RestExtractor, |
31 | private hotkeysService: HotkeysService, | 39 | private hotkeysService: HotkeysService, |
32 | private screenService: ScreenService | 40 | private screenService: ScreenService, |
41 | private markdown: MarkdownService | ||
33 | ) { } | 42 | ) { } |
34 | 43 | ||
35 | ngOnInit () { | 44 | ngOnInit () { |
@@ -43,16 +52,14 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
43 | HttpStatusCode.NOT_FOUND_404 | 52 | HttpStatusCode.NOT_FOUND_404 |
44 | ])) | 53 | ])) |
45 | ) | 54 | ) |
46 | .subscribe(videoChannel => { | 55 | .subscribe(async videoChannel => { |
56 | this.channelDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.description) | ||
57 | this.ownerDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.ownerAccount.description) | ||
58 | |||
59 | // After the markdown renderer to avoid layout changes | ||
47 | this.videoChannel = videoChannel | 60 | this.videoChannel = videoChannel |
48 | 61 | ||
49 | if (this.authService.isLoggedIn()) { | 62 | this.loadChannelVideosCount() |
50 | this.authService.userInformationLoaded | ||
51 | .subscribe(() => { | ||
52 | const channelUserId = this.videoChannel.ownerAccount.userId | ||
53 | this.isChannelManageable = channelUserId && channelUserId === this.authService.getUser().id | ||
54 | }) | ||
55 | } | ||
56 | }) | 63 | }) |
57 | 64 | ||
58 | this.hotkeys = [ | 65 | this.hotkeys = [ |
@@ -67,8 +74,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
67 | 74 | ||
68 | this.links = [ | 75 | this.links = [ |
69 | { label: $localize`VIDEOS`, routerLink: 'videos' }, | 76 | { label: $localize`VIDEOS`, routerLink: 'videos' }, |
70 | { label: $localize`VIDEO PLAYLISTS`, routerLink: 'video-playlists' }, | 77 | { label: $localize`PLAYLISTS`, routerLink: 'video-playlists' } |
71 | { label: $localize`ABOUT`, routerLink: 'about' } | ||
72 | ] | 78 | ] |
73 | } | 79 | } |
74 | 80 | ||
@@ -79,7 +85,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
79 | if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) | 85 | if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) |
80 | } | 86 | } |
81 | 87 | ||
82 | get isInSmallView () { | 88 | isInSmallView () { |
83 | return this.screenService.isInSmallView() | 89 | return this.screenService.isInSmallView() |
84 | } | 90 | } |
85 | 91 | ||
@@ -87,12 +93,36 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
87 | return this.authService.isLoggedIn() | 93 | return this.authService.isLoggedIn() |
88 | } | 94 | } |
89 | 95 | ||
90 | get isManageable () { | 96 | isManageable () { |
91 | if (!this.isUserLoggedIn()) return false | 97 | if (!this.isUserLoggedIn()) return false |
92 | return this.videoChannel.ownerAccount.userId === this.authService.getUser().id | 98 | |
99 | return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id | ||
93 | } | 100 | } |
94 | 101 | ||
95 | activateCopiedMessage () { | 102 | activateCopiedMessage () { |
96 | this.notifier.success($localize`Username copied`) | 103 | this.notifier.success($localize`Username copied`) |
97 | } | 104 | } |
105 | |||
106 | hasShowMoreDescription () { | ||
107 | return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100 | ||
108 | } | ||
109 | |||
110 | showSupportModal () { | ||
111 | this.supportModal.show() | ||
112 | } | ||
113 | |||
114 | getAccountUrl () { | ||
115 | return [ '/accounts', this.videoChannel.ownerBy ] | ||
116 | } | ||
117 | |||
118 | private loadChannelVideosCount () { | ||
119 | this.videoService.getVideoChannelVideos({ | ||
120 | videoChannel: this.videoChannel, | ||
121 | videoPagination: { | ||
122 | currentPage: 1, | ||
123 | itemsPerPage: 0 | ||
124 | }, | ||
125 | sort: '-publishedAt' | ||
126 | }).subscribe(res => this.channelVideosCount = res.total) | ||
127 | } | ||
98 | } | 128 | } |
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts index 05236ff85..408f86225 100644 --- a/client/src/app/+video-channels/video-channels.module.ts +++ b/client/src/app/+video-channels/video-channels.module.ts | |||
@@ -2,10 +2,10 @@ import { NgModule } from '@angular/core' | |||
2 | import { SharedFormModule } from '@app/shared/shared-forms' | 2 | import { SharedFormModule } from '@app/shared/shared-forms' |
3 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' | 3 | import { SharedGlobalIconModule } from '@app/shared/shared-icons' |
4 | import { SharedMainModule } from '@app/shared/shared-main' | 4 | import { SharedMainModule } from '@app/shared/shared-main' |
5 | import { SharedSupportModal } from '@app/shared/shared-support-modal' | ||
5 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 6 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
6 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | 7 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' |
7 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' | 8 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' |
8 | import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component' | ||
9 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' | 9 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' |
10 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' | 10 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' |
11 | import { VideoChannelsRoutingModule } from './video-channels-routing.module' | 11 | import { VideoChannelsRoutingModule } from './video-channels-routing.module' |
@@ -20,13 +20,13 @@ import { VideoChannelsComponent } from './video-channels.component' | |||
20 | SharedVideoPlaylistModule, | 20 | SharedVideoPlaylistModule, |
21 | SharedVideoMiniatureModule, | 21 | SharedVideoMiniatureModule, |
22 | SharedUserSubscriptionModule, | 22 | SharedUserSubscriptionModule, |
23 | SharedGlobalIconModule | 23 | SharedGlobalIconModule, |
24 | SharedSupportModal | ||
24 | ], | 25 | ], |
25 | 26 | ||
26 | declarations: [ | 27 | declarations: [ |
27 | VideoChannelsComponent, | 28 | VideoChannelsComponent, |
28 | VideoChannelVideosComponent, | 29 | VideoChannelVideosComponent, |
29 | VideoChannelAboutComponent, | ||
30 | VideoChannelPlaylistsComponent | 30 | VideoChannelPlaylistsComponent |
31 | ], | 31 | ], |
32 | 32 | ||
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts index 8780ca567..8e035b6bb 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | 1 | ||
2 | import { forkJoin } from 'rxjs' | 2 | import { forkJoin } from 'rxjs' |
3 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | 3 | import { AfterViewChecked, AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core' |
4 | import { Router } from '@angular/router' | 4 | import { Router } from '@angular/router' |
5 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
6 | import { scrollToTop } from '@app/helpers' | 6 | import { scrollToTop } from '@app/helpers' |
7 | import { FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormValidatorService } from '@app/shared/shared-forms' |
8 | import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 8 | import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
@@ -19,7 +19,7 @@ import { VideoSend } from './video-send' | |||
19 | './video-send.scss' | 19 | './video-send.scss' |
20 | ] | 20 | ] |
21 | }) | 21 | }) |
22 | export class VideoGoLiveComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 22 | export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate { |
23 | @Output() firstStepDone = new EventEmitter<string>() | 23 | @Output() firstStepDone = new EventEmitter<string>() |
24 | @Output() firstStepError = new EventEmitter<void>() | 24 | @Output() firstStepError = new EventEmitter<void>() |
25 | 25 | ||
@@ -41,7 +41,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon | |||
41 | protected videoService: VideoService, | 41 | protected videoService: VideoService, |
42 | protected videoCaptionService: VideoCaptionService, | 42 | protected videoCaptionService: VideoCaptionService, |
43 | private liveVideoService: LiveVideoService, | 43 | private liveVideoService: LiveVideoService, |
44 | private router: Router | 44 | private router: Router, |
45 | private hooks: HooksService | ||
45 | ) { | 46 | ) { |
46 | super() | 47 | super() |
47 | } | 48 | } |
@@ -50,6 +51,10 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon | |||
50 | super.ngOnInit() | 51 | super.ngOnInit() |
51 | } | 52 | } |
52 | 53 | ||
54 | ngAfterViewInit () { | ||
55 | this.hooks.runAction('action:go-live.init', 'video-edit') | ||
56 | } | ||
57 | |||
53 | canDeactivate () { | 58 | canDeactivate () { |
54 | return { canDeactivate: true } | 59 | return { canDeactivate: true } |
55 | } | 60 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss index 1fef74994..dd87641fc 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss | |||
@@ -3,8 +3,8 @@ | |||
3 | 3 | ||
4 | .first-step-block { | 4 | .first-step-block { |
5 | .torrent-or-magnet { | 5 | .torrent-or-magnet { |
6 | @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuColor)); | 6 | @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuBackgroundColor)); |
7 | 7 | ||
8 | &[data-content] { | 8 | &[data-content] { |
9 | margin: 1.5rem 0; | 9 | margin: 1.5rem 0; |
10 | } | 10 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts index 01087e525..3aae24732 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' | 3 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
4 | import { scrollToTop } from '@app/helpers' | 4 | import { scrollToTop } from '@app/helpers' |
5 | import { FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormValidatorService } from '@app/shared/shared-forms' |
6 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 6 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' |
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send' | |||
18 | './video-send.scss' | 18 | './video-send.scss' |
19 | ] | 19 | ] |
20 | }) | 20 | }) |
21 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 21 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate { |
22 | @Output() firstStepDone = new EventEmitter<string>() | 22 | @Output() firstStepDone = new EventEmitter<string>() |
23 | @Output() firstStepError = new EventEmitter<void>() | 23 | @Output() firstStepError = new EventEmitter<void>() |
24 | @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> | 24 | @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> |
@@ -43,7 +43,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
43 | protected videoService: VideoService, | 43 | protected videoService: VideoService, |
44 | protected videoCaptionService: VideoCaptionService, | 44 | protected videoCaptionService: VideoCaptionService, |
45 | private router: Router, | 45 | private router: Router, |
46 | private videoImportService: VideoImportService | 46 | private videoImportService: VideoImportService, |
47 | private hooks: HooksService | ||
47 | ) { | 48 | ) { |
48 | super() | 49 | super() |
49 | } | 50 | } |
@@ -52,6 +53,10 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
52 | super.ngOnInit() | 53 | super.ngOnInit() |
53 | } | 54 | } |
54 | 55 | ||
56 | ngAfterViewInit () { | ||
57 | this.hooks.runAction('action:video-torrent-import.init', 'video-edit') | ||
58 | } | ||
59 | |||
55 | canDeactivate () { | 60 | canDeactivate () { |
56 | return { canDeactivate: true } | 61 | return { canDeactivate: true } |
57 | } | 62 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts index c447c179d..7a9fe369f 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { map, switchMap } from 'rxjs/operators' | 1 | import { map, switchMap } from 'rxjs/operators' |
2 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | 2 | import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core' |
3 | import { Router } from '@angular/router' | 3 | import { Router } from '@angular/router' |
4 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
5 | import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers' | 5 | import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers' |
6 | import { FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormValidatorService } from '@app/shared/shared-forms' |
7 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' |
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send' | |||
18 | './video-send.scss' | 18 | './video-send.scss' |
19 | ] | 19 | ] |
20 | }) | 20 | }) |
21 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 21 | export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate { |
22 | @Output() firstStepDone = new EventEmitter<string>() | 22 | @Output() firstStepDone = new EventEmitter<string>() |
23 | @Output() firstStepError = new EventEmitter<void>() | 23 | @Output() firstStepError = new EventEmitter<void>() |
24 | 24 | ||
@@ -42,8 +42,9 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom | |||
42 | protected videoService: VideoService, | 42 | protected videoService: VideoService, |
43 | protected videoCaptionService: VideoCaptionService, | 43 | protected videoCaptionService: VideoCaptionService, |
44 | private router: Router, | 44 | private router: Router, |
45 | private videoImportService: VideoImportService | 45 | private videoImportService: VideoImportService, |
46 | ) { | 46 | private hooks: HooksService |
47 | ) { | ||
47 | super() | 48 | super() |
48 | } | 49 | } |
49 | 50 | ||
@@ -51,6 +52,10 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom | |||
51 | super.ngOnInit() | 52 | super.ngOnInit() |
52 | } | 53 | } |
53 | 54 | ||
55 | ngAfterViewInit () { | ||
56 | this.hooks.runAction('action:video-url-import.init', 'video-edit') | ||
57 | } | ||
58 | |||
54 | canDeactivate () { | 59 | canDeactivate () { |
55 | return { canDeactivate: true } | 60 | return { canDeactivate: true } |
56 | } | 61 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index ca21b61cd..effb37077 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts | |||
@@ -1,15 +1,15 @@ | |||
1 | import { Subscription } from 'rxjs' | 1 | import { Subscription } from 'rxjs' |
2 | import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' | 2 | import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' |
3 | import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' | 3 | import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' |
4 | import { Router } from '@angular/router' | 4 | import { Router } from '@angular/router' |
5 | import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' | 5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' |
6 | import { scrollToTop, uploadErrorHandler } from '@app/helpers' | 6 | import { scrollToTop, uploadErrorHandler } from '@app/helpers' |
7 | import { FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormValidatorService } from '@app/shared/shared-forms' |
8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 8 | import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
9 | import { LoadingBarService } from '@ngx-loading-bar/core' | 9 | import { LoadingBarService } from '@ngx-loading-bar/core' |
10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
10 | import { VideoPrivacy } from '@shared/models' | 11 | import { VideoPrivacy } from '@shared/models' |
11 | import { VideoSend } from './video-send' | 12 | import { VideoSend } from './video-send' |
12 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
13 | 13 | ||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-video-upload', | 15 | selector: 'my-video-upload', |
@@ -20,7 +20,7 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | |||
20 | './video-send.scss' | 20 | './video-send.scss' |
21 | ] | 21 | ] |
22 | }) | 22 | }) |
23 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { | 23 | export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate { |
24 | @Output() firstStepDone = new EventEmitter<string>() | 24 | @Output() firstStepDone = new EventEmitter<string>() |
25 | @Output() firstStepError = new EventEmitter<void>() | 25 | @Output() firstStepError = new EventEmitter<void>() |
26 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> | 26 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> |
@@ -60,7 +60,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
60 | protected videoService: VideoService, | 60 | protected videoService: VideoService, |
61 | protected videoCaptionService: VideoCaptionService, | 61 | protected videoCaptionService: VideoCaptionService, |
62 | private userService: UserService, | 62 | private userService: UserService, |
63 | private router: Router | 63 | private router: Router, |
64 | private hooks: HooksService | ||
64 | ) { | 65 | ) { |
65 | super() | 66 | super() |
66 | } | 67 | } |
@@ -79,6 +80,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
79 | }) | 80 | }) |
80 | } | 81 | } |
81 | 82 | ||
83 | ngAfterViewInit () { | ||
84 | this.hooks.runAction('action:video-upload.init', 'video-edit') | ||
85 | } | ||
86 | |||
82 | ngOnDestroy () { | 87 | ngOnDestroy () { |
83 | if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() | 88 | if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() |
84 | } | 89 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add.component.scss b/client/src/app/+videos/+video-edit/video-add.component.scss index 5db9e823d..1ebee946b 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.scss +++ b/client/src/app/+videos/+video-edit/video-add.component.scss | |||
@@ -67,7 +67,7 @@ $nav-link-height: 40px; | |||
67 | &.active { | 67 | &.active { |
68 | border-color: $border-color; | 68 | border-color: $border-color; |
69 | border-bottom-color: transparent; | 69 | border-bottom-color: transparent; |
70 | background-color: pvar(--submenuColor) !important; | 70 | background-color: pvar(--submenuBackgroundColor) !important; |
71 | 71 | ||
72 | span { | 72 | span { |
73 | border-bottom-color: pvar(--mainColor); | 73 | border-bottom-color: pvar(--mainColor); |
@@ -84,7 +84,7 @@ $nav-link-height: 40px; | |||
84 | border: $border-width $border-type $border-color; | 84 | border: $border-width $border-type $border-color; |
85 | border-top: transparent; | 85 | border-top: transparent; |
86 | 86 | ||
87 | background-color: pvar(--submenuColor); | 87 | background-color: pvar(--submenuBackgroundColor); |
88 | border-bottom-left-radius: 3px; | 88 | border-bottom-left-radius: 3px; |
89 | border-bottom-right-radius: 3px; | 89 | border-bottom-right-radius: 3px; |
90 | width: 100%; | 90 | width: 100%; |
diff --git a/client/src/app/+videos/+video-watch/comment/video-comments.component.html b/client/src/app/+videos/+video-watch/comment/video-comments.component.html index 4a6426d30..9e6fde2e0 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comments.component.html +++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.html | |||
@@ -1,12 +1,7 @@ | |||
1 | <div> | 1 | <div> |
2 | <div class="title-block"> | 2 | <div class="title-block"> |
3 | <h2 class="title-page title-page-single"> | 3 | <h2 class="title-page title-page-single"> |
4 | <ng-container *ngIf="totalNotDeletedComments > 0; then hasComments; else noComments"></ng-container> | 4 | {totalNotDeletedComments, plural, =0 {Comments} =1 {1 Comment} other {{{totalNotDeletedComments}} Comments}} |
5 | <ng-template #hasComments> | ||
6 | <ng-container i18n *ngIf="totalNotDeletedComments === 1; else manyComments">1 Comment</ng-container> | ||
7 | <ng-template i18n #manyComments>{{ totalNotDeletedComments }} Comments</ng-template> | ||
8 | </ng-template> | ||
9 | <ng-template i18n #noComments>Comments</ng-template> | ||
10 | </h2> | 5 | </h2> |
11 | 6 | ||
12 | <my-feed [syndicationItems]="syndicationItems"></my-feed> | 7 | <my-feed [syndicationItems]="syndicationItems"></my-feed> |
@@ -79,15 +74,17 @@ | |||
79 | <span class="glyphicon glyphicon-menu-down"></span> | 74 | <span class="glyphicon glyphicon-menu-down"></span> |
80 | 75 | ||
81 | <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container> | 76 | <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container> |
77 | |||
82 | <ng-template #hasAuthorComments> | 78 | <ng-template #hasAuthorComments> |
83 | <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n> | 79 | <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n> |
84 | View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} and others | 80 | View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }} and others |
85 | </ng-container> | 81 | </ng-container> |
86 | <ng-template i18n #onlyAuthorComments> | 82 | <ng-template i18n #onlyAuthorComments> |
87 | View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} | 83 | View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }} |
88 | </ng-template> | 84 | </ng-template> |
89 | </ng-template> | 85 | </ng-template> |
90 | <ng-template i18n #noAuthorComments>View {{ comment.totalReplies }} replies</ng-template> | 86 | |
87 | <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template> | ||
91 | 88 | ||
92 | <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader> | 89 | <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader> |
93 | </div> | 90 | </div> |
diff --git a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts index d36dd9e34..210236b61 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts | |||
@@ -5,7 +5,6 @@ import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifie | |||
5 | import { HooksService } from '@app/core/plugins/hooks.service' | 5 | import { HooksService } from '@app/core/plugins/hooks.service' |
6 | import { Syndication, VideoDetails } from '@app/shared/shared-main' | 6 | import { Syndication, VideoDetails } from '@app/shared/shared-main' |
7 | import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment' | 7 | import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment' |
8 | import { ThisReceiver } from '@angular/compiler' | ||
9 | 8 | ||
10 | @Component({ | 9 | @Component({ |
11 | selector: 'my-video-comments', | 10 | selector: 'my-video-comments', |
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.ts b/client/src/app/+videos/+video-watch/modal/video-support.component.ts deleted file mode 100644 index bd5290a72..000000000 --- a/client/src/app/+videos/+video-watch/modal/video-support.component.ts +++ /dev/null | |||
@@ -1,31 +0,0 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | ||
2 | import { MarkdownService } from '@app/core' | ||
3 | import { VideoDetails } from '@app/shared/shared-main' | ||
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-video-support', | ||
8 | templateUrl: './video-support.component.html', | ||
9 | styleUrls: [ './video-support.component.scss' ] | ||
10 | }) | ||
11 | export class VideoSupportComponent { | ||
12 | @Input() video: VideoDetails = null | ||
13 | |||
14 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
15 | |||
16 | videoHTMLSupport = '' | ||
17 | |||
18 | constructor ( | ||
19 | private markdownService: MarkdownService, | ||
20 | private modalService: NgbModal | ||
21 | ) { } | ||
22 | |||
23 | show () { | ||
24 | const modalRef = this.modalService.open(this.modal, { centered: true }) | ||
25 | |||
26 | this.markdownService.enhancedMarkdownToHTML(this.video.support) | ||
27 | .then(r => this.videoHTMLSupport = r) | ||
28 | |||
29 | return modalRef | ||
30 | } | ||
31 | } | ||
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html index 3c7c679b8..e0e9f92e7 100644 --- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html +++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <div class="other-videos"> | 1 | <div class="other-videos" [ngClass]="{ 'display-as-row': displayAsRow }"> |
2 | <ng-container *ngIf="hasVideos$ | async"> | 2 | <ng-container *ngIf="hasVideos$ | async"> |
3 | <div class="title-page-container"> | 3 | <div class="title-page-container"> |
4 | <h2 i18n class="title-page title-page-single"> | 4 | <h2 i18n class="title-page title-page-single"> |
@@ -14,7 +14,7 @@ | |||
14 | 14 | ||
15 | <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count"> | 15 | <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count"> |
16 | <my-video-miniature | 16 | <my-video-miniature |
17 | [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" | 17 | [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow" |
18 | (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"> | 18 | (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"> |
19 | </my-video-miniature> | 19 | </my-video-miniature> |
20 | 20 | ||
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss index b278c9654..c9fae6f27 100644 --- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss +++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss | |||
@@ -1,3 +1,6 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
1 | .title-page-container { | 4 | .title-page-container { |
2 | display: flex; | 5 | display: flex; |
3 | justify-content: space-between; | 6 | justify-content: space-between; |
@@ -11,6 +14,10 @@ | |||
11 | } | 14 | } |
12 | } | 15 | } |
13 | 16 | ||
17 | .title-page { | ||
18 | margin-top: 0; | ||
19 | } | ||
20 | |||
14 | .title-page-autoplay { | 21 | .title-page-autoplay { |
15 | display: flex; | 22 | display: flex; |
16 | width: max-content; | 23 | width: max-content; |
@@ -29,3 +36,29 @@ | |||
29 | hr { | 36 | hr { |
30 | margin-top: 0; | 37 | margin-top: 0; |
31 | } | 38 | } |
39 | |||
40 | my-video-miniature { | ||
41 | display: block; | ||
42 | } | ||
43 | |||
44 | .other-videos:not(.display-as-row) my-video-miniature { | ||
45 | min-width: $video-thumbnail-medium-width; | ||
46 | max-width: $video-thumbnail-medium-width; | ||
47 | } | ||
48 | |||
49 | .display-as-row { | ||
50 | my-video-miniature { | ||
51 | margin-bottom: 20px; | ||
52 | } | ||
53 | |||
54 | hr { | ||
55 | display: none; | ||
56 | } | ||
57 | |||
58 | @media screen and (max-width: $mobile-view) { | ||
59 | my-video-miniature { | ||
60 | margin-bottom: 10px; | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | |||
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts index a1c8e0661..89b9c01b6 100644 --- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts +++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts | |||
@@ -16,6 +16,8 @@ import { RecommendedVideosStore } from './recommended-videos.store' | |||
16 | export class RecommendedVideosComponent implements OnInit, OnChanges { | 16 | export class RecommendedVideosComponent implements OnInit, OnChanges { |
17 | @Input() inputRecommendation: RecommendationInfo | 17 | @Input() inputRecommendation: RecommendationInfo |
18 | @Input() playlist: VideoPlaylist | 18 | @Input() playlist: VideoPlaylist |
19 | @Input() displayAsRow: boolean | ||
20 | |||
19 | @Output() gotRecommendations = new EventEmitter<Video[]>() | 21 | @Output() gotRecommendations = new EventEmitter<Video[]>() |
20 | 22 | ||
21 | autoPlayNextVideo: boolean | 23 | autoPlayNextVideo: boolean |
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html index 310cc926f..5058f05dd 100644 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html | |||
@@ -1,8 +1,9 @@ | |||
1 | <div class="wrapper" [ngClass]="'avatar-' + size"> | 1 | <div class="wrapper" [ngClass]="'avatar-' + size"> |
2 | <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel"> | 2 | <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel"> |
3 | <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> | 3 | <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> |
4 | <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> | 4 | <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" /> |
5 | </a> | 5 | </a> |
6 | |||
6 | <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> | 7 | <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> |
7 | <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" /> | 8 | <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" /> |
8 | </a> | 9 | </a> |
@@ -14,7 +15,7 @@ | |||
14 | </a> | 15 | </a> |
15 | 16 | ||
16 | <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> | 17 | <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> |
17 | <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> | 18 | <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" /> |
18 | </a> | 19 | </a> |
19 | </ng-container> | 20 | </ng-container> |
20 | 21 | ||
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss index 37709fce6..4998e85fa 100644 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss | |||
@@ -25,8 +25,12 @@ | |||
25 | position: absolute; | 25 | position: absolute; |
26 | top:50%; | 26 | top:50%; |
27 | left:50%; | 27 | left:50%; |
28 | border-radius: 50%; | 28 | transform: translate(-50%,-50%); |
29 | transform: translate(-50%,-50%) | 29 | border-radius: 5px; |
30 | |||
31 | &:not(.channel-avatar) { | ||
32 | border-radius: 50%; | ||
33 | } | ||
30 | } | 34 | } |
31 | 35 | ||
32 | a:nth-of-type(2) img { | 36 | a:nth-of-type(2) img { |
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts index 440e2b522..0b6e796df 100644 --- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts +++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { Video } from '../video/video.model' | 2 | import { Video } from '@app/shared/shared-main/video' |
3 | 3 | ||
4 | @Component({ | 4 | @Component({ |
5 | selector: 'my-video-avatar-channel', | 5 | selector: 'my-video-avatar-channel', |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html index b17f898ce..99103c2c3 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.html +++ b/client/src/app/+videos/+video-watch/video-watch.component.html | |||
@@ -142,7 +142,7 @@ | |||
142 | <ng-container *ngIf="isUserLoggedIn()"> | 142 | <ng-container *ngIf="isUserLoggedIn()"> |
143 | <my-video-actions-dropdown | 143 | <my-video-actions-dropdown |
144 | placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions" | 144 | placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions" |
145 | [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" (modalOpened)="onModalOpened()" | 145 | [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" |
146 | ></my-video-actions-dropdown> | 146 | ></my-video-actions-dropdown> |
147 | </ng-container> | 147 | </ng-container> |
148 | </div> | 148 | </div> |
@@ -289,6 +289,7 @@ | |||
289 | </div> | 289 | </div> |
290 | 290 | ||
291 | <my-recommended-videos | 291 | <my-recommended-videos |
292 | [displayAsRow]="displayOtherVideosAsRow()" | ||
292 | [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" | 293 | [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" |
293 | [playlist]="playlist" | 294 | [playlist]="playlist" |
294 | (gotRecommendations)="onRecommendations($event)" | 295 | (gotRecommendations)="onRecommendations($event)" |
@@ -313,6 +314,6 @@ | |||
313 | </div> | 314 | </div> |
314 | 315 | ||
315 | <ng-container *ngIf="video !== null"> | 316 | <ng-container *ngIf="video !== null"> |
316 | <my-video-support #videoSupportModal [video]="video"></my-video-support> | 317 | <my-support-modal #supportModal [video]="video"></my-support-modal> |
317 | <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share> | 318 | <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share> |
318 | </ng-container> | 319 | </ng-container> |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss index 555126cbc..2e566e3fb 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.scss +++ b/client/src/app/+videos/+video-watch/video-watch.component.scss | |||
@@ -3,7 +3,7 @@ | |||
3 | @import '_bootstrap-variables'; | 3 | @import '_bootstrap-variables'; |
4 | @import '_miniature'; | 4 | @import '_miniature'; |
5 | 5 | ||
6 | $player-factor: 1.7; // 16/9 | 6 | $player-factor: #{16/9}; |
7 | $video-info-margin-left: 44px; | 7 | $video-info-margin-left: 44px; |
8 | 8 | ||
9 | @function getPlayerHeight($width){ | 9 | @function getPlayerHeight($width){ |
@@ -179,12 +179,6 @@ $video-info-margin-left: 44px; | |||
179 | &:hover { | 179 | &:hover { |
180 | opacity: 0.8; | 180 | opacity: 0.8; |
181 | } | 181 | } |
182 | |||
183 | img { | ||
184 | @include avatar(18px); | ||
185 | |||
186 | margin: -2px 5px 0 0; | ||
187 | } | ||
188 | } | 182 | } |
189 | 183 | ||
190 | .video-info-channel-left { | 184 | .video-info-channel-left { |
@@ -413,37 +407,12 @@ $video-info-margin-left: 44px; | |||
413 | } | 407 | } |
414 | } | 408 | } |
415 | } | 409 | } |
410 | } | ||
416 | 411 | ||
417 | ::ng-deep .other-videos { | 412 | my-recommended-videos { |
418 | padding-left: 15px; | 413 | display: block; |
419 | min-width: $video-miniature-width; | 414 | padding-left: 15px; |
420 | 415 | min-width: 250px; | |
421 | @media screen and (min-width: 1800px - (3* $video-miniature-width)) { | ||
422 | width: min-content; | ||
423 | } | ||
424 | |||
425 | .title-page { | ||
426 | margin: 0 !important; | ||
427 | } | ||
428 | |||
429 | .video-miniature { | ||
430 | display: flex; | ||
431 | width: max-content; | ||
432 | height: 100%; | ||
433 | padding-bottom: 20px; | ||
434 | flex-wrap: wrap; | ||
435 | } | ||
436 | |||
437 | .video-bottom { | ||
438 | @media screen and (max-width: 1800px - (3* $video-miniature-width)) { | ||
439 | margin-left: 1rem; | ||
440 | } | ||
441 | @media screen and (max-width: 500px) { | ||
442 | margin-left: 0; | ||
443 | margin-top: .5rem; | ||
444 | } | ||
445 | } | ||
446 | } | ||
447 | } | 416 | } |
448 | 417 | ||
449 | my-video-comments { | 418 | my-video-comments { |
@@ -537,6 +506,7 @@ my-video-comments { | |||
537 | } | 506 | } |
538 | } | 507 | } |
539 | 508 | ||
509 | // Use the same breakpoint than in the typescript component to display the other video miniatures as row | ||
540 | @media screen and (max-width: 1100px) { | 510 | @media screen and (max-width: 1100px) { |
541 | #video-wrapper { | 511 | #video-wrapper { |
542 | flex-direction: column; | 512 | flex-direction: column; |
@@ -549,15 +519,10 @@ my-video-comments { | |||
549 | 519 | ||
550 | .video-bottom { | 520 | .video-bottom { |
551 | flex-direction: column; | 521 | flex-direction: column; |
522 | } | ||
552 | 523 | ||
553 | ::ng-deep .other-videos { | 524 | my-recommended-videos { |
554 | padding-left: 0 !important; | 525 | padding-left: 0; |
555 | |||
556 | ::ng-deep .video-miniature { | ||
557 | flex-direction: row; | ||
558 | width: auto; | ||
559 | } | ||
560 | } | ||
561 | } | 526 | } |
562 | } | 527 | } |
563 | 528 | ||
@@ -579,10 +544,6 @@ my-video-comments { | |||
579 | } | 544 | } |
580 | } | 545 | } |
581 | 546 | ||
582 | ::ng-deep .other-videos .video-miniature { | ||
583 | flex-direction: column; | ||
584 | } | ||
585 | |||
586 | .privacy-concerns { | 547 | .privacy-concerns { |
587 | width: 100%; | 548 | width: 100%; |
588 | } | 549 | } |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 7a98cab3b..de5fb4ed0 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -21,6 +21,7 @@ import { RedirectService } from '@app/core/routing/redirect.service' | |||
21 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 21 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' |
22 | import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 22 | import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' |
23 | import { VideoShareComponent } from '@app/shared/shared-share-modal' | 23 | import { VideoShareComponent } from '@app/shared/shared-share-modal' |
24 | import { SupportModalComponent } from '@app/shared/shared-support-modal' | ||
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 25 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' | 26 | import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' |
26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 27 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
@@ -28,7 +29,12 @@ import { MetaService } from '@ngx-meta/core' | |||
28 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | 29 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' |
29 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 30 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
30 | import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' | 31 | import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' |
31 | import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage' | 32 | import { |
33 | cleanupVideoWatch, | ||
34 | getStoredP2PEnabled, | ||
35 | getStoredTheater, | ||
36 | getStoredVideoWatchHistory | ||
37 | } from '../../../assets/player/peertube-player-local-storage' | ||
32 | import { | 38 | import { |
33 | CustomizationOptions, | 39 | CustomizationOptions, |
34 | P2PMediaLoaderOptions, | 40 | P2PMediaLoaderOptions, |
@@ -39,7 +45,6 @@ import { | |||
39 | } from '../../../assets/player/peertube-player-manager' | 45 | } from '../../../assets/player/peertube-player-manager' |
40 | import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' | 46 | import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' |
41 | import { environment } from '../../../environments/environment' | 47 | import { environment } from '../../../environments/environment' |
42 | import { VideoSupportComponent } from './modal/video-support.component' | ||
43 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' | 48 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' |
44 | 49 | ||
45 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | 50 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } |
@@ -54,7 +59,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
54 | 59 | ||
55 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent | 60 | @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent |
56 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent | 61 | @ViewChild('videoShareModal') videoShareModal: VideoShareComponent |
57 | @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent | 62 | @ViewChild('supportModal') supportModal: SupportModalComponent |
58 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 63 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
59 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent | 64 | @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent |
60 | 65 | ||
@@ -195,6 +200,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
195 | this.theaterEnabled = getStoredTheater() | 200 | this.theaterEnabled = getStoredTheater() |
196 | 201 | ||
197 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 202 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
203 | |||
204 | setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI | ||
198 | } | 205 | } |
199 | 206 | ||
200 | ngOnDestroy () { | 207 | ngOnDestroy () { |
@@ -277,23 +284,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
277 | } | 284 | } |
278 | 285 | ||
279 | showSupportModal () { | 286 | showSupportModal () { |
280 | // Check video was playing before opening support modal | 287 | this.supportModal.show() |
281 | const isVideoPlaying = this.isPlaying() | ||
282 | |||
283 | this.pausePlayer() | ||
284 | |||
285 | const modalRef = this.videoSupportModal.show() | ||
286 | |||
287 | modalRef.result.then(() => { | ||
288 | if (isVideoPlaying) { | ||
289 | this.resumePlayer() | ||
290 | } | ||
291 | }) | ||
292 | } | 288 | } |
293 | 289 | ||
294 | showShareModal () { | 290 | showShareModal () { |
295 | this.pausePlayer() | ||
296 | |||
297 | this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition) | 291 | this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition) |
298 | } | 292 | } |
299 | 293 | ||
@@ -316,10 +310,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
316 | } | 310 | } |
317 | } | 311 | } |
318 | 312 | ||
319 | onModalOpened () { | ||
320 | this.pausePlayer() | ||
321 | } | ||
322 | |||
323 | onVideoRemoved () { | 313 | onVideoRemoved () { |
324 | this.redirectService.redirectToHomepage() | 314 | this.redirectService.redirectToHomepage() |
325 | } | 315 | } |
@@ -396,6 +386,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
396 | this.loadVideo(videoId) | 386 | this.loadVideo(videoId) |
397 | } | 387 | } |
398 | 388 | ||
389 | displayOtherVideosAsRow () { | ||
390 | // Use the same value as in the SASS file | ||
391 | return this.screenService.getWindowInnerWidth() <= 1100 | ||
392 | } | ||
393 | |||
399 | private loadVideo (videoId: string) { | 394 | private loadVideo (videoId: string) { |
400 | // Video did not change | 395 | // Video did not change |
401 | if (this.video && this.video.uuid === videoId) return | 396 | if (this.video && this.video.uuid === videoId) return |
@@ -768,9 +763,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
768 | const getStartTime = () => { | 763 | const getStartTime = () => { |
769 | const byUrl = urlOptions.startTime !== undefined | 764 | const byUrl = urlOptions.startTime !== undefined |
770 | const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) | 765 | const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) |
766 | const byLocalStorage = getStoredVideoWatchHistory(video.uuid) | ||
771 | 767 | ||
772 | if (byUrl) return timeToInt(urlOptions.startTime) | 768 | if (byUrl) return timeToInt(urlOptions.startTime) |
773 | if (byHistory) return video.userHistory.currentTime | 769 | if (byHistory) return video.userHistory.currentTime |
770 | if (byLocalStorage) return byLocalStorage.duration | ||
774 | 771 | ||
775 | return 0 | 772 | return 0 |
776 | } | 773 | } |
@@ -815,6 +812,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
815 | ? this.videoService.getVideoViewUrl(video.uuid) | 812 | ? this.videoService.getVideoViewUrl(video.uuid) |
816 | : null, | 813 | : null, |
817 | embedUrl: video.embedUrl, | 814 | embedUrl: video.embedUrl, |
815 | embedTitle: video.name, | ||
818 | 816 | ||
819 | isLive: video.isLive, | 817 | isLive: video.isLive, |
820 | 818 | ||
@@ -827,7 +825,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
827 | 825 | ||
828 | serverUrl: environment.apiUrl, | 826 | serverUrl: environment.apiUrl, |
829 | 827 | ||
830 | videoCaptions: playerCaptions | 828 | videoCaptions: playerCaptions, |
829 | |||
830 | videoUUID: video.uuid | ||
831 | }, | 831 | }, |
832 | 832 | ||
833 | webtorrent: { | 833 | webtorrent: { |
@@ -867,24 +867,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
867 | return { playerMode: mode, playerOptions: options } | 867 | return { playerMode: mode, playerOptions: options } |
868 | } | 868 | } |
869 | 869 | ||
870 | private pausePlayer () { | ||
871 | if (!this.player) return | ||
872 | |||
873 | this.player.pause() | ||
874 | } | ||
875 | |||
876 | private resumePlayer () { | ||
877 | if (!this.player) return | ||
878 | |||
879 | this.player.play() | ||
880 | } | ||
881 | |||
882 | private isPlaying () { | ||
883 | if (!this.player) return | ||
884 | |||
885 | return !this.player.paused() | ||
886 | } | ||
887 | |||
888 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { | 870 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { |
889 | if (!this.liveVideosSub) { | 871 | if (!this.liveVideosSub) { |
890 | this.liveVideosSub = this.buildLiveEventsSubscription() | 872 | this.liveVideosSub = this.buildLiveEventsSubscription() |
diff --git a/client/src/app/+videos/+video-watch/video-watch.module.ts b/client/src/app/+videos/+video-watch/video-watch.module.ts index fbda9b9c4..3e9f3822e 100644 --- a/client/src/app/+videos/+video-watch/video-watch.module.ts +++ b/client/src/app/+videos/+video-watch/video-watch.module.ts | |||
@@ -4,6 +4,7 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons' | |||
4 | import { SharedMainModule } from '@app/shared/shared-main' | 4 | import { SharedMainModule } from '@app/shared/shared-main' |
5 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 5 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
6 | import { SharedShareModal } from '@app/shared/shared-share-modal' | 6 | import { SharedShareModal } from '@app/shared/shared-share-modal' |
7 | import { SharedSupportModal } from '@app/shared/shared-support-modal' | ||
7 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 8 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
8 | import { SharedVideoModule } from '@app/shared/shared-video' | 9 | import { SharedVideoModule } from '@app/shared/shared-video' |
9 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' | 10 | import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' |
@@ -13,9 +14,9 @@ import { VideoCommentService } from '../../shared/shared-video-comment/video-com | |||
13 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' | 14 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' |
14 | import { VideoCommentComponent } from './comment/video-comment.component' | 15 | import { VideoCommentComponent } from './comment/video-comment.component' |
15 | import { VideoCommentsComponent } from './comment/video-comments.component' | 16 | import { VideoCommentsComponent } from './comment/video-comments.component' |
16 | import { VideoSupportComponent } from './modal/video-support.component' | ||
17 | import { RecommendationsModule } from './recommendations/recommendations.module' | 17 | import { RecommendationsModule } from './recommendations/recommendations.module' |
18 | import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' | 18 | import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' |
19 | import { VideoAvatarChannelComponent } from './video-avatar-channel.component' | ||
19 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' | 20 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' |
20 | import { VideoWatchRoutingModule } from './video-watch-routing.module' | 21 | import { VideoWatchRoutingModule } from './video-watch-routing.module' |
21 | import { VideoWatchComponent } from './video-watch.component' | 22 | import { VideoWatchComponent } from './video-watch.component' |
@@ -34,18 +35,20 @@ import { VideoWatchComponent } from './video-watch.component' | |||
34 | SharedGlobalIconModule, | 35 | SharedGlobalIconModule, |
35 | SharedVideoCommentModule, | 36 | SharedVideoCommentModule, |
36 | SharedShareModal, | 37 | SharedShareModal, |
37 | SharedVideoModule | 38 | SharedVideoModule, |
39 | SharedSupportModal | ||
38 | ], | 40 | ], |
39 | 41 | ||
40 | declarations: [ | 42 | declarations: [ |
41 | VideoWatchComponent, | 43 | VideoWatchComponent, |
42 | VideoWatchPlaylistComponent, | 44 | VideoWatchPlaylistComponent, |
43 | 45 | ||
44 | VideoSupportComponent, | ||
45 | VideoCommentsComponent, | 46 | VideoCommentsComponent, |
46 | VideoCommentAddComponent, | 47 | VideoCommentAddComponent, |
47 | VideoCommentComponent, | 48 | VideoCommentComponent, |
48 | 49 | ||
50 | VideoAvatarChannelComponent, | ||
51 | |||
49 | TimestampRouteTransformerDirective, | 52 | TimestampRouteTransformerDirective, |
50 | TimestampRouteTransformerDirective | 53 | TimestampRouteTransformerDirective |
51 | ], | 54 | ], |
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html index ca986c634..639a96c43 100644 --- a/client/src/app/+videos/video-list/overview/video-overview.component.html +++ b/client/src/app/+videos/video-list/overview/video-overview.component.html | |||
@@ -14,7 +14,7 @@ | |||
14 | </h1> | 14 | </h1> |
15 | 15 | ||
16 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> | 16 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> |
17 | <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> | 17 | <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true"> |
18 | </my-video-miniature> | 18 | </my-video-miniature> |
19 | </div> | 19 | </div> |
20 | </div> | 20 | </div> |
@@ -25,7 +25,7 @@ | |||
25 | </h2> | 25 | </h2> |
26 | 26 | ||
27 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> | 27 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> |
28 | <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> | 28 | <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true"> |
29 | </my-video-miniature> | 29 | </my-video-miniature> |
30 | </div> | 30 | </div> |
31 | </div> | 31 | </div> |
@@ -40,7 +40,7 @@ | |||
40 | </div> | 40 | </div> |
41 | 41 | ||
42 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> | 42 | <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> |
43 | <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> | 43 | <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true"> |
44 | </my-video-miniature> | 44 | </my-video-miniature> |
45 | </div> | 45 | </div> |
46 | </div> | 46 | </div> |
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.scss b/client/src/app/+videos/video-list/overview/video-overview.component.scss index c1d10188a..ec73c628c 100644 --- a/client/src/app/+videos/video-list/overview/video-overview.component.scss +++ b/client/src/app/+videos/video-list/overview/video-overview.component.scss | |||
@@ -8,9 +8,84 @@ | |||
8 | } | 8 | } |
9 | 9 | ||
10 | .margin-content { | 10 | .margin-content { |
11 | @include fluid-videos-miniature-layout; | 11 | @include grid-videos-miniature-layout; |
12 | } | 12 | } |
13 | 13 | ||
14 | .section { | 14 | .section { |
15 | @include miniature-rows; | 15 | &:first-child { |
16 | padding-top: 30px; | ||
17 | |||
18 | .section-title { | ||
19 | border-top: none !important; | ||
20 | } | ||
21 | } | ||
22 | |||
23 | .section-title { | ||
24 | font-size: 24px; | ||
25 | font-weight: $font-semibold; | ||
26 | padding-top: 15px; | ||
27 | margin-bottom: 15px; | ||
28 | display: flex; | ||
29 | justify-content: space-between; | ||
30 | |||
31 | &:not(h2) { | ||
32 | border-top: 1px solid $separator-border-color; | ||
33 | } | ||
34 | |||
35 | a { | ||
36 | &:hover, &:focus:not(.focus-visible), &:active { | ||
37 | text-decoration: none; | ||
38 | outline: none; | ||
39 | } | ||
40 | |||
41 | color: pvar(--mainForegroundColor); | ||
42 | } | ||
43 | } | ||
44 | |||
45 | &.channel { | ||
46 | .section-title { | ||
47 | a { | ||
48 | display: flex; | ||
49 | width: fit-content; | ||
50 | align-items: center; | ||
51 | |||
52 | img { | ||
53 | @include channel-avatar(28px); | ||
54 | |||
55 | margin-right: 8px; | ||
56 | } | ||
57 | } | ||
58 | |||
59 | .followers { | ||
60 | color: pvar(--greyForegroundColor); | ||
61 | font-weight: normal; | ||
62 | font-size: 14px; | ||
63 | margin-left: 10px; | ||
64 | position: relative; | ||
65 | top: 2px; | ||
66 | } | ||
67 | } | ||
68 | } | ||
69 | |||
70 | .show-more { | ||
71 | position: relative; | ||
72 | top: -5px; | ||
73 | display: inline-block; | ||
74 | font-size: 16px; | ||
75 | text-transform: uppercase; | ||
76 | color: pvar(--greyForegroundColor); | ||
77 | margin-bottom: 10px; | ||
78 | font-weight: $font-semibold; | ||
79 | text-decoration: none; | ||
80 | } | ||
81 | |||
82 | @media screen and (max-width: $mobile-view) { | ||
83 | max-height: initial; | ||
84 | overflow: initial; | ||
85 | |||
86 | .section-title { | ||
87 | font-size: 17px; | ||
88 | margin-left: 10px; | ||
89 | } | ||
90 | } | ||
16 | } | 91 | } |
diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts index e352a2b2c..6aabb93a5 100644 --- a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts | |||
@@ -7,7 +7,7 @@ import { HooksService } from '@app/core/plugins/hooks.service' | |||
7 | import { immutableAssign } from '@app/helpers' | 7 | import { immutableAssign } from '@app/helpers' |
8 | import { VideoService } from '@app/shared/shared-main' | 8 | import { VideoService } from '@app/shared/shared-main' |
9 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' | 9 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' |
10 | import { AbstractVideoList, OwnerDisplayType } from '@app/shared/shared-video-miniature' | 10 | import { AbstractVideoList } from '@app/shared/shared-video-miniature' |
11 | import { FeedFormat, VideoSortField } from '@shared/models' | 11 | import { FeedFormat, VideoSortField } from '@shared/models' |
12 | import { environment } from '../../../environments/environment' | 12 | import { environment } from '../../../environments/environment' |
13 | import { copyToClipboard } from '../../../root-helpers/utils' | 13 | import { copyToClipboard } from '../../../root-helpers/utils' |
@@ -20,7 +20,6 @@ import { copyToClipboard } from '../../../root-helpers/utils' | |||
20 | export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { | 20 | export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { |
21 | titlePage: string | 21 | titlePage: string |
22 | sort = '-publishedAt' as VideoSortField | 22 | sort = '-publishedAt' as VideoSortField |
23 | ownerDisplayType: OwnerDisplayType = 'auto' | ||
24 | groupByDate = true | 23 | groupByDate = true |
25 | 24 | ||
26 | constructor ( | 25 | constructor ( |
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index e8447719a..42293e412 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss | |||
@@ -43,6 +43,10 @@ $assets-path: '../assets'; | |||
43 | background-color: pvar(--mainForegroundColor); | 43 | background-color: pvar(--mainForegroundColor); |
44 | mask-image: url('#{$assets-path}/images/misc/menu.svg'); | 44 | mask-image: url('#{$assets-path}/images/misc/menu.svg'); |
45 | margin: 0 18px 0 20px; | 45 | margin: 0 18px 0 20px; |
46 | |||
47 | @media screen and (max-width: $mobile-view) { | ||
48 | margin: 0 10px; | ||
49 | } | ||
46 | } | 50 | } |
47 | } | 51 | } |
48 | 52 | ||
@@ -71,16 +75,11 @@ $assets-path: '../assets'; | |||
71 | } | 75 | } |
72 | 76 | ||
73 | @media screen and (max-width: $mobile-view) { | 77 | @media screen and (max-width: $mobile-view) { |
74 | width: 70px; | ||
75 | 78 | ||
76 | .peertube-title { | 79 | .peertube-title { |
77 | display: none; | 80 | display: none; |
78 | } | 81 | } |
79 | } | 82 | } |
80 | |||
81 | @media screen and (max-width: 350px) { | ||
82 | flex: auto; | ||
83 | } | ||
84 | } | 83 | } |
85 | 84 | ||
86 | .header-right { | 85 | .header-right { |
diff --git a/client/src/app/core/notification/peertube-socket.service.ts b/client/src/app/core/notification/peertube-socket.service.ts index bc3f7b893..eab1c63f2 100644 --- a/client/src/app/core/notification/peertube-socket.service.ts +++ b/client/src/app/core/notification/peertube-socket.service.ts | |||
@@ -58,12 +58,11 @@ export class PeerTubeSocket { | |||
58 | this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', { | 58 | this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', { |
59 | query: { accessToken: this.auth.getAccessToken() } | 59 | query: { accessToken: this.auth.getAccessToken() } |
60 | }) | 60 | }) |
61 | |||
62 | this.notificationSocket.on('new-notification', (n: UserNotificationServer) => { | ||
63 | this.ngZone.run(() => this.dispatchNotificationEvent('new', n)) | ||
64 | }) | ||
65 | }) | 61 | }) |
66 | 62 | ||
63 | this.notificationSocket.on('new-notification', (n: UserNotificationServer) => { | ||
64 | this.ngZone.run(() => this.dispatchNotificationEvent('new', n)) | ||
65 | }) | ||
67 | } | 66 | } |
68 | 67 | ||
69 | private async initLiveVideosSocket () { | 68 | private async initLiveVideosSocket () { |
diff --git a/client/src/app/core/plugins/hooks.service.ts b/client/src/app/core/plugins/hooks.service.ts index ec47aa48c..ddde198d2 100644 --- a/client/src/app/core/plugins/hooks.service.ts +++ b/client/src/app/core/plugins/hooks.service.ts | |||
@@ -3,13 +3,29 @@ import { mergeMap, switchMap } from 'rxjs/operators' | |||
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { PluginService } from '@app/core/plugins/plugin.service' | 4 | import { PluginService } from '@app/core/plugins/plugin.service' |
5 | import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models' | 5 | import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models' |
6 | import { AuthService, AuthStatus } from '../auth' | ||
6 | 7 | ||
7 | type RawFunction<U, T> = (params: U) => T | 8 | type RawFunction<U, T> = (params: U) => T |
8 | type ObservableFunction<U, T> = RawFunction<U, Observable<T>> | 9 | type ObservableFunction<U, T> = RawFunction<U, Observable<T>> |
9 | 10 | ||
10 | @Injectable() | 11 | @Injectable() |
11 | export class HooksService { | 12 | export class HooksService { |
12 | constructor (private pluginService: PluginService) { } | 13 | constructor ( |
14 | private authService: AuthService, | ||
15 | private pluginService: PluginService | ||
16 | ) { | ||
17 | // Run auth hooks | ||
18 | this.authService.userInformationLoaded | ||
19 | .subscribe(() => this.runAction('action:auth-user.information-loaded', 'common', { user: this.authService.getUser() })) | ||
20 | |||
21 | this.authService.loginChangedSource.subscribe(obj => { | ||
22 | if (obj === AuthStatus.LoggedIn) { | ||
23 | this.runAction('action:auth-user.logged-in', 'common') | ||
24 | } else if (obj === AuthStatus.LoggedOut) { | ||
25 | this.runAction('action:auth-user.logged-out', 'common') | ||
26 | } | ||
27 | }) | ||
28 | } | ||
13 | 29 | ||
14 | wrapObsFun | 30 | wrapObsFun |
15 | <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName> | 31 | <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName> |
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index b755fda2c..54dba5e17 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts | |||
@@ -235,6 +235,12 @@ export class PluginService implements ClientHook { | |||
235 | .toPromise() | 235 | .toPromise() |
236 | }, | 236 | }, |
237 | 237 | ||
238 | getServerConfig: () => { | ||
239 | return this.server.getConfig() | ||
240 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
241 | .toPromise() | ||
242 | }, | ||
243 | |||
238 | isLoggedIn: () => { | 244 | isLoggedIn: () => { |
239 | return this.authService.isLoggedIn() | 245 | return this.authService.isLoggedIn() |
240 | }, | 246 | }, |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 11288fc54..906191ae1 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -98,6 +98,12 @@ export class ServerService { | |||
98 | extensions: [] | 98 | extensions: [] |
99 | } | 99 | } |
100 | }, | 100 | }, |
101 | banner: { | ||
102 | file: { | ||
103 | size: { max: 0 }, | ||
104 | extensions: [] | ||
105 | } | ||
106 | }, | ||
101 | video: { | 107 | video: { |
102 | image: { | 108 | image: { |
103 | size: { max: 0 }, | 109 | size: { max: 0 }, |
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index 15a4f7f82..8aaaa238d 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Account } from '@app/shared/shared-main/account/account.model' | 1 | import { Account } from '@app/shared/shared-main/account/account.model' |
2 | import { hasUserRight } from '@shared/core-utils/users' | 2 | import { hasUserRight } from '@shared/core-utils/users' |
3 | import { | 3 | import { |
4 | Avatar, | 4 | ActorImage, |
5 | NSFWPolicyType, | 5 | NSFWPolicyType, |
6 | User as UserServerModel, | 6 | User as UserServerModel, |
7 | UserAdminFlag, | 7 | UserAdminFlag, |
@@ -131,7 +131,7 @@ export class User implements UserServerModel { | |||
131 | } | 131 | } |
132 | } | 132 | } |
133 | 133 | ||
134 | updateAccountAvatar (newAccountAvatar?: Avatar) { | 134 | updateAccountAvatar (newAccountAvatar?: ActorImage) { |
135 | if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) | 135 | if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) |
136 | else this.account.resetAvatar() | 136 | else this.account.resetAvatar() |
137 | } | 137 | } |
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index 33cc1f668..3de83152c 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts | |||
@@ -7,8 +7,7 @@ import { AuthService } from '@app/core/auth' | |||
7 | import { getBytes } from '@root-helpers/bytes' | 7 | import { getBytes } from '@root-helpers/bytes' |
8 | import { UserLocalStorageKeys } from '@root-helpers/users' | 8 | import { UserLocalStorageKeys } from '@root-helpers/users' |
9 | import { | 9 | import { |
10 | Avatar, | 10 | ActorImage, |
11 | NSFWPolicyType, | ||
12 | ResultList, | 11 | ResultList, |
13 | User as UserServerModel, | 12 | User as UserServerModel, |
14 | UserCreate, | 13 | UserCreate, |
@@ -136,7 +135,7 @@ export class UserService { | |||
136 | changeAvatar (avatarForm: FormData) { | 135 | changeAvatar (avatarForm: FormData) { |
137 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' | 136 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' |
138 | 137 | ||
139 | return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) | 138 | return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm) |
140 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 139 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
141 | } | 140 | } |
142 | 141 | ||
diff --git a/client/src/app/core/wrappers/screen.service.ts b/client/src/app/core/wrappers/screen.service.ts index a085e5bdc..c133b5fe9 100644 --- a/client/src/app/core/wrappers/screen.service.ts +++ b/client/src/app/core/wrappers/screen.service.ts | |||
@@ -38,11 +38,10 @@ export class ScreenService { | |||
38 | 38 | ||
39 | let numberOfVideos = 1 | 39 | let numberOfVideos = 1 |
40 | 40 | ||
41 | if (screenWidth > 1850) numberOfVideos = 7 | 41 | if (screenWidth > 1850) numberOfVideos = 5 |
42 | else if (screenWidth > 1600) numberOfVideos = 6 | 42 | else if (screenWidth > 1600) numberOfVideos = 4 |
43 | else if (screenWidth > 1370) numberOfVideos = 5 | 43 | else if (screenWidth > 1370) numberOfVideos = 3 |
44 | else if (screenWidth > 1100) numberOfVideos = 4 | 44 | else if (screenWidth > 1100) numberOfVideos = 2 |
45 | else if (screenWidth > 850) numberOfVideos = 3 | ||
46 | 45 | ||
47 | return numberOfVideos | 46 | return numberOfVideos |
48 | } | 47 | } |
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html index 03e86b8e6..f84086b4a 100644 --- a/client/src/app/header/search-typeahead.component.html +++ b/client/src/app/header/search-typeahead.component.html | |||
@@ -34,7 +34,8 @@ | |||
34 | 34 | ||
35 | <!-- search instructions, when search input is empty --> | 35 | <!-- search instructions, when search input is empty --> |
36 | <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden"> | 36 | <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden"> |
37 | <div class="d-flex justify-content-between"> | 37 | <span class="text-muted" i18n>Your query will be matched against video names or descriptions, channel names.</span> |
38 | <div class="d-flex justify-content-between mt-3"> | ||
38 | <label class="small-title" i18n>ADVANCED SEARCH</label> | 39 | <label class="small-title" i18n>ADVANCED SEARCH</label> |
39 | <div class="advanced-search-status c-help"> | 40 | <div class="advanced-search-status c-help"> |
40 | <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows."> | 41 | <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows."> |
@@ -55,7 +56,6 @@ | |||
55 | <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span> | 56 | <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span> |
56 | </li> | 57 | </li> |
57 | </ul> | 58 | </ul> |
58 | <span class="text-muted" i18n>Any other input will return matching video or channel names.</span> | ||
59 | </div> | 59 | </div> |
60 | </div> | 60 | </div> |
61 | 61 | ||
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss index f8d68e986..a60aa38d6 100644 --- a/client/src/app/header/search-typeahead.component.scss +++ b/client/src/app/header/search-typeahead.component.scss | |||
@@ -86,7 +86,7 @@ li.suggestion { | |||
86 | flex: 1; | 86 | flex: 1; |
87 | 87 | ||
88 | input { | 88 | input { |
89 | width: unset; | 89 | width: 70px; |
90 | } | 90 | } |
91 | } | 91 | } |
92 | 92 | ||
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index 2ea66e57d..aa247d268 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss | |||
@@ -3,6 +3,7 @@ | |||
3 | 3 | ||
4 | $menu-link-icon-size: 22px; | 4 | $menu-link-icon-size: 22px; |
5 | $menu-link-icon-margin-right: 18px; | 5 | $menu-link-icon-margin-right: 18px; |
6 | $footer-links-base-opacity: .8; | ||
6 | 7 | ||
7 | @mixin menu-link { | 8 | @mixin menu-link { |
8 | display: flex; | 9 | display: flex; |
@@ -91,168 +92,168 @@ menu { | |||
91 | align-items: center; | 92 | align-items: center; |
92 | justify-content: left; | 93 | justify-content: left; |
93 | 94 | ||
94 | .logged-in-more { | 95 | my-notification { |
95 | $main-radius: 25px; | 96 | margin-left: auto; |
97 | margin-right: 15px; | ||
98 | } | ||
99 | } | ||
100 | } | ||
96 | 101 | ||
97 | flex: 1; | 102 | .logged-in-more { |
98 | margin-left: 13px; | 103 | $main-radius: 25px; |
99 | border-radius: $main-radius; | ||
100 | transition: all .1s ease-in-out; | ||
101 | cursor: pointer; | ||
102 | 104 | ||
103 | *, & { | 105 | flex: 1; |
104 | line-height: 1; | 106 | margin-left: 13px; |
105 | } | 107 | border-radius: $main-radius; |
108 | transition: all .1s ease-in-out; | ||
109 | cursor: pointer; | ||
106 | 110 | ||
107 | &.show { | 111 | *, & { |
108 | background-color: rgba(255, 255, 255, 0.20); | 112 | line-height: 1; |
109 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325); | 113 | } |
110 | } | ||
111 | 114 | ||
112 | @mixin display-hints($is-mobile: false) { | 115 | &.show { |
113 | background-color: rgba(255, 255, 255, 0.15); | 116 | background-color: rgba(255, 255, 255, 0.20); |
114 | 117 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325); | |
115 | @if $is-mobile { | 118 | } |
116 | .dropdown-toggle-indicator { | 119 | |
117 | display: inherit !important; | 120 | @mixin display-hints($is-mobile: false) { |
118 | } | 121 | background-color: rgba(255, 255, 255, 0.15); |
119 | .dropdown-toggle:first-child { | ||
120 | padding-right: 30px !important; | ||
121 | } | ||
122 | } | ||
123 | } | ||
124 | 122 | ||
125 | &:hover { | 123 | @if $is-mobile { |
126 | @include display-hints; | 124 | .dropdown-toggle-indicator { |
125 | display: inherit !important; | ||
126 | } | ||
127 | .dropdown-toggle:first-child { | ||
128 | padding-right: 30px !important; | ||
127 | } | 129 | } |
130 | } | ||
131 | } | ||
128 | 132 | ||
129 | /* smartphones and touchscreens */ | 133 | &:hover { |
130 | @media (hover: none) and (pointer: coarse) { | 134 | @include display-hints; |
131 | @include display-hints($is-mobile: true); | 135 | } |
132 | 136 | ||
133 | /* fill space when on mobile */ | 137 | /* smartphones and touchscreens */ |
134 | max-width: calc(100% - 80px); | 138 | @media (hover: none) and (pointer: coarse) { |
135 | .dropdown-toggle { | 139 | @include display-hints($is-mobile: true); |
136 | max-width: 100%; | ||
137 | } | ||
138 | .logged-in-info { | ||
139 | max-width: calc(100% - 45px) !important; | ||
140 | } | ||
141 | 140 | ||
142 | } | 141 | /* fill space when on mobile */ |
142 | max-width: calc(100% - 80px); | ||
143 | .dropdown-toggle { | ||
144 | max-width: 100%; | ||
145 | } | ||
146 | .logged-in-info { | ||
147 | max-width: calc(100% - 45px) !important; | ||
148 | } | ||
143 | 149 | ||
144 | .dropdown-toggle-indicator { | 150 | } |
145 | position: relative; | ||
146 | width: 0; | ||
147 | display: none; | ||
148 | |||
149 | span { | ||
150 | position: absolute; | ||
151 | right: -35px; | ||
152 | top: -8px; | ||
153 | color: grey; | ||
154 | width: $main-radius; | ||
155 | } | ||
156 | } | ||
157 | 151 | ||
158 | .dropdown-toggle { | 152 | .dropdown-toggle-indicator { |
159 | &::after { | 153 | position: relative; |
160 | border: none; | 154 | width: 0; |
161 | } | 155 | display: none; |
162 | } | ||
163 | 156 | ||
164 | .dropdown-toggle:first-child { | 157 | span { |
165 | display: flex; | 158 | position: absolute; |
166 | align-items: center; | 159 | right: -35px; |
167 | padding: 5px 7px; | 160 | top: -8px; |
168 | border-radius: $main-radius; | 161 | color: grey; |
169 | } | 162 | width: $main-radius; |
163 | } | ||
164 | } | ||
170 | 165 | ||
171 | img { | 166 | .dropdown-toggle { |
172 | @include avatar(34px); | 167 | &::after { |
168 | border: none; | ||
169 | } | ||
170 | } | ||
173 | 171 | ||
174 | margin-right: 10px; | 172 | .dropdown-toggle:first-child { |
175 | } | 173 | display: flex; |
174 | align-items: center; | ||
175 | padding: 5px 7px; | ||
176 | border-radius: $main-radius; | ||
177 | } | ||
178 | |||
179 | img { | ||
180 | @include avatar(34px); | ||
176 | 181 | ||
177 | .logged-in-info { | 182 | margin-right: 10px; |
178 | max-width: 105px; | 183 | } |
184 | } | ||
179 | 185 | ||
180 | flex-grow: 1; | 186 | .logged-in-info { |
187 | max-width: 105px; | ||
181 | 188 | ||
182 | .logged-in-display-name, | 189 | flex-grow: 1; |
183 | .logged-in-username { | ||
184 | @include ellipsis; | ||
185 | } | ||
186 | 190 | ||
187 | .logged-in-display-name { | 191 | .logged-in-display-name, |
188 | font-size: 16px; | 192 | .logged-in-username { |
189 | font-weight: $font-semibold; | 193 | @include ellipsis; |
190 | color: pvar(--menuForegroundColor); | 194 | } |
191 | 195 | ||
192 | @include disable-default-a-behaviour; | 196 | .logged-in-display-name { |
193 | } | 197 | font-size: 16px; |
198 | font-weight: $font-semibold; | ||
199 | color: pvar(--menuForegroundColor); | ||
194 | 200 | ||
195 | .logged-in-username { | 201 | @include disable-default-a-behaviour; |
196 | font-size: 13px; | 202 | } |
197 | color: #C6C6C6; | ||
198 | margin-top: 3px; | ||
199 | } | ||
200 | } | ||
201 | } | ||
202 | 203 | ||
203 | my-notification { | 204 | .logged-in-username { |
204 | margin-left: auto; | 205 | font-size: 13px; |
205 | margin-right: 15px; | 206 | color: #C6C6C6; |
206 | } | 207 | margin-top: 3px; |
207 | } | 208 | } |
209 | } | ||
208 | 210 | ||
209 | .logged-in-menu { | 211 | .logged-in-menu { |
210 | display: flex; | 212 | display: flex; |
211 | flex-direction: column; | 213 | flex-direction: column; |
212 | align-items: flex-start; | 214 | align-items: flex-start; |
213 | border-top: 1px solid var(--greyForegroundColor); | 215 | border-top: 1px solid var(--greyForegroundColor); |
214 | line-height: $line-height-normal; | 216 | line-height: $line-height-normal; |
215 | 217 | ||
216 | a { | 218 | a { |
217 | @include menu-link; | 219 | @include menu-link; |
218 | @include disable-default-a-behaviour; | 220 | @include disable-default-a-behaviour; |
219 | 221 | ||
220 | $icon-size: 13px; | 222 | $icon-size: 13px; |
221 | $additional-margin: ($menu-link-icon-size - $icon-size) / 2; | 223 | $additional-margin: ($menu-link-icon-size - $icon-size) / 2; |
222 | 224 | ||
223 | font-size: 14px; | 225 | font-size: 14px; |
224 | width: 100%; | 226 | width: 100%; |
225 | min-height: 35px; | 227 | min-height: 35px; |
226 | 228 | ||
227 | my-global-icon { | 229 | my-global-icon { |
228 | width: $icon-size; | 230 | width: $icon-size; |
229 | height: $icon-size; | 231 | height: $icon-size; |
230 | 232 | ||
231 | // Keep aligned with other icons | 233 | // Keep aligned with other icons |
232 | margin-left: $additional-margin; | 234 | margin-left: $additional-margin; |
233 | 235 | ||
234 | &[iconName="channel"] { | 236 | &[iconName="channel"] { |
235 | margin-top: -2px; | 237 | margin-top: -2px; |
236 | } | ||
237 | } | 238 | } |
239 | } | ||
238 | 240 | ||
239 | &.active, | 241 | &.active, |
240 | &:hover, | 242 | &:hover, |
241 | &:focus-visible { | 243 | &:focus-visible { |
242 | my-global-icon { | 244 | my-global-icon { |
243 | @include apply-svg-color(var(--menuForegroundColor)); | 245 | @include apply-svg-color(var(--menuForegroundColor)); |
244 | } | ||
245 | } | 246 | } |
247 | } | ||
246 | 248 | ||
247 | &.active { | 249 | &.active { |
248 | $border-left-width: 4px; | 250 | $border-left-width: 4px; |
249 | 251 | ||
250 | font-weight: $font-semibold; | 252 | font-weight: $font-semibold; |
251 | border-left: $border-left-width solid var(--mainColor); | 253 | border-left: $border-left-width solid var(--mainColor); |
252 | 254 | ||
253 | my-global-icon { | 255 | my-global-icon { |
254 | margin-left: $additional-margin - $border-left-width; | 256 | margin-left: $additional-margin - $border-left-width; |
255 | } | ||
256 | } | 257 | } |
257 | } | 258 | } |
258 | } | 259 | } |
@@ -333,50 +334,48 @@ menu { | |||
333 | flex-direction: column; | 334 | flex-direction: column; |
334 | padding: 0 $menu-lateral-padding; | 335 | padding: 0 $menu-lateral-padding; |
335 | } | 336 | } |
337 | } | ||
336 | 338 | ||
337 | $footer-links-base-opacity: .8; | 339 | .footer-links { |
338 | 340 | &, > div { | |
339 | .footer-links { | 341 | display: flex; |
340 | &, > div { | 342 | flex-wrap: wrap; |
341 | display: flex; | 343 | } |
342 | flex-wrap: wrap; | ||
343 | } | ||
344 | 344 | ||
345 | a, span[role=button] { | 345 | a, span[role=button] { |
346 | display: inline-block; | 346 | display: inline-block; |
347 | text-decoration: none; | 347 | text-decoration: none; |
348 | color: pvar(--menuForegroundColor); | 348 | color: pvar(--menuForegroundColor); |
349 | opacity: $footer-links-base-opacity; | 349 | opacity: $footer-links-base-opacity; |
350 | white-space: nowrap; | ||
351 | font-size: 90%; | ||
352 | font-weight: 500; | ||
353 | line-height: 1.4rem; | ||
354 | margin-right: 8px; | ||
355 | |||
356 | &.inline-global-icon { | ||
357 | display: inline-flex; | ||
358 | align-items: center; | ||
350 | white-space: nowrap; | 359 | white-space: nowrap; |
351 | font-size: 90%; | 360 | height: 1.4rem; |
352 | font-weight: 500; | 361 | |
353 | line-height: 1.4rem; | 362 | my-global-icon { |
354 | margin-right: 8px; | 363 | @include apply-svg-color(pvar(--menuForegroundColor)); |
355 | 364 | ||
356 | &.inline-global-icon { | 365 | display: flex; |
357 | display: inline-flex; | 366 | width: auto; |
358 | align-items: center; | 367 | height: 90%; |
359 | white-space: nowrap; | 368 | margin-right: .2rem; |
360 | height: 1.4rem; | ||
361 | |||
362 | my-global-icon { | ||
363 | @include apply-svg-color(pvar(--menuForegroundColor)); | ||
364 | |||
365 | display: flex; | ||
366 | width: auto; | ||
367 | height: 90%; | ||
368 | margin-right: .2rem; | ||
369 | } | ||
370 | } | 369 | } |
371 | } | 370 | } |
372 | } | 371 | } |
372 | } | ||
373 | 373 | ||
374 | .footer-copyleft small a { | 374 | .footer-copyleft small a { |
375 | @include disable-default-a-behaviour; | 375 | @include disable-default-a-behaviour; |
376 | 376 | ||
377 | color: pvar(--menuForegroundColor); | 377 | color: pvar(--menuForegroundColor); |
378 | opacity: $footer-links-base-opacity - .2; | 378 | opacity: $footer-links-base-opacity - .2; |
379 | } | ||
380 | } | 379 | } |
381 | 380 | ||
382 | .dropdown { | 381 | .dropdown { |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index ed20d9c01..9b6b7cda5 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -10,6 +10,7 @@ import { LanguageChooserComponent } from '@app/menu/language-chooser.component' | |||
10 | import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' | 10 | import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' |
11 | import { ServerConfig, UserRight, VideoConstant } from '@shared/models' | 11 | import { ServerConfig, UserRight, VideoConstant } from '@shared/models' |
12 | import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap' | 12 | import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap' |
13 | import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service' | ||
13 | 14 | ||
14 | const logger = debug('peertube:menu:MenuComponent') | 15 | const logger = debug('peertube:menu:MenuComponent') |
15 | 16 | ||
@@ -54,6 +55,7 @@ export class MenuComponent implements OnInit { | |||
54 | private hotkeysService: HotkeysService, | 55 | private hotkeysService: HotkeysService, |
55 | private screenService: ScreenService, | 56 | private screenService: ScreenService, |
56 | private menuService: MenuService, | 57 | private menuService: MenuService, |
58 | private modalService: PeertubeModalService, | ||
57 | private dropdownConfig: NgbDropdownConfig, | 59 | private dropdownConfig: NgbDropdownConfig, |
58 | private router: Router | 60 | private router: Router |
59 | ) { | 61 | ) { |
@@ -130,6 +132,9 @@ export class MenuComponent implements OnInit { | |||
130 | this.authService.userInformationLoaded | 132 | this.authService.userInformationLoaded |
131 | .subscribe(() => this.buildUserLanguages()) | 133 | .subscribe(() => this.buildUserLanguages()) |
132 | }) | 134 | }) |
135 | |||
136 | this.modalService.openQuickSettingsSubject | ||
137 | .subscribe(() => this.openQuickSettings()) | ||
133 | } | 138 | } |
134 | 139 | ||
135 | isRegistrationAllowed () { | 140 | isRegistrationAllowed () { |
diff --git a/client/src/app/menu/notification.component.scss b/client/src/app/menu/notification.component.scss index 40feb9e66..c65787779 100644 --- a/client/src/app/menu/notification.component.scss +++ b/client/src/app/menu/notification.component.scss | |||
@@ -1,6 +1,9 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .content { | ||
5 | scrollbar-color: auto; | ||
6 | } | ||
4 | 7 | ||
5 | .notification-inbox-popover { | 8 | .notification-inbox-popover { |
6 | padding: 10px; | 9 | padding: 10px; |
diff --git a/client/src/app/modal/instance-config-warning-modal.component.html b/client/src/app/modal/instance-config-warning-modal.component.html index 5a8adf726..498adfeff 100644 --- a/client/src/app/modal/instance-config-warning-modal.component.html +++ b/client/src/app/modal/instance-config-warning-modal.component.html | |||
@@ -15,7 +15,7 @@ | |||
15 | 15 | ||
16 | <li i18n *ngIf="!about.instance.administrator">Who you are</li> | 16 | <li i18n *ngIf="!about.instance.administrator">Who you are</li> |
17 | <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li> | 17 | <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li> |
18 | <li i18n *ngIf="!about.instance.businessModel">How you plan to pay your instance</li> | 18 | <li i18n *ngIf="!about.instance.businessModel">How you plan to pay for keeping your instance running</li> |
19 | 19 | ||
20 | <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li> | 20 | <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li> |
21 | <li i18n *ngIf="!about.instance.terms">Instance terms</li> | 21 | <li i18n *ngIf="!about.instance.terms">Instance terms</li> |
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 e34836a18..eeb9f128b 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 | |||
@@ -117,7 +117,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV | |||
117 | warningTitle: false, | 117 | warningTitle: false, |
118 | startTime: abuse.video.startAt, | 118 | startTime: abuse.video.startAt, |
119 | stopTime: abuse.video.endAt | 119 | stopTime: abuse.video.endAt |
120 | }) | 120 | }), |
121 | abuse.video.name | ||
121 | ) | 122 | ) |
122 | } | 123 | } |
123 | 124 | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html new file mode 100644 index 000000000..0829263f4 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html | |||
@@ -0,0 +1,41 @@ | |||
1 | <div class="actor" *ngIf="actor"> | ||
2 | <div class="d-flex"> | ||
3 | <img [ngClass]="{ channel: isChannel() }" [src]="preview || actor.avatarUrl" alt="Avatar" /> | ||
4 | |||
5 | <div class="actor-img-edit-container"> | ||
6 | |||
7 | <div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body"> | ||
8 | <my-global-icon iconName="upload"></my-global-icon> | ||
9 | <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label> | ||
10 | <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> | ||
11 | </div> | ||
12 | |||
13 | <div | ||
14 | *ngIf="editable && hasAvatar()" class="actor-img-edit-button" | ||
15 | #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right" | ||
16 | > | ||
17 | <my-global-icon iconName="edit"></my-global-icon> | ||
18 | <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label> | ||
19 | </div> | ||
20 | |||
21 | </div> | ||
22 | </div> | ||
23 | |||
24 | <div class="actor-info"> | ||
25 | <div class="actor-info-display-name">{{ actor.displayName }}</div> | ||
26 | <div *ngIf="displayUsername" class="actor-info-username">{{ actor.name }}</div> | ||
27 | <div *ngIf="displaySubscribers" i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div> | ||
28 | </div> | ||
29 | </div> | ||
30 | |||
31 | <ng-template #avatarEditContent> | ||
32 | <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body"> | ||
33 | <my-global-icon iconName="upload"></my-global-icon> | ||
34 | <span for="avatarfile" i18n>Upload a new avatar</span> | ||
35 | <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> | ||
36 | </div> | ||
37 | <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()"> | ||
38 | <my-global-icon iconName="delete"></my-global-icon> | ||
39 | <span i18n>Remove avatar</span> | ||
40 | </div> | ||
41 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss new file mode 100644 index 000000000..8b0172315 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss | |||
@@ -0,0 +1,54 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .actor { | ||
5 | display: flex; | ||
6 | |||
7 | img { | ||
8 | margin-right: 15px; | ||
9 | |||
10 | &:not(.channel) { | ||
11 | @include avatar(100px); | ||
12 | } | ||
13 | |||
14 | &.channel { | ||
15 | @include channel-avatar(100px); | ||
16 | } | ||
17 | } | ||
18 | |||
19 | .actor-info { | ||
20 | display: inline-flex; | ||
21 | flex-direction: column; | ||
22 | |||
23 | .actor-info-display-name { | ||
24 | font-size: 20px; | ||
25 | font-weight: $font-bold; | ||
26 | |||
27 | @media screen and (max-width: $small-view) { | ||
28 | font-size: 16px; | ||
29 | } | ||
30 | } | ||
31 | |||
32 | .actor-info-username { | ||
33 | position: relative; | ||
34 | font-size: 14px; | ||
35 | color: pvar(--greyForegroundColor); | ||
36 | } | ||
37 | |||
38 | .actor-info-followers { | ||
39 | font-size: 15px; | ||
40 | padding-bottom: .5rem; | ||
41 | } | ||
42 | } | ||
43 | } | ||
44 | |||
45 | .actor-img-edit-container { | ||
46 | position: relative; | ||
47 | width: 0; | ||
48 | } | ||
49 | |||
50 | .actor-img-edit-button { | ||
51 | top: 55px; | ||
52 | right: 45px; | ||
53 | border-radius: 50%; | ||
54 | } | ||
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts index b459c591f..d0d269489 100644 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts +++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts | |||
@@ -1,21 +1,27 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' | ||
2 | import { Notifier, ServerService } from '@app/core' | 3 | import { Notifier, ServerService } from '@app/core' |
4 | import { Account, VideoChannel } from '@app/shared/shared-main' | ||
3 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | 5 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' |
4 | import { getBytes } from '@root-helpers/bytes' | 6 | import { getBytes } from '@root-helpers/bytes' |
5 | import { Account } from '../account/account.model' | ||
6 | import { VideoChannel } from '../video-channel/video-channel.model' | ||
7 | import { Actor } from './actor.model' | ||
8 | 7 | ||
9 | @Component({ | 8 | @Component({ |
10 | selector: 'my-actor-avatar-info', | 9 | selector: 'my-actor-avatar-edit', |
11 | templateUrl: './actor-avatar-info.component.html', | 10 | templateUrl: './actor-avatar-edit.component.html', |
12 | styleUrls: [ './actor-avatar-info.component.scss' ] | 11 | styleUrls: [ |
12 | './actor-image-edit.scss', | ||
13 | './actor-avatar-edit.component.scss' | ||
14 | ] | ||
13 | }) | 15 | }) |
14 | export class ActorAvatarInfoComponent implements OnInit, OnChanges { | 16 | export class ActorAvatarEditComponent implements OnInit { |
15 | @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> | 17 | @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> |
16 | @ViewChild('avatarPopover') avatarPopover: NgbPopover | 18 | @ViewChild('avatarPopover') avatarPopover: NgbPopover |
17 | 19 | ||
18 | @Input() actor: VideoChannel | Account | 20 | @Input() actor: VideoChannel | Account |
21 | @Input() editable = true | ||
22 | @Input() displaySubscribers = true | ||
23 | @Input() displayUsername = true | ||
24 | @Input() previewImage = false | ||
19 | 25 | ||
20 | @Output() avatarChange = new EventEmitter<FormData>() | 26 | @Output() avatarChange = new EventEmitter<FormData>() |
21 | @Output() avatarDelete = new EventEmitter<void>() | 27 | @Output() avatarDelete = new EventEmitter<void>() |
@@ -24,9 +30,10 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges { | |||
24 | maxAvatarSize = 0 | 30 | maxAvatarSize = 0 |
25 | avatarExtensions = '' | 31 | avatarExtensions = '' |
26 | 32 | ||
27 | private avatarUrl: string | 33 | preview: SafeResourceUrl |
28 | 34 | ||
29 | constructor ( | 35 | constructor ( |
36 | private sanitizer: DomSanitizer, | ||
30 | private serverService: ServerService, | 37 | private serverService: ServerService, |
31 | private notifier: Notifier | 38 | private notifier: Notifier |
32 | ) { } | 39 | ) { } |
@@ -42,12 +49,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges { | |||
42 | }) | 49 | }) |
43 | } | 50 | } |
44 | 51 | ||
45 | ngOnChanges (changes: SimpleChanges) { | ||
46 | if (changes['actor']) { | ||
47 | this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | onAvatarChange (input: HTMLInputElement) { | 52 | onAvatarChange (input: HTMLInputElement) { |
52 | this.avatarfileInput = new ElementRef(input) | 53 | this.avatarfileInput = new ElementRef(input) |
53 | 54 | ||
@@ -61,13 +62,22 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges { | |||
61 | formData.append('avatarfile', avatarfile) | 62 | formData.append('avatarfile', avatarfile) |
62 | this.avatarPopover?.close() | 63 | this.avatarPopover?.close() |
63 | this.avatarChange.emit(formData) | 64 | this.avatarChange.emit(formData) |
65 | |||
66 | if (this.previewImage) { | ||
67 | this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(avatarfile)) | ||
68 | } | ||
64 | } | 69 | } |
65 | 70 | ||
66 | deleteAvatar () { | 71 | deleteAvatar () { |
72 | this.preview = undefined | ||
67 | this.avatarDelete.emit() | 73 | this.avatarDelete.emit() |
68 | } | 74 | } |
69 | 75 | ||
70 | hasAvatar () { | 76 | hasAvatar () { |
71 | return !!this.avatarUrl | 77 | return !!this.preview || !!this.actor.avatar |
78 | } | ||
79 | |||
80 | isChannel () { | ||
81 | return !!(this.actor as VideoChannel).ownerAccount | ||
72 | } | 82 | } |
73 | } | 83 | } |
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html new file mode 100644 index 000000000..266fc26c5 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html | |||
@@ -0,0 +1,34 @@ | |||
1 | <div class="actor" *ngIf="actor"> | ||
2 | <div class="actor-img-edit-container"> | ||
3 | <div class="banner-placeholder"> | ||
4 | <img *ngIf="hasBanner()" [src]="preview || actor.bannerUrl" alt="Banner" /> | ||
5 | </div> | ||
6 | |||
7 | <div *ngIf="!hasBanner()" class="actor-img-edit-button" [ngbTooltip]="bannerFormat" placement="right" container="body"> | ||
8 | <my-global-icon iconName="upload"></my-global-icon> | ||
9 | <label for="bannerfile" i18n>Upload a new banner</label> | ||
10 | <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/> | ||
11 | </div> | ||
12 | |||
13 | <div | ||
14 | *ngIf="hasBanner()" class="actor-img-edit-button" | ||
15 | #bannerPopover="ngbPopover" [ngbPopover]="bannerEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right" | ||
16 | > | ||
17 | <my-global-icon iconName="edit"></my-global-icon> | ||
18 | <label for="bannerMenu" i18n>Change your banner</label> | ||
19 | </div> | ||
20 | </div> | ||
21 | </div> | ||
22 | |||
23 | <ng-template #bannerEditContent> | ||
24 | <div class="dropdown-item c-hand" [ngbTooltip]="bannerFormat" placement="right" container="body"> | ||
25 | <my-global-icon iconName="upload"></my-global-icon> | ||
26 | <span for="bannerfile" i18n>Upload a new banner</span> | ||
27 | <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/> | ||
28 | </div> | ||
29 | |||
30 | <div class="dropdown-item c-hand" (click)="deleteBanner()" (key.enter)="deleteBanner()"> | ||
31 | <my-global-icon iconName="delete"></my-global-icon> | ||
32 | <span i18n>Remove banner</span> | ||
33 | </div> | ||
34 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss new file mode 100644 index 000000000..23606f871 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss | |||
@@ -0,0 +1,27 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .banner-placeholder { | ||
5 | @include block-ratio('> div, > img', $banner-inverted-ratio); | ||
6 | } | ||
7 | |||
8 | .banner-placeholder { | ||
9 | background-color: pvar(--greyBackgroundColor); | ||
10 | } | ||
11 | |||
12 | .actor-img-edit-container { | ||
13 | position: relative; | ||
14 | display: flex; | ||
15 | justify-content: center; | ||
16 | align-items: center; | ||
17 | } | ||
18 | |||
19 | .actor-img-edit-button { | ||
20 | position: absolute; | ||
21 | width: auto; | ||
22 | |||
23 | label { | ||
24 | font-weight: $font-semibold; | ||
25 | margin-bottom: 0; | ||
26 | } | ||
27 | } | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts new file mode 100644 index 000000000..8c12d3c4c --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts | |||
@@ -0,0 +1,76 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' | ||
2 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' | ||
3 | import { Notifier, ServerService } from '@app/core' | ||
4 | import { VideoChannel } from '@app/shared/shared-main' | ||
5 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | ||
6 | import { getBytes } from '@root-helpers/bytes' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-actor-banner-edit', | ||
10 | templateUrl: './actor-banner-edit.component.html', | ||
11 | styleUrls: [ | ||
12 | './actor-image-edit.scss', | ||
13 | './actor-banner-edit.component.scss' | ||
14 | ] | ||
15 | }) | ||
16 | export class ActorBannerEditComponent implements OnInit { | ||
17 | @ViewChild('bannerfileInput') bannerfileInput: ElementRef<HTMLInputElement> | ||
18 | @ViewChild('bannerPopover') bannerPopover: NgbPopover | ||
19 | |||
20 | @Input() actor: VideoChannel | ||
21 | @Input() previewImage = false | ||
22 | |||
23 | @Output() bannerChange = new EventEmitter<FormData>() | ||
24 | @Output() bannerDelete = new EventEmitter<void>() | ||
25 | |||
26 | bannerFormat = '' | ||
27 | maxBannerSize = 0 | ||
28 | bannerExtensions = '' | ||
29 | |||
30 | preview: SafeResourceUrl | ||
31 | |||
32 | constructor ( | ||
33 | private sanitizer: DomSanitizer, | ||
34 | private serverService: ServerService, | ||
35 | private notifier: Notifier | ||
36 | ) { } | ||
37 | |||
38 | ngOnInit (): void { | ||
39 | this.serverService.getConfig() | ||
40 | .subscribe(config => { | ||
41 | this.maxBannerSize = config.banner.file.size.max | ||
42 | this.bannerExtensions = config.banner.file.extensions.join(', ') | ||
43 | |||
44 | // tslint:disable:max-line-length | ||
45 | this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}` | ||
46 | }) | ||
47 | } | ||
48 | |||
49 | onBannerChange (input: HTMLInputElement) { | ||
50 | this.bannerfileInput = new ElementRef(input) | ||
51 | |||
52 | const bannerfile = this.bannerfileInput.nativeElement.files[ 0 ] | ||
53 | if (bannerfile.size > this.maxBannerSize) { | ||
54 | this.notifier.error('Error', $localize`This image is too large.`) | ||
55 | return | ||
56 | } | ||
57 | |||
58 | const formData = new FormData() | ||
59 | formData.append('bannerfile', bannerfile) | ||
60 | this.bannerPopover?.close() | ||
61 | this.bannerChange.emit(formData) | ||
62 | |||
63 | if (this.previewImage) { | ||
64 | this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(bannerfile)) | ||
65 | } | ||
66 | } | ||
67 | |||
68 | deleteBanner () { | ||
69 | this.preview = undefined | ||
70 | this.bannerDelete.emit() | ||
71 | } | ||
72 | |||
73 | hasBanner () { | ||
74 | return !!this.preview || !!this.actor.bannerUrl | ||
75 | } | ||
76 | } | ||
diff --git a/client/src/app/shared/shared-actor-image/actor-image-edit.scss b/client/src/app/shared/shared-actor-image/actor-image-edit.scss new file mode 100644 index 000000000..918955a89 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/actor-image-edit.scss | |||
@@ -0,0 +1,35 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .actor ::ng-deep .popover-image-info .popover-body { | ||
5 | padding: 0; | ||
6 | |||
7 | .dropdown-item { | ||
8 | padding: 6px 10px; | ||
9 | border-radius: 4px; | ||
10 | |||
11 | &:first-child { | ||
12 | @include peertube-file; | ||
13 | display: block; | ||
14 | } | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .actor-img-edit-button { | ||
19 | @include peertube-button-file(21px); | ||
20 | @include button-with-icon(19px); | ||
21 | @include orange-button; | ||
22 | |||
23 | margin-top: 10px; | ||
24 | margin-bottom: 5px; | ||
25 | cursor: pointer; | ||
26 | |||
27 | input { | ||
28 | width: 30px; | ||
29 | height: 30px; | ||
30 | } | ||
31 | |||
32 | my-global-icon { | ||
33 | right: 7px; | ||
34 | } | ||
35 | } | ||
diff --git a/client/src/app/shared/shared-actor-image/index.ts b/client/src/app/shared/shared-actor-image/index.ts new file mode 100644 index 000000000..18a9038eb --- /dev/null +++ b/client/src/app/shared/shared-actor-image/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './shared-actor-image.module' | |||
diff --git a/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts new file mode 100644 index 000000000..6044f9925 --- /dev/null +++ b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts | |||
@@ -0,0 +1,29 @@ | |||
1 | |||
2 | import { CommonModule } from '@angular/common' | ||
3 | import { NgModule } from '@angular/core' | ||
4 | import { SharedGlobalIconModule } from '../shared-icons' | ||
5 | import { SharedMainModule } from '../shared-main' | ||
6 | import { ActorAvatarEditComponent } from './actor-avatar-edit.component' | ||
7 | import { ActorBannerEditComponent } from './actor-banner-edit.component' | ||
8 | |||
9 | @NgModule({ | ||
10 | imports: [ | ||
11 | CommonModule, | ||
12 | |||
13 | SharedMainModule, | ||
14 | SharedGlobalIconModule | ||
15 | ], | ||
16 | |||
17 | declarations: [ | ||
18 | ActorAvatarEditComponent, | ||
19 | ActorBannerEditComponent | ||
20 | ], | ||
21 | |||
22 | exports: [ | ||
23 | ActorAvatarEditComponent, | ||
24 | ActorBannerEditComponent | ||
25 | ], | ||
26 | |||
27 | providers: [ ] | ||
28 | }) | ||
29 | export class SharedActorImageModule { } | ||
diff --git a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html index e7441e4c1..9f252f299 100644 --- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html +++ b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html | |||
@@ -12,9 +12,10 @@ | |||
12 | 12 | ||
13 | <button | 13 | <button |
14 | *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" | 14 | *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" |
15 | class="btn btn-outline-secondary" i18n-title title="Copy" | 15 | class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy" |
16 | > | 16 | > |
17 | <span class="glyphicon glyphicon-copy"></span> | 17 | <span class="glyphicon glyphicon-duplicate"></span> |
18 | Copy | ||
18 | </button> | 19 | </button> |
19 | </div> | 20 | </div> |
20 | </div> | 21 | </div> |
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.scss b/client/src/app/shared/shared-forms/markdown-textarea.component.scss index fcddfea03..8203c7d1c 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.scss +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.scss | |||
@@ -131,7 +131,7 @@ $input-border-radius: 3px; | |||
131 | border-right: none; | 131 | border-right: none; |
132 | 132 | ||
133 | :last-child { | 133 | :last-child { |
134 | margin-right: $not-expanded-horizontal-margins; | 134 | margin-right: pvar(--horizontalMarginContent); |
135 | } | 135 | } |
136 | } | 136 | } |
137 | 137 | ||
diff --git a/client/src/app/shared/shared-forms/select/select-options.component.ts b/client/src/app/shared/shared-forms/select/select-options.component.ts index 2890670e5..8482b9dea 100644 --- a/client/src/app/shared/shared-forms/select/select-options.component.ts +++ b/client/src/app/shared/shared-forms/select/select-options.component.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, forwardRef, HostListener, Input } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' | 3 | import { SelectOptionsItem } from '../../../../types/select-options-item.model' |
4 | 4 | ||
@@ -26,6 +26,13 @@ export class SelectOptionsComponent implements ControlValueAccessor { | |||
26 | 26 | ||
27 | propagateChange = (_: any) => { /* empty */ } | 27 | propagateChange = (_: any) => { /* empty */ } |
28 | 28 | ||
29 | // Allow plugins to update our value | ||
30 | @HostListener('change', [ '$event.target' ]) | ||
31 | handleChange (event: any) { | ||
32 | this.writeValue(event.value) | ||
33 | this.onModelChange() | ||
34 | } | ||
35 | |||
29 | writeValue (id: number | string) { | 36 | writeValue (id: number | string) { |
30 | this.selectedId = id | 37 | this.selectedId = id |
31 | } | 38 | } |
diff --git a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss index 275600d60..2f6b420e3 100644 --- a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss +++ b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss | |||
@@ -31,7 +31,7 @@ ngb-accordion ::ng-deep { | |||
31 | padding: 0; | 31 | padding: 0; |
32 | 32 | ||
33 | & + .collapse.show { | 33 | & + .collapse.show { |
34 | background-color: var(--submenuColor); | 34 | background-color: var(--submenuBackgroundColor); |
35 | } | 35 | } |
36 | } | 36 | } |
37 | } | 37 | } |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html index ce2557147..d505b6739 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.html +++ b/client/src/app/shared/shared-instance/instance-features-table.component.html | |||
@@ -11,7 +11,7 @@ | |||
11 | <tr> | 11 | <tr> |
12 | <th i18n class="label" scope="row"> | 12 | <th i18n class="label" scope="row"> |
13 | <div>Default NSFW/sensitive videos policy</div> | 13 | <div>Default NSFW/sensitive videos policy</div> |
14 | <div class="more-info">can be redefined by the users</div> | 14 | <div class="c-hand more-info" (click)="openQuickSettingsHighlight()">can be redefined by the users</div> |
15 | </th> | 15 | </th> |
16 | 16 | ||
17 | <td class="value">{{ buildNSFWLabel() }}</td> | 17 | <td class="value">{{ buildNSFWLabel() }}</td> |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts index 0166157f9..c3b3dfdfd 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.ts +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
3 | import { ServerConfig } from '@shared/models' | 3 | import { ServerConfig } from '@shared/models' |
4 | import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service' | ||
4 | 5 | ||
5 | @Component({ | 6 | @Component({ |
6 | selector: 'my-instance-features-table', | 7 | selector: 'my-instance-features-table', |
@@ -11,7 +12,10 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
11 | quotaHelpIndication = '' | 12 | quotaHelpIndication = '' |
12 | serverConfig: ServerConfig | 13 | serverConfig: ServerConfig |
13 | 14 | ||
14 | constructor (private serverService: ServerService) { } | 15 | constructor ( |
16 | private serverService: ServerService, | ||
17 | private modalService: PeertubeModalService | ||
18 | ) { } | ||
15 | 19 | ||
16 | get initialUserVideoQuota () { | 20 | get initialUserVideoQuota () { |
17 | return this.serverConfig.user.videoQuota | 21 | return this.serverConfig.user.videoQuota |
@@ -56,6 +60,10 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
56 | return this.serverService.getServerVersionAndCommit() | 60 | return this.serverService.getServerVersionAndCommit() |
57 | } | 61 | } |
58 | 62 | ||
63 | openQuickSettingsHighlight () { | ||
64 | this.modalService.openQuickSettingsSubject.next() | ||
65 | } | ||
66 | |||
59 | private getApproximateTime (seconds: number) { | 67 | private getApproximateTime (seconds: number) { |
60 | const hours = Math.floor(seconds / 3600) | 68 | const hours = Math.floor(seconds / 3600) |
61 | let pluralSuffix = '' | 69 | let pluralSuffix = '' |
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 b71a893d1..17fddff09 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Account as ServerAccount, Avatar } from '@shared/models' | 1 | import { Account as ServerAccount, ActorImage } from '@shared/models' |
2 | import { Actor } from './actor.model' | 2 | import { Actor } from './actor.model' |
3 | 3 | ||
4 | export class Account extends Actor implements ServerAccount { | 4 | export class Account extends Actor implements ServerAccount { |
@@ -38,7 +38,7 @@ export class Account extends Actor implements ServerAccount { | |||
38 | this.mutedServerByInstance = false | 38 | this.mutedServerByInstance = false |
39 | } | 39 | } |
40 | 40 | ||
41 | updateAvatar (newAvatar: Avatar) { | 41 | updateAvatar (newAvatar: ActorImage) { |
42 | this.avatar = newAvatar | 42 | this.avatar = newAvatar |
43 | 43 | ||
44 | this.updateComputedAttributes() | 44 | this.updateComputedAttributes() |
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html b/client/src/app/shared/shared-main/account/actor-avatar-info.component.html deleted file mode 100644 index 30584fd00..000000000 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html +++ /dev/null | |||
@@ -1,43 +0,0 @@ | |||
1 | <ng-container *ngIf="actor"> | ||
2 | <div class="actor"> | ||
3 | <div class="d-flex"> | ||
4 | <img [src]="actor.avatarUrl" alt="Avatar" /> | ||
5 | |||
6 | <div class="actor-img-edit-container"> | ||
7 | |||
8 | <div *ngIf="!hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body"> | ||
9 | <my-global-icon iconName="upload"></my-global-icon> | ||
10 | <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label> | ||
11 | <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> | ||
12 | </div> | ||
13 | |||
14 | <div *ngIf="hasAvatar()" class="actor-img-edit-button" #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-avatar-info" autoClose="outside" placement="right"> | ||
15 | <my-global-icon iconName="edit"></my-global-icon> | ||
16 | <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label> | ||
17 | </div> | ||
18 | |||
19 | </div> | ||
20 | </div> | ||
21 | |||
22 | |||
23 | <div class="actor-info"> | ||
24 | <div class="actor-info-names"> | ||
25 | <div class="actor-info-display-name">{{ actor.displayName }}</div> | ||
26 | <div class="actor-info-username">{{ actor.name }}</div> | ||
27 | </div> | ||
28 | <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div> | ||
29 | </div> | ||
30 | </div> | ||
31 | </ng-container> | ||
32 | |||
33 | <ng-template #avatarEditContent> | ||
34 | <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body"> | ||
35 | <my-global-icon iconName="upload"></my-global-icon> | ||
36 | <span for="avatarfile" i18n>Upload a new avatar</span> | ||
37 | <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/> | ||
38 | </div> | ||
39 | <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()"> | ||
40 | <my-global-icon iconName="delete"></my-global-icon> | ||
41 | <span i18n>Remove avatar</span> | ||
42 | </div> | ||
43 | </ng-template> | ||
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss b/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss deleted file mode 100644 index 57c298508..000000000 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss +++ /dev/null | |||
@@ -1,86 +0,0 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .actor { | ||
5 | display: flex; | ||
6 | |||
7 | img { | ||
8 | @include avatar(100px); | ||
9 | |||
10 | margin-right: 15px; | ||
11 | } | ||
12 | |||
13 | .actor-img-edit-container { | ||
14 | position: relative; | ||
15 | width: 0; | ||
16 | |||
17 | .actor-img-edit-button { | ||
18 | @include peertube-button-file(21px); | ||
19 | @include button-with-icon(19px); | ||
20 | @include orange-button; | ||
21 | |||
22 | margin-top: 10px; | ||
23 | margin-bottom: 5px; | ||
24 | border-radius: 50%; | ||
25 | top: 55px; | ||
26 | right: 45px; | ||
27 | cursor: pointer; | ||
28 | |||
29 | input { | ||
30 | width: 30px; | ||
31 | height: 30px; | ||
32 | } | ||
33 | |||
34 | my-global-icon { | ||
35 | right: 7px; | ||
36 | } | ||
37 | } | ||
38 | } | ||
39 | |||
40 | .actor-info { | ||
41 | justify-content: center; | ||
42 | display: inline-flex; | ||
43 | flex-direction: column; | ||
44 | |||
45 | .actor-info-names { | ||
46 | display: flex; | ||
47 | align-items: center; | ||
48 | |||
49 | .actor-info-display-name { | ||
50 | font-size: 20px; | ||
51 | font-weight: $font-bold; | ||
52 | |||
53 | @media screen and (max-width: $small-view) { | ||
54 | font-size: 16px; | ||
55 | } | ||
56 | } | ||
57 | |||
58 | .actor-info-username { | ||
59 | margin-left: 7px; | ||
60 | position: relative; | ||
61 | top: 2px; | ||
62 | font-size: 14px; | ||
63 | color: $grey-actor-name; | ||
64 | } | ||
65 | } | ||
66 | |||
67 | .actor-info-followers { | ||
68 | font-size: 15px; | ||
69 | padding-bottom: .5rem; | ||
70 | } | ||
71 | } | ||
72 | } | ||
73 | |||
74 | .actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body { | ||
75 | padding: 0; | ||
76 | |||
77 | .dropdown-item { | ||
78 | padding: 6px 10px; | ||
79 | border-radius: 4px; | ||
80 | |||
81 | &:first-child { | ||
82 | @include peertube-file; | ||
83 | display: block; | ||
84 | } | ||
85 | } | ||
86 | } | ||
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 8222c9769..1ee0c297e 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts | |||
@@ -1,17 +1,20 @@ | |||
1 | import { Actor as ActorServer, Avatar } from '@shared/models' | 1 | import { Actor as ActorServer, ActorImage } from '@shared/models' |
2 | import { getAbsoluteAPIUrl } from '@app/helpers' | 2 | import { getAbsoluteAPIUrl } from '@app/helpers' |
3 | 3 | ||
4 | export abstract class Actor implements ActorServer { | 4 | export abstract class Actor implements ActorServer { |
5 | id: number | 5 | id: number |
6 | url: string | ||
7 | name: string | 6 | name: string |
7 | |||
8 | host: string | 8 | host: string |
9 | url: string | ||
10 | |||
9 | followingCount: number | 11 | followingCount: number |
10 | followersCount: number | 12 | followersCount: number |
13 | |||
11 | createdAt: Date | string | 14 | createdAt: Date | string |
12 | updatedAt: Date | string | 15 | updatedAt: Date | string |
13 | avatar: Avatar | ||
14 | 16 | ||
17 | avatar: ActorImage | ||
15 | avatarUrl: string | 18 | avatarUrl: string |
16 | 19 | ||
17 | isLocal: boolean | 20 | isLocal: boolean |
@@ -24,6 +27,8 @@ export abstract class Actor implements ActorServer { | |||
24 | 27 | ||
25 | return absoluteAPIUrl + actor.avatar.path | 28 | return absoluteAPIUrl + actor.avatar.path |
26 | } | 29 | } |
30 | |||
31 | return '' | ||
27 | } | 32 | } |
28 | 33 | ||
29 | static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { | 34 | static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { |
@@ -42,11 +47,11 @@ export abstract class Actor implements ActorServer { | |||
42 | return host.trim() === thisHost | 47 | return host.trim() === thisHost |
43 | } | 48 | } |
44 | 49 | ||
45 | protected constructor (hash: ActorServer) { | 50 | protected constructor (hash: Partial<ActorServer>) { |
46 | this.id = hash.id | 51 | this.id = hash.id |
47 | this.url = hash.url | 52 | this.url = hash.url ?? '' |
48 | this.name = hash.name | 53 | this.name = hash.name ?? '' |
49 | this.host = hash.host | 54 | this.host = hash.host ?? '' |
50 | this.followingCount = hash.followingCount | 55 | this.followingCount = hash.followingCount |
51 | this.followersCount = hash.followersCount | 56 | this.followersCount = hash.followersCount |
52 | 57 | ||
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts index 61c800e56..b80ddb9f5 100644 --- a/client/src/app/shared/shared-main/account/index.ts +++ b/client/src/app/shared/shared-main/account/index.ts | |||
@@ -1,5 +1,3 @@ | |||
1 | export * from './account.model' | 1 | export * from './account.model' |
2 | export * from './account.service' | 2 | export * from './account.service' |
3 | export * from './actor-avatar-info.component' | ||
4 | export * from './actor.model' | 3 | export * from './actor.model' |
5 | export * from './video-avatar-channel.component' | ||
diff --git a/client/src/app/shared/shared-main/angular/autofocus.directive.ts b/client/src/app/shared/shared-main/angular/autofocus.directive.ts new file mode 100644 index 000000000..5f087d79d --- /dev/null +++ b/client/src/app/shared/shared-main/angular/autofocus.directive.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { AfterViewInit, Directive, ElementRef } from '@angular/core' | ||
2 | |||
3 | @Directive({ | ||
4 | selector: '[autofocus]' | ||
5 | }) | ||
6 | export class AutofocusDirective implements AfterViewInit { | ||
7 | constructor (private host: ElementRef) { } | ||
8 | |||
9 | ngAfterViewInit () { | ||
10 | this.host.nativeElement.focus() | ||
11 | } | ||
12 | } | ||
diff --git a/client/src/app/shared/shared-main/angular/index.ts b/client/src/app/shared/shared-main/angular/index.ts index 29f8b3650..8ea47bb33 100644 --- a/client/src/app/shared/shared-main/angular/index.ts +++ b/client/src/app/shared/shared-main/angular/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './autofocus.directive' | ||
1 | export * from './bytes.pipe' | 2 | export * from './bytes.pipe' |
2 | export * from './duration-formatter.pipe' | 3 | export * from './duration-formatter.pipe' |
3 | export * from './from-now.pipe' | 4 | export * from './from-now.pipe' |
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts index 3ddaffbdf..4fe3b964d 100644 --- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts +++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts | |||
@@ -27,7 +27,9 @@ export class AuthInterceptor implements HttpInterceptor { | |||
27 | catchError((err: HttpErrorResponse) => { | 27 | catchError((err: HttpErrorResponse) => { |
28 | if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { | 28 | if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { |
29 | return this.handleTokenExpired(req, next) | 29 | return this.handleTokenExpired(req, next) |
30 | } else if (err.status === HttpStatusCode.UNAUTHORIZED_401) { | 30 | } |
31 | |||
32 | if (err.status === HttpStatusCode.UNAUTHORIZED_401) { | ||
31 | return this.handleNotAuthenticated(err) | 33 | return this.handleNotAuthenticated(err) |
32 | } | 34 | } |
33 | 35 | ||
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html index fb0d97122..c20c02e23 100644 --- a/client/src/app/shared/shared-main/misc/simple-search-input.component.html +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.html | |||
@@ -1,14 +1,15 @@ | |||
1 | <span> | 1 | <div class="root"> |
2 | <my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon> | ||
3 | |||
4 | <input | 2 | <input |
5 | #ref | 3 | #ref |
6 | type="text" | 4 | type="text" |
7 | [(ngModel)]="value" | 5 | [(ngModel)]="value" |
8 | (focusout)="focusLost()" | ||
9 | (keyup.enter)="searchChange()" | 6 | (keyup.enter)="searchChange()" |
10 | [hidden]="!shown" | 7 | [hidden]="!inputShown" |
11 | [name]="name" | 8 | [name]="name" |
12 | [placeholder]="placeholder" | 9 | [placeholder]="placeholder" |
13 | > | 10 | > |
14 | </span> | 11 | |
12 | <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon> | ||
13 | |||
14 | <my-global-icon *ngIf="!alwaysShow && inputShown" i18n-title title="Close search" iconName="cross" (click)="hideInput()"></my-global-icon> | ||
15 | </div> | ||
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss index 591b04fb2..5ae48f81b 100644 --- a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss | |||
@@ -1,29 +1,29 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | span { | 4 | .root { |
5 | opacity: .6; | 5 | display: flex; |
6 | |||
7 | &:focus-within { | ||
8 | opacity: 1; | ||
9 | } | ||
10 | } | 6 | } |
11 | 7 | ||
12 | my-global-icon { | 8 | my-global-icon { |
13 | height: 18px; | 9 | height: 28px; |
14 | position: relative; | 10 | width: 28px; |
15 | top: -2px; | 11 | margin-left: 10px; |
16 | } | 12 | cursor: pointer; |
17 | 13 | ||
18 | input { | 14 | &:hover { |
19 | @include peertube-input-text(150px); | 15 | color: pvar(--mainHoverColor); |
16 | } | ||
20 | 17 | ||
21 | height: 22px; // maximum height for the account/video-channels links | 18 | &[iconName=search] { |
22 | padding-left: 10px; | 19 | color: pvar(--mainForegroundColor); |
23 | background-color: transparent; | 20 | } |
24 | border: none; | ||
25 | 21 | ||
26 | &::placeholder { | 22 | &[iconName=cross] { |
27 | font-size: 15px; | 23 | color: pvar(--mainForegroundColor); |
28 | } | 24 | } |
29 | } | 25 | } |
26 | |||
27 | input { | ||
28 | @include peertube-input-text(200px); | ||
29 | } | ||
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts index 86ae9ab42..224d71134 100644 --- a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts +++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subject } from 'rxjs' | 1 | import { Subject } from 'rxjs' |
4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 2 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
3 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | ||
4 | import { ActivatedRoute, Router } from '@angular/router' | ||
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'simple-search-input', | 7 | selector: 'simple-search-input', |
@@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit { | |||
13 | 13 | ||
14 | @Input() name = 'search' | 14 | @Input() name = 'search' |
15 | @Input() placeholder = $localize`Search` | 15 | @Input() placeholder = $localize`Search` |
16 | @Input() iconTitle = $localize`Search` | ||
17 | @Input() alwaysShow = true | ||
16 | 18 | ||
17 | @Output() searchChanged = new EventEmitter<string>() | 19 | @Output() searchChanged = new EventEmitter<string>() |
20 | @Output() inputDisplayChanged = new EventEmitter<boolean>() | ||
18 | 21 | ||
19 | value = '' | 22 | value = '' |
20 | shown: boolean | 23 | inputShown: boolean |
21 | 24 | ||
22 | private searchSubject = new Subject<string>() | 25 | private searchSubject = new Subject<string>() |
23 | 26 | ||
@@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit { | |||
35 | .subscribe(value => this.searchChanged.emit(value)) | 38 | .subscribe(value => this.searchChanged.emit(value)) |
36 | 39 | ||
37 | this.searchSubject.next(this.value) | 40 | this.searchSubject.next(this.value) |
41 | |||
42 | if (this.isInputShown()) this.showInput(false) | ||
38 | } | 43 | } |
39 | 44 | ||
40 | showInput () { | 45 | isInputShown () { |
41 | this.shown = true | 46 | if (this.alwaysShow) return true |
42 | setTimeout(() => this.input.nativeElement.focus()) | 47 | |
48 | return this.inputShown | ||
49 | } | ||
50 | |||
51 | onIconClick () { | ||
52 | if (!this.isInputShown()) { | ||
53 | this.showInput() | ||
54 | return | ||
55 | } | ||
56 | |||
57 | this.searchChange() | ||
58 | } | ||
59 | |||
60 | showInput (focus = true) { | ||
61 | this.inputShown = true | ||
62 | this.inputDisplayChanged.emit(this.inputShown) | ||
63 | |||
64 | if (focus) { | ||
65 | setTimeout(() => this.input.nativeElement.focus()) | ||
66 | } | ||
67 | } | ||
68 | |||
69 | hideInput () { | ||
70 | this.inputShown = false | ||
71 | |||
72 | if (this.isInputShown() === false) { | ||
73 | this.inputDisplayChanged.emit(this.inputShown) | ||
74 | } | ||
43 | } | 75 | } |
44 | 76 | ||
45 | focusLost () { | 77 | focusLost () { |
46 | if (this.value !== '') return | 78 | if (this.value) return |
47 | this.shown = false | 79 | |
80 | this.hideInput() | ||
48 | } | 81 | } |
49 | 82 | ||
50 | searchChange () { | 83 | searchChange () { |
51 | this.router.navigate(['./search'], { relativeTo: this.route }) | 84 | this.router.navigate([ './search' ], { relativeTo: this.route }) |
85 | |||
52 | this.searchSubject.next(this.value) | 86 | this.searchSubject.next(this.value) |
53 | } | 87 | } |
54 | } | 88 | } |
diff --git a/client/src/app/shared/shared-main/peertube-modal/index.ts b/client/src/app/shared/shared-main/peertube-modal/index.ts new file mode 100644 index 000000000..d631522e4 --- /dev/null +++ b/client/src/app/shared/shared-main/peertube-modal/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './peertube-modal.service' | |||
diff --git a/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts new file mode 100644 index 000000000..79da08a5c --- /dev/null +++ b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { Subject } from 'rxjs' | ||
3 | |||
4 | @Injectable({ providedIn: 'root' }) | ||
5 | export class PeertubeModalService { | ||
6 | openQuickSettingsSubject = new Subject<void>() | ||
7 | } | ||
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 9d550996d..16d230f46 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -6,19 +6,20 @@ 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 { RouterModule } from '@angular/router' |
8 | import { | 8 | import { |
9 | NgbButtonsModule, | ||
9 | NgbCollapseModule, | 10 | NgbCollapseModule, |
10 | NgbDropdownModule, | 11 | NgbDropdownModule, |
11 | NgbModalModule, | 12 | NgbModalModule, |
12 | NgbNavModule, | 13 | NgbNavModule, |
13 | NgbPopoverModule, | 14 | NgbPopoverModule, |
14 | NgbTooltipModule, | 15 | NgbTooltipModule |
15 | NgbButtonsModule | ||
16 | } from '@ng-bootstrap/ng-bootstrap' | 16 | } from '@ng-bootstrap/ng-bootstrap' |
17 | import { LoadingBarModule } from '@ngx-loading-bar/core' | 17 | import { LoadingBarModule } from '@ngx-loading-bar/core' |
18 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | 18 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' |
19 | import { SharedGlobalIconModule } from '../shared-icons' | 19 | import { SharedGlobalIconModule } from '../shared-icons' |
20 | import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account' | 20 | import { AccountService } from './account' |
21 | import { | 21 | import { |
22 | AutofocusDirective, | ||
22 | BytesPipe, | 23 | BytesPipe, |
23 | DurationFormatterPipe, | 24 | DurationFormatterPipe, |
24 | FromNowPipe, | 25 | FromNowPipe, |
@@ -31,7 +32,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu | |||
31 | import { DateToggleComponent } from './date' | 32 | import { DateToggleComponent } from './date' |
32 | import { FeedComponent } from './feeds' | 33 | import { FeedComponent } from './feeds' |
33 | import { LoaderComponent, SmallLoaderComponent } from './loaders' | 34 | import { LoaderComponent, SmallLoaderComponent } from './loaders' |
34 | import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc' | 35 | import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc' |
35 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' | 36 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
36 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' | 37 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' |
37 | import { VideoCaptionService } from './video-caption' | 38 | import { VideoCaptionService } from './video-caption' |
@@ -64,13 +65,11 @@ import { VideoChannelService } from './video-channel' | |||
64 | ], | 65 | ], |
65 | 66 | ||
66 | declarations: [ | 67 | declarations: [ |
67 | VideoAvatarChannelComponent, | ||
68 | ActorAvatarInfoComponent, | ||
69 | |||
70 | FromNowPipe, | 68 | FromNowPipe, |
71 | NumberFormatterPipe, | 69 | NumberFormatterPipe, |
72 | BytesPipe, | 70 | BytesPipe, |
73 | DurationFormatterPipe, | 71 | DurationFormatterPipe, |
72 | AutofocusDirective, | ||
74 | 73 | ||
75 | InfiniteScrollerDirective, | 74 | InfiniteScrollerDirective, |
76 | PeerTubeTemplateDirective, | 75 | PeerTubeTemplateDirective, |
@@ -118,13 +117,11 @@ import { VideoChannelService } from './video-channel' | |||
118 | 117 | ||
119 | PrimeSharedModule, | 118 | PrimeSharedModule, |
120 | 119 | ||
121 | VideoAvatarChannelComponent, | ||
122 | ActorAvatarInfoComponent, | ||
123 | |||
124 | FromNowPipe, | 120 | FromNowPipe, |
125 | BytesPipe, | 121 | BytesPipe, |
126 | NumberFormatterPipe, | 122 | NumberFormatterPipe, |
127 | DurationFormatterPipe, | 123 | DurationFormatterPipe, |
124 | AutofocusDirective, | ||
128 | 125 | ||
129 | InfiniteScrollerDirective, | 126 | InfiniteScrollerDirective, |
130 | PeerTubeTemplateDirective, | 127 | PeerTubeTemplateDirective, |
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 1211995fd..88a4811da 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 | |||
@@ -6,6 +6,7 @@ import { | |||
6 | AbuseState, | 6 | AbuseState, |
7 | ActorInfo, | 7 | ActorInfo, |
8 | FollowState, | 8 | FollowState, |
9 | PluginType, | ||
9 | UserNotification as UserNotificationServer, | 10 | UserNotification as UserNotificationServer, |
10 | UserNotificationType, | 11 | UserNotificationType, |
11 | UserRight, | 12 | UserRight, |
@@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer { | |||
74 | } | 75 | } |
75 | } | 76 | } |
76 | 77 | ||
78 | plugin?: { | ||
79 | name: string | ||
80 | type: PluginType | ||
81 | latestVersion: string | ||
82 | } | ||
83 | |||
84 | peertube?: { | ||
85 | latestVersion: string | ||
86 | } | ||
87 | |||
77 | createdAt: string | 88 | createdAt: string |
78 | updatedAt: string | 89 | updatedAt: string |
79 | 90 | ||
80 | // Additional fields | 91 | // Additional fields |
81 | videoUrl?: string | 92 | videoUrl?: string |
82 | commentUrl?: any[] | 93 | commentUrl?: any[] |
94 | |||
83 | abuseUrl?: string | 95 | abuseUrl?: string |
84 | abuseQueryParams?: { [id: string]: string } = {} | 96 | abuseQueryParams?: { [id: string]: string } = {} |
97 | |||
85 | videoAutoBlacklistUrl?: string | 98 | videoAutoBlacklistUrl?: string |
99 | |||
86 | accountUrl?: string | 100 | accountUrl?: string |
101 | |||
87 | videoImportIdentifier?: string | 102 | videoImportIdentifier?: string |
88 | videoImportUrl?: string | 103 | videoImportUrl?: string |
104 | |||
89 | instanceFollowUrl?: string | 105 | instanceFollowUrl?: string |
90 | 106 | ||
107 | peertubeVersionLink?: string | ||
108 | |||
109 | pluginUrl?: string | ||
110 | pluginQueryParams?: { [id: string]: string } = {} | ||
111 | |||
91 | constructor (hash: UserNotificationServer, user: AuthUser) { | 112 | constructor (hash: UserNotificationServer, user: AuthUser) { |
92 | this.id = hash.id | 113 | this.id = hash.id |
93 | this.type = hash.type | 114 | this.type = hash.type |
@@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer { | |||
114 | this.actorFollow = hash.actorFollow | 135 | this.actorFollow = hash.actorFollow |
115 | if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower) | 136 | if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower) |
116 | 137 | ||
138 | this.plugin = hash.plugin | ||
139 | this.peertube = hash.peertube | ||
140 | |||
117 | this.createdAt = hash.createdAt | 141 | this.createdAt = hash.createdAt |
118 | this.updatedAt = hash.updatedAt | 142 | this.updatedAt = hash.updatedAt |
119 | 143 | ||
@@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer { | |||
197 | case UserNotificationType.AUTO_INSTANCE_FOLLOWING: | 221 | case UserNotificationType.AUTO_INSTANCE_FOLLOWING: |
198 | this.instanceFollowUrl = '/admin/follows/following-list' | 222 | this.instanceFollowUrl = '/admin/follows/following-list' |
199 | break | 223 | break |
224 | |||
225 | case UserNotificationType.NEW_PEERTUBE_VERSION: | ||
226 | this.peertubeVersionLink = 'https://joinpeertube.org/news' | ||
227 | break | ||
228 | |||
229 | case UserNotificationType.NEW_PLUGIN_VERSION: | ||
230 | this.pluginUrl = `/admin/plugins/list-installed` | ||
231 | this.pluginQueryParams.pluginType = this.plugin.type + '' | ||
232 | break | ||
200 | } | 233 | } |
201 | } catch (err) { | 234 | } catch (err) { |
202 | this.type = null | 235 | this.type = null |
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index 265af8d55..325f0eaae 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html | |||
@@ -4,7 +4,7 @@ | |||
4 | <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> | 4 | <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> |
5 | 5 | ||
6 | <ng-container [ngSwitch]="notification.type"> | 6 | <ng-container [ngSwitch]="notification.type"> |
7 | <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> | 7 | <ng-container *ngSwitchCase="1"> <!-- UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION --> |
8 | <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container> | 8 | <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container> |
9 | 9 | ||
10 | <ng-template #hasVideo> | 10 | <ng-template #hasVideo> |
@@ -26,7 +26,7 @@ | |||
26 | </ng-template> | 26 | </ng-template> |
27 | </ng-container> | 27 | </ng-container> |
28 | 28 | ||
29 | <ng-container *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> | 29 | <ng-container *ngSwitchCase="5"> <!-- UserNotificationType.UNBLACKLIST_ON_MY_VIDEO --> |
30 | <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon> | 30 | <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon> |
31 | 31 | ||
32 | <div class="message" i18n> | 32 | <div class="message" i18n> |
@@ -34,7 +34,7 @@ | |||
34 | </div> | 34 | </div> |
35 | </ng-container> | 35 | </ng-container> |
36 | 36 | ||
37 | <ng-container *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> | 37 | <ng-container *ngSwitchCase="4"> <!-- UserNotificationType.BLACKLIST_ON_MY_VIDEO --> |
38 | <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> | 38 | <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> |
39 | 39 | ||
40 | <div class="message" i18n> | 40 | <div class="message" i18n> |
@@ -42,7 +42,7 @@ | |||
42 | </div> | 42 | </div> |
43 | </ng-container> | 43 | </ng-container> |
44 | 44 | ||
45 | <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS"> | 45 | <ng-container *ngSwitchCase="3"> <!-- UserNotificationType.NEW_ABUSE_FOR_MODERATORS --> |
46 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> | 46 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> |
47 | 47 | ||
48 | <div class="message" *ngIf="notification.videoUrl" i18n> | 48 | <div class="message" *ngIf="notification.videoUrl" i18n> |
@@ -63,7 +63,7 @@ | |||
63 | </div> | 63 | </div> |
64 | </ng-container> | 64 | </ng-container> |
65 | 65 | ||
66 | <ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE"> | 66 | <ng-container *ngSwitchCase="15"> <!-- UserNotificationType.ABUSE_STATE_CHANGE --> |
67 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> | 67 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> |
68 | 68 | ||
69 | <div class="message" i18n> | 69 | <div class="message" i18n> |
@@ -73,7 +73,7 @@ | |||
73 | </div> | 73 | </div> |
74 | </ng-container> | 74 | </ng-container> |
75 | 75 | ||
76 | <ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE"> | 76 | <ng-container *ngSwitchCase="16"> <!-- UserNotificationType.ABUSE_NEW_MESSAGE --> |
77 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> | 77 | <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> |
78 | 78 | ||
79 | <div class="message" i18n> | 79 | <div class="message" i18n> |
@@ -81,7 +81,7 @@ | |||
81 | </div> | 81 | </div> |
82 | </ng-container> | 82 | </ng-container> |
83 | 83 | ||
84 | <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS"> | 84 | <ng-container *ngSwitchCase="12"> <!-- UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS --> |
85 | <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> | 85 | <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> |
86 | 86 | ||
87 | <div class="message" i18n> | 87 | <div class="message" i18n> |
@@ -89,7 +89,7 @@ | |||
89 | </div> | 89 | </div> |
90 | </ng-container> | 90 | </ng-container> |
91 | 91 | ||
92 | <ng-container *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> | 92 | <ng-container *ngSwitchCase="2"> |
93 | <ng-container *ngIf="notification.comment"> | 93 | <ng-container *ngIf="notification.comment"> |
94 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> | 94 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> |
95 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> | 95 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> |
@@ -109,7 +109,7 @@ | |||
109 | </ng-container> | 109 | </ng-container> |
110 | </ng-container> | 110 | </ng-container> |
111 | 111 | ||
112 | <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> | 112 | <ng-container *ngSwitchCase="6"> <!-- UserNotificationType.MY_VIDEO_PUBLISHED --> |
113 | <my-global-icon iconName="film" aria-hidden="true"></my-global-icon> | 113 | <my-global-icon iconName="film" aria-hidden="true"></my-global-icon> |
114 | 114 | ||
115 | <div class="message" i18n> | 115 | <div class="message" i18n> |
@@ -117,7 +117,7 @@ | |||
117 | </div> | 117 | </div> |
118 | </ng-container> | 118 | </ng-container> |
119 | 119 | ||
120 | <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> | 120 | <ng-container *ngSwitchCase="7"> <!-- UserNotificationType.MY_VIDEO_IMPORT_SUCCESS --> |
121 | <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon> | 121 | <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon> |
122 | 122 | ||
123 | <div class="message" i18n> | 123 | <div class="message" i18n> |
@@ -125,7 +125,7 @@ | |||
125 | </div> | 125 | </div> |
126 | </ng-container> | 126 | </ng-container> |
127 | 127 | ||
128 | <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> | 128 | <ng-container *ngSwitchCase="8"> <!-- UserNotificationType.MY_VIDEO_IMPORT_ERROR --> |
129 | <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon> | 129 | <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon> |
130 | 130 | ||
131 | <div class="message" i18n> | 131 | <div class="message" i18n> |
@@ -133,7 +133,7 @@ | |||
133 | </div> | 133 | </div> |
134 | </ng-container> | 134 | </ng-container> |
135 | 135 | ||
136 | <ng-container *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> | 136 | <ng-container *ngSwitchCase="9"> <!-- UserNotificationType.NEW_USER_REGISTRATION --> |
137 | <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> | 137 | <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> |
138 | 138 | ||
139 | <div class="message" i18n> | 139 | <div class="message" i18n> |
@@ -141,7 +141,7 @@ | |||
141 | </div> | 141 | </div> |
142 | </ng-container> | 142 | </ng-container> |
143 | 143 | ||
144 | <ng-container *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> | 144 | <ng-container *ngSwitchCase="10"> <!-- UserNotificationType.NEW_FOLLOW --> |
145 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> | 145 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> |
146 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> | 146 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> |
147 | </a> | 147 | </a> |
@@ -154,7 +154,7 @@ | |||
154 | </div> | 154 | </div> |
155 | </ng-container> | 155 | </ng-container> |
156 | 156 | ||
157 | <ng-container *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> | 157 | <ng-container *ngSwitchCase="11"> |
158 | <ng-container *ngIf="notification.comment"> | 158 | <ng-container *ngIf="notification.comment"> |
159 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> | 159 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> |
160 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> | 160 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> |
@@ -174,7 +174,7 @@ | |||
174 | </ng-container> | 174 | </ng-container> |
175 | </ng-container> | 175 | </ng-container> |
176 | 176 | ||
177 | <ng-container *ngSwitchCase="UserNotificationType.NEW_INSTANCE_FOLLOWER"> | 177 | <ng-container *ngSwitchCase="13"> <!-- UserNotificationType.NEW_INSTANCE_FOLLOWER --> |
178 | <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> | 178 | <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> |
179 | 179 | ||
180 | <div class="message" i18n> | 180 | <div class="message" i18n> |
@@ -183,7 +183,7 @@ | |||
183 | </div> | 183 | </div> |
184 | </ng-container> | 184 | </ng-container> |
185 | 185 | ||
186 | <ng-container *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING"> | 186 | <ng-container *ngSwitchCase="14"> <!-- UserNotificationType.AUTO_INSTANCE_FOLLOWING --> |
187 | <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> | 187 | <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> |
188 | 188 | ||
189 | <div class="message" i18n> | 189 | <div class="message" i18n> |
@@ -191,6 +191,22 @@ | |||
191 | </div> | 191 | </div> |
192 | </ng-container> | 192 | </ng-container> |
193 | 193 | ||
194 | <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION --> | ||
195 | <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> | ||
196 | |||
197 | <div class="message" i18n> | ||
198 | <a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">A new version of the plugin/theme {{ notification.plugin.name }}</a> is available: {{ notification.plugin.latestVersion }} | ||
199 | </div> | ||
200 | </ng-container> | ||
201 | |||
202 | <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION --> | ||
203 | <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> | ||
204 | |||
205 | <div class="message" i18n> | ||
206 | <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }} | ||
207 | </div> | ||
208 | </ng-container> | ||
209 | |||
194 | <ng-container *ngSwitchDefault> | 210 | <ng-container *ngSwitchDefault> |
195 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> | 211 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> |
196 | 212 | ||
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts index 387c49d94..d7c722355 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.ts +++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts | |||
@@ -21,9 +21,6 @@ export class UserNotificationsComponent implements OnInit { | |||
21 | notifications: UserNotification[] = [] | 21 | notifications: UserNotification[] = [] |
22 | sortField = 'createdAt' | 22 | sortField = 'createdAt' |
23 | 23 | ||
24 | // So we can access it in the template | ||
25 | UserNotificationType = UserNotificationType | ||
26 | |||
27 | componentPagination: ComponentPagination | 24 | componentPagination: ComponentPagination |
28 | 25 | ||
29 | onDataSubject = new Subject<any[]>() | 26 | onDataSubject = new Subject<any[]>() |
@@ -48,7 +45,7 @@ export class UserNotificationsComponent implements OnInit { | |||
48 | } | 45 | } |
49 | 46 | ||
50 | loadNotifications (reset?: boolean) { | 47 | loadNotifications (reset?: boolean) { |
51 | this.userNotificationService.listMyNotifications({ | 48 | const options = { |
52 | pagination: this.componentPagination, | 49 | pagination: this.componentPagination, |
53 | ignoreLoadingBar: this.ignoreLoadingBar, | 50 | ignoreLoadingBar: this.ignoreLoadingBar, |
54 | sort: { | 51 | sort: { |
@@ -56,7 +53,9 @@ export class UserNotificationsComponent implements OnInit { | |||
56 | // if we order by creation date, we want DESC. all other fields are ASC (like unread). | 53 | // if we order by creation date, we want DESC. all other fields are ASC (like unread). |
57 | order: this.sortField === 'createdAt' ? -1 : 1 | 54 | order: this.sortField === 'createdAt' ? -1 : 1 |
58 | } | 55 | } |
59 | }) | 56 | } |
57 | |||
58 | this.userNotificationService.listMyNotifications(options) | ||
60 | .subscribe( | 59 | .subscribe( |
61 | result => { | 60 | result => { |
62 | this.notifications = reset ? result.data : this.notifications.concat(result.data) | 61 | this.notifications = reset ? result.data : this.notifications.concat(result.data) |
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 c6a63fe6c..1ba3fcc0e 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 | |||
@@ -1,15 +1,22 @@ | |||
1 | import { VideoChannel as ServerVideoChannel, ViewsPerDate, Account, Avatar } from '@shared/models' | 1 | import { getAbsoluteAPIUrl } from '@app/helpers' |
2 | import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models' | ||
3 | import { Account } from '../account/account.model' | ||
2 | import { Actor } from '../account/actor.model' | 4 | import { Actor } from '../account/actor.model' |
3 | 5 | ||
4 | export class VideoChannel extends Actor implements ServerVideoChannel { | 6 | export class VideoChannel extends Actor implements ServerVideoChannel { |
5 | displayName: string | 7 | displayName: string |
6 | description: string | 8 | description: string |
7 | support: string | 9 | support: string |
10 | |||
8 | isLocal: boolean | 11 | isLocal: boolean |
12 | |||
9 | nameWithHost: string | 13 | nameWithHost: string |
10 | nameWithHostForced: string | 14 | nameWithHostForced: string |
11 | 15 | ||
12 | ownerAccount?: Account | 16 | banner: ActorImage |
17 | bannerUrl: string | ||
18 | |||
19 | ownerAccount?: ServerAccount | ||
13 | ownerBy?: string | 20 | ownerBy?: string |
14 | ownerAvatarUrl?: string | 21 | ownerAvatarUrl?: string |
15 | 22 | ||
@@ -21,19 +28,33 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
21 | return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL() | 28 | return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL() |
22 | } | 29 | } |
23 | 30 | ||
31 | static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) { | ||
32 | if (channel?.banner?.url) return channel.banner.url | ||
33 | |||
34 | if (channel && channel.banner) { | ||
35 | const absoluteAPIUrl = getAbsoluteAPIUrl() | ||
36 | |||
37 | return absoluteAPIUrl + channel.banner.path | ||
38 | } | ||
39 | |||
40 | return '' | ||
41 | } | ||
42 | |||
24 | static GET_DEFAULT_AVATAR_URL () { | 43 | static GET_DEFAULT_AVATAR_URL () { |
25 | return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png` | 44 | return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png` |
26 | } | 45 | } |
27 | 46 | ||
28 | constructor (hash: ServerVideoChannel) { | 47 | constructor (hash: Partial<ServerVideoChannel>) { |
29 | super(hash) | 48 | super(hash) |
30 | 49 | ||
31 | this.updateComputedAttributes() | ||
32 | |||
33 | this.displayName = hash.displayName | 50 | this.displayName = hash.displayName |
34 | this.description = hash.description | 51 | this.description = hash.description |
35 | this.support = hash.support | 52 | this.support = hash.support |
53 | |||
54 | this.banner = hash.banner | ||
55 | |||
36 | this.isLocal = hash.isLocal | 56 | this.isLocal = hash.isLocal |
57 | |||
37 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) | 58 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) |
38 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) | 59 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) |
39 | 60 | ||
@@ -46,22 +67,34 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
46 | if (hash.ownerAccount) { | 67 | if (hash.ownerAccount) { |
47 | this.ownerAccount = hash.ownerAccount | 68 | this.ownerAccount = hash.ownerAccount |
48 | this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) | 69 | this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) |
49 | this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount) | 70 | this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount) |
50 | } | 71 | } |
72 | |||
73 | this.updateComputedAttributes() | ||
51 | } | 74 | } |
52 | 75 | ||
53 | updateAvatar (newAvatar: Avatar) { | 76 | updateAvatar (newAvatar: ActorImage) { |
54 | this.avatar = newAvatar | 77 | this.avatar = newAvatar |
55 | 78 | ||
56 | this.updateComputedAttributes() | 79 | this.updateComputedAttributes() |
57 | } | 80 | } |
58 | 81 | ||
59 | resetAvatar () { | 82 | resetAvatar () { |
60 | this.avatar = null | 83 | this.updateAvatar(null) |
61 | this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL() | 84 | } |
85 | |||
86 | updateBanner (newBanner: ActorImage) { | ||
87 | this.banner = newBanner | ||
88 | |||
89 | this.updateComputedAttributes() | ||
90 | } | ||
91 | |||
92 | resetBanner () { | ||
93 | this.updateBanner(null) | ||
62 | } | 94 | } |
63 | 95 | ||
64 | private updateComputedAttributes () { | 96 | updateComputedAttributes () { |
65 | this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) | 97 | this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) |
98 | this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this) | ||
66 | } | 99 | } |
67 | } | 100 | } |
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 eff3fad4d..e65261763 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 | |||
@@ -3,7 +3,7 @@ import { catchError, map, tap } from 'rxjs/operators' | |||
3 | import { HttpClient, HttpParams } from '@angular/common/http' | 3 | import { HttpClient, HttpParams } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' | 5 | import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' |
6 | import { Avatar, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' | 6 | import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' |
7 | import { environment } from '../../../../environments/environment' | 7 | import { environment } from '../../../../environments/environment' |
8 | import { Account } from '../account' | 8 | import { Account } from '../account' |
9 | import { AccountService } from '../account/account.service' | 9 | import { AccountService } from '../account/account.service' |
@@ -82,15 +82,15 @@ export class VideoChannelService { | |||
82 | ) | 82 | ) |
83 | } | 83 | } |
84 | 84 | ||
85 | changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) { | 85 | changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') { |
86 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick' | 86 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick' |
87 | 87 | ||
88 | return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) | 88 | return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm) |
89 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 89 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
90 | } | 90 | } |
91 | 91 | ||
92 | deleteVideoChannelAvatar (videoChannelName: string) { | 92 | deleteVideoChannelImage (videoChannelName: string, type: 'avatar' | 'banner') { |
93 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar' | 93 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type |
94 | 94 | ||
95 | return this.authHttp.delete(url) | 95 | return this.authHttp.delete(url) |
96 | .pipe( | 96 | .pipe( |
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 adb6e884f..1c2c4a575 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -6,7 +6,7 @@ import { Actor } from '@app/shared/shared-main/account/actor.model' | |||
6 | import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' | 6 | import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' |
7 | import { peertubeTranslate } from '@shared/core-utils/i18n' | 7 | import { peertubeTranslate } from '@shared/core-utils/i18n' |
8 | import { | 8 | import { |
9 | Avatar, | 9 | ActorImage, |
10 | ServerConfig, | 10 | ServerConfig, |
11 | UserRight, | 11 | UserRight, |
12 | Video as VideoServerModel, | 12 | Video as VideoServerModel, |
@@ -72,7 +72,7 @@ export class Video implements VideoServerModel { | |||
72 | displayName: string | 72 | displayName: string |
73 | url: string | 73 | url: string |
74 | host: string | 74 | host: string |
75 | avatar?: Avatar | 75 | avatar?: ActorImage |
76 | } | 76 | } |
77 | 77 | ||
78 | channel: { | 78 | channel: { |
@@ -81,7 +81,7 @@ export class Video implements VideoServerModel { | |||
81 | displayName: string | 81 | displayName: string |
82 | url: string | 82 | url: string |
83 | host: string | 83 | host: string |
84 | avatar?: Avatar | 84 | avatar?: ActorImage |
85 | } | 85 | } |
86 | 86 | ||
87 | userHistory?: { | 87 | userHistory?: { |
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss index 4a4e05535..cdcc12fe0 100644 --- a/client/src/app/shared/shared-moderation/moderation.scss +++ b/client/src/app/shared/shared-moderation/moderation.scss | |||
@@ -32,7 +32,7 @@ | |||
32 | color: pvar(--inputPlaceholderColor); | 32 | color: pvar(--inputPlaceholderColor); |
33 | } | 33 | } |
34 | 34 | ||
35 | @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { | 35 | @include block-ratio($selector: 'div, ::ng-deep iframe') { |
36 | width: 100% !important; | 36 | width: 100% !important; |
37 | height: 100% !important; | 37 | height: 100% !important; |
38 | left: 0; | 38 | left: 0; |
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss index b2606cbd8..0567330f5 100644 --- a/client/src/app/shared/shared-moderation/report-modals/report.component.scss +++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss | |||
@@ -21,7 +21,7 @@ textarea { | |||
21 | } | 21 | } |
22 | 22 | ||
23 | .screenratio { | 23 | .screenratio { |
24 | @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { | 24 | @include block-ratio($selector: 'div, ::ng-deep iframe') { |
25 | left: 0; | 25 | left: 0; |
26 | }; | 26 | }; |
27 | } | 27 | } |
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts index 5b06c0bc7..4ca6f52ad 100644 --- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts +++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts | |||
@@ -61,7 +61,8 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
61 | baseUrl: this.video.embedUrl, | 61 | baseUrl: this.video.embedUrl, |
62 | title: false, | 62 | title: false, |
63 | warningTitle: false | 63 | warningTitle: false |
64 | }) | 64 | }), |
65 | this.video.name | ||
65 | ) | 66 | ) |
66 | ) | 67 | ) |
67 | } | 68 | } |
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 b06ff3751..e8760bfcc 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 | |||
@@ -86,14 +86,14 @@ export class VideoShareComponent { | |||
86 | const options = this.getVideoOptions(this.video.embedUrl) | 86 | const options = this.getVideoOptions(this.video.embedUrl) |
87 | 87 | ||
88 | const embedUrl = buildVideoLink(options) | 88 | const embedUrl = buildVideoLink(options) |
89 | return buildVideoOrPlaylistEmbed(embedUrl) | 89 | return buildVideoOrPlaylistEmbed(embedUrl, this.video.name) |
90 | } | 90 | } |
91 | 91 | ||
92 | getPlaylistIframeCode () { | 92 | getPlaylistIframeCode () { |
93 | const options = this.getPlaylistOptions(this.playlist.embedUrl) | 93 | const options = this.getPlaylistOptions(this.playlist.embedUrl) |
94 | 94 | ||
95 | const embedUrl = buildPlaylistLink(options) | 95 | const embedUrl = buildPlaylistLink(options) |
96 | return buildVideoOrPlaylistEmbed(embedUrl) | 96 | return buildVideoOrPlaylistEmbed(embedUrl, this.playlist.displayName) |
97 | } | 97 | } |
98 | 98 | ||
99 | getVideoUrl () { | 99 | getVideoUrl () { |
diff --git a/client/src/app/shared/shared-support-modal/index.ts b/client/src/app/shared/shared-support-modal/index.ts new file mode 100644 index 000000000..f41bb4bc2 --- /dev/null +++ b/client/src/app/shared/shared-support-modal/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './support-modal.component' | ||
2 | |||
3 | export * from './shared-support-modal.module' | ||
diff --git a/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts new file mode 100644 index 000000000..1101d5535 --- /dev/null +++ b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SharedFormModule } from '../shared-forms' | ||
3 | import { SharedGlobalIconModule } from '../shared-icons' | ||
4 | import { SharedMainModule } from '../shared-main/shared-main.module' | ||
5 | import { SupportModalComponent } from './support-modal.component' | ||
6 | |||
7 | @NgModule({ | ||
8 | imports: [ | ||
9 | SharedMainModule, | ||
10 | SharedFormModule, | ||
11 | SharedGlobalIconModule | ||
12 | ], | ||
13 | |||
14 | declarations: [ | ||
15 | SupportModalComponent | ||
16 | ], | ||
17 | |||
18 | exports: [ | ||
19 | SupportModalComponent | ||
20 | ], | ||
21 | |||
22 | providers: [ ] | ||
23 | }) | ||
24 | export class SharedSupportModal { } | ||
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.html b/client/src/app/shared/shared-support-modal/support-modal.component.html index 935656d23..4a967987f 100644 --- a/client/src/app/+videos/+video-watch/modal/video-support.component.html +++ b/client/src/app/shared/shared-support-modal/support-modal.component.html | |||
@@ -1,10 +1,10 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Support {{ video.account.displayName }}</h4> | 3 | <h4 i18n class="modal-title">Support {{ displayName }}</h4> |
4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> | 7 | <div class="modal-body" [innerHTML]="htmlSupport"></div> |
8 | 8 | ||
9 | <div class="modal-footer inputs"> | 9 | <div class="modal-footer inputs"> |
10 | <input | 10 | <input |
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.scss b/client/src/app/shared/shared-support-modal/support-modal.component.scss index 184e09027..184e09027 100644 --- a/client/src/app/+videos/+video-watch/modal/video-support.component.scss +++ b/client/src/app/shared/shared-support-modal/support-modal.component.scss | |||
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.ts b/client/src/app/shared/shared-support-modal/support-modal.component.ts new file mode 100644 index 000000000..ae603c7a8 --- /dev/null +++ b/client/src/app/shared/shared-support-modal/support-modal.component.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | ||
2 | import { MarkdownService } from '@app/core' | ||
3 | import { VideoDetails } from '@app/shared/shared-main' | ||
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
5 | import { VideoChannel } from '@shared/models' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-support-modal', | ||
9 | templateUrl: './support-modal.component.html', | ||
10 | styleUrls: [ './support-modal.component.scss' ] | ||
11 | }) | ||
12 | export class SupportModalComponent { | ||
13 | @Input() video: VideoDetails = null | ||
14 | @Input() videoChannel: VideoChannel = null | ||
15 | |||
16 | @ViewChild('modal', { static: true }) modal: NgbModal | ||
17 | |||
18 | htmlSupport = '' | ||
19 | displayName = '' | ||
20 | |||
21 | constructor ( | ||
22 | private markdownService: MarkdownService, | ||
23 | private modalService: NgbModal | ||
24 | ) { } | ||
25 | |||
26 | show () { | ||
27 | const modalRef = this.modalService.open(this.modal, { centered: true }) | ||
28 | |||
29 | const support = this.video?.support || this.videoChannel.support | ||
30 | |||
31 | this.markdownService.enhancedMarkdownToHTML(support) | ||
32 | .then(r => this.htmlSupport = r) | ||
33 | |||
34 | this.displayName = this.video | ||
35 | ? this.video.channel.displayName | ||
36 | : this.videoChannel.displayName | ||
37 | |||
38 | return modalRef | ||
39 | } | ||
40 | } | ||
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.html b/client/src/app/shared/shared-video-miniature/abstract-video-list.html index 07f79cd6d..ee5df28be 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.html +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.html | |||
@@ -43,7 +43,7 @@ | |||
43 | <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div> | 43 | <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div> |
44 | <div | 44 | <div |
45 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" | 45 | myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" |
46 | class="videos" | 46 | class="videos" [ngClass]="{ 'display-as-row': displayAsRow() }" |
47 | > | 47 | > |
48 | <ng-container *ngFor="let video of videos; trackBy: videoById;"> | 48 | <ng-container *ngFor="let video of videos; trackBy: videoById;"> |
49 | <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> | 49 | <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> |
@@ -52,8 +52,7 @@ | |||
52 | 52 | ||
53 | <div class="video-wrapper"> | 53 | <div class="video-wrapper"> |
54 | <my-video-miniature | 54 | <my-video-miniature |
55 | [fitWidth]="true" | 55 | [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow()" |
56 | [video]="video" [user]="userMiniature" [ownerDisplayType]="ownerDisplayType" | ||
57 | [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" | 56 | [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" |
58 | (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" | 57 | (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" |
59 | > | 58 | > |
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss index 0a8aa8fa4..6570b63d0 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss | |||
@@ -69,7 +69,16 @@ $iconSize: 16px; | |||
69 | } | 69 | } |
70 | 70 | ||
71 | .margin-content { | 71 | .margin-content { |
72 | @include fluid-videos-miniature-layout; | 72 | @include grid-videos-miniature-layout; |
73 | } | ||
74 | |||
75 | .display-as-row.videos { | ||
76 | margin-left: pvar(--horizontalMarginContent); | ||
77 | margin-right: pvar(--horizontalMarginContent); | ||
78 | |||
79 | .video-wrapper { | ||
80 | margin-bottom: 15px; | ||
81 | } | ||
73 | } | 82 | } |
74 | 83 | ||
75 | @media screen and (max-width: $mobile-view) { | 84 | @media screen and (max-width: $mobile-view) { |
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts index c13cb3748..f83380513 100644 --- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts +++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts | |||
@@ -28,8 +28,8 @@ import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@sha | |||
28 | import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' | 28 | import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' |
29 | import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' | 29 | import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' |
30 | import { Syndication, Video } from '../shared-main' | 30 | import { Syndication, Video } from '../shared-main' |
31 | import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' | ||
32 | import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component' | 31 | import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component' |
32 | import { MiniatureDisplayOptions } from './video-miniature.component' | ||
33 | 33 | ||
34 | enum GroupDate { | 34 | enum GroupDate { |
35 | UNKNOWN = 0, | 35 | UNKNOWN = 0, |
@@ -65,7 +65,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte | |||
65 | loadOnInit = true | 65 | loadOnInit = true |
66 | loadUserVideoPreferences = false | 66 | loadUserVideoPreferences = false |
67 | 67 | ||
68 | ownerDisplayType: OwnerDisplayType = 'account' | ||
69 | displayModerationBlock = false | 68 | displayModerationBlock = false |
70 | titleTooltip: string | 69 | titleTooltip: string |
71 | displayVideoActions = true | 70 | displayVideoActions = true |
@@ -320,6 +319,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte | |||
320 | viewContainerRef.createComponent(componentFactory, 0, injector) | 319 | viewContainerRef.createComponent(componentFactory, 0, injector) |
321 | } | 320 | } |
322 | 321 | ||
322 | // Can be redefined by child | ||
323 | displayAsRow () { | ||
324 | return false | ||
325 | } | ||
326 | |||
323 | // On videos hook for children that want to do something | 327 | // On videos hook for children that want to do something |
324 | protected onMoreVideos () { /* empty */ } | 328 | protected onMoreVideos () { /* empty */ } |
325 | 329 | ||
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html index 4608e93e7..8a9218343 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.html +++ b/client/src/app/shared/shared-video-miniature/video-download.component.html | |||
@@ -17,85 +17,116 @@ | |||
17 | </div> | 17 | </div> |
18 | 18 | ||
19 | <div class="modal-body"> | 19 | <div class="modal-body"> |
20 | <div class="form-group"> | 20 | <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n> |
21 | <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n> | 21 | The following link contains a private token and should not be shared with anyone. |
22 | The following link contains a private token and should not be shared with anyone. | 22 | </div> |
23 | </div> | ||
24 | 23 | ||
24 | <ng-container *ngIf="type === 'subtitles'"> | ||
25 | <div class="input-group input-group-sm"> | 25 | <div class="input-group input-group-sm"> |
26 | <div class="input-group-prepend peertube-select-container"> | ||
27 | <select *ngIf="type === 'video'" [(ngModel)]="resolutionId" (ngModelChange)="onResolutionIdChange()"> | ||
28 | <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option> | ||
29 | </select> | ||
30 | |||
31 | <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId"> | ||
32 | <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option> | ||
33 | </select> | ||
34 | </div> | ||
35 | |||
36 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> | 26 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> |
37 | <div class="input-group-append" *ngIf="!isConfidentialVideo()"> | 27 | <div class="input-group-append" *ngIf="!isConfidentialVideo()"> |
38 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | 28 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> |
39 | <span class="glyphicon glyphicon-copy"></span> | 29 | <span class="glyphicon glyphicon-duplicate"></span> |
40 | </button> | 30 | </button> |
41 | </div> | 31 | </div> |
42 | </div> | 32 | </div> |
43 | </div> | 33 | </ng-container> |
44 | 34 | ||
45 | <ng-container *ngIf="type === 'video' && videoFile?.metadata"> | 35 | <ng-container *ngIf="type === 'video'"> |
46 | <div ngbNav #nav="ngbNav" class="nav-tabs"> | 36 | <div ngbNav #resolutionNav="ngbNav" class="nav-tabs" [activeId]="resolutionId" (activeIdChange)="onResolutionIdChange($event)"> |
47 | 37 | ||
48 | <ng-container ngbNavItem> | 38 | <ng-container *ngFor="let file of getVideoFiles()" [ngbNavItem]="file.resolution.id"> |
49 | <a ngbNavLink i18n>Format</a> | 39 | <a ngbNavLink i18n>{{ file.resolution.label }}</a> |
40 | |||
50 | <ng-template ngbNavContent> | 41 | <ng-template ngbNavContent> |
51 | <div class="file-metadata"> | 42 | <div class="nav-content"> |
52 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue"> | 43 | <div class="input-group input-group-sm"> |
53 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | 44 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> |
54 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | 45 | <div class="input-group-append" *ngIf="!isConfidentialVideo()"> |
46 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | ||
47 | <span class="glyphicon glyphicon-duplicate"></span> | ||
48 | </button> | ||
49 | </div> | ||
55 | </div> | 50 | </div> |
56 | </div> | 51 | </div> |
57 | </ng-template> | 52 | </ng-template> |
58 | </ng-container> | 53 | </ng-container> |
59 | 54 | </div> | |
60 | <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined"> | 55 | <div [ngbNavOutlet]="resolutionNav"></div> |
61 | <a ngbNavLink i18n>Video stream</a> | 56 | |
62 | <ng-template ngbNavContent> | 57 | <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed"> |
63 | <div class="file-metadata"> | 58 | <ng-container *ngIf="videoFile?.metadata"> |
64 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue"> | 59 | <div ngbNav #nav="ngbNav" class="nav-tabs nav-metadata"> |
65 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | 60 | <ng-container ngbNavItem> |
66 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | 61 | <a ngbNavLink i18n>Format</a> |
67 | </div> | 62 | <ng-template ngbNavContent> |
63 | <div class="file-metadata"> | ||
64 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue"> | ||
65 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | ||
66 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | ||
67 | </div> | ||
68 | </div> | ||
69 | </ng-template> | ||
70 | |||
71 | <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined"> | ||
72 | <a ngbNavLink i18n>Video stream</a> | ||
73 | <ng-template ngbNavContent> | ||
74 | <div class="file-metadata"> | ||
75 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue"> | ||
76 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | ||
77 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | ||
78 | </div> | ||
79 | </div> | ||
80 | </ng-template> | ||
81 | </ng-container> | ||
82 | |||
83 | <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined"> | ||
84 | <a ngbNavLink i18n>Audio stream</a> | ||
85 | <ng-template ngbNavContent> | ||
86 | <div class="file-metadata"> | ||
87 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue"> | ||
88 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | ||
89 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | ||
90 | </div> | ||
91 | </div> | ||
92 | </ng-template> | ||
93 | </ng-container> | ||
94 | |||
95 | </ng-container> | ||
96 | </div> | ||
97 | <div [ngbNavOutlet]="nav"></div> | ||
98 | <div class="download-type"> | ||
99 | <div class="peertube-radio-container"> | ||
100 | <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct"> | ||
101 | <label i18n for="download-direct">Direct download</label> | ||
68 | </div> | 102 | </div> |
69 | </ng-template> | 103 | <div class="peertube-radio-container"> |
70 | </ng-container> | 104 | <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> |
71 | 105 | <label i18n for="download-torrent">Torrent (.torrent file)</label> | |
72 | <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined"> | ||
73 | <a ngbNavLink i18n>Audio stream</a> | ||
74 | <ng-template ngbNavContent> | ||
75 | <div class="file-metadata"> | ||
76 | <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue"> | ||
77 | <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> | ||
78 | <span class="metadata-attribute-value">{{ item.value.value }}</span> | ||
79 | </div> | ||
80 | </div> | 106 | </div> |
81 | </ng-template> | 107 | </div> |
82 | </ng-container> | 108 | </ng-container> |
83 | </div> | 109 | </div> |
84 | 110 | ||
85 | <div [ngbNavOutlet]="nav"></div> | 111 | <div (click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed" role="button" class="advanced-filters-button" |
86 | </ng-container> | 112 | [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic"> |
87 | 113 | <ng-container *ngIf="isAdvancedCustomizationCollapsed"> | |
88 | <div class="download-type" *ngIf="type === 'video'"> | 114 | <span class="glyphicon glyphicon-menu-down"></span> |
89 | <div class="peertube-radio-container"> | 115 | |
90 | <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct"> | 116 | <ng-container i18n> |
91 | <label i18n for="download-direct">Direct download</label> | 117 | Advanced |
92 | </div> | 118 | </ng-container> |
93 | 119 | </ng-container> | |
94 | <div class="peertube-radio-container"> | 120 | |
95 | <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> | 121 | <ng-container *ngIf="!isAdvancedCustomizationCollapsed"> |
96 | <label i18n for="download-torrent">Torrent (.torrent file)</label> | 122 | <span class="glyphicon glyphicon-menu-up"></span> |
123 | |||
124 | <ng-container i18n> | ||
125 | Simple | ||
126 | </ng-container> | ||
127 | </ng-container> | ||
97 | </div> | 128 | </div> |
98 | </div> | 129 | </ng-container> |
99 | </div> | 130 | </div> |
100 | 131 | ||
101 | <div class="modal-footer inputs"> | 132 | <div class="modal-footer inputs"> |
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.scss b/client/src/app/shared/shared-video-miniature/video-download.component.scss index d407e9531..199c3dac8 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-download.component.scss | |||
@@ -1,6 +1,28 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .nav-content { | ||
5 | margin-top: 30px; | ||
6 | } | ||
7 | |||
8 | .advanced-filters-button { | ||
9 | display: flex; | ||
10 | justify-content: center; | ||
11 | align-items: center; | ||
12 | margin-top: 20px; | ||
13 | font-size: 16px; | ||
14 | font-weight: 600; | ||
15 | cursor: pointer; | ||
16 | |||
17 | .nav-tabs { | ||
18 | margin-top: 10x; | ||
19 | } | ||
20 | |||
21 | .glyphicon { | ||
22 | margin-right: 5px; | ||
23 | } | ||
24 | } | ||
25 | |||
4 | .peertube-select-container { | 26 | .peertube-select-container { |
5 | @include peertube-select-container(85px); | 27 | @include peertube-select-container(85px); |
6 | 28 | ||
@@ -15,12 +37,21 @@ | |||
15 | } | 37 | } |
16 | } | 38 | } |
17 | 39 | ||
40 | .action-button-cancel { | ||
41 | @include peertube-button-link; | ||
42 | } | ||
43 | |||
44 | .action-button-submit { | ||
45 | @include peertube-button-link; | ||
46 | @include orange-button; | ||
47 | } | ||
48 | |||
18 | #dropdownDownloadType { | 49 | #dropdownDownloadType { |
19 | cursor: pointer; | 50 | cursor: pointer; |
20 | } | 51 | } |
21 | 52 | ||
22 | .download-type { | 53 | .download-type { |
23 | margin-top: 30px; | 54 | margin-top: 20px; |
24 | 55 | ||
25 | .peertube-radio-container { | 56 | .peertube-radio-container { |
26 | @include peertube-radio-container; | 57 | @include peertube-radio-container; |
@@ -30,6 +61,10 @@ | |||
30 | } | 61 | } |
31 | } | 62 | } |
32 | 63 | ||
64 | .nav-metadata { | ||
65 | margin-top: 20px; | ||
66 | } | ||
67 | |||
33 | .file-metadata { | 68 | .file-metadata { |
34 | padding: 1rem; | 69 | padding: 1rem; |
35 | } | 70 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts index 90f4daf7c..1e3745d94 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { mapValues, pick } from 'lodash-es' | 1 | import { mapValues, pick } from 'lodash-es' |
2 | import { pipe } from 'rxjs' | ||
3 | import { tap } from 'rxjs/operators' | ||
2 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 5 | import { AuthService, HooksService, Notifier } from '@app/core' |
4 | import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
5 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' | 7 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' |
6 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' | 8 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' |
7 | 9 | ||
@@ -16,7 +18,7 @@ type FileMetadata = { [key: string]: { label: string, value: string }} | |||
16 | export class VideoDownloadComponent { | 18 | export class VideoDownloadComponent { |
17 | @ViewChild('modal', { static: true }) modal: ElementRef | 19 | @ViewChild('modal', { static: true }) modal: ElementRef |
18 | 20 | ||
19 | downloadType: 'direct' | 'torrent' = 'torrent' | 21 | downloadType: 'direct' | 'torrent' = 'direct' |
20 | resolutionId: number | string = -1 | 22 | resolutionId: number | string = -1 |
21 | subtitleLanguageId: string | 23 | subtitleLanguageId: string |
22 | 24 | ||
@@ -26,7 +28,9 @@ export class VideoDownloadComponent { | |||
26 | videoFileMetadataVideoStream: FileMetadata | undefined | 28 | videoFileMetadataVideoStream: FileMetadata | undefined |
27 | videoFileMetadataAudioStream: FileMetadata | undefined | 29 | videoFileMetadataAudioStream: FileMetadata | undefined |
28 | videoCaptions: VideoCaption[] | 30 | videoCaptions: VideoCaption[] |
29 | activeModal: NgbActiveModal | 31 | activeModal: NgbModalRef |
32 | |||
33 | isAdvancedCustomizationCollapsed = true | ||
30 | 34 | ||
31 | type: DownloadType = 'video' | 35 | type: DownloadType = 'video' |
32 | 36 | ||
@@ -38,7 +42,8 @@ export class VideoDownloadComponent { | |||
38 | private notifier: Notifier, | 42 | private notifier: Notifier, |
39 | private modalService: NgbModal, | 43 | private modalService: NgbModal, |
40 | private videoService: VideoService, | 44 | private videoService: VideoService, |
41 | private auth: AuthService | 45 | private auth: AuthService, |
46 | private hooks: HooksService | ||
42 | ) { | 47 | ) { |
43 | this.bytesPipe = new BytesPipe() | 48 | this.bytesPipe = new BytesPipe() |
44 | this.numbersPipe = new NumberFormatterPipe(this.localeId) | 49 | this.numbersPipe = new NumberFormatterPipe(this.localeId) |
@@ -62,9 +67,13 @@ export class VideoDownloadComponent { | |||
62 | 67 | ||
63 | this.activeModal = this.modalService.open(this.modal, { centered: true }) | 68 | this.activeModal = this.modalService.open(this.modal, { centered: true }) |
64 | 69 | ||
65 | this.resolutionId = this.getVideoFiles()[0].resolution.id | 70 | this.onResolutionIdChange(this.getVideoFiles()[0].resolution.id) |
66 | this.onResolutionIdChange() | 71 | |
67 | if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id | 72 | if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id |
73 | |||
74 | this.activeModal.shown.subscribe(() => { | ||
75 | this.hooks.runAction('action:modal.video-download.shown', 'common') | ||
76 | }) | ||
68 | } | 77 | } |
69 | 78 | ||
70 | onClose () { | 79 | onClose () { |
@@ -83,11 +92,15 @@ export class VideoDownloadComponent { | |||
83 | : this.getVideoFileLink() | 92 | : this.getVideoFileLink() |
84 | } | 93 | } |
85 | 94 | ||
86 | async onResolutionIdChange () { | 95 | async onResolutionIdChange (resolutionId: number) { |
96 | this.resolutionId = resolutionId | ||
87 | this.videoFile = this.getVideoFile() | 97 | this.videoFile = this.getVideoFile() |
88 | if (this.videoFile.metadata || !this.videoFile.metadataUrl) return | ||
89 | 98 | ||
90 | await this.hydrateMetadataFromMetadataUrl(this.videoFile) | 99 | if (!this.videoFile.metadata) { |
100 | if (!this.videoFile.metadataUrl) return | ||
101 | |||
102 | await this.hydrateMetadataFromMetadataUrl(this.videoFile) | ||
103 | } | ||
91 | 104 | ||
92 | this.videoFileMetadataFormat = this.videoFile | 105 | this.videoFileMetadataFormat = this.videoFile |
93 | ? this.getMetadataFormat(this.videoFile.metadata.format) | 106 | ? this.getMetadataFormat(this.videoFile.metadata.format) |
@@ -101,9 +114,6 @@ export class VideoDownloadComponent { | |||
101 | } | 114 | } |
102 | 115 | ||
103 | getVideoFile () { | 116 | getVideoFile () { |
104 | // HTML select send us a string, so convert it to a number | ||
105 | this.resolutionId = parseInt(this.resolutionId.toString(), 10) | ||
106 | |||
107 | const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId) | 117 | const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId) |
108 | if (!file) { | 118 | if (!file) { |
109 | console.error('Could not find file with resolution %d.', this.resolutionId) | 119 | console.error('Could not find file with resolution %d.', this.resolutionId) |
@@ -201,7 +211,7 @@ export class VideoDownloadComponent { | |||
201 | 211 | ||
202 | private hydrateMetadataFromMetadataUrl (file: VideoFile) { | 212 | private hydrateMetadataFromMetadataUrl (file: VideoFile) { |
203 | const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) | 213 | const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) |
204 | observable.subscribe(res => file.metadata = res) | 214 | .pipe(tap(res => file.metadata = res)) |
205 | 215 | ||
206 | return observable.toPromise() | 216 | return observable.toPromise() |
207 | } | 217 | } |
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 7a6df7b64..bac8bcc2d 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 | |||
@@ -1,4 +1,4 @@ | |||
1 | <div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow, 'fit-width': fitWidth }" (mouseenter)="loadActions()"> | 1 | <div class="video-miniature" [ngClass]="getClasses()" (mouseenter)="loadActions()"> |
2 | <my-video-thumbnail | 2 | <my-video-thumbnail |
3 | [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget" | 3 | [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget" |
4 | [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)" | 4 | [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)" |
@@ -9,9 +9,9 @@ | |||
9 | 9 | ||
10 | <div class="video-bottom"> | 10 | <div class="video-bottom"> |
11 | <div class="video-miniature-information"> | 11 | <div class="video-miniature-information"> |
12 | <div class="d-inline-flex video-miniature-meta"> | 12 | <div class="d-flex video-miniature-meta"> |
13 | <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> | 13 | <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> |
14 | <img [src]="getAvatarUrl()" alt="" /> | 14 | <img [src]="getAvatarUrl()" alt="" [ngClass]="{ channel: displayOwnerVideoChannel() }" /> |
15 | </a> | 15 | </a> |
16 | 16 | ||
17 | <div class="w-100 d-flex flex-column"> | 17 | <div class="w-100 d-flex flex-column"> |
@@ -33,7 +33,7 @@ | |||
33 | </span> | 33 | </span> |
34 | </span> | 34 | </span> |
35 | 35 | ||
36 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> | 36 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> |
37 | {{ video.byAccount }} | 37 | {{ video.byAccount }} |
38 | </a> | 38 | </a> |
39 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> | 39 | <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> |
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 38cac5b6e..621951919 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 | |||
@@ -3,198 +3,205 @@ | |||
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | $more-button-width: 40px; | 5 | $more-button-width: 40px; |
6 | $more-margin-right: 15px; | ||
7 | 6 | ||
8 | .video-miniature { | 7 | .video-miniature-name { |
9 | display: inline-flex; | 8 | @include miniature-name; |
10 | flex-direction: column; | 9 | } |
11 | padding-bottom: $video-miniature-margin-bottom; | ||
12 | vertical-align: top; | ||
13 | 10 | ||
14 | .video-bottom { | 11 | .video-miniature-information { |
15 | display: flex; | 12 | width: calc(100% - #{$more-button-width}); |
13 | } | ||
16 | 14 | ||
17 | .video-miniature-information { | 15 | .avatar { |
18 | width: $video-miniature-width - $more-button-width - $more-margin-right; | 16 | margin: 10px 10px 0 0; |
19 | line-height: normal; | ||
20 | 17 | ||
21 | .avatar { | 18 | img:not(.channel) { |
22 | margin: 10px 10px 0 0; | 19 | @include avatar(40px); |
20 | } | ||
23 | 21 | ||
24 | img { | 22 | img.channel { |
25 | @include avatar(40px); | 23 | @include channel-avatar(40px); |
26 | } | 24 | } |
27 | } | 25 | } |
28 | 26 | ||
29 | .video-miniature-name { | 27 | .video-miniature-created-at-views { |
30 | @include miniature-name; | 28 | font-size: 13px; |
31 | width: calc(100% - #{$more-button-width}); | 29 | } |
32 | } | ||
33 | 30 | ||
34 | .video-miniature-meta { | 31 | .video-miniature-account, |
35 | width: calc(100% + #{$more-button-width}); | 32 | .video-miniature-channel { |
36 | overflow: hidden; | 33 | @include disable-default-a-behaviour; |
37 | } | 34 | @include ellipsis; |
38 | 35 | ||
39 | .video-miniature-created-at-views { | 36 | display: block; |
40 | display: block; | 37 | font-size: 13px; |
41 | font-size: 13px; | 38 | color: pvar(--greyForegroundColor); |
42 | } | ||
43 | 39 | ||
44 | .video-miniature-account, | 40 | &:hover { |
45 | .video-miniature-channel { | 41 | color: $grey-foreground-hover-color; |
46 | @include disable-default-a-behaviour; | 42 | } |
47 | @include ellipsis; | 43 | } |
48 | 44 | ||
49 | display: block; | 45 | .video-info-privacy, |
50 | font-size: 13px; | 46 | .video-info-blocked .blocked-label, |
51 | color: pvar(--greyForegroundColor); | 47 | .video-info-nsfw { |
48 | font-weight: $font-semibold; | ||
49 | } | ||
52 | 50 | ||
53 | &:hover { | 51 | .video-info-blocked { |
54 | color: $grey-foreground-hover-color; | 52 | color: red; |
55 | } | ||
56 | } | ||
57 | 53 | ||
58 | .video-info-privacy, | 54 | .blocked-reason::before { |
59 | .video-info-blocked .blocked-label, | 55 | content: ' - '; |
60 | .video-info-nsfw { | 56 | } |
61 | font-weight: $font-semibold; | 57 | } |
62 | } | ||
63 | 58 | ||
64 | .video-info-blocked { | 59 | .video-info-nsfw { |
65 | color: red; | 60 | color: red; |
61 | } | ||
66 | 62 | ||
67 | .blocked-reason::before { | 63 | .video-actions { |
68 | content: ' - '; | 64 | width: $more-button-width; |
69 | } | 65 | height: 30px; |
70 | } | ||
71 | 66 | ||
72 | .video-info-nsfw { | 67 | ::ng-deep .dropdown-root:not(.show) { |
73 | color: red; | 68 | opacity: 0; |
74 | } | 69 | } |
75 | } | ||
76 | 70 | ||
77 | .video-actions { | 71 | ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { |
78 | margin-top: 3px; | 72 | opacity: 1; |
79 | width: $more-button-width; | 73 | } |
80 | height: 30px; | ||
81 | 74 | ||
82 | ::ng-deep .dropdown-root:not(.show) { | 75 | ::ng-deep .more-icon { |
83 | opacity: 0; | 76 | opacity: .6; |
84 | } | ||
85 | 77 | ||
86 | ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { | 78 | &:hover { |
87 | opacity: 1; | 79 | opacity: 1; |
88 | } | 80 | } |
81 | } | ||
82 | } | ||
89 | 83 | ||
90 | ::ng-deep .more-icon { | 84 | .video-miniature { |
91 | opacity: .6; | 85 | &:hover ::ng-deep .video-thumbnail-actions-overlay, |
86 | &:hover .video-actions ::ng-deep .dropdown-root { | ||
87 | opacity: 1 !important; | ||
88 | } | ||
89 | } | ||
92 | 90 | ||
93 | &:hover { | 91 | // Grid mode |
94 | opacity: 1; | 92 | // Takes all the width on mobile |
95 | } | 93 | .video-miniature:not(.display-as-row) { |
96 | } | 94 | display: flex; |
97 | } | 95 | flex-direction: column; |
96 | padding-bottom: $video-miniature-margin-bottom; | ||
97 | width: 100%; | ||
98 | 98 | ||
99 | @media screen and (max-width: $small-view) { | 99 | my-video-thumbnail { |
100 | .video-miniature-information { | 100 | @include block-ratio($selector: '::ng-deep .video-thumbnail'); |
101 | margin: 0 10px; | 101 | } |
102 | } | ||
103 | 102 | ||
104 | .video-actions { | 103 | .video-bottom { |
105 | margin: 0; | 104 | display: flex; |
106 | top: -3px; | 105 | width: 100%; |
106 | } | ||
107 | 107 | ||
108 | ::ng-deep .dropdown-root { | 108 | .video-miniature-name { |
109 | opacity: 1 !important; | 109 | margin-top: 10px; |
110 | } | 110 | margin-bottom: 5px; |
111 | } | ||
112 | } | ||
113 | } | 111 | } |
114 | 112 | ||
115 | &:hover ::ng-deep .video-thumbnail .video-thumbnail-actions-overlay, | 113 | .video-miniature-created-at-views { |
116 | &:hover .video-bottom .video-actions ::ng-deep .dropdown-root { | 114 | display: block; |
117 | opacity: 1; | ||
118 | } | 115 | } |
119 | 116 | ||
120 | &.fit-width { | 117 | .video-actions { |
118 | margin-top: 3px; | ||
119 | } | ||
120 | |||
121 | @media screen and (max-width: $small-view) { | ||
121 | width: 100%; | 122 | width: 100%; |
123 | margin-bottom: 25px; | ||
124 | |||
125 | .video-miniature-information { | ||
126 | margin: 0 10px; | ||
127 | |||
128 | width: 100%; | ||
129 | text-align: left; | ||
130 | } | ||
122 | 131 | ||
123 | .video-bottom { | 132 | .video-actions { |
124 | width: 100% !important; | 133 | margin: 0; |
134 | top: -3px; | ||
125 | 135 | ||
126 | .video-miniature-information { | 136 | ::ng-deep .dropdown-root { |
127 | width: calc(100% - #{$more-button-width}) !important; | 137 | opacity: 1 !important; |
128 | } | 138 | } |
129 | } | 139 | } |
130 | 140 | ||
131 | my-video-thumbnail { | 141 | ::ng-deep .video-thumbnail { |
132 | @include large-screen-ratio($selector: '::ng-deep .video-thumbnail'); | 142 | border-radius: 0; |
133 | } | 143 | } |
134 | } | 144 | } |
145 | } | ||
146 | |||
147 | .video-miniature.display-as-row { | ||
148 | --rowThumbnailWidth: #{$video-thumbnail-width}; | ||
149 | --rowThumbnailHeight: #{$video-thumbnail-height}; | ||
150 | |||
151 | display: flex; | ||
152 | flex-direction: row; | ||
135 | 153 | ||
136 | &.display-as-row { | 154 | .video-bottom { |
137 | flex-direction: row; | ||
138 | padding-bottom: 0; | ||
139 | height: auto; | ||
140 | display: flex; | 155 | display: flex; |
141 | flex-grow: 1; | 156 | } |
142 | 157 | ||
143 | my-video-thumbnail { | 158 | // We don't display avatar in row mode |
144 | margin-right: 10px; | 159 | .avatar { |
145 | } | 160 | display: none; |
161 | } | ||
146 | 162 | ||
147 | .video-bottom { | 163 | my-video-thumbnail { |
148 | .video-miniature-information { | 164 | min-width: var(--rowThumbnailWidth); |
149 | @media screen and (min-width: $small-view) { | 165 | max-width: var(--rowThumbnailWidth); |
150 | width: auto; | 166 | height: var(--rowThumbnailHeight); |
151 | min-width: 500px; | 167 | margin-right: 10px; |
152 | } | 168 | } |
153 | |||
154 | .video-miniature-name { | ||
155 | @include ellipsis-multiline(1.3em, 2); | ||
156 | |||
157 | margin-top: 2px; | ||
158 | margin-bottom: 5px; | ||
159 | } | ||
160 | |||
161 | .video-miniature-created-at-views, | ||
162 | .video-miniature-account, | ||
163 | .video-miniature-channel { | ||
164 | font-size: 95%; | ||
165 | width: fit-content; | ||
166 | } | ||
167 | |||
168 | .video-miniature-created-at-views + .video-miniature-channel { | ||
169 | margin-top: 5px; | ||
170 | } | ||
171 | |||
172 | .video-info-privacy { | ||
173 | margin-top: 5px; | ||
174 | } | ||
175 | |||
176 | .video-info-blocked { | ||
177 | margin-top: 3px; | ||
178 | } | ||
179 | } | ||
180 | 169 | ||
181 | .video-actions { | 170 | .video-miniature-name { |
182 | margin: 0; | 171 | @include ellipsis-multiline($video-miniature-row-name-font-size, 2); |
183 | top: -3px; | 172 | } |
184 | } | 173 | |
185 | } | 174 | .video-miniature-created-at-views, |
175 | .video-miniature-account, | ||
176 | .video-miniature-channel { | ||
177 | font-size: $video-miniature-row-info-font-size; | ||
178 | } | ||
186 | 179 | ||
187 | @media screen and (max-width: $small-view) { | 180 | .video-actions { |
188 | flex-direction: column; | 181 | margin-top: -3px; |
189 | height: auto; | 182 | } |
183 | } | ||
190 | 184 | ||
191 | my-video-thumbnail { | 185 | @include on-small-main-col { |
192 | margin-right: 0; | 186 | .video-miniature.display-as-row { |
193 | } | 187 | --rowThumbnailWidth: #{$video-thumbnail-medium-width}; |
188 | --rowThumbnailHeight: #{$video-thumbnail-medium-height}; | ||
189 | } | ||
190 | } | ||
194 | 191 | ||
195 | .video-miniature-information { | 192 | @include on-mobile-main-col { |
196 | min-width: initial; | 193 | .video-miniature.display-as-row { |
197 | } | 194 | --rowThumbnailWidth: #{$video-thumbnail-small-width}; |
195 | --rowThumbnailHeight: #{$video-thumbnail-small-height}; | ||
196 | |||
197 | .video-miniature-name { | ||
198 | font-size: $video-miniature-row-info-font-size; | ||
199 | } | ||
200 | |||
201 | .video-miniature-created-at-views, | ||
202 | .video-miniature-account, | ||
203 | .video-miniature-channel { | ||
204 | font-size: $video-miniature-row-mobile-info-font-size; | ||
198 | } | 205 | } |
199 | } | 206 | } |
200 | } | 207 | } |
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 cc5665ab1..48da92d6b 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 | |||
@@ -16,7 +16,6 @@ import { Video } from '../shared-main' | |||
16 | import { VideoPlaylistService } from '../shared-video-playlist' | 16 | import { VideoPlaylistService } from '../shared-video-playlist' |
17 | import { VideoActionsDisplayType } from './video-actions-dropdown.component' | 17 | import { VideoActionsDisplayType } from './video-actions-dropdown.component' |
18 | 18 | ||
19 | export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' | ||
20 | export type MiniatureDisplayOptions = { | 19 | export type MiniatureDisplayOptions = { |
21 | date?: boolean | 20 | date?: boolean |
22 | views?: boolean | 21 | views?: boolean |
@@ -40,7 +39,6 @@ export class VideoMiniatureComponent implements OnInit { | |||
40 | @Input() user: User | 39 | @Input() user: User |
41 | @Input() video: Video | 40 | @Input() video: Video |
42 | 41 | ||
43 | @Input() ownerDisplayType: OwnerDisplayType = 'account' | ||
44 | @Input() displayOptions: MiniatureDisplayOptions = { | 42 | @Input() displayOptions: MiniatureDisplayOptions = { |
45 | date: true, | 43 | date: true, |
46 | views: true, | 44 | views: true, |
@@ -51,9 +49,9 @@ export class VideoMiniatureComponent implements OnInit { | |||
51 | state: false, | 49 | state: false, |
52 | blacklistInfo: false | 50 | blacklistInfo: false |
53 | } | 51 | } |
54 | @Input() displayAsRow = false | ||
55 | @Input() displayVideoActions = true | 52 | @Input() displayVideoActions = true |
56 | @Input() fitWidth = false | 53 | |
54 | @Input() displayAsRow = false | ||
57 | 55 | ||
58 | @Input() videoLinkType: VideoLinkType = 'internal' | 56 | @Input() videoLinkType: VideoLinkType = 'internal' |
59 | 57 | ||
@@ -89,7 +87,7 @@ export class VideoMiniatureComponent implements OnInit { | |||
89 | videoHref: string | 87 | videoHref: string |
90 | videoTarget: string | 88 | videoTarget: string |
91 | 89 | ||
92 | private ownerDisplayTypeChosen: 'account' | 'videoChannel' | 90 | private ownerDisplayType: 'account' | 'videoChannel' |
93 | 91 | ||
94 | constructor ( | 92 | constructor ( |
95 | private screenService: ScreenService, | 93 | private screenService: ScreenService, |
@@ -140,11 +138,11 @@ export class VideoMiniatureComponent implements OnInit { | |||
140 | } | 138 | } |
141 | 139 | ||
142 | displayOwnerAccount () { | 140 | displayOwnerAccount () { |
143 | return this.ownerDisplayTypeChosen === 'account' | 141 | return this.ownerDisplayType === 'account' |
144 | } | 142 | } |
145 | 143 | ||
146 | displayOwnerVideoChannel () { | 144 | displayOwnerVideoChannel () { |
147 | return this.ownerDisplayTypeChosen === 'videoChannel' | 145 | return this.ownerDisplayType === 'videoChannel' |
148 | } | 146 | } |
149 | 147 | ||
150 | isUnlistedVideo () { | 148 | isUnlistedVideo () { |
@@ -183,7 +181,7 @@ export class VideoMiniatureComponent implements OnInit { | |||
183 | } | 181 | } |
184 | 182 | ||
185 | getAvatarUrl () { | 183 | getAvatarUrl () { |
186 | if (this.ownerDisplayTypeChosen === 'account') { | 184 | if (this.displayOwnerAccount()) { |
187 | return this.video.accountAvatarUrl | 185 | return this.video.accountAvatarUrl |
188 | } | 186 | } |
189 | 187 | ||
@@ -244,21 +242,26 @@ export class VideoMiniatureComponent implements OnInit { | |||
244 | return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined | 242 | return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined |
245 | } | 243 | } |
246 | 244 | ||
247 | private setUpBy () { | 245 | getClasses () { |
248 | if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { | 246 | return { |
249 | this.ownerDisplayTypeChosen = this.ownerDisplayType | 247 | 'display-as-row': this.displayAsRow |
250 | return | ||
251 | } | 248 | } |
249 | } | ||
250 | |||
251 | private setUpBy () { | ||
252 | const accountName = this.video.account.name | ||
252 | 253 | ||
253 | // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) | 254 | // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) |
255 | // Or has not been customized (default created channel display name) | ||
254 | // -> Use the account name | 256 | // -> Use the account name |
255 | if ( | 257 | if ( |
256 | this.video.channel.name === `${this.video.account.name}_channel` || | 258 | this.video.channel.displayName === `Default ${accountName} channel` || |
259 | this.video.channel.displayName === `Main ${accountName} channel` || | ||
257 | this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) | 260 | this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) |
258 | ) { | 261 | ) { |
259 | this.ownerDisplayTypeChosen = 'account' | 262 | this.ownerDisplayType = 'account' |
260 | } else { | 263 | } else { |
261 | this.ownerDisplayTypeChosen = 'videoChannel' | 264 | this.ownerDisplayType = 'videoChannel' |
262 | } | 265 | } |
263 | } | 266 | } |
264 | 267 | ||
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html index 8caeaf092..dec9e99f3 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html | |||
@@ -9,8 +9,7 @@ | |||
9 | 9 | ||
10 | <my-video-miniature | 10 | <my-video-miniature |
11 | [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" | 11 | [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" |
12 | [displayVideoActions]="false" [ownerDisplayType]="ownerDisplayType" | 12 | [displayVideoActions]="false" [user]="user" |
13 | [user]="user" | ||
14 | ></my-video-miniature> | 13 | ></my-video-miniature> |
15 | 14 | ||
16 | <!-- Display only once --> | 15 | <!-- Display only once --> |
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss index c33e11889..a2939d521 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss | |||
@@ -5,24 +5,24 @@ | |||
5 | display: flex; | 5 | display: flex; |
6 | justify-content: flex-end; | 6 | justify-content: flex-end; |
7 | flex-grow: 1; | 7 | flex-grow: 1; |
8 | } | ||
8 | 9 | ||
9 | .action-selection-mode-child { | 10 | .action-selection-mode-child { |
10 | position: fixed; | 11 | position: fixed; |
11 | |||
12 | .action-button { | ||
13 | display: block; | ||
14 | margin-left: 55px; | ||
15 | } | ||
16 | 12 | ||
17 | .action-button-cancel-selection { | 13 | .action-button { |
18 | @include peertube-button; | 14 | display: block; |
19 | @include grey-button; | 15 | margin-left: 55px; |
20 | } | ||
21 | } | 16 | } |
22 | } | 17 | } |
23 | 18 | ||
19 | .action-button-cancel-selection { | ||
20 | @include peertube-button; | ||
21 | @include grey-button; | ||
22 | } | ||
23 | |||
24 | .video { | 24 | .video { |
25 | @include row-blocks; | 25 | @include row-blocks($column-responsive: false); |
26 | 26 | ||
27 | &:first-child { | 27 | &:first-child { |
28 | margin-top: 47px; | 28 | margin-top: 47px; |
@@ -40,18 +40,16 @@ | |||
40 | } | 40 | } |
41 | } | 41 | } |
42 | 42 | ||
43 | @media screen and (max-width: $small-view) { | ||
44 | .video { | ||
45 | flex-direction: column; | ||
46 | height: auto; | ||
47 | 43 | ||
48 | .checkbox-container { | 44 | @include on-small-main-col { |
49 | display: none; | 45 | .video { |
50 | } | 46 | flex-wrap: wrap; |
47 | } | ||
48 | } | ||
51 | 49 | ||
52 | my-button { | 50 | @include on-mobile-main-col { |
53 | margin-top: 10px; | 51 | .checkbox-container { |
54 | } | 52 | display: none; |
55 | } | 53 | } |
56 | 54 | ||
57 | .action-selection-mode { | 55 | .action-selection-mode { |
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts index ca1cf2264..f8c3800d7 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts | |||
@@ -17,7 +17,7 @@ import { AuthService, ComponentPagination, LocalStorageService, Notifier, Screen | |||
17 | import { ResultList, VideoSortField } from '@shared/models' | 17 | import { ResultList, VideoSortField } from '@shared/models' |
18 | import { PeerTubeTemplateDirective, Video } from '../shared-main' | 18 | import { PeerTubeTemplateDirective, Video } from '../shared-main' |
19 | import { AbstractVideoList } from './abstract-video-list' | 19 | import { AbstractVideoList } from './abstract-video-list' |
20 | import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' | 20 | import { MiniatureDisplayOptions } from './video-miniature.component' |
21 | 21 | ||
22 | export type SelectionType = { [ id: number ]: boolean } | 22 | export type SelectionType = { [ id: number ]: boolean } |
23 | 23 | ||
@@ -31,7 +31,6 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni | |||
31 | @Input() pagination: ComponentPagination | 31 | @Input() pagination: ComponentPagination |
32 | @Input() titlePage: string | 32 | @Input() titlePage: string |
33 | @Input() miniatureDisplayOptions: MiniatureDisplayOptions | 33 | @Input() miniatureDisplayOptions: MiniatureDisplayOptions |
34 | @Input() ownerDisplayType: OwnerDisplayType | ||
35 | 34 | ||
36 | @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> | 35 | @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> |
37 | 36 | ||
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 86f6664cb..f50f95003 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 | |||
@@ -1,4 +1,4 @@ | |||
1 | <div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage }"> | 1 | <div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage, 'display-as-row': displayAsRow }"> |
2 | <a | 2 | <a |
3 | [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" | 3 | [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" |
4 | class="miniature-thumbnail" | 4 | class="miniature-thumbnail" |
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss index 1b16dbb01..c5be5f292 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss +++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss | |||
@@ -4,6 +4,7 @@ | |||
4 | 4 | ||
5 | .miniature { | 5 | .miniature { |
6 | display: inline-block; | 6 | display: inline-block; |
7 | width: 100%; | ||
7 | 8 | ||
8 | &.no-videos:not(.to-manage){ | 9 | &.no-videos:not(.to-manage){ |
9 | a { | 10 | a { |
@@ -17,62 +18,92 @@ | |||
17 | display: none; | 18 | display: none; |
18 | } | 19 | } |
19 | } | 20 | } |
21 | } | ||
20 | 22 | ||
21 | .miniature-thumbnail { | 23 | .miniature-thumbnail { |
22 | @include miniature-thumbnail; | 24 | @include miniature-thumbnail; |
23 | 25 | ||
24 | .miniature-playlist-info-overlay { | 26 | .miniature-playlist-info-overlay { |
25 | @include static-thumbnail-overlay; | 27 | @include static-thumbnail-overlay; |
26 | 28 | ||
27 | position: absolute; | 29 | position: absolute; |
28 | right: 0; | 30 | right: 0; |
29 | bottom: 0; | 31 | bottom: 0; |
30 | height: $video-thumbnail-height; | 32 | height: 100%; |
31 | padding: 0 10px; | 33 | padding: 0 10px; |
32 | display: flex; | 34 | display: flex; |
33 | align-items: center; | 35 | align-items: center; |
34 | font-size: 14px; | 36 | font-size: 14px; |
35 | font-weight: $font-semibold; | 37 | font-weight: $font-semibold; |
36 | } | ||
37 | } | 38 | } |
39 | } | ||
38 | 40 | ||
39 | .miniature-info { | 41 | .miniature-info { |
40 | width: 200px; | ||
41 | margin-top: 2px; | ||
42 | line-height: normal; | ||
43 | |||
44 | .miniature-name { | ||
45 | @include miniature-name; | ||
46 | 42 | ||
47 | @include ellipsis-multiline(1.3em, 2); | 43 | .miniature-name { |
44 | @include miniature-name; | ||
45 | @include ellipsis-multiline(1.3em, 2); | ||
48 | 46 | ||
49 | margin: 0; | 47 | margin: 0; |
50 | } | 48 | } |
51 | 49 | ||
52 | .by { | 50 | .by { |
53 | @include disable-default-a-behaviour; | 51 | @include disable-default-a-behaviour; |
54 | 52 | ||
55 | display: block; | 53 | display: block; |
56 | color: pvar(--greyForegroundColor); | 54 | color: pvar(--greyForegroundColor); |
57 | } | 55 | } |
58 | 56 | ||
59 | .privacy-date { | 57 | .privacy-date { |
60 | margin-top: 5px; | 58 | margin-top: 5px; |
61 | 59 | ||
62 | .video-info-privacy { | 60 | .video-info-privacy { |
63 | font-size: 14px; | 61 | font-size: 14px; |
64 | font-weight: $font-semibold; | 62 | font-weight: $font-semibold; |
65 | 63 | ||
66 | &::after { | 64 | &::after { |
67 | content: '-'; | 65 | content: '-'; |
68 | margin: 0 3px; | 66 | margin: 0 3px; |
69 | } | ||
70 | } | 67 | } |
71 | } | 68 | } |
69 | } | ||
72 | 70 | ||
73 | .video-info-description { | 71 | .video-info-description { |
74 | margin-top: 10px; | 72 | margin-top: 10px; |
75 | color: pvar(--greyForegroundColor); | 73 | color: pvar(--greyForegroundColor); |
76 | } | 74 | } |
75 | } | ||
76 | |||
77 | .miniature:not(.display-as-row) { | ||
78 | .miniature-thumbnail { | ||
79 | margin-top: 10px; | ||
80 | margin-bottom: 5px; | ||
81 | } | ||
82 | } | ||
83 | |||
84 | .miniature.display-as-row { | ||
85 | --rowThumbnailWidth: #{$video-thumbnail-width}; | ||
86 | --rowThumbnailHeight: #{$video-thumbnail-height}; | ||
87 | |||
88 | display: flex; | ||
89 | |||
90 | .miniature-thumbnail { | ||
91 | width: var(--rowThumbnailWidth); | ||
92 | height: var(--rowThumbnailHeight); | ||
93 | margin-right: 10px; | ||
94 | } | ||
95 | } | ||
96 | |||
97 | @include on-small-main-col { | ||
98 | .miniature.display-as-row { | ||
99 | --rowThumbnailWidth: #{$video-thumbnail-medium-width}; | ||
100 | --rowThumbnailHeight: #{$video-thumbnail-medium-height}; | ||
101 | } | ||
102 | } | ||
103 | |||
104 | @include on-mobile-main-col { | ||
105 | .miniature.display-as-row { | ||
106 | --rowThumbnailWidth: #{$video-thumbnail-small-width}; | ||
107 | --rowThumbnailHeight: #{$video-thumbnail-small-height}; | ||
77 | } | 108 | } |
78 | } | 109 | } |
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 251aa868a..6b0b1056f 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 | |||
@@ -12,6 +12,7 @@ export class VideoPlaylistMiniatureComponent { | |||
12 | @Input() displayChannel = false | 12 | @Input() displayChannel = false |
13 | @Input() displayDescription = false | 13 | @Input() displayDescription = false |
14 | @Input() displayPrivacy = false | 14 | @Input() displayPrivacy = false |
15 | @Input() displayAsRow = false | ||
15 | 16 | ||
16 | getPlaylistUrl () { | 17 | getPlaylistUrl () { |
17 | if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] | 18 | if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] |