diff options
author | Chocobozzz <me@florianbigard.com> | 2021-03-25 13:42:55 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-03-31 09:05:51 +0200 |
commit | 60c35932f6a14cfe83bb0e54407427cce70171ea (patch) | |
tree | 6d15665a5375e14bea8eb6d63acc8f4139372dad | |
parent | 4097c6d66cb2919c28b5bce44b259e630923fbe0 (diff) | |
download | PeerTube-60c35932f6a14cfe83bb0e54407427cce70171ea.tar.gz PeerTube-60c35932f6a14cfe83bb0e54407427cce70171ea.tar.zst PeerTube-60c35932f6a14cfe83bb0e54407427cce70171ea.zip |
Redesign channel page
11 files changed, 507 insertions, 196 deletions
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-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 b3ea19768..f63110bf5 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html | |||
@@ -1,50 +1,114 @@ | |||
1 | <div *ngIf="videoChannel" class="row"> | 1 | <div class="root" *ngIf="videoChannel"> |
2 | <div class="sub-menu"> | 2 | <div class="channel-info"> |
3 | 3 | ||
4 | <div class="actor"> | 4 | <ng-template #buttonsTemplate> |
5 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | 5 | <a *ngIf="isManageable() && !isInSmallView()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n> |
6 | Manage channel | ||
7 | </a> | ||
8 | |||
9 | <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> | ||
10 | </ng-template> | ||
11 | |||
12 | <ng-template #ownerTemplate> | ||
13 | <div class="owner-block"> | ||
14 | <div class="avatar-row"> | ||
15 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | ||
16 | |||
17 | <div class="actor-info"> | ||
18 | <h4>{{ videoChannel.ownerAccount.displayName }}</h4> | ||
6 | 19 | ||
7 | <div class="actor-info"> | 20 | <div class="actor-handle">@{{ videoChannel.ownerBy }}</div> |
8 | <div class="actor-names"> | ||
9 | <div class="actor-display-name">{{ videoChannel.displayName }}</div> | ||
10 | <div class="actor-name"> | ||
11 | <span>{{ videoChannel.nameWithHost }}</span> | ||
12 | <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" | ||
13 | class="btn btn-outline-secondary btn-sm copy-button" | ||
14 | > | ||
15 | <span class="glyphicon glyphicon-duplicate"></span> | ||
16 | </button> | ||
17 | </div> | 21 | </div> |
18 | </div> | 22 | </div> |
19 | 23 | ||
20 | <div class="right-buttons"> | 24 | <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> | 25 | <div class="description-html" [innerHTML]="ownerDescriptionHTML"></div> |
22 | Manage channel | ||
23 | </a> | ||
24 | <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> | ||
25 | </div> | 26 | </div> |
26 | 27 | ||
27 | <div class="actor-lower"> | 28 | <a class="view-account short" [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n> |
28 | <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | 29 | View account |
30 | </a> | ||
31 | |||
32 | <a class="view-account complete" [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n> | ||
33 | View owner account | ||
34 | </a> | ||
35 | </div> | ||
36 | </ng-template> | ||
37 | |||
38 | <div class="channel-avatar-row"> | ||
39 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | ||
40 | |||
41 | <div> | ||
42 | <div class="section-label" i18n>VIDEO CHANNEL</div> | ||
43 | |||
44 | <div class="actor-info"> | ||
45 | <div> | ||
46 | <div class="actor-display-name"> | ||
47 | <h1>{{ videoChannel.displayName }}</h1> | ||
48 | </div> | ||
49 | |||
50 | <div class="actor-handle"> | ||
51 | <span>@{{ videoChannel.nameWithHost }}</span> | ||
52 | <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" | ||
53 | class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title | ||
54 | > | ||
55 | <span class="glyphicon glyphicon-duplicate"></span> | ||
56 | </button> | ||
57 | </div> | ||
58 | |||
59 | <div class="actor-counters"> | ||
60 | <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span> | ||
61 | |||
62 | <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n> | ||
63 | {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}} | ||
64 | </span> | ||
65 | </div> | ||
66 | </div> | ||
29 | 67 | ||
30 | <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> | 68 | <div class="channel-buttons right"> |
31 | <span class="d-inline-flex"><span i18n class="d-none d-sm-block mr-1">Created by</span>{{ videoChannel.ownerBy }}</span> | 69 | <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template> |
32 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | 70 | </div> |
33 | </a> | ||
34 | </div> | 71 | </div> |
35 | </div> | 72 | </div> |
36 | </div> | 73 | </div> |
37 | 74 | ||
38 | <div class="links w-100"> | 75 | <div class="channel-description" [ngClass]="{ expanded: channelDescriptionExpanded }"> |
39 | <ng-template #linkTemplate let-item="item"> | 76 | <div class="description-html" [innerHTML]="channelDescriptionHTML"></div> |
40 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | 77 | |
41 | </ng-template> | 78 | <div class="created-at" i18n>Channel created on {{ videoChannel.createdAt | date }}</div> |
79 | </div> | ||
42 | 80 | ||
43 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | 81 | <div *ngIf="!channelDescriptionExpanded" class="show-more" role="button" |
82 | (click)="channelDescriptionExpanded = !channelDescriptionExpanded" | ||
83 | title="Show the complete description" i18n-title i18n | ||
84 | > | ||
85 | Show more... | ||
44 | </div> | 86 | </div> |
87 | |||
88 | <div class="channel-buttons bottom"> | ||
89 | <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template> | ||
90 | </div> | ||
91 | |||
92 | <div class="owner-card"> | ||
93 | <div class="section-label" i18n>OWNER ACCOUNT</div> | ||
94 | |||
95 | <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template> | ||
96 | </div> | ||
97 | </div> | ||
98 | |||
99 | <div class="bottom-owner"> | ||
100 | <div class="section-label" i18n>OWNER ACCOUNT</div> | ||
101 | |||
102 | <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template> | ||
45 | </div> | 103 | </div> |
46 | 104 | ||
47 | <div class="margin-content"> | 105 | <div class="links"> |
48 | <router-outlet></router-outlet> | 106 | <ng-template #linkTemplate let-item="item"> |
107 | <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> | ||
108 | </ng-template> | ||
109 | |||
110 | <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> | ||
49 | </div> | 111 | </div> |
112 | |||
113 | <router-outlet></router-outlet> | ||
50 | </div> | 114 | </div> |
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 22f21dcc6..16e13c578 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss | |||
@@ -1,89 +1,345 @@ | |||
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 '_miniature'; | ||
9 | 4 | ||
10 | .sub-menu { | 5 | .root { |
11 | @include sub-menu-with-actor; | 6 | --myGlobalPadding: 60px; |
7 | --myChannelImgMargin: 30px; | ||
8 | --myFontSize: 16px; | ||
9 | --myGreyChannelFontSize: 16px; | ||
10 | --myGreyOwnerFontSize: 14px; | ||
11 | } | ||
12 | 12 | ||
13 | .actor, .actor-info { | 13 | .section-label { |
14 | width: 100%; | 14 | color: pvar(--mainColor); |
15 | font-size: 12px; | ||
16 | margin-bottom: 15px; | ||
17 | font-weight: $font-bold; | ||
18 | letter-spacing: 2.5px; | ||
19 | } | ||
20 | |||
21 | .links { | ||
22 | @include fluid-videos-miniature-layout; | ||
23 | } | ||
24 | |||
25 | .channel-info { | ||
26 | display: grid; | ||
27 | grid-template-columns: 1fr auto; | ||
28 | grid-template-rows: auto auto; | ||
29 | |||
30 | background-color: pvar(--channelBackgroundColor); | ||
31 | margin-bottom: 45px; | ||
32 | padding: var(--myGlobalPadding) var(--myGlobalPadding) 0 var(--myGlobalPadding); | ||
33 | font-size: var(--myFontSize); | ||
34 | } | ||
35 | |||
36 | .channel-avatar-row { | ||
37 | display: flex; | ||
38 | grid-column: 1; | ||
39 | margin-bottom: 30px; | ||
40 | |||
41 | img { | ||
42 | @include channel-avatar(120px); | ||
43 | } | ||
44 | |||
45 | > div { | ||
46 | margin-left: var(--myChannelImgMargin); | ||
15 | } | 47 | } |
16 | 48 | ||
17 | .actor-info { | 49 | .actor-info { |
18 | display: grid !important; | 50 | display: flex; |
19 | grid-template-columns: 1fr auto; | ||
20 | grid-template-rows: 1fr auto / 1fr auto; | ||
21 | grid-template-areas: "name buttons" "lower buttons"; | ||
22 | 51 | ||
23 | @include media-breakpoint-down(lg) { | 52 | > div:first-child { |
24 | grid-template-areas: "name name" "lower buttons"; | 53 | flex-grow: 1; |
25 | } | 54 | } |
26 | } | 55 | } |
27 | 56 | ||
28 | .actor-names { | 57 | .actor-display-name { |
29 | grid-area: name; | 58 | display: flex; |
59 | flex-wrap: wrap; | ||
30 | } | 60 | } |
31 | 61 | ||
32 | .actor-name { | 62 | h1 { |
33 | flex-grow: 1; | 63 | font-size: 28px; |
64 | font-weight: $font-bold; | ||
65 | margin: 0; | ||
66 | } | ||
34 | 67 | ||
35 | .copy-button { | 68 | .actor-handle, |
36 | border: none; | 69 | .actor-counters { |
37 | padding: 5px; | 70 | color: pvar(--greyForegroundColor); |
38 | margin-top: -2px; | 71 | font-size: var(--myGreyChannelFontSize); |
39 | } | 72 | } |
73 | |||
74 | .actor-counters > *:not(:last-child)::after { | ||
75 | content: '•'; | ||
76 | margin: 0 10px; | ||
77 | color: pvar(--mainColor); | ||
40 | } | 78 | } |
41 | } | 79 | } |
42 | 80 | ||
43 | .margin-content { | 81 | .channel-description { |
44 | // margin-content is required, but child views have their own margins | 82 | grid-column: 1; |
45 | // that match views outside the scope of accounts, so we only align | ||
46 | // them with the margins of .sub-menu when required. | ||
47 | margin: 0; | ||
48 | } | 83 | } |
49 | 84 | ||
50 | .right-buttons { | 85 | .show-more { |
86 | display: none; | ||
87 | color: pvar(--mainColor); | ||
88 | cursor: pointer; | ||
89 | margin: 10px auto 45px auto; | ||
90 | } | ||
91 | |||
92 | |||
93 | .channel-buttons { | ||
51 | display: flex; | 94 | display: flex; |
52 | height: max-content; | 95 | flex-wrap: wrap; |
53 | margin-left: auto; | 96 | |
54 | margin-top: 10px; | 97 | > *:not(:last-child) { |
98 | margin-right: 15px; | ||
99 | } | ||
100 | } | ||
101 | |||
102 | .channel-buttons.right { | ||
103 | margin-left: 45px; | ||
104 | } | ||
105 | |||
106 | // Only used by mobile | ||
107 | .channel-buttons.bottom { | ||
108 | display: none; | ||
109 | } | ||
110 | |||
111 | .created-at { | ||
112 | margin-top: 15px; | ||
113 | color: pvar(--greyForegroundColor); | ||
114 | padding-bottom: 60px; | ||
115 | } | ||
116 | |||
117 | .owner-card { | ||
118 | margin-left: 105px; | ||
119 | grid-column: 2; | ||
120 | // Takes all the column | ||
121 | grid-row: 1 / 3; | ||
122 | place-self: end; | ||
123 | } | ||
124 | |||
125 | // Only used on mobile | ||
126 | .bottom-owner { | ||
127 | display: none; | ||
128 | } | ||
129 | |||
130 | .owner-block { | ||
131 | background-color: pvar(--mainBackgroundColor); | ||
132 | padding: 30px; | ||
133 | width: 300px; | ||
134 | font-size: var(--myFontSize); | ||
135 | |||
136 | .avatar-row { | ||
137 | display: flex; | ||
138 | margin-bottom: 15px; | ||
139 | |||
140 | img { | ||
141 | @include avatar(48px); | ||
142 | } | ||
143 | |||
144 | .actor-info { | ||
145 | margin-left: 15px; | ||
146 | } | ||
147 | |||
148 | h4 { | ||
149 | font-size: 18px; | ||
150 | margin: 0; | ||
151 | } | ||
152 | |||
153 | .actor-handle { | ||
154 | font-size: var(--myGreyOwnerFontSize); | ||
155 | color: pvar(--greyForegroundColor); | ||
156 | } | ||
157 | } | ||
158 | |||
159 | .owner-description { | ||
160 | height: 140px; | ||
161 | |||
162 | @include fade-text(120px, pvar(--mainBackgroundColor)); | ||
163 | } | ||
164 | } | ||
165 | |||
166 | .view-account.short { | ||
167 | @include peertube-button-link; | ||
168 | @include orange-button-inverted; | ||
169 | |||
170 | margin-top: 30px; | ||
171 | } | ||
172 | |||
173 | .view-account.complete { | ||
174 | display: none; | ||
175 | } | ||
176 | |||
177 | .copy-button { | ||
178 | border: none; | ||
179 | } | ||
180 | |||
181 | @media screen and (max-width: 1400px) { | ||
182 | // Takes all the row width | ||
183 | .channel-avatar-row { | ||
184 | grid-column: 1 / 3; | ||
185 | } | ||
186 | |||
187 | .owner-card { | ||
188 | grid-row: 2; | ||
189 | margin-left: 60px; | ||
190 | } | ||
191 | } | ||
192 | |||
193 | @media screen and (max-width: 1100px) { | ||
194 | .root { | ||
195 | --myGlobalPadding: 45px; | ||
196 | --myChannelImgMargin: 15px; | ||
197 | } | ||
198 | |||
199 | .channel-info { | ||
200 | display: flex; | ||
201 | flex-direction: column; | ||
202 | margin-bottom: 0; | ||
203 | } | ||
204 | |||
205 | .channel-description:not(.expanded) { | ||
206 | max-height: 70px; | ||
207 | |||
208 | @include fade-text(30px, pvar(--channelBackgroundColor)); | ||
209 | } | ||
210 | |||
211 | .show-more { | ||
212 | display: inline-block; | ||
213 | } | ||
214 | |||
215 | .channel-buttons.bottom { | ||
216 | display: flex; | ||
217 | justify-content: center; | ||
218 | margin-bottom: 30px; | ||
219 | } | ||
220 | |||
221 | .channel-buttons.right { | ||
222 | display: none; | ||
223 | } | ||
224 | |||
225 | .owner-card { | ||
226 | display: none; | ||
227 | } | ||
55 | 228 | ||
56 | grid-row: buttons-start / span buttons-end; | 229 | .bottom-owner { |
57 | grid-column: buttons-start; | 230 | display: block; |
231 | width: 100%; | ||
232 | border-bottom: 2px solid $separator-border-color; | ||
233 | padding: var(--myGlobalPadding) 45px; | ||
234 | margin-bottom: 60px; | ||
235 | } | ||
58 | 236 | ||
59 | @include media-breakpoint-down(lg) { | 237 | .owner-block { |
60 | flex-flow: column-reverse; | 238 | display: grid; |
239 | width: 100%; | ||
240 | padding: 0; | ||
61 | 241 | ||
62 | a { | 242 | .avatar-row { |
63 | margin-top: 0.25rem; | 243 | grid-column: 1; |
64 | margin-right: 0 !important; | 244 | margin-right: 30px; |
245 | } | ||
246 | |||
247 | .owner-description { | ||
248 | grid-column: 2; | ||
249 | max-height: 70px; | ||
250 | |||
251 | @include fade-text(30px, pvar(--mainBackgroundColor)); | ||
252 | } | ||
253 | |||
254 | .view-account { | ||
255 | grid-column: 2; | ||
65 | } | 256 | } |
66 | } | 257 | } |
67 | 258 | ||
68 | a { | 259 | .view-account.complete { |
69 | @include peertube-button-outline; | 260 | display: inline-block; |
70 | line-height: 1.8; | 261 | margin-top: 10px; |
262 | color: pvar(--mainColor); | ||
71 | } | 263 | } |
72 | 264 | ||
73 | my-subscribe-button { | 265 | .view-account.short { |
74 | height: min-content; | 266 | display: none; |
75 | } | 267 | } |
76 | } | 268 | } |
77 | 269 | ||
78 | @media screen and (max-width: $mobile-view) { | 270 | @media screen and (max-width: $mobile-view) { |
79 | .sub-menu { | 271 | .root { |
80 | .actor { | 272 | --myGlobalPadding: 15px; |
81 | flex-direction: column; | 273 | --myFontSize: 14px; |
274 | --myGreyChannelFontSize: 13px; | ||
275 | --myGreyOwnerFontSize: 13px; | ||
276 | } | ||
277 | |||
278 | .links { | ||
279 | margin: auto !important; | ||
280 | width: min-content; | ||
281 | } | ||
282 | |||
283 | .section-label { | ||
284 | font-size: 10px; | ||
285 | letter-spacing: 2.1px; | ||
286 | margin-bottom: 5px; | ||
287 | } | ||
288 | |||
289 | .channel-avatar-row { | ||
290 | margin-bottom: 15px; | ||
291 | |||
292 | h1 { | ||
293 | font-size: 22px; | ||
294 | } | ||
295 | |||
296 | img { | ||
297 | @include channel-avatar(80px); | ||
298 | } | ||
299 | } | ||
300 | |||
301 | .show-more { | ||
302 | margin-bottom: 30px; | ||
303 | } | ||
82 | 304 | ||
83 | .actor-info .actor-names { | 305 | .bottom-owner { |
306 | padding: 15px; | ||
307 | margin-bottom: 30px; | ||
308 | |||
309 | .section-label { | ||
310 | display: none; | ||
311 | } | ||
312 | } | ||
313 | |||
314 | .owner-block { | ||
315 | display: block; | ||
316 | |||
317 | .avatar-row { | ||
318 | display: flex; | ||
319 | flex-direction: row-reverse; | ||
320 | margin: 0; | ||
321 | |||
322 | h4 { | ||
323 | font-size: 16px; | ||
324 | } | ||
325 | |||
326 | .actor-info { | ||
327 | display: flex; | ||
84 | flex-direction: column; | 328 | flex-direction: column; |
85 | align-items: normal; | 329 | align-items: flex-end; |
330 | justify-content: flex-end; | ||
331 | margin-top: -5px; | ||
332 | } | ||
333 | |||
334 | img { | ||
335 | @include channel-avatar(64px); | ||
336 | |||
337 | margin: -30px 0 0 15px; | ||
86 | } | 338 | } |
87 | } | 339 | } |
340 | |||
341 | .owner-description { | ||
342 | display: none; | ||
343 | } | ||
88 | } | 344 | } |
89 | } | 345 | } |
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts index bb601e227..037c108f2 100644 --- a/client/src/app/+video-channels/video-channels.component.ts +++ b/client/src/app/+video-channels/video-channels.component.ts | |||
@@ -3,8 +3,8 @@ 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 { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 8 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
10 | 10 | ||
@@ -20,6 +20,11 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
20 | links: ListOverflowItem[] = [] | 20 | links: ListOverflowItem[] = [] |
21 | isChannelManageable = false | 21 | isChannelManageable = false |
22 | 22 | ||
23 | channelVideosCount: number | ||
24 | ownerDescriptionHTML = '' | ||
25 | channelDescriptionHTML = '' | ||
26 | channelDescriptionExpanded = false | ||
27 | |||
23 | private routeSub: Subscription | 28 | private routeSub: Subscription |
24 | 29 | ||
25 | constructor ( | 30 | constructor ( |
@@ -27,9 +32,11 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
27 | private notifier: Notifier, | 32 | private notifier: Notifier, |
28 | private authService: AuthService, | 33 | private authService: AuthService, |
29 | private videoChannelService: VideoChannelService, | 34 | private videoChannelService: VideoChannelService, |
35 | private videoService: VideoService, | ||
30 | private restExtractor: RestExtractor, | 36 | private restExtractor: RestExtractor, |
31 | private hotkeysService: HotkeysService, | 37 | private hotkeysService: HotkeysService, |
32 | private screenService: ScreenService | 38 | private screenService: ScreenService, |
39 | private markdown: MarkdownService | ||
33 | ) { } | 40 | ) { } |
34 | 41 | ||
35 | ngOnInit () { | 42 | ngOnInit () { |
@@ -43,16 +50,14 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
43 | HttpStatusCode.NOT_FOUND_404 | 50 | HttpStatusCode.NOT_FOUND_404 |
44 | ])) | 51 | ])) |
45 | ) | 52 | ) |
46 | .subscribe(videoChannel => { | 53 | .subscribe(async videoChannel => { |
54 | this.channelDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.description) | ||
55 | this.ownerDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.ownerAccount.description) | ||
56 | |||
57 | // After the markdown renderer to avoid layout changes | ||
47 | this.videoChannel = videoChannel | 58 | this.videoChannel = videoChannel |
48 | 59 | ||
49 | if (this.authService.isLoggedIn()) { | 60 | 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 | }) | 61 | }) |
57 | 62 | ||
58 | this.hotkeys = [ | 63 | this.hotkeys = [ |
@@ -67,8 +72,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
67 | 72 | ||
68 | this.links = [ | 73 | this.links = [ |
69 | { label: $localize`VIDEOS`, routerLink: 'videos' }, | 74 | { label: $localize`VIDEOS`, routerLink: 'videos' }, |
70 | { label: $localize`VIDEO PLAYLISTS`, routerLink: 'video-playlists' }, | 75 | { label: $localize`VIDEO PLAYLISTS`, routerLink: 'video-playlists' } |
71 | { label: $localize`ABOUT`, routerLink: 'about' } | ||
72 | ] | 76 | ] |
73 | } | 77 | } |
74 | 78 | ||
@@ -79,7 +83,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
79 | if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) | 83 | if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) |
80 | } | 84 | } |
81 | 85 | ||
82 | get isInSmallView () { | 86 | isInSmallView () { |
83 | return this.screenService.isInSmallView() | 87 | return this.screenService.isInSmallView() |
84 | } | 88 | } |
85 | 89 | ||
@@ -87,12 +91,24 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
87 | return this.authService.isLoggedIn() | 91 | return this.authService.isLoggedIn() |
88 | } | 92 | } |
89 | 93 | ||
90 | get isManageable () { | 94 | isManageable () { |
91 | if (!this.isUserLoggedIn()) return false | 95 | if (!this.isUserLoggedIn()) return false |
96 | |||
92 | return this.videoChannel.ownerAccount.userId === this.authService.getUser().id | 97 | return this.videoChannel.ownerAccount.userId === this.authService.getUser().id |
93 | } | 98 | } |
94 | 99 | ||
95 | activateCopiedMessage () { | 100 | activateCopiedMessage () { |
96 | this.notifier.success($localize`Username copied`) | 101 | this.notifier.success($localize`Username copied`) |
97 | } | 102 | } |
103 | |||
104 | private loadChannelVideosCount () { | ||
105 | this.videoService.getVideoChannelVideos({ | ||
106 | videoChannel: this.videoChannel, | ||
107 | videoPagination: { | ||
108 | currentPage: 1, | ||
109 | itemsPerPage: 0 | ||
110 | }, | ||
111 | sort: '-publishedAt' | ||
112 | }).subscribe(res => this.channelVideosCount = res.total) | ||
113 | } | ||
98 | } | 114 | } |
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts index 05236ff85..1b58a1d92 100644 --- a/client/src/app/+video-channels/video-channels.module.ts +++ b/client/src/app/+video-channels/video-channels.module.ts | |||
@@ -5,7 +5,6 @@ import { SharedMainModule } from '@app/shared/shared-main' | |||
5 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' | 5 | import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' |
6 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' | 6 | import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' |
7 | import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' | 7 | 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' | 8 | import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' |
10 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' | 9 | import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' |
11 | import { VideoChannelsRoutingModule } from './video-channels-routing.module' | 10 | import { VideoChannelsRoutingModule } from './video-channels-routing.module' |
@@ -26,7 +25,6 @@ import { VideoChannelsComponent } from './video-channels.component' | |||
26 | declarations: [ | 25 | declarations: [ |
27 | VideoChannelsComponent, | 26 | VideoChannelsComponent, |
28 | VideoChannelVideosComponent, | 27 | VideoChannelVideosComponent, |
29 | VideoChannelAboutComponent, | ||
30 | VideoChannelPlaylistsComponent | 28 | VideoChannelPlaylistsComponent |
31 | ], | 29 | ], |
32 | 30 | ||
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index a0009eecc..0a92afef3 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss | |||
@@ -36,7 +36,9 @@ body { | |||
36 | 36 | ||
37 | --menuBackgroundColor: #{$menu-background}; | 37 | --menuBackgroundColor: #{$menu-background}; |
38 | --menuForegroundColor: #{$menu-color}; | 38 | --menuForegroundColor: #{$menu-color}; |
39 | |||
39 | --submenuColor: #{$sub-menu-color}; | 40 | --submenuColor: #{$sub-menu-color}; |
41 | --channelBackgroundColor: #{$channel-background-color}; | ||
40 | 42 | ||
41 | --inputForegroundColor: #{$input-foreground-color}; | 43 | --inputForegroundColor: #{$input-foreground-color}; |
42 | --inputBackgroundColor: #{$input-background-color}; | 44 | --inputBackgroundColor: #{$input-background-color}; |
@@ -277,11 +279,6 @@ my-input-toggle-hidden ::ng-deep input { | |||
277 | font-weight: bold; | 279 | font-weight: bold; |
278 | } | 280 | } |
279 | 281 | ||
280 | @keyframes spin { | ||
281 | from { transform: scale(1) rotate(0deg);} | ||
282 | to { transform: scale(1) rotate(360deg);} | ||
283 | } | ||
284 | |||
285 | // In tables, don't have a hover different background | 282 | // In tables, don't have a hover different background |
286 | table { | 283 | table { |
287 | .action-button-edit, .action-button-delete { | 284 | .action-button-edit, .action-button-delete { |
@@ -468,3 +465,21 @@ ngx-loading-bar { | |||
468 | } | 465 | } |
469 | } | 466 | } |
470 | } | 467 | } |
468 | |||
469 | // Utils | ||
470 | |||
471 | .peertube-button { | ||
472 | @include peertube-button; | ||
473 | } | ||
474 | |||
475 | .peertube-button-link { | ||
476 | @include peertube-button-link; | ||
477 | } | ||
478 | |||
479 | .orange-button { | ||
480 | @include orange-button; | ||
481 | } | ||
482 | |||
483 | .orange-button-inverted { | ||
484 | @include orange-button-inverted; | ||
485 | } | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index cf5ac8fd8..82c60a59d 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -31,9 +31,19 @@ | |||
31 | text-overflow: ellipsis; | 31 | text-overflow: ellipsis; |
32 | } | 32 | } |
33 | 33 | ||
34 | @mixin prefix($property, $parameters...) { | 34 | @mixin fade-text ($fade-after, $background-color) { |
35 | @each $prefix in -webkit-, -moz-, -ms-, -o-, "" { | 35 | position: relative; |
36 | #{$prefix}#{$property}: $parameters; | 36 | overflow: hidden; |
37 | |||
38 | &:after { | ||
39 | content: ''; | ||
40 | pointer-events: none; | ||
41 | width: 100%; | ||
42 | height: 100%; | ||
43 | position: absolute; | ||
44 | left: 0; | ||
45 | top: 0; | ||
46 | background: linear-gradient(transparent $fade-after, $background-color); | ||
37 | } | 47 | } |
38 | } | 48 | } |
39 | 49 | ||
@@ -138,6 +148,33 @@ | |||
138 | } | 148 | } |
139 | } | 149 | } |
140 | 150 | ||
151 | @mixin orange-button-inverted { | ||
152 | @include button-focus(pvar(--mainColorLightest)); | ||
153 | |||
154 | border: 2px solid pvar(--mainColor); | ||
155 | font-weight: $font-regular; | ||
156 | |||
157 | &, &:active, &:focus { | ||
158 | color: pvar(--mainColor); | ||
159 | background-color: pvar(--mainBackgroundColor); | ||
160 | } | ||
161 | |||
162 | &:hover { | ||
163 | color: pvar(--mainColor); | ||
164 | background-color: pvar(--mainColorLightest); | ||
165 | } | ||
166 | |||
167 | &[disabled], &.disabled { | ||
168 | cursor: default; | ||
169 | color: pvar(--mainColor); | ||
170 | background-color: #C6C6C6; | ||
171 | } | ||
172 | |||
173 | my-global-icon { | ||
174 | @include apply-svg-color(pvar(--mainColor)) | ||
175 | } | ||
176 | } | ||
177 | |||
141 | @mixin tertiary-button { | 178 | @mixin tertiary-button { |
142 | @include button-focus($grey-button-outline-color); | 179 | @include button-focus($grey-button-outline-color); |
143 | 180 | ||
@@ -509,6 +546,13 @@ | |||
509 | min-height: $size; | 546 | min-height: $size; |
510 | } | 547 | } |
511 | 548 | ||
549 | @mixin channel-avatar ($size) { | ||
550 | width: $size; | ||
551 | height: $size; | ||
552 | min-width: $size; | ||
553 | min-height: $size; | ||
554 | } | ||
555 | |||
512 | @mixin chevron ($size, $border-width) { | 556 | @mixin chevron ($size, $border-width) { |
513 | border-style: solid; | 557 | border-style: solid; |
514 | border-width: $border-width $border-width 0 0; | 558 | border-width: $border-width $border-width 0 0; |
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index c8316473d..bcd28215b 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss | |||
@@ -16,9 +16,10 @@ $grey-foreground-hover-color: #303030; | |||
16 | $grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%); | 16 | $grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%); |
17 | 17 | ||
18 | $main-color: hsl(24, 90%, 50%); | 18 | $main-color: hsl(24, 90%, 50%); |
19 | $main-hover-color: lighten($main-color, 5%); | ||
20 | $main-color-lighter: lighten($main-color, 10%); | 19 | $main-color-lighter: lighten($main-color, 10%); |
21 | $main-color-lightest: lighten($main-color, 40%); | 20 | $main-color-lightest: lighten($main-color, 40%); |
21 | $main-hover-color: lighten($main-color, 5%); | ||
22 | |||
22 | $secondary-color: hsl(187, 77%, 34%); | 23 | $secondary-color: hsl(187, 77%, 34%); |
23 | 24 | ||
24 | $support-button: inherit; | 25 | $support-button: inherit; |
@@ -50,6 +51,8 @@ $menu-lateral-padding: 26px; | |||
50 | $sub-menu-color: #F7F7F7; | 51 | $sub-menu-color: #F7F7F7; |
51 | $sub-menu-height: 81px; | 52 | $sub-menu-height: 81px; |
52 | 53 | ||
54 | $channel-background-color: #f6ede8; | ||
55 | |||
53 | $footer-height: 30px; | 56 | $footer-height: 30px; |
54 | $footer-margin: 30px; | 57 | $footer-margin: 30px; |
55 | 58 | ||
@@ -98,7 +101,9 @@ $variables: ( | |||
98 | 101 | ||
99 | --menuBackgroundColor: var(--menuBackgroundColor), | 102 | --menuBackgroundColor: var(--menuBackgroundColor), |
100 | --menuForegroundColor: var(--menuForegroundColor), | 103 | --menuForegroundColor: var(--menuForegroundColor), |
104 | |||
101 | --submenuColor: var(--submenuColor), | 105 | --submenuColor: var(--submenuColor), |
106 | --channelBackgroundColor: var(--channelBackgroundColor), | ||
102 | 107 | ||
103 | --inputForegroundColor: var(--inputForegroundColor), | 108 | --inputForegroundColor: var(--inputForegroundColor), |
104 | --inputBackgroundColor: var(--inputBackgroundColor), | 109 | --inputBackgroundColor: var(--inputBackgroundColor), |