ui service, background images transition
@ -1,7 +1,8 @@
|
|||||||
import { Component, OnInit, AfterViewInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Router, RouterEvent } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { LocalStorage, LocalStorageService } from 'ngx-webstorage'
|
import { LocalStorage } from 'ngx-webstorage'
|
||||||
import { GoWebViewInit } from './services/go';
|
import { GoWebViewInit } from './services/go';
|
||||||
|
import { UiService } from './services/ui.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -12,7 +13,10 @@ import { GoWebViewInit } from './services/go';
|
|||||||
export class AppComponent implements OnInit{
|
export class AppComponent implements OnInit{
|
||||||
title = 'go-web';
|
title = 'go-web';
|
||||||
|
|
||||||
constructor(private router: Router, private storage: LocalStorageService) { }
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private ui: UiService
|
||||||
|
) { }
|
||||||
|
|
||||||
@LocalStorage('theme', 'dark')
|
@LocalStorage('theme', 'dark')
|
||||||
public theme!: string
|
public theme!: string
|
||||||
@ -23,17 +27,11 @@ export class AppComponent implements OnInit{
|
|||||||
console.log("Navigating to route:" + event.detail)
|
console.log("Navigating to route:" + event.detail)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setTheme(this.theme)
|
this.ui.setTheme(this.theme)
|
||||||
this.storage.observe('theme').subscribe(value => {
|
this.ui.detectThemeChange()
|
||||||
this.setTheme(this.theme)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(){
|
ngAfterViewInit(){
|
||||||
GoWebViewInit()
|
GoWebViewInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
setTheme (theme: string) {
|
|
||||||
document.querySelector('html')?.setAttribute('data-theme', theme)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
backdrop-filter: blur(64px);
|
backdrop-filter: blur(64px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
z-index: 20;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.menu-search-panel {
|
.menu-search-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<div class="main-wrapper" [style.background-image]="'url(' + image + ')'">
|
<div class="main-wrapper">
|
||||||
|
<div class="backdrop-image">
|
||||||
|
</div>
|
||||||
<app-main-menu></app-main-menu>
|
<app-main-menu></app-main-menu>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
@ -5,27 +5,54 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
img {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 50%;
|
|
||||||
translate: -50% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
position: inherit;
|
position: inherit;
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: var(--sk-right-content);
|
background-color: var(--sk-right-content);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
backdrop-filter: blur(2rem);
|
backdrop-filter: blur(2rem);
|
||||||
// overflow-y: auto;
|
// overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host::ng-deep {
|
||||||
|
|
||||||
|
.backdrop-image {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--sk-background);
|
||||||
|
z-index: 0;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
.layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
z-index: 10;
|
||||||
|
transition: opacity 3s linear;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, ElementRef, inject, OnInit } from '@angular/core';
|
||||||
|
import { asapScheduler, asyncScheduler, of, queueScheduler, scheduled, Subscription } from 'rxjs';
|
||||||
|
import { UiService } from 'src/app/services/ui.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-main-root',
|
selector: 'app-main-root',
|
||||||
@ -6,9 +8,91 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
styleUrls: ['./main-root.component.scss'],
|
styleUrls: ['./main-root.component.scss'],
|
||||||
})
|
})
|
||||||
export class MainRootComponent implements OnInit {
|
export class MainRootComponent implements OnInit {
|
||||||
image: string = 'assets/games/minecraft/backgrounds/7.webp';
|
image: string = 'assets/games/minecraft/backgrounds/compressed/7.webp';
|
||||||
|
uiService = inject(UiService)
|
||||||
|
subs: Map<string, Subscription> = new Map()
|
||||||
|
timer: number = 0
|
||||||
|
|
||||||
constructor() {}
|
queue: string[] = []
|
||||||
|
transitionEnded: boolean = true
|
||||||
|
|
||||||
ngOnInit(): void {}
|
constructor(
|
||||||
|
private element: ElementRef<HTMLElement>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.observeImageChange()
|
||||||
|
this.setImage(this.image, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
//Called once, before the instance is destroyed.
|
||||||
|
//Add 'implements OnDestroy' to the class.
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe())
|
||||||
|
}
|
||||||
|
|
||||||
|
observeImageChange() {
|
||||||
|
this.subs.set(
|
||||||
|
'image-change',
|
||||||
|
this.uiService.imageChange.subscribe({
|
||||||
|
next: (url: string) => {
|
||||||
|
this.queueImageChange(url)
|
||||||
|
// this.setImage(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
queueImageChange(url: string) {
|
||||||
|
if (this.queue.length > 0 || !this.transitionEnded) {
|
||||||
|
this.queue.push(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setImage(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
if (!this.queue.length) return
|
||||||
|
|
||||||
|
this.setImage(this.queue.splice(0, 1)[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(url: string, init = false) {
|
||||||
|
if (!init)
|
||||||
|
this.transitionEnded = false
|
||||||
|
|
||||||
|
this.image = url
|
||||||
|
|
||||||
|
let wrapper = this.element.nativeElement.querySelector<HTMLElement>('.backdrop-image')
|
||||||
|
if (!wrapper) return
|
||||||
|
|
||||||
|
const image = document.createElement('img')
|
||||||
|
image.onload = () => {
|
||||||
|
if (!wrapper || init) return
|
||||||
|
|
||||||
|
let current = wrapper.querySelector('.layer:not(.secondary)'), secondary = wrapper.querySelectorAll('.layer.secondary')
|
||||||
|
|
||||||
|
current?.classList.add('hidden')
|
||||||
|
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
this.timer = window.setTimeout(() => {
|
||||||
|
secondary.forEach(value => value.classList.remove('secondary'))
|
||||||
|
current?.remove()
|
||||||
|
this.transitionEnded = true
|
||||||
|
|
||||||
|
this.next()
|
||||||
|
}, 3200)
|
||||||
|
}
|
||||||
|
image.src = url
|
||||||
|
|
||||||
|
const layer = document.createElement('div')
|
||||||
|
layer.classList.add('layer')
|
||||||
|
|
||||||
|
if (!init)
|
||||||
|
layer.classList.add('secondary')
|
||||||
|
|
||||||
|
layer.style.backgroundImage = `url(${url})`
|
||||||
|
wrapper.append(layer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #field let-field="field">
|
<ng-template #field let-field="field">
|
||||||
<div class="field">
|
<div class="field" [ngClass]="{'filled': field.large}">
|
||||||
<skirda-subtitle>{{field.label}}</skirda-subtitle>
|
<skirda-subtitle>{{field.label}}</skirda-subtitle>
|
||||||
<skirda-text *ngIf="!field.large">{{field.value}}</skirda-text>
|
<skirda-text *ngIf="!field.large">{{field.value}}</skirda-text>
|
||||||
<skirda-heading size="4" *ngIf="field.large">{{field.value}}</skirda-heading>
|
<skirda-heading size="4" *ngIf="field.large">{{field.value}}</skirda-heading>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--sk-gap-xxxl);
|
gap: var(--sk-gap-m);
|
||||||
padding-bottom: var(--sk-gap-xxxl);
|
padding-bottom: var(--sk-gap-xxxl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,6 +16,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--sk-gap-s);
|
gap: var(--sk-gap-s);
|
||||||
|
|
||||||
|
&.filled {
|
||||||
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
skirda-text {
|
skirda-text {
|
||||||
line-height: 140%;
|
line-height: 140%;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { OnDestroy, TemplateRef, ViewChild } from '@angular/core';
|
import { inject, OnDestroy, TemplateRef, ViewChild } from '@angular/core';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
import { PopupConfiguration } from 'projects/ui/src/lib/popup/popup.class';
|
import { PopupConfiguration } from 'projects/ui/src/lib/popup/popup.class';
|
||||||
import { PopupDirective } from 'projects/ui/src/lib/popup/popup.directive';
|
import { PopupDirective } from 'projects/ui/src/lib/popup/popup.directive';
|
||||||
import { PopupService } from 'projects/ui/src/lib/popup/popup.service';
|
import { PopupService } from 'projects/ui/src/lib/popup/popup.service';
|
||||||
import { first, Subscription } from 'rxjs';
|
import { first, Subscription } from 'rxjs';
|
||||||
import { Game } from 'src/app/interfaces/game.interface';
|
import { Game } from 'src/app/interfaces/game.interface';
|
||||||
|
import { UiService } from 'src/app/services/ui.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-game-page',
|
selector: 'app-game-page',
|
||||||
@ -13,6 +15,7 @@ import { Game } from 'src/app/interfaces/game.interface';
|
|||||||
})
|
})
|
||||||
export class GamePageComponent implements OnInit, OnDestroy {
|
export class GamePageComponent implements OnInit, OnDestroy {
|
||||||
// @ViewChild(PopupDirective, { static: true }) popupPortal!: PopupDirective;
|
// @ViewChild(PopupDirective, { static: true }) popupPortal!: PopupDirective;
|
||||||
|
uiService = inject(UiService)
|
||||||
subs: Map<string, Subscription> = new Map();
|
subs: Map<string, Subscription> = new Map();
|
||||||
game: Game = {
|
game: Game = {
|
||||||
gameId: 'minecraft',
|
gameId: 'minecraft',
|
||||||
@ -24,14 +27,43 @@ export class GamePageComponent implements OnInit, OnDestroy {
|
|||||||
currentSection: string = 'overview';
|
currentSection: string = 'overview';
|
||||||
friends: string[] = ['svensken', 'cyberdream', 'e11te'];
|
friends: string[] = ['svensken', 'cyberdream', 'e11te'];
|
||||||
|
|
||||||
constructor() {} // private popup: PopupService
|
tempImages = [
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/0.webp',
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/1.webp',
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/2.webp',
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/3.webp',
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/4.webp',
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/5.webp',
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/6.webp',
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/7.webp',
|
||||||
|
'assets/games/minecraft/backgrounds/compressed/8.webp',
|
||||||
|
]
|
||||||
|
|
||||||
ngOnInit(): void {}
|
constructor(
|
||||||
|
private router: Router
|
||||||
|
) {} // private popup: PopupService
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.observeNavigation()
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subs.forEach((value) => value.unsubscribe());
|
this.subs.forEach((value) => value.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeNavigation() {
|
||||||
|
this.router.events.subscribe({
|
||||||
|
next: (event) => {
|
||||||
|
if (event instanceof NavigationEnd)
|
||||||
|
this.uiService.setImage(this.getRandomImage())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomImage() {
|
||||||
|
return this.tempImages[Math.floor(0 + Math.random() * ((this.tempImages.length - 1) + 1 - 0))]
|
||||||
|
}
|
||||||
|
|
||||||
// openPopup(template?: TemplateRef<any>) {
|
// openPopup(template?: TemplateRef<any>) {
|
||||||
// this.subs.set(
|
// this.subs.set(
|
||||||
// 'popup-events',
|
// 'popup-events',
|
||||||
|
16
src/app/services/ui.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UiService } from './ui.service';
|
||||||
|
|
||||||
|
describe('UiService', () => {
|
||||||
|
let service: UiService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(UiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
27
src/app/services/ui.service.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { EventEmitter, inject, Injectable, Output } from '@angular/core';
|
||||||
|
import { LocalStorageService } from 'ngx-webstorage';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UiService {
|
||||||
|
storage = inject(LocalStorageService)
|
||||||
|
|
||||||
|
@Output() imageChange: EventEmitter<string> = new EventEmitter()
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
setTheme (theme: string) {
|
||||||
|
document.querySelector('html')?.setAttribute('data-theme', theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
detectThemeChange() {
|
||||||
|
this.storage.observe('theme').subscribe(value => {
|
||||||
|
this.setTheme(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(url: string) {
|
||||||
|
this.imageChange.emit(url)
|
||||||
|
}
|
||||||
|
}
|
BIN
src/assets/games/minecraft/backgrounds/compressed/0.webp
Normal file
After Width: | Height: | Size: 892 KiB |
BIN
src/assets/games/minecraft/backgrounds/compressed/1.webp
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
src/assets/games/minecraft/backgrounds/compressed/2.webp
Normal file
After Width: | Height: | Size: 232 KiB |
BIN
src/assets/games/minecraft/backgrounds/compressed/3.webp
Normal file
After Width: | Height: | Size: 254 KiB |
BIN
src/assets/games/minecraft/backgrounds/compressed/4.webp
Normal file
After Width: | Height: | Size: 188 KiB |
BIN
src/assets/games/minecraft/backgrounds/compressed/5.webp
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
src/assets/games/minecraft/backgrounds/compressed/6.webp
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
src/assets/games/minecraft/backgrounds/compressed/7.webp
Normal file
After Width: | Height: | Size: 264 KiB |
BIN
src/assets/games/minecraft/backgrounds/compressed/8.webp
Normal file
After Width: | Height: | Size: 239 KiB |
@ -1,6 +1,6 @@
|
|||||||
@use 'src/light';
|
@use 'src/light';
|
||||||
@use 'src/dark';
|
@use 'src/dark';
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700;800;900&display=swap');
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
background-color: var(--sk-background);
|
background-color: var(--sk-background);
|
||||||
@ -9,10 +9,12 @@ html, body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
letter-spacing: 0.04rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input, textarea {
|
button, input, textarea {
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
|
letter-spacing: 0.04rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@ -36,12 +38,15 @@ html[data-theme="dark"]:root {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--font: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
--font: 'Inter Tight', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Works on Firefox */
|
/* Works on Firefox */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
// cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
|