diff options
Diffstat (limited to 'client/src')
11 files changed, 90 insertions, 32 deletions
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.html b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.html index 6bf2294a3..685efc9ab 100644 --- a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.html +++ b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.html | |||
@@ -1 +1 @@ | |||
<div class="custom-markup-container" #contentWrapper></div> | <div [hidden]="!displayed" class="custom-markup-container" #contentWrapper></div> | ||
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts index 3d49c6768..4e802b14d 100644 --- a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts +++ b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts | |||
@@ -10,6 +10,8 @@ export class CustomMarkupContainerComponent implements OnChanges { | |||
10 | 10 | ||
11 | @Input() content: string | 11 | @Input() content: string |
12 | 12 | ||
13 | displayed = false | ||
14 | |||
13 | constructor ( | 15 | constructor ( |
14 | private customMarkupService: CustomMarkupService | 16 | private customMarkupService: CustomMarkupService |
15 | ) { } | 17 | ) { } |
@@ -19,8 +21,13 @@ export class CustomMarkupContainerComponent implements OnChanges { | |||
19 | } | 21 | } |
20 | 22 | ||
21 | private async buildElement () { | 23 | private async buildElement () { |
22 | const element = await this.customMarkupService.buildElement(this.content) | 24 | if (!this.content) return |
23 | this.contentWrapper.nativeElement.appendChild(element) | 25 | |
24 | } | 26 | const { rootElement, componentsLoaded } = await this.customMarkupService.buildElement(this.content) |
27 | this.contentWrapper.nativeElement.appendChild(rootElement) | ||
25 | 28 | ||
29 | await componentsLoaded | ||
30 | |||
31 | this.displayed = true | ||
32 | } | ||
26 | } | 33 | } |
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts index cb1110593..15da94709 100644 --- a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts +++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { first } from 'rxjs/operators' | ||
1 | import { ComponentRef, Injectable } from '@angular/core' | 2 | import { ComponentRef, Injectable } from '@angular/core' |
2 | import { MarkdownService } from '@app/core' | 3 | import { MarkdownService } from '@app/core' |
3 | import { | 4 | import { |
@@ -19,8 +20,9 @@ import { | |||
19 | VideoMiniatureMarkupComponent, | 20 | VideoMiniatureMarkupComponent, |
20 | VideosListMarkupComponent | 21 | VideosListMarkupComponent |
21 | } from './peertube-custom-tags' | 22 | } from './peertube-custom-tags' |
23 | import { CustomMarkupComponent } from './peertube-custom-tags/shared' | ||
22 | 24 | ||
23 | type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<any> | 25 | type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<CustomMarkupComponent> |
24 | type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement | 26 | type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement |
25 | 27 | ||
26 | @Injectable() | 28 | @Injectable() |
@@ -45,7 +47,10 @@ export class CustomMarkupService { | |||
45 | private dynamicElementService: DynamicElementService, | 47 | private dynamicElementService: DynamicElementService, |
46 | private markdown: MarkdownService | 48 | private markdown: MarkdownService |
47 | ) { | 49 | ) { |
48 | this.customMarkdownRenderer = async (text: string) => this.buildElement(text) | 50 | this.customMarkdownRenderer = (text: string) => { |
51 | return this.buildElement(text) | ||
52 | .then(({ rootElement }) => rootElement) | ||
53 | } | ||
49 | } | 54 | } |
50 | 55 | ||
51 | getCustomMarkdownRenderer () { | 56 | getCustomMarkdownRenderer () { |
@@ -60,23 +65,30 @@ export class CustomMarkupService { | |||
60 | 65 | ||
61 | for (const selector of Object.keys(this.htmlBuilders)) { | 66 | for (const selector of Object.keys(this.htmlBuilders)) { |
62 | rootElement.querySelectorAll(selector) | 67 | rootElement.querySelectorAll(selector) |
63 | .forEach((e: HTMLElement) => { | 68 | .forEach((e: HTMLElement) => { |
64 | try { | 69 | try { |
65 | const element = this.execHTMLBuilder(selector, e) | 70 | const element = this.execHTMLBuilder(selector, e) |
66 | // Insert as first child | 71 | // Insert as first child |
67 | e.insertBefore(element, e.firstChild) | 72 | e.insertBefore(element, e.firstChild) |
68 | } catch (err) { | 73 | } catch (err) { |
69 | console.error('Cannot inject component %s.', selector, err) | 74 | console.error('Cannot inject component %s.', selector, err) |
70 | } | 75 | } |
71 | }) | 76 | }) |
72 | } | 77 | } |
73 | 78 | ||
79 | const loadedPromises: Promise<boolean>[] = [] | ||
80 | |||
74 | for (const selector of Object.keys(this.angularBuilders)) { | 81 | for (const selector of Object.keys(this.angularBuilders)) { |
75 | rootElement.querySelectorAll(selector) | 82 | rootElement.querySelectorAll(selector) |
76 | .forEach((e: HTMLElement) => { | 83 | .forEach((e: HTMLElement) => { |
77 | try { | 84 | try { |
78 | const component = this.execAngularBuilder(selector, e) | 85 | const component = this.execAngularBuilder(selector, e) |
79 | 86 | ||
87 | if (component.instance.loaded) { | ||
88 | const p = component.instance.loaded.pipe(first()).toPromise() | ||
89 | loadedPromises.push(p) | ||
90 | } | ||
91 | |||
80 | this.dynamicElementService.injectElement(e, component) | 92 | this.dynamicElementService.injectElement(e, component) |
81 | } catch (err) { | 93 | } catch (err) { |
82 | console.error('Cannot inject component %s.', selector, err) | 94 | console.error('Cannot inject component %s.', selector, err) |
@@ -84,7 +96,7 @@ export class CustomMarkupService { | |||
84 | }) | 96 | }) |
85 | } | 97 | } |
86 | 98 | ||
87 | return rootElement | 99 | return { rootElement, componentsLoaded: Promise.all(loadedPromises) } |
88 | } | 100 | } |
89 | 101 | ||
90 | private getSupportedTags () { | 102 | private getSupportedTags () { |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts index 987b37d19..1af060548 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { VideoChannel } from '../../shared-main' | 2 | import { VideoChannel } from '../../shared-main' |
3 | import { CustomMarkupComponent } from './shared' | ||
3 | 4 | ||
4 | /* | 5 | /* |
5 | * Markup component that creates a button | 6 | * Markup component that creates a button |
@@ -10,13 +11,14 @@ import { VideoChannel } from '../../shared-main' | |||
10 | templateUrl: 'button-markup.component.html', | 11 | templateUrl: 'button-markup.component.html', |
11 | styleUrls: [ 'button-markup.component.scss' ] | 12 | styleUrls: [ 'button-markup.component.scss' ] |
12 | }) | 13 | }) |
13 | export class ButtonMarkupComponent { | 14 | export class ButtonMarkupComponent implements CustomMarkupComponent { |
14 | @Input() theme: 'primary' | 'secondary' | 15 | @Input() theme: 'primary' | 'secondary' |
15 | @Input() href: string | 16 | @Input() href: string |
16 | @Input() label: string | 17 | @Input() label: string |
17 | @Input() blankTarget?: boolean | 18 | @Input() blankTarget?: boolean |
18 | 19 | ||
19 | channel: VideoChannel | 20 | channel: VideoChannel |
21 | loaded: undefined | ||
20 | 22 | ||
21 | getTarget () { | 23 | getTarget () { |
22 | if (this.blankTarget === true) return '_blank' | 24 | if (this.blankTarget === true) return '_blank' |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts index 87caec8a5..a91debbef 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { map, switchMap } from 'rxjs/operators' | 1 | import { map, switchMap } from 'rxjs/operators' |
2 | import { Component, Input, OnInit } from '@angular/core' | 2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { MarkdownService, UserService } from '@app/core' | 3 | import { MarkdownService, UserService } from '@app/core' |
4 | import { Video, VideoSortField } from '@shared/models/videos' | 4 | import { Video, VideoSortField } from '@shared/models/videos' |
5 | import { VideoChannel, VideoChannelService, VideoService } from '../../shared-main' | 5 | import { VideoChannel, VideoChannelService, VideoService } from '../../shared-main' |
6 | import { CustomMarkupComponent } from './shared' | ||
6 | 7 | ||
7 | /* | 8 | /* |
8 | * Markup component that creates a channel miniature only | 9 | * Markup component that creates a channel miniature only |
@@ -13,11 +14,13 @@ import { VideoChannel, VideoChannelService, VideoService } from '../../shared-ma | |||
13 | templateUrl: 'channel-miniature-markup.component.html', | 14 | templateUrl: 'channel-miniature-markup.component.html', |
14 | styleUrls: [ 'channel-miniature-markup.component.scss' ] | 15 | styleUrls: [ 'channel-miniature-markup.component.scss' ] |
15 | }) | 16 | }) |
16 | export class ChannelMiniatureMarkupComponent implements OnInit { | 17 | export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
17 | @Input() name: string | 18 | @Input() name: string |
18 | @Input() displayLatestVideo: boolean | 19 | @Input() displayLatestVideo: boolean |
19 | @Input() displayDescription: boolean | 20 | @Input() displayDescription: boolean |
20 | 21 | ||
22 | @Output() loaded = new EventEmitter<boolean>() | ||
23 | |||
21 | channel: VideoChannel | 24 | channel: VideoChannel |
22 | descriptionHTML: string | 25 | descriptionHTML: string |
23 | totalVideos: number | 26 | totalVideos: number |
@@ -61,9 +64,13 @@ export class ChannelMiniatureMarkupComponent implements OnInit { | |||
61 | map(user => user.nsfwPolicy), | 64 | map(user => user.nsfwPolicy), |
62 | switchMap(nsfwPolicy => this.videoService.getVideoChannelVideos({ ...videoOptions, nsfwPolicy })) | 65 | switchMap(nsfwPolicy => this.videoService.getVideoChannelVideos({ ...videoOptions, nsfwPolicy })) |
63 | ) | 66 | ) |
64 | .subscribe(({ total, data }) => { | 67 | .subscribe({ |
65 | this.totalVideos = total | 68 | next: ({ total, data }) => { |
66 | this.video = data[0] | 69 | this.totalVideos = total |
70 | this.video = data[0] | ||
71 | }, | ||
72 | |||
73 | complete: () => this.loaded.emit(true) | ||
67 | }) | 74 | }) |
68 | } | 75 | } |
69 | } | 76 | } |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts index a854d89f6..4462903db 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/embed-markup.component.ts | |||
@@ -1,15 +1,18 @@ | |||
1 | import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' | 1 | import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' |
2 | import { environment } from 'src/environments/environment' | 2 | import { environment } from 'src/environments/environment' |
3 | import { Component, ElementRef, Input, OnInit } from '@angular/core' | 3 | import { Component, ElementRef, Input, OnInit } from '@angular/core' |
4 | import { CustomMarkupComponent } from './shared' | ||
4 | 5 | ||
5 | @Component({ | 6 | @Component({ |
6 | selector: 'my-embed-markup', | 7 | selector: 'my-embed-markup', |
7 | template: '' | 8 | template: '' |
8 | }) | 9 | }) |
9 | export class EmbedMarkupComponent implements OnInit { | 10 | export class EmbedMarkupComponent implements CustomMarkupComponent, OnInit { |
10 | @Input() uuid: string | 11 | @Input() uuid: string |
11 | @Input() type: 'video' | 'playlist' = 'video' | 12 | @Input() type: 'video' | 'playlist' = 'video' |
12 | 13 | ||
14 | loaded: undefined | ||
15 | |||
13 | constructor (private el: ElementRef) { } | 16 | constructor (private el: ElementRef) { } |
14 | 17 | ||
15 | ngOnInit () { | 18 | ngOnInit () { |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts index eddc3636e..42a42d711 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' | 2 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' |
3 | import { VideoPlaylist, VideoPlaylistService } from '../../shared-video-playlist' | 3 | import { VideoPlaylist, VideoPlaylistService } from '../../shared-video-playlist' |
4 | import { CustomMarkupComponent } from './shared' | ||
4 | 5 | ||
5 | /* | 6 | /* |
6 | * Markup component that creates a playlist miniature only | 7 | * Markup component that creates a playlist miniature only |
@@ -11,9 +12,11 @@ import { VideoPlaylist, VideoPlaylistService } from '../../shared-video-playlist | |||
11 | templateUrl: 'playlist-miniature-markup.component.html', | 12 | templateUrl: 'playlist-miniature-markup.component.html', |
12 | styleUrls: [ 'playlist-miniature-markup.component.scss' ] | 13 | styleUrls: [ 'playlist-miniature-markup.component.scss' ] |
13 | }) | 14 | }) |
14 | export class PlaylistMiniatureMarkupComponent implements OnInit { | 15 | export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
15 | @Input() uuid: string | 16 | @Input() uuid: string |
16 | 17 | ||
18 | @Output() loaded = new EventEmitter<boolean>() | ||
19 | |||
17 | playlist: VideoPlaylist | 20 | playlist: VideoPlaylist |
18 | 21 | ||
19 | displayOptions: MiniatureDisplayOptions = { | 22 | displayOptions: MiniatureDisplayOptions = { |
@@ -33,6 +36,10 @@ export class PlaylistMiniatureMarkupComponent implements OnInit { | |||
33 | 36 | ||
34 | ngOnInit () { | 37 | ngOnInit () { |
35 | this.playlistService.getVideoPlaylist(this.uuid) | 38 | this.playlistService.getVideoPlaylist(this.uuid) |
36 | .subscribe(playlist => this.playlist = playlist) | 39 | .subscribe({ |
40 | next: playlist => this.playlist = playlist, | ||
41 | |||
42 | complete: () => this.loaded.emit(true) | ||
43 | }) | ||
37 | } | 44 | } |
38 | } | 45 | } |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/shared/custom-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/shared/custom-markup.component.ts new file mode 100644 index 000000000..adfd48c55 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/shared/custom-markup.component.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | import { EventEmitter } from '@angular/core' | ||
2 | |||
3 | export interface CustomMarkupComponent { | ||
4 | loaded: EventEmitter<boolean> | undefined | ||
5 | } | ||
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/shared/index.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/shared/index.ts new file mode 100644 index 000000000..a6a7bbd92 --- /dev/null +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './custom-markup.component' | |||
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts index dfb4c497f..6ee5123e0 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { AuthService } from '@app/core' | 2 | import { AuthService } from '@app/core' |
3 | import { Video, VideoService } from '../../shared-main' | 3 | import { Video, VideoService } from '../../shared-main' |
4 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' | 4 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' |
5 | import { CustomMarkupComponent } from './shared' | ||
5 | 6 | ||
6 | /* | 7 | /* |
7 | * Markup component that creates a video miniature only | 8 | * Markup component that creates a video miniature only |
@@ -12,10 +13,12 @@ import { MiniatureDisplayOptions } from '../../shared-video-miniature' | |||
12 | templateUrl: 'video-miniature-markup.component.html', | 13 | templateUrl: 'video-miniature-markup.component.html', |
13 | styleUrls: [ 'video-miniature-markup.component.scss' ] | 14 | styleUrls: [ 'video-miniature-markup.component.scss' ] |
14 | }) | 15 | }) |
15 | export class VideoMiniatureMarkupComponent implements OnInit { | 16 | export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
16 | @Input() uuid: string | 17 | @Input() uuid: string |
17 | @Input() onlyDisplayTitle: boolean | 18 | @Input() onlyDisplayTitle: boolean |
18 | 19 | ||
20 | @Output() loaded = new EventEmitter<boolean>() | ||
21 | |||
19 | video: Video | 22 | video: Video |
20 | 23 | ||
21 | displayOptions: MiniatureDisplayOptions = { | 24 | displayOptions: MiniatureDisplayOptions = { |
@@ -46,6 +49,10 @@ export class VideoMiniatureMarkupComponent implements OnInit { | |||
46 | } | 49 | } |
47 | 50 | ||
48 | this.videoService.getVideo({ videoId: this.uuid }) | 51 | this.videoService.getVideo({ videoId: this.uuid }) |
49 | .subscribe(video => this.video = video) | 52 | .subscribe({ |
53 | next: video => this.video = video, | ||
54 | |||
55 | complete: () => this.loaded.emit(true) | ||
56 | }) | ||
50 | } | 57 | } |
51 | } | 58 | } |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts index d4402dd9f..02738022e 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { AuthService } from '@app/core' | 2 | import { AuthService } from '@app/core' |
3 | import { VideoFilter, VideoSortField } from '@shared/models' | 3 | import { VideoFilter, VideoSortField } from '@shared/models' |
4 | import { Video, VideoService } from '../../shared-main' | 4 | import { Video, VideoService } from '../../shared-main' |
5 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' | 5 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' |
6 | import { CustomMarkupComponent } from './shared' | ||
6 | 7 | ||
7 | /* | 8 | /* |
8 | * Markup component list videos depending on criterias | 9 | * Markup component list videos depending on criterias |
@@ -13,7 +14,7 @@ import { MiniatureDisplayOptions } from '../../shared-video-miniature' | |||
13 | templateUrl: 'videos-list-markup.component.html', | 14 | templateUrl: 'videos-list-markup.component.html', |
14 | styleUrls: [ 'videos-list-markup.component.scss' ] | 15 | styleUrls: [ 'videos-list-markup.component.scss' ] |
15 | }) | 16 | }) |
16 | export class VideosListMarkupComponent implements OnInit { | 17 | export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit { |
17 | @Input() sort: string | 18 | @Input() sort: string |
18 | @Input() categoryOneOf: number[] | 19 | @Input() categoryOneOf: number[] |
19 | @Input() languageOneOf: string[] | 20 | @Input() languageOneOf: string[] |
@@ -22,6 +23,8 @@ export class VideosListMarkupComponent implements OnInit { | |||
22 | @Input() filter: VideoFilter | 23 | @Input() filter: VideoFilter |
23 | @Input() maxRows: number | 24 | @Input() maxRows: number |
24 | 25 | ||
26 | @Output() loaded = new EventEmitter<boolean>() | ||
27 | |||
25 | videos: Video[] | 28 | videos: Video[] |
26 | 29 | ||
27 | displayOptions: MiniatureDisplayOptions = { | 30 | displayOptions: MiniatureDisplayOptions = { |
@@ -73,6 +76,10 @@ export class VideosListMarkupComponent implements OnInit { | |||
73 | } | 76 | } |
74 | 77 | ||
75 | this.videoService.getVideos(options) | 78 | this.videoService.getVideos(options) |
76 | .subscribe(({ data }) => this.videos = data) | 79 | .subscribe({ |
80 | next: ({ data }) => this.videos = data, | ||
81 | |||
82 | complete: () => this.loaded.emit(true) | ||
83 | }) | ||
77 | } | 84 | } |
78 | } | 85 | } |