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