diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2021-01-13 09:12:55 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-13 09:12:55 +0100 |
commit | 1ea7da819e5bfae7b443ed722c18c4165d101439 (patch) | |
tree | 17cea3786dfb3a59a2ad5559de9ebf106a0440a2 /client/src | |
parent | 75dd1b641f987e1e09dbaa3329e08c6e98a858f3 (diff) | |
download | PeerTube-1ea7da819e5bfae7b443ed722c18c4165d101439.tar.gz PeerTube-1ea7da819e5bfae7b443ed722c18c4165d101439.tar.zst PeerTube-1ea7da819e5bfae7b443ed722c18c4165d101439.zip |
add ability to remove one's avatar for account and channels (#3467)
* add ability to remove one's avatar for account and channels
* add ability to remove one's avatar for account and channels
* only display avatar edition options after input change
Diffstat (limited to 'client/src')
14 files changed, 140 insertions, 19 deletions
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 40505b92f..b0d2ec58d 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)"></my-actor-avatar-info> | 6 | <my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info> |
7 | </div> | 7 | </div> |
8 | </div> | 8 | </div> |
9 | 9 | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index d5d019b35..c16368952 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts | |||
@@ -53,4 +53,17 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | |||
53 | }) | 53 | }) |
54 | ) | 54 | ) |
55 | } | 55 | } |
56 | |||
57 | onAvatarDelete () { | ||
58 | this.userService.deleteAvatar() | ||
59 | .subscribe( | ||
60 | data => { | ||
61 | this.notifier.success($localize`Avatar deleted.`) | ||
62 | |||
63 | this.user.updateAccountAvatar() | ||
64 | }, | ||
65 | |||
66 | (err: HttpErrorResponse) => this.notifier.error(err.message) | ||
67 | ) | ||
68 | } | ||
56 | } | 69 | } |
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 5ea000400..735f9e3ba 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 | |||
@@ -46,7 +46,7 @@ | |||
46 | 46 | ||
47 | <my-actor-avatar-info | 47 | <my-actor-avatar-info |
48 | *ngIf="!isCreation() && videoChannelToUpdate" | 48 | *ngIf="!isCreation() && videoChannelToUpdate" |
49 | [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" | 49 | [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()" |
50 | ></my-actor-avatar-info> | 50 | ></my-actor-avatar-info> |
51 | 51 | ||
52 | <div class="form-group"> | 52 | <div class="form-group"> |
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 09db0df9d..3e20a27ee 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 | |||
@@ -14,6 +14,7 @@ export abstract class MyVideoChannelEdit extends FormReactive { | |||
14 | 14 | ||
15 | // We need this method so angular does not complain in child template that doesn't need this | 15 | // We need this method so angular does not complain in child template that doesn't need this |
16 | onAvatarChange (formData: FormData) { /* empty */ } | 16 | onAvatarChange (formData: FormData) { /* empty */ } |
17 | onAvatarDelete () { /* empty */ } | ||
17 | 18 | ||
18 | // Should be implemented by the child | 19 | // Should be implemented by the child |
19 | isBulkUpdateVideosDisplayed () { | 20 | isBulkUpdateVideosDisplayed () { |
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 c6cb5ade6..6cd1ff503 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 | |||
@@ -11,6 +11,8 @@ import { FormValidatorService } from '@app/shared/shared-forms' | |||
11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
12 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' | 12 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' |
13 | import { MyVideoChannelEdit } from './my-video-channel-edit' | 13 | import { MyVideoChannelEdit } from './my-video-channel-edit' |
14 | import { HttpErrorResponse } from '@angular/common/http' | ||
15 | import { uploadErrorHandler } from '@app/helpers' | ||
14 | 16 | ||
15 | @Component({ | 17 | @Component({ |
16 | selector: 'my-video-channel-update', | 18 | selector: 'my-video-channel-update', |
@@ -107,10 +109,27 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements | |||
107 | this.videoChannelToUpdate.updateAvatar(data.avatar) | 109 | this.videoChannelToUpdate.updateAvatar(data.avatar) |
108 | }, | 110 | }, |
109 | 111 | ||
110 | err => this.notifier.error(err.message) | 112 | (err: HttpErrorResponse) => uploadErrorHandler({ |
113 | err, | ||
114 | name: $localize`avatar`, | ||
115 | notifier: this.notifier | ||
116 | }) | ||
111 | ) | 117 | ) |
112 | } | 118 | } |
113 | 119 | ||
120 | onAvatarDelete () { | ||
121 | this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name) | ||
122 | .subscribe( | ||
123 | data => { | ||
124 | this.notifier.success($localize`Avatar deleted.`) | ||
125 | |||
126 | this.videoChannelToUpdate.resetAvatar() | ||
127 | }, | ||
128 | |||
129 | err => this.notifier.error(err.message) | ||
130 | ) | ||
131 | } | ||
132 | |||
114 | get maxAvatarSize () { | 133 | get maxAvatarSize () { |
115 | return this.serverConfig.avatar.file.size.max | 134 | return this.serverConfig.avatar.file.size.max |
116 | } | 135 | } |
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index 7c9569ed4..15a4f7f82 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts | |||
@@ -131,8 +131,9 @@ export class User implements UserServerModel { | |||
131 | } | 131 | } |
132 | } | 132 | } |
133 | 133 | ||
134 | updateAccountAvatar (newAccountAvatar: Avatar) { | 134 | updateAccountAvatar (newAccountAvatar?: Avatar) { |
135 | this.account.updateAvatar(newAccountAvatar) | 135 | if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) |
136 | else this.account.resetAvatar() | ||
136 | } | 137 | } |
137 | 138 | ||
138 | isUploadDisabled () { | 139 | isUploadDisabled () { |
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index 2f3945169..99ed3b407 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts | |||
@@ -123,6 +123,16 @@ export class UserService { | |||
123 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 123 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
124 | } | 124 | } |
125 | 125 | ||
126 | deleteAvatar () { | ||
127 | const url = UserService.BASE_USERS_URL + 'me/avatar' | ||
128 | |||
129 | return this.authHttp.delete(url) | ||
130 | .pipe( | ||
131 | map(this.restExtractor.extractDataBool), | ||
132 | catchError(err => this.restExtractor.handleError(err)) | ||
133 | ) | ||
134 | } | ||
135 | |||
126 | signup (userCreate: UserRegister) { | 136 | signup (userCreate: UserRegister) { |
127 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) | 137 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) |
128 | .pipe( | 138 | .pipe( |
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 b3dc6cfe5..b71a893d1 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts | |||
@@ -44,6 +44,11 @@ export class Account extends Actor implements ServerAccount { | |||
44 | this.updateComputedAttributes() | 44 | this.updateComputedAttributes() |
45 | } | 45 | } |
46 | 46 | ||
47 | resetAvatar () { | ||
48 | this.avatar = null | ||
49 | this.avatarUrl = Account.GET_DEFAULT_AVATAR_URL() | ||
50 | } | ||
51 | |||
47 | private updateComputedAttributes () { | 52 | private updateComputedAttributes () { |
48 | this.avatarUrl = Account.GET_ACTOR_AVATAR_URL(this) | 53 | this.avatarUrl = Account.GET_ACTOR_AVATAR_URL(this) |
49 | } | 54 | } |
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 index e63d8de2d..a34d27b26 100644 --- 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 | |||
@@ -4,12 +4,18 @@ | |||
4 | <img [src]="actor.avatarUrl" alt="Avatar" /> | 4 | <img [src]="actor.avatarUrl" alt="Avatar" /> |
5 | 5 | ||
6 | <div class="actor-img-edit-container"> | 6 | <div class="actor-img-edit-container"> |
7 | <div class="actor-img-edit-button" [ngbTooltip]="avatarFormat" | 7 | |
8 | placement="right" container="body"> | 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"> | ||
9 | <my-global-icon iconName="edit"></my-global-icon> | 15 | <my-global-icon iconName="edit"></my-global-icon> |
10 | <label for="avatarfile" i18n>Change your avatar</label> | 16 | <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label> |
11 | <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/> | ||
12 | </div> | 17 | </div> |
18 | |||
13 | </div> | 19 | </div> |
14 | </div> | 20 | </div> |
15 | 21 | ||
@@ -22,4 +28,16 @@ | |||
22 | <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div> | 28 | <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div> |
23 | </div> | 29 | </div> |
24 | </div> | 30 | </div> |
25 | </ng-container> \ No newline at end of file | 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> \ No newline at end of file | ||
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 index 7118e9471..57c298508 100644 --- 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 | |||
@@ -70,3 +70,17 @@ | |||
70 | } | 70 | } |
71 | } | 71 | } |
72 | } | 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-avatar-info.component.ts b/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts index de78a390e..451bbbba3 100644 --- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts +++ b/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts | |||
@@ -1,22 +1,27 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' |
2 | import { Notifier, ServerService } from '@app/core' | 2 | import { Notifier, ServerService } from '@app/core' |
3 | import { getBytes } from '@root-helpers/bytes' | 3 | import { getBytes } from '@root-helpers/bytes' |
4 | import { ServerConfig } from '@shared/models' | 4 | import { ServerConfig } from '@shared/models' |
5 | import { VideoChannel } from '../video-channel/video-channel.model' | 5 | import { VideoChannel } from '../video-channel/video-channel.model' |
6 | import { Account } from '../account/account.model' | 6 | import { Account } from '../account/account.model' |
7 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | ||
8 | import { Actor } from './actor.model' | ||
7 | 9 | ||
8 | @Component({ | 10 | @Component({ |
9 | selector: 'my-actor-avatar-info', | 11 | selector: 'my-actor-avatar-info', |
10 | templateUrl: './actor-avatar-info.component.html', | 12 | templateUrl: './actor-avatar-info.component.html', |
11 | styleUrls: [ './actor-avatar-info.component.scss' ] | 13 | styleUrls: [ './actor-avatar-info.component.scss' ] |
12 | }) | 14 | }) |
13 | export class ActorAvatarInfoComponent implements OnInit { | 15 | export class ActorAvatarInfoComponent implements OnInit, OnChanges { |
14 | @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> | 16 | @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> |
17 | @ViewChild('avatarPopover') avatarPopover: NgbPopover | ||
15 | 18 | ||
16 | @Input() actor: VideoChannel | Account | 19 | @Input() actor: VideoChannel | Account |
17 | 20 | ||
18 | @Output() avatarChange = new EventEmitter<FormData>() | 21 | @Output() avatarChange = new EventEmitter<FormData>() |
22 | @Output() avatarDelete = new EventEmitter<void>() | ||
19 | 23 | ||
24 | private avatarUrl: string | ||
20 | private serverConfig: ServerConfig | 25 | private serverConfig: ServerConfig |
21 | 26 | ||
22 | constructor ( | 27 | constructor ( |
@@ -30,19 +35,31 @@ export class ActorAvatarInfoComponent implements OnInit { | |||
30 | .subscribe(config => this.serverConfig = config) | 35 | .subscribe(config => this.serverConfig = config) |
31 | } | 36 | } |
32 | 37 | ||
33 | onAvatarChange () { | 38 | ngOnChanges (changes: SimpleChanges) { |
39 | if (changes['actor']) { | ||
40 | this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | onAvatarChange (input: HTMLInputElement) { | ||
45 | this.avatarfileInput = new ElementRef(input) | ||
46 | |||
34 | const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] | 47 | const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] |
35 | if (avatarfile.size > this.maxAvatarSize) { | 48 | if (avatarfile.size > this.maxAvatarSize) { |
36 | this.notifier.error('Error', 'This image is too large.') | 49 | this.notifier.error('Error', $localize`This image is too large.`) |
37 | return | 50 | return |
38 | } | 51 | } |
39 | 52 | ||
40 | const formData = new FormData() | 53 | const formData = new FormData() |
41 | formData.append('avatarfile', avatarfile) | 54 | formData.append('avatarfile', avatarfile) |
42 | 55 | this.avatarPopover?.close() | |
43 | this.avatarChange.emit(formData) | 56 | this.avatarChange.emit(formData) |
44 | } | 57 | } |
45 | 58 | ||
59 | deleteAvatar () { | ||
60 | this.avatarDelete.emit() | ||
61 | } | ||
62 | |||
46 | get maxAvatarSize () { | 63 | get maxAvatarSize () { |
47 | return this.serverConfig.avatar.file.size.max | 64 | return this.serverConfig.avatar.file.size.max |
48 | } | 65 | } |
@@ -58,4 +75,8 @@ export class ActorAvatarInfoComponent implements OnInit { | |||
58 | get avatarFormat () { | 75 | get avatarFormat () { |
59 | return `${$localize`max size`}: 192*192px, ${this.maxAvatarSizeInBytes} ${$localize`extensions`}: ${this.avatarExtensions}` | 76 | return `${$localize`max size`}: 192*192px, ${this.maxAvatarSizeInBytes} ${$localize`extensions`}: ${this.avatarExtensions}` |
60 | } | 77 | } |
78 | |||
79 | get hasAvatar () { | ||
80 | return !!this.avatarUrl | ||
81 | } | ||
61 | } | 82 | } |
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 4f1f5b65d..c6a63fe6c 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 | |||
@@ -56,6 +56,11 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
56 | this.updateComputedAttributes() | 56 | this.updateComputedAttributes() |
57 | } | 57 | } |
58 | 58 | ||
59 | resetAvatar () { | ||
60 | this.avatar = null | ||
61 | this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL() | ||
62 | } | ||
63 | |||
59 | private updateComputedAttributes () { | 64 | private updateComputedAttributes () { |
60 | this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) | 65 | this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) |
61 | } | 66 | } |
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 64dcf638a..eff3fad4d 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 | |||
@@ -89,6 +89,16 @@ export class VideoChannelService { | |||
89 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 89 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
90 | } | 90 | } |
91 | 91 | ||
92 | deleteVideoChannelAvatar (videoChannelName: string) { | ||
93 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar' | ||
94 | |||
95 | return this.authHttp.delete(url) | ||
96 | .pipe( | ||
97 | map(this.restExtractor.extractDataBool), | ||
98 | catchError(err => this.restExtractor.handleError(err)) | ||
99 | ) | ||
100 | } | ||
101 | |||
92 | removeVideoChannel (videoChannel: VideoChannel) { | 102 | removeVideoChannel (videoChannel: VideoChannel) { |
93 | return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost) | 103 | return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost) |
94 | .pipe( | 104 | .pipe( |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 0ce22354e..51cf4c3ed 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -260,15 +260,12 @@ | |||
260 | } | 260 | } |
261 | } | 261 | } |
262 | 262 | ||
263 | @mixin peertube-button-file ($width) { | 263 | @mixin peertube-file { |
264 | position: relative; | 264 | position: relative; |
265 | overflow: hidden; | 265 | overflow: hidden; |
266 | display: inline-block; | 266 | display: inline-block; |
267 | width: $width; | ||
268 | min-height: 30px; | 267 | min-height: 30px; |
269 | 268 | ||
270 | @include peertube-button; | ||
271 | |||
272 | input[type=file] { | 269 | input[type=file] { |
273 | position: absolute; | 270 | position: absolute; |
274 | top: 0; | 271 | top: 0; |
@@ -286,6 +283,13 @@ | |||
286 | } | 283 | } |
287 | } | 284 | } |
288 | 285 | ||
286 | @mixin peertube-button-file ($width) { | ||
287 | width: $width; | ||
288 | |||
289 | @include peertube-file; | ||
290 | @include peertube-button; | ||
291 | } | ||
292 | |||
289 | @mixin icon ($size) { | 293 | @mixin icon ($size) { |
290 | display: inline-block; | 294 | display: inline-block; |
291 | background-repeat: no-repeat; | 295 | background-repeat: no-repeat; |