+
+
+ 0?'volume-high':'volume-high-outline'">
+
+
{{numberOfPlayingClients +"/"+ totalClients}} Client(s) playing
-
+
{{lastServerResponseDeltaInSeconds < 240?'Online':'Offline'}}: {{lastServerResponseTime |
date: 'dd.MM.yy HH:mm' }}
-
+
0?'musical-note':'musical-note-outline'">
@@ -57,12 +56,12 @@
-
+
-
+
@@ -91,8 +90,8 @@
-
-
+
diff --git a/src/app/pages/dashboard/dashboard.page.scss b/src/app/pages/dashboard/dashboard.page.scss
index b48bb0c..bd6dc29 100644
--- a/src/app/pages/dashboard/dashboard.page.scss
+++ b/src/app/pages/dashboard/dashboard.page.scss
@@ -43,4 +43,11 @@
font-size: 8px;
width: 80%;
text-align: center;
+ }
+
+ .fab-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
}
\ No newline at end of file
diff --git a/src/app/pages/dashboard/dashboard.page.ts b/src/app/pages/dashboard/dashboard.page.ts
index 2627a78..60f2612 100644
--- a/src/app/pages/dashboard/dashboard.page.ts
+++ b/src/app/pages/dashboard/dashboard.page.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, HostListener, OnInit } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { first, firstValueFrom, interval, Observable } from 'rxjs';
import { Client, Group, ServerDetail, SnapCastServerStatusResponse, Stream } from 'src/app/model/snapcast.model';
@@ -8,6 +8,8 @@ import { environment } from 'src/environments/environment';
import { omit } from 'lodash-es';
import { Preferences } from '@capacitor/preferences';
import { UserPreference } from 'src/app/enum/user-preference.enum';
+import { Speaker } from 'src/app/model/speaker.model';
+import { HttpClient } from '@angular/common/http';
@@ -19,6 +21,31 @@ import { UserPreference } from 'src/app/enum/user-preference.enum';
})
export class DashboardPage implements OnInit {
+ @HostListener('window:resize', ['$event'])
+ getScreenSize(event: any) {
+ this.scrHeight = window.innerHeight;
+ this.scrWidth = window.innerWidth;
+ // console.log(this.scrHeight, this.scrWidth);
+ if (this.scrWidth < 1024) {
+ this.swiperConfig = this.defaultConfigMd
+ } else {
+ this.swiperConfig = this.defaulConfigXl
+ this.swiperConfig.pagination = { clickable: true };
+ }
+ }
+
+ defaultConfigMd: SwiperOptions = {
+ slidesPerView: 1.6,
+ spaceBetween: 10,
+ };
+
+ defaulConfigXl: SwiperOptions = {
+ slidesPerView: 3.2,
+ spaceBetween: 10,
+ }
+ scrHeight: number;
+ scrWidth: number;
+
swiperConfig: SwiperOptions = {
slidesPerView: 1.3,
@@ -42,10 +69,13 @@ export class DashboardPage implements OnInit {
lastServerResponseTime?: Date;
lastServerResponseDeltaInSeconds?: number;
+ speakerData: Speaker[] = [];
+
constructor(
private snapcastService: SnapcastService,
+ private http: HttpClient
) {
// this.groups$ = this.snapcastService.groups$;
// this.streams$ = this.snapcastService.streams$;
@@ -54,6 +84,8 @@ export class DashboardPage implements OnInit {
async ngOnInit() {
// this.snapcastService.connect();
+ this.getScreenSize(null); // Initialize screen size
+ this.loadSpeakerData();
this.userPreferenceServerUrl = await this.getUserPreferenceServerUrl();
this.userPreeferenceUsername = await this.getUserName();
@@ -198,7 +230,19 @@ export class DashboardPage implements OnInit {
this.isLoading = false;
}
-
+ loadSpeakerData(): void {
+ this.http.get<{ speakers: Speaker[] }>('assets/speakers/speakers-data.json').subscribe({
+ next: (response) => {
+ this.speakerData = response.speakers;
+ console.log('Speaker data loaded:', this.speakerData);
+ },
+ error: (error) => {
+ console.error('Error loading speaker data:', error);
+ }
+ });
+ }
+
+
diff --git a/src/app/services/snapcast.service.ts b/src/app/services/snapcast.service.ts
index f892c26..f1bac09 100644
--- a/src/app/services/snapcast.service.ts
+++ b/src/app/services/snapcast.service.ts
@@ -379,6 +379,9 @@ export class SnapcastService implements OnDestroy {
}
+ // GROUP ACTIONS
+
+
setGroupName(groupId: string, name: string): Observable {
@@ -409,16 +412,109 @@ export class SnapcastService implements OnDestroy {
}
+ setGroupClients(groupId: string, clientIds: string[]): Observable {
+ return this.rpc('Group.SetClients', { id: groupId, clients: clientIds }).pipe(
+ map((): void => void 0),
+ catchError(err => {
+ console.error(`SnapcastService: Failed to set clients for group ${groupId}`, err);
+ return throwError(() => err);
+ })
+ );
+ }
+
+ setGroupMute(groupId: string, mute: boolean): Observable {
+ return this.rpc('Group.SetMute', { id: groupId, mute }).pipe(
+ map((): void => void 0),
+ catchError(err => {
+ console.error(`SnapcastService: Failed to set mute for group ${groupId}`, err);
+ return throwError(() => err);
+ })
+ );
+ }
+
+ getGroupStatus(groupId: string): Observable {
+ return this.rpc('Group.GetStatus', { id: groupId }).pipe(
+ map(response => response.result as Group | undefined),
+ catchError(err => {
+ console.error(`SnapcastService: Failed to get status for group ${groupId}`, err);
+ return throwError(() => err);
+ })
+ );
+ }
+
+
+ // SERVER ACTIONS
+
+ getServerStatus(): Observable {
+ return this.rpc('Server.GetStatus').pipe(
+ map(response => response.result),
+ catchError(err => {
+ console.error('SnapcastService: Failed to get server status', err);
+ return throwError(() => err);
+ })
+ );
+ }
+
+ getServerRpcVersion(): Observable {
+ return this.rpc('Server.GetRpcVersion').pipe(
+ map(response => response.result?.version),
+ catchError(err => {
+ console.error('SnapcastService: Failed to get server RPC version', err);
+ return throwError(() => err);
+ })
+ );
+
+ }
+ deleteServerClient(clientId: string): Observable {
+ return this.rpc('Server.DeleteClient', { id: clientId }).pipe(
+ map((): void => void 0),
+ catchError(err => {
+ console.error(`SnapcastService: Failed to delete client ${clientId}`, err);
+ return throwError(() => err);
+ })
+ );
+ }
+ // Stream Actions
+ setStreamProperty(streamId: string, property: keyof Stream, value: any): Observable {
+ const params: StreamSetPropertyRpcPayloadParams = { id: streamId, property, value };
+ return this.rpc('Stream.SetProperty', params).pipe(
+ map((): void => void 0),
+ catchError(err => {
+ console.error(`SnapcastService: Failed to set property ${property} for stream ${streamId}`, err);
+ return throwError(() => err);
+ })
+ );
+ }
+ addStream(stream: Stream): Observable {
+ return this.rpc('Stream.Add', { stream }).pipe(
+ map((): void => void 0),
+ catchError(err => {
+ console.error(`SnapcastService: Failed to add stream`, err);
+ return throwError(() => err);
+ })
+ );
+ }
- // --- Data Access Helpers ---
- public getClient(clientId: string): Observable {
- return this.state$.pipe(map(state => state?.server?.groups.flatMap(g => g.clients).find(c => c.id === clientId)));
+ removeStream(streamId: string): Observable {
+ return this.rpc('Stream.Remove', { id: streamId }).pipe(
+ map((): void => void 0),
+ catchError(err => {
+ console.error(`SnapcastService: Failed to remove stream ${streamId}`, err);
+ return throwError(() => err);
+ })
+ );
}
+
+
+
+
+ // Mock server state for testing purposes
+
public mockServerState(): void {
const url = "assets/mock/json/server-state.json"
this.http.get(url).subscribe({
diff --git a/src/app/tabs/tabs.page.html b/src/app/tabs/tabs.page.html
index 47e8b9e..e3a919b 100644
--- a/src/app/tabs/tabs.page.html
+++ b/src/app/tabs/tabs.page.html
@@ -5,8 +5,12 @@
-->
+
+
+
+ [breakpoints]="[0.05,0.25, 0.5, 0.75]" [backdropDismiss]="false" [backdropBreakpoint]="0.5"
+ [cssClass]="'player-modal'" [animated]="true" [keyboardClose]="true" [showBackdrop]="true">
@@ -35,5 +39,5 @@
menu
-
-
\ No newline at end of file
+
+
diff --git a/src/app/tabs/tabs.page.scss b/src/app/tabs/tabs.page.scss
index df9328a..95f8857 100644
--- a/src/app/tabs/tabs.page.scss
+++ b/src/app/tabs/tabs.page.scss
@@ -28,4 +28,8 @@
background: rgba(0,0,0,.3);
z-index: 5;
}
+
+ .page-padding-bottom {
+ margin-bottom: 300px; // Adjust as needed to ensure content is not obscured by the tab bar
+ }
\ No newline at end of file
diff --git a/src/assets/mock/json/server-state.json b/src/assets/mock/json/server-state.json
index 59b5b48..8709163 100644
--- a/src/assets/mock/json/server-state.json
+++ b/src/assets/mock/json/server-state.json
@@ -7,7 +7,7 @@
"config": {
"instance": 1,
"latency": 0,
- "name": "Beans",
+ "name": "sp.1",
"volume": {
"muted": false,
"percent": 100
@@ -37,6 +37,43 @@
"muted": false,
"name": "Office",
"stream_id": "AirPlay"
+ },
+ {
+ "clients": [
+ {
+ "config": {
+ "instance": 1,
+ "latency": 0,
+ "name": "sp.2",
+ "volume": {
+ "muted": false,
+ "percent": 100
+ }
+ },
+ "connected": true,
+ "host": {
+ "arch": "aarch64",
+ "ip": "::ffff:192.168.1.151",
+ "mac": "2c:cf:67:f0:f9:4a",
+ "name": "beatnik-server",
+ "os": "Debian GNU/Linux 12 (bookworm)"
+ },
+ "id": "2c:cf:67:f0:f9:4b",
+ "lastSeen": {
+ "sec": 1752491174,
+ "usec": 663848
+ },
+ "snapclient": {
+ "name": "Snapclient",
+ "protocolVersion": 2,
+ "version": "0.31.0"
+ }
+ }
+ ],
+ "id": "k23057aa-ba82-449a-41cd-12144c04dfd3",
+ "muted": false,
+ "name": "Living Room",
+ "stream_id": "AirPlay"
}
],
"server": {
diff --git a/src/assets/speakers/img/JSE_Infinite_Slope.webp b/src/assets/speakers/img/JSE_Infinite_Slope.webp
new file mode 100644
index 0000000..3aa543f
Binary files /dev/null and b/src/assets/speakers/img/JSE_Infinite_Slope.webp differ
diff --git a/src/assets/speakers/img/KEF_Concerto_1971.webp b/src/assets/speakers/img/KEF_Concerto_1971.webp
new file mode 100644
index 0000000..0c6be38
Binary files /dev/null and b/src/assets/speakers/img/KEF_Concerto_1971.webp differ
diff --git a/src/assets/speakers/speakers-data.json b/src/assets/speakers/speakers-data.json
new file mode 100644
index 0000000..80b3cc8
--- /dev/null
+++ b/src/assets/speakers/speakers-data.json
@@ -0,0 +1,25 @@
+{
+ "speakers": [
+ {
+ "id": "sp.1",
+ "image": "assets/speakers/img/KEF_Concerto_1971.webp",
+ "manufacturer": "KEF",
+ "model": "Concerto",
+ "year": 1971
+ },
+ {
+ "id": "sp.2",
+ "image": "assets/speakers/img/JSE_Infinite_Slope.webp",
+ "manufacturer": "JSE",
+ "model": "Infinite Slope",
+ "year": 1980
+ },
+ {
+ "id": "sp.3",
+ "image": "assets/speakers/klipsch_kg4.jpg",
+ "manufacturer": "Pioneer",
+ "model": "CS-E500",
+ "year": 1980
+ }
+ ]
+}
diff --git a/src/global.scss b/src/global.scss
index 3bcc4ae..095753b 100644
--- a/src/global.scss
+++ b/src/global.scss
@@ -51,7 +51,7 @@
@import "swiper/scss/effect-cards";
-ion-modal {
+.player-modal {
// --height: 50%;
--border-radius: 16px;
// --box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);