diff options
author | Kim <1877318+kimsible@users.noreply.github.com> | 2020-07-24 08:53:25 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-24 08:53:25 +0200 |
commit | 2e7f262724dd64a209e0bad5930ba29bb4f801c3 (patch) | |
tree | ae722bf8df3331442a5f9eaa34eb5c12db21f6cf | |
parent | b40a219338fed042072decea203838ca5e2b265f (diff) | |
download | PeerTube-2e7f262724dd64a209e0bad5930ba29bb4f801c3.tar.gz PeerTube-2e7f262724dd64a209e0bad5930ba29bb4f801c3.tar.zst PeerTube-2e7f262724dd64a209e0bad5930ba29bb4f801c3.zip |
Display user quota progress bars above upload form (#2981)
* Move user-quota to my-user-quota shared component
* Add user-quota to upload form
* Increase progress bar height and make it focusable
* Correct syntax parenthesis
* Add explicit title to user-quota bars + tooltip with quota values
* Hide user-quota in second upload step
* Customize focus styles on user-quota
Co-authored-by: kimsible <kimsible@users.noreply.github.com>
11 files changed, 141 insertions, 77 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 eb7bb0d6f..2ad014f01 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 | |||
@@ -14,21 +14,7 @@ | |||
14 | 14 | ||
15 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> | 15 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> |
16 | 16 | ||
17 | <div class="user-quota mb-3"> | 17 | <my-user-quota [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota> |
18 | <div> | ||
19 | <div class="progress" i18n-title title="Total video quota"> | ||
20 | <div class="progress-bar" role="progressbar" [style]="{ width: userVideoQuotaPercentage + '%' }" [attr.aria-valuenow]="userVideoQuotaUsed" aria-valuemin="0" [attr.aria-valuemax]="user.videoQuota">{{ userVideoQuotaUsed | bytes: 0 }}</div> | ||
21 | <span class="ml-auto mr-2">{{ userVideoQuota }}</span> | ||
22 | </div> | ||
23 | </div> | ||
24 | |||
25 | <div *ngIf="hasDailyQuota()" class="mt-3"> | ||
26 | <div class="progress" i18n-title title="Daily video quota"> | ||
27 | <div class="progress-bar secondary" role="progressbar" [style]="{ width: userVideoQuotaDailyPercentage + '%' }" [attr.aria-valuenow]="userVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.videoQuotaDaily">{{ userVideoQuotaUsedDaily | bytes: 0 }}</div> | ||
28 | <span class="ml-auto mr-2">{{ userVideoQuotaDaily }}</span> | ||
29 | </div> | ||
30 | </div> | ||
31 | </div> | ||
32 | 18 | ||
33 | <my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile> | 19 | <my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile> |
34 | </div> | 20 | </div> |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss b/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss index 1cdb1fab4..d17cd931e 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss | |||
@@ -1,15 +1,6 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .user-quota { | ||
5 | font-size: 15px; | ||
6 | margin-top: 20px; | ||
7 | |||
8 | label { | ||
9 | margin-right: 5px; | ||
10 | } | ||
11 | } | ||
12 | |||
13 | .account-title { | 4 | .account-title { |
14 | @include settings-big-title; | 5 | @include settings-big-title; |
15 | 6 | ||
@@ -18,14 +9,6 @@ | |||
18 | } | 9 | } |
19 | } | 10 | } |
20 | 11 | ||
21 | .progress { | 12 | .form-group { |
22 | @include progressbar; | 13 | max-width: 500px; |
23 | width: 500px; | ||
24 | max-width: 100%; | ||
25 | } | ||
26 | |||
27 | @media screen and (max-width: $small-view) { | ||
28 | .progress { | ||
29 | width: 100%; | ||
30 | } | ||
31 | } | 14 | } |
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 a9a150e21..a3a8ff1f1 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 | |||
@@ -1,4 +1,3 @@ | |||
1 | import { BytesPipe } from 'ngx-pipes' | ||
2 | import { ViewportScroller } from '@angular/common' | 1 | import { ViewportScroller } from '@angular/common' |
3 | import { AfterViewChecked, Component, OnInit } from '@angular/core' | 2 | import { AfterViewChecked, Component, OnInit } from '@angular/core' |
4 | import { AuthService, Notifier, User, UserService } from '@app/core' | 3 | import { AuthService, Notifier, User, UserService } from '@app/core' |
@@ -12,14 +11,6 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
12 | export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | 11 | export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { |
13 | user: User = null | 12 | user: User = null |
14 | 13 | ||
15 | userVideoQuota = '0' | ||
16 | userVideoQuotaUsed = 0 | ||
17 | userVideoQuotaPercentage = 15 | ||
18 | |||
19 | userVideoQuotaDaily = '0' | ||
20 | userVideoQuotaUsedDaily = 0 | ||
21 | userVideoQuotaDailyPercentage = 15 | ||
22 | |||
23 | private lastScrollHash: string | 14 | private lastScrollHash: string |
24 | 15 | ||
25 | constructor ( | 16 | constructor ( |
@@ -36,31 +27,6 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | |||
36 | 27 | ||
37 | ngOnInit () { | 28 | ngOnInit () { |
38 | this.user = this.authService.getUser() | 29 | this.user = this.authService.getUser() |
39 | |||
40 | this.authService.userInformationLoaded.subscribe( | ||
41 | () => { | ||
42 | if (this.user.videoQuota !== -1) { | ||
43 | this.userVideoQuota = new BytesPipe().transform(this.user.videoQuota, 0).toString() | ||
44 | } else { | ||
45 | this.userVideoQuota = this.i18n('Unlimited') | ||
46 | } | ||
47 | |||
48 | if (this.user.videoQuotaDaily !== -1) { | ||
49 | this.userVideoQuotaDaily = new BytesPipe().transform(this.user.videoQuotaDaily, 0).toString() | ||
50 | } else { | ||
51 | this.userVideoQuotaDaily = this.i18n('Unlimited') | ||
52 | } | ||
53 | } | ||
54 | ) | ||
55 | |||
56 | this.userService.getMyVideoQuotaUsed() | ||
57 | .subscribe(data => { | ||
58 | this.userVideoQuotaUsed = data.videoQuotaUsed | ||
59 | this.userVideoQuotaPercentage = this.userVideoQuotaUsed * 100 / this.user.videoQuota | ||
60 | |||
61 | this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily | ||
62 | this.userVideoQuotaDailyPercentage = this.userVideoQuotaUsedDaily * 100 / this.user.videoQuotaDaily | ||
63 | }) | ||
64 | } | 30 | } |
65 | 31 | ||
66 | ngAfterViewChecked () { | 32 | ngAfterViewChecked () { |
@@ -83,8 +49,4 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { | |||
83 | err => this.notifier.error(err.message) | 49 | err => this.notifier.error(err.message) |
84 | ) | 50 | ) |
85 | } | 51 | } |
86 | |||
87 | hasDailyQuota () { | ||
88 | return this.user.videoQuotaDaily !== -1 | ||
89 | } | ||
90 | } | 52 | } |
diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html index 79bfc6e5c..5690ac37f 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.html +++ b/client/src/app/+videos/+video-edit/video-add.component.html | |||
@@ -5,6 +5,8 @@ | |||
5 | Instead, <a routerLink="/admin/users">create a dedicated account</a> to upload your videos. | 5 | Instead, <a routerLink="/admin/users">create a dedicated account</a> to upload your videos. |
6 | </div> | 6 | </div> |
7 | 7 | ||
8 | <my-user-quota *ngIf="!isInSecondStep()" [user]="user" [userInformationLoaded]="userInformationLoaded"></my-user-quota> | ||
9 | |||
8 | <div class="title-page title-page-single" *ngIf="isInSecondStep()"> | 10 | <div class="title-page title-page-single" *ngIf="isInSecondStep()"> |
9 | <ng-container *ngIf="secondStepType === 'import-url' || secondStepType === 'import-torrent'" i18n>Import {{ videoName }}</ng-container> | 11 | <ng-container *ngIf="secondStepType === 'import-url' || secondStepType === 'import-torrent'" i18n>Import {{ videoName }}</ng-container> |
10 | <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> | 12 | <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> |
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 b9fef9fce..f9977bda0 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.scss +++ b/client/src/app/+videos/+video-edit/video-add.component.scss | |||
@@ -7,7 +7,7 @@ $border-color: #EAEAEA; | |||
7 | $nav-link-height: 40px; | 7 | $nav-link-height: 40px; |
8 | 8 | ||
9 | .margin-content { | 9 | .margin-content { |
10 | padding-top: 50px; | 10 | padding-top: 20px; |
11 | } | 11 | } |
12 | 12 | ||
13 | .alert { | 13 | .alert { |
@@ -16,7 +16,7 @@ $nav-link-height: 40px; | |||
16 | 16 | ||
17 | ::ng-deep .video-add-nav { | 17 | ::ng-deep .video-add-nav { |
18 | border-bottom: $border-width $border-type $border-color; | 18 | border-bottom: $border-width $border-type $border-color; |
19 | margin: 50px 0 0 0 !important; | 19 | margin: 20px 0 0 0 !important; |
20 | 20 | ||
21 | &.hide-nav { | 21 | &.hide-nav { |
22 | display: none !important; | 22 | display: none !important; |
@@ -64,7 +64,7 @@ $nav-link-height: 40px; | |||
64 | padding-bottom: 20px; | 64 | padding-bottom: 20px; |
65 | display: flex; | 65 | display: flex; |
66 | justify-content: center; | 66 | justify-content: center; |
67 | align-items: center; | 67 | padding-top: 20px; |
68 | 68 | ||
69 | &.dragover { | 69 | &.dragover { |
70 | border: 3px dashed pvar(--mainColor); | 70 | border: 3px dashed pvar(--mainColor); |
diff --git a/client/src/app/+videos/+video-edit/video-add.component.ts b/client/src/app/+videos/+video-edit/video-add.component.ts index 5bd768809..016791d59 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.ts +++ b/client/src/app/+videos/+video-edit/video-add.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, HostListener, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, HostListener, OnInit, ViewChild } from '@angular/core' |
2 | import { AuthService, CanComponentDeactivate, ServerService } from '@app/core' | 2 | import { AuthService, CanComponentDeactivate, ServerService, User } from '@app/core' |
3 | import { ServerConfig } from '@shared/models' | 3 | import { ServerConfig } from '@shared/models' |
4 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' | 4 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' |
5 | import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' | 5 | import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' |
@@ -15,6 +15,8 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
15 | @ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent | 15 | @ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent |
16 | @ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent | 16 | @ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent |
17 | 17 | ||
18 | user: User = null | ||
19 | |||
18 | secondStepType: 'upload' | 'import-url' | 'import-torrent' | 20 | secondStepType: 'upload' | 'import-url' | 'import-torrent' |
19 | videoName: string | 21 | videoName: string |
20 | serverConfig: ServerConfig | 22 | serverConfig: ServerConfig |
@@ -24,7 +26,13 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
24 | private serverService: ServerService | 26 | private serverService: ServerService |
25 | ) {} | 27 | ) {} |
26 | 28 | ||
29 | get userInformationLoaded () { | ||
30 | return this.auth.userInformationLoaded | ||
31 | } | ||
32 | |||
27 | ngOnInit () { | 33 | ngOnInit () { |
34 | this.user = this.auth.getUser() | ||
35 | |||
28 | this.serverConfig = this.serverService.getTmpConfig() | 36 | this.serverConfig = this.serverService.getTmpConfig() |
29 | 37 | ||
30 | this.serverService.getConfig() | 38 | this.serverService.getConfig() |
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 732914e34..22a207e51 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -26,7 +26,7 @@ import { DateToggleComponent } from './date' | |||
26 | import { FeedComponent } from './feeds' | 26 | import { FeedComponent } from './feeds' |
27 | import { LoaderComponent, SmallLoaderComponent } from './loaders' | 27 | import { LoaderComponent, SmallLoaderComponent } from './loaders' |
28 | import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent } from './misc' | 28 | import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent } from './misc' |
29 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService } from './users' | 29 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
30 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' | 30 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' |
31 | import { VideoCaptionService } from './video-caption' | 31 | import { VideoCaptionService } from './video-caption' |
32 | import { VideoChannelService } from './video-channel' | 32 | import { VideoChannelService } from './video-channel' |
@@ -83,6 +83,7 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | |||
83 | ListOverflowComponent, | 83 | ListOverflowComponent, |
84 | TopMenuDropdownComponent, | 84 | TopMenuDropdownComponent, |
85 | 85 | ||
86 | UserQuotaComponent, | ||
86 | UserNotificationsComponent | 87 | UserNotificationsComponent |
87 | ], | 88 | ], |
88 | 89 | ||
@@ -132,6 +133,7 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | |||
132 | ListOverflowComponent, | 133 | ListOverflowComponent, |
133 | TopMenuDropdownComponent, | 134 | TopMenuDropdownComponent, |
134 | 135 | ||
136 | UserQuotaComponent, | ||
135 | UserNotificationsComponent | 137 | UserNotificationsComponent |
136 | ], | 138 | ], |
137 | 139 | ||
diff --git a/client/src/app/shared/shared-main/users/index.ts b/client/src/app/shared/shared-main/users/index.ts index 83401ab52..130082af6 100644 --- a/client/src/app/shared/shared-main/users/index.ts +++ b/client/src/app/shared/shared-main/users/index.ts | |||
@@ -2,3 +2,4 @@ export * from './user-history.service' | |||
2 | export * from './user-notification.model' | 2 | export * from './user-notification.model' |
3 | export * from './user-notification.service' | 3 | export * from './user-notification.service' |
4 | export * from './user-notifications.component' | 4 | export * from './user-notifications.component' |
5 | export * from './user-quota.component' | ||
diff --git a/client/src/app/shared/shared-main/users/user-quota.component.html b/client/src/app/shared/shared-main/users/user-quota.component.html new file mode 100644 index 000000000..f9fb32133 --- /dev/null +++ b/client/src/app/shared/shared-main/users/user-quota.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <div class="user-quota mb-3"> | ||
2 | <div> | ||
3 | <strong class="user-quota-title" tabindex="0" i18n>Total video quota</strong> | ||
4 | <div class="progress" tabindex="0" [ngbTooltip]="titleVideoQuota()"> | ||
5 | <div class="progress-bar" tabindex="0" role="progressbar" [style]="{ width: userVideoQuotaPercentage + '%' }" | ||
6 | [attr.aria-valuenow]="userVideoQuotaUsed" aria-valuemin="0" [attr.aria-valuemax]="user.videoQuota"> | ||
7 | {{ userVideoQuotaUsed | bytes: 0 }}</div> | ||
8 | <span class="ml-auto mr-2">{{ userVideoQuota }}</span> | ||
9 | </div> | ||
10 | </div> | ||
11 | |||
12 | <div *ngIf="hasDailyQuota()" class="mt-3"> | ||
13 | <strong class="user-quota-title" tabindex="0" i18n>Daily video quota</strong> | ||
14 | <div class="progress" tabindex="0" [ngbTooltip]="titleVideoQuotaDaily()"> | ||
15 | <div class="progress-bar secondary" role="progressbar" [style]="{ width: userVideoQuotaDailyPercentage + '%' }" | ||
16 | [attr.aria-valuenow]="userVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.videoQuotaDaily"> | ||
17 | {{ userVideoQuotaUsedDaily | bytes: 0 }}</div> | ||
18 | <span class="ml-auto mr-2">{{ userVideoQuotaDaily }}</span> | ||
19 | </div> | ||
20 | </div> | ||
21 | </div> \ No newline at end of file | ||
diff --git a/client/src/app/shared/shared-main/users/user-quota.component.scss b/client/src/app/shared/shared-main/users/user-quota.component.scss new file mode 100644 index 000000000..904add0f4 --- /dev/null +++ b/client/src/app/shared/shared-main/users/user-quota.component.scss | |||
@@ -0,0 +1,31 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .user-quota { | ||
5 | font-size: 15px; | ||
6 | margin-top: 20px; | ||
7 | |||
8 | label { | ||
9 | margin-right: 5px; | ||
10 | } | ||
11 | |||
12 | &, .progress { | ||
13 | width: 100% !important; | ||
14 | } | ||
15 | |||
16 | .user-quota-title, .progress { | ||
17 | @include disable-outline; | ||
18 | @include button-focus(pvar(--mainColorLightest)); | ||
19 | } | ||
20 | |||
21 | .progress { | ||
22 | @include progressbar; | ||
23 | |||
24 | height: 2rem; | ||
25 | |||
26 | span { | ||
27 | align-self: center; | ||
28 | } | ||
29 | } | ||
30 | } | ||
31 | |||
diff --git a/client/src/app/shared/shared-main/users/user-quota.component.ts b/client/src/app/shared/shared-main/users/user-quota.component.ts new file mode 100644 index 000000000..a8f56e4d6 --- /dev/null +++ b/client/src/app/shared/shared-main/users/user-quota.component.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import { Subject } from 'rxjs' | ||
2 | import { BytesPipe } from 'ngx-pipes' | ||
3 | import { Component, Input, OnInit } from '@angular/core' | ||
4 | import { User, UserService } from '@app/core' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-user-quota', | ||
9 | templateUrl: './user-quota.component.html', | ||
10 | styleUrls: ['./user-quota.component.scss'] | ||
11 | }) | ||
12 | |||
13 | export class UserQuotaComponent implements OnInit { | ||
14 | @Input() user: User = null | ||
15 | @Input() userInformationLoaded: Subject<any> | ||
16 | |||
17 | userVideoQuota = '0' | ||
18 | userVideoQuotaUsed = 0 | ||
19 | userVideoQuotaPercentage = 15 | ||
20 | |||
21 | userVideoQuotaDaily = '0' | ||
22 | userVideoQuotaUsedDaily = 0 | ||
23 | userVideoQuotaDailyPercentage = 15 | ||
24 | |||
25 | constructor ( | ||
26 | private userService: UserService, | ||
27 | private i18n: I18n | ||
28 | ) { } | ||
29 | |||
30 | ngOnInit () { | ||
31 | this.userInformationLoaded.subscribe( | ||
32 | () => { | ||
33 | if (this.user.videoQuota !== -1) { | ||
34 | this.userVideoQuota = new BytesPipe().transform(this.user.videoQuota, 0).toString() | ||
35 | } else { | ||
36 | this.userVideoQuota = this.i18n('Unlimited') | ||
37 | } | ||
38 | |||
39 | if (this.user.videoQuotaDaily !== -1) { | ||
40 | this.userVideoQuotaDaily = new BytesPipe().transform(this.user.videoQuotaDaily, 0).toString() | ||
41 | } else { | ||
42 | this.userVideoQuotaDaily = this.i18n('Unlimited') | ||
43 | } | ||
44 | } | ||
45 | ) | ||
46 | |||
47 | this.userService.getMyVideoQuotaUsed() | ||
48 | .subscribe(data => { | ||
49 | this.userVideoQuotaUsed = data.videoQuotaUsed | ||
50 | this.userVideoQuotaPercentage = this.userVideoQuotaUsed * 100 / this.user.videoQuota | ||
51 | |||
52 | this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily | ||
53 | this.userVideoQuotaDailyPercentage = this.userVideoQuotaUsedDaily * 100 / this.user.videoQuotaDaily | ||
54 | }) | ||
55 | } | ||
56 | |||
57 | hasDailyQuota () { | ||
58 | return this.user.videoQuotaDaily !== -1 | ||
59 | } | ||
60 | |||
61 | titleVideoQuota () { | ||
62 | return `${new BytesPipe().transform(this.userVideoQuotaUsed, 0).toString()} / ${this.userVideoQuota}` | ||
63 | } | ||
64 | |||
65 | titleVideoQuotaDaily () { | ||
66 | return `${new BytesPipe().transform(this.userVideoQuotaUsedDaily, 0).toString()} / ${this.userVideoQuotaDaily}` | ||
67 | } | ||
68 | } | ||