diff options
Diffstat (limited to 'client/src/app/shared/shared-main/angular')
5 files changed, 170 insertions, 0 deletions
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts new file mode 100644 index 000000000..9851468ee --- /dev/null +++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { Pipe, PipeTransform } from '@angular/core' | ||
2 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
3 | |||
4 | // Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site | ||
5 | @Pipe({ name: 'myFromNow' }) | ||
6 | export class FromNowPipe implements PipeTransform { | ||
7 | |||
8 | constructor (private i18n: I18n) { } | ||
9 | |||
10 | transform (arg: number | Date | string) { | ||
11 | const argDate = new Date(arg) | ||
12 | const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) | ||
13 | |||
14 | let interval = Math.floor(seconds / 31536000) | ||
15 | if (interval > 1) return this.i18n('{{interval}} years ago', { interval }) | ||
16 | if (interval === 1) return this.i18n('{{interval}} year ago', { interval }) | ||
17 | |||
18 | interval = Math.floor(seconds / 2592000) | ||
19 | if (interval > 1) return this.i18n('{{interval}} months ago', { interval }) | ||
20 | if (interval === 1) return this.i18n('{{interval}} month ago', { interval }) | ||
21 | |||
22 | interval = Math.floor(seconds / 604800) | ||
23 | if (interval > 1) return this.i18n('{{interval}} weeks ago', { interval }) | ||
24 | if (interval === 1) return this.i18n('{{interval}} week ago', { interval }) | ||
25 | |||
26 | interval = Math.floor(seconds / 86400) | ||
27 | if (interval > 1) return this.i18n('{{interval}} days ago', { interval }) | ||
28 | if (interval === 1) return this.i18n('{{interval}} day ago', { interval }) | ||
29 | |||
30 | interval = Math.floor(seconds / 3600) | ||
31 | if (interval > 1) return this.i18n('{{interval}} hours ago', { interval }) | ||
32 | if (interval === 1) return this.i18n('{{interval}} hour ago', { interval }) | ||
33 | |||
34 | interval = Math.floor(seconds / 60) | ||
35 | if (interval >= 1) return this.i18n('{{interval}} min ago', { interval }) | ||
36 | |||
37 | return this.i18n('just now') | ||
38 | } | ||
39 | } | ||
diff --git a/client/src/app/shared/shared-main/angular/index.ts b/client/src/app/shared/shared-main/angular/index.ts new file mode 100644 index 000000000..3b072fb84 --- /dev/null +++ b/client/src/app/shared/shared-main/angular/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './from-now.pipe' | ||
2 | export * from './infinite-scroller.directive' | ||
3 | export * from './number-formatter.pipe' | ||
4 | export * from './peertube-template.directive' | ||
diff --git a/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts b/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts new file mode 100644 index 000000000..f09c3d1fc --- /dev/null +++ b/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts | |||
@@ -0,0 +1,96 @@ | |||
1 | import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators' | ||
2 | import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' | ||
3 | import { fromEvent, Observable, Subscription } from 'rxjs' | ||
4 | |||
5 | @Directive({ | ||
6 | selector: '[myInfiniteScroller]' | ||
7 | }) | ||
8 | export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterContentChecked { | ||
9 | @Input() percentLimit = 70 | ||
10 | @Input() autoInit = false | ||
11 | @Input() onItself = false | ||
12 | @Input() dataObservable: Observable<any[]> | ||
13 | |||
14 | @Output() nearOfBottom = new EventEmitter<void>() | ||
15 | |||
16 | private decimalLimit = 0 | ||
17 | private lastCurrentBottom = -1 | ||
18 | private scrollDownSub: Subscription | ||
19 | private container: HTMLElement | ||
20 | |||
21 | private checkScroll = false | ||
22 | |||
23 | constructor (private el: ElementRef) { | ||
24 | this.decimalLimit = this.percentLimit / 100 | ||
25 | } | ||
26 | |||
27 | ngAfterContentChecked () { | ||
28 | if (this.checkScroll) { | ||
29 | this.checkScroll = false | ||
30 | |||
31 | console.log('Checking if the initial state has a scroll.') | ||
32 | |||
33 | if (this.hasScroll() === false) this.nearOfBottom.emit() | ||
34 | } | ||
35 | } | ||
36 | |||
37 | ngOnInit () { | ||
38 | if (this.autoInit === true) return this.initialize() | ||
39 | } | ||
40 | |||
41 | ngOnDestroy () { | ||
42 | if (this.scrollDownSub) this.scrollDownSub.unsubscribe() | ||
43 | } | ||
44 | |||
45 | initialize () { | ||
46 | this.container = this.onItself | ||
47 | ? this.el.nativeElement | ||
48 | : document.documentElement | ||
49 | |||
50 | // Emit the last value | ||
51 | const throttleOptions = { leading: true, trailing: true } | ||
52 | |||
53 | const scrollableElement = this.onItself ? this.container : window | ||
54 | const scrollObservable = fromEvent(scrollableElement, 'scroll') | ||
55 | .pipe( | ||
56 | startWith(true), | ||
57 | throttleTime(200, undefined, throttleOptions), | ||
58 | map(() => this.getScrollInfo()), | ||
59 | distinctUntilChanged((o1, o2) => o1.current === o2.current), | ||
60 | share() | ||
61 | ) | ||
62 | |||
63 | // Scroll Down | ||
64 | this.scrollDownSub = scrollObservable | ||
65 | .pipe( | ||
66 | filter(({ current }) => this.isScrollingDown(current)), | ||
67 | filter(({ current, maximumScroll }) => (current / maximumScroll) > this.decimalLimit) | ||
68 | ) | ||
69 | .subscribe(() => this.nearOfBottom.emit()) | ||
70 | |||
71 | if (this.dataObservable) { | ||
72 | this.dataObservable | ||
73 | .pipe(filter(d => d.length !== 0)) | ||
74 | .subscribe(() => this.checkScroll = true) | ||
75 | } | ||
76 | } | ||
77 | |||
78 | private getScrollInfo () { | ||
79 | return { current: this.container.scrollTop, maximumScroll: this.getMaximumScroll() } | ||
80 | } | ||
81 | |||
82 | private getMaximumScroll () { | ||
83 | return this.container.scrollHeight - window.innerHeight | ||
84 | } | ||
85 | |||
86 | private hasScroll () { | ||
87 | return this.getMaximumScroll() > 0 | ||
88 | } | ||
89 | |||
90 | private isScrollingDown (current: number) { | ||
91 | const result = this.lastCurrentBottom < current | ||
92 | |||
93 | this.lastCurrentBottom = current | ||
94 | return result | ||
95 | } | ||
96 | } | ||
diff --git a/client/src/app/shared/shared-main/angular/number-formatter.pipe.ts b/client/src/app/shared/shared-main/angular/number-formatter.pipe.ts new file mode 100644 index 000000000..8a0756a36 --- /dev/null +++ b/client/src/app/shared/shared-main/angular/number-formatter.pipe.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { Pipe, PipeTransform } from '@angular/core' | ||
2 | |||
3 | // Thanks: https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts | ||
4 | |||
5 | @Pipe({ name: 'myNumberFormatter' }) | ||
6 | export class NumberFormatterPipe implements PipeTransform { | ||
7 | private dictionary: Array<{max: number, type: string}> = [ | ||
8 | { max: 1000, type: '' }, | ||
9 | { max: 1000000, type: 'K' }, | ||
10 | { max: 1000000000, type: 'M' } | ||
11 | ] | ||
12 | |||
13 | transform (value: number) { | ||
14 | const format = this.dictionary.find(d => value < d.max) || this.dictionary[this.dictionary.length - 1] | ||
15 | const calc = Math.floor(value / (format.max / 1000)) | ||
16 | |||
17 | return `${calc}${format.type}` | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/shared/shared-main/angular/peertube-template.directive.ts b/client/src/app/shared/shared-main/angular/peertube-template.directive.ts new file mode 100644 index 000000000..e04c25d9a --- /dev/null +++ b/client/src/app/shared/shared-main/angular/peertube-template.directive.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { Directive, Input, TemplateRef } from '@angular/core' | ||
2 | |||
3 | @Directive({ | ||
4 | selector: '[ptTemplate]' | ||
5 | }) | ||
6 | export class PeerTubeTemplateDirective <T extends string> { | ||
7 | @Input('ptTemplate') name: T | ||
8 | |||
9 | constructor (public template: TemplateRef<any>) { | ||
10 | // empty | ||
11 | } | ||
12 | } | ||