mirror of https://github.com/ghostfolio/ghostfolio
16 changed files with 205 additions and 83 deletions
@ -0,0 +1,44 @@ |
|||||
|
:host { |
||||
|
display: inline-flex; |
||||
|
} |
||||
|
|
||||
|
.dot { |
||||
|
background-color: var(--bs-success, #198754); |
||||
|
border-radius: 50%; |
||||
|
display: inline-block; |
||||
|
height: 0.5rem; |
||||
|
opacity: 0.65; |
||||
|
position: relative; |
||||
|
width: 0.5rem; |
||||
|
|
||||
|
&::after { |
||||
|
border: 2px solid var(--bs-success, #198754); |
||||
|
border-radius: 50%; |
||||
|
content: ''; |
||||
|
inset: -2px; |
||||
|
opacity: 0; |
||||
|
position: absolute; |
||||
|
} |
||||
|
|
||||
|
&.pulse::after { |
||||
|
animation: gf-live-pulse 1.2s ease-out; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes gf-live-pulse { |
||||
|
from { |
||||
|
opacity: 0.8; |
||||
|
transform: scale(1); |
||||
|
} |
||||
|
|
||||
|
to { |
||||
|
opacity: 0; |
||||
|
transform: scale(2.6); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@media (prefers-reduced-motion: reduce) { |
||||
|
.dot.pulse::after { |
||||
|
animation: none; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
import { AutoRefreshService } from '@ghostfolio/client/services/auto-refresh/auto-refresh.service'; |
||||
|
|
||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
Component, |
||||
|
computed, |
||||
|
effect, |
||||
|
ElementRef, |
||||
|
inject, |
||||
|
viewChild |
||||
|
} from '@angular/core'; |
||||
|
|
||||
|
/** |
||||
|
* A small dot which pulses each time the numbers of the page have been |
||||
|
* refreshed in the background. The title shows the time of the last refresh. |
||||
|
*/ |
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
selector: 'gf-live-indicator', |
||||
|
styleUrls: ['./live-indicator.component.scss'], |
||||
|
template: ` |
||||
|
@if (lastUpdated()) { |
||||
|
<span |
||||
|
class="align-items-center d-flex" |
||||
|
role="status" |
||||
|
[attr.aria-label]="title()" |
||||
|
[title]="title()" |
||||
|
> |
||||
|
<span #dot class="dot"></span> |
||||
|
</span> |
||||
|
} |
||||
|
` |
||||
|
}) |
||||
|
export class GfLiveIndicatorComponent { |
||||
|
protected readonly lastUpdated = inject(AutoRefreshService).lastUpdated; |
||||
|
|
||||
|
protected readonly title = computed(() => { |
||||
|
const lastUpdated = this.lastUpdated(); |
||||
|
|
||||
|
return lastUpdated |
||||
|
? $localize`Live` + |
||||
|
' · ' + |
||||
|
$localize`Updated` + |
||||
|
' ' + |
||||
|
lastUpdated.toLocaleTimeString() |
||||
|
: ''; |
||||
|
}); |
||||
|
|
||||
|
private readonly dot = viewChild<ElementRef<HTMLElement>>('dot'); |
||||
|
|
||||
|
public constructor() { |
||||
|
effect(() => { |
||||
|
this.lastUpdated(); |
||||
|
|
||||
|
const element = this.dot()?.nativeElement; |
||||
|
|
||||
|
if (element) { |
||||
|
element.classList.remove('pulse'); |
||||
|
|
||||
|
// Force a reflow to restart the animation
|
||||
|
void element.offsetWidth; |
||||
|
|
||||
|
element.classList.add('pulse'); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue