Skip to content

Commit f41d5fc

Browse files
committed
chore(signage-manager): link playlists to item view
1 parent a2ae7b0 commit f41d5fc

4 files changed

Lines changed: 130 additions & 91 deletions

File tree

apps/signage-manager/src/app/media/playlist-sidebar.component.ts

Lines changed: 91 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop';
22
import { Component, computed, effect, inject, signal } from '@angular/core';
33
import { toSignal } from '@angular/core/rxjs-interop';
44
import { FormsModule } from '@angular/forms';
5+
import { MatRippleModule } from '@angular/material/core';
56
import { MatFormFieldModule } from '@angular/material/form-field';
67
import { MatInputModule } from '@angular/material/input';
8+
import { RouterLink } from '@angular/router';
79
import {
810
AuthenticatedImageDirective,
911
IconComponent,
@@ -39,103 +41,106 @@ type PlaylistStatus = 'expired' | 'pending' | 'awaiting_approval' | null;
3941
<div class="flex-1 overflow-auto p-2">
4042
@if (filtered_playlists()?.length) {
4143
@for (playlist of filtered_playlists(); track playlist.id) {
42-
<div
43-
cdkDropList
44-
[id]="'playlist-' + $index"
45-
[cdkDropListConnectedTo]="['media-list']"
46-
[cdkDropListData]="playlist"
47-
(cdkDropListDropped)="onDrop(playlist, $event)"
48-
class="border-base-300 mb-2 flex items-center gap-3 rounded-lg border p-0.5 transition-colors"
49-
>
44+
<a [routerLink]="['/playlists', playlist.id]">
5045
<div
51-
class="border-base-200 relative h-12 w-12 shrink-0 overflow-hidden rounded-md border"
46+
cdkDropList
47+
[id]="'playlist-' + $index"
48+
[cdkDropListConnectedTo]="['media-list']"
49+
[cdkDropListData]="playlist"
50+
(cdkDropListDropped)="onDrop(playlist, $event)"
51+
class="border-base-300 mb-2 flex items-center gap-3 rounded-lg border p-0.5 transition-colors"
52+
matRipple
5253
>
53-
@if (
54-
playlist_thumbnail_media()[playlist.id]
55-
?.length
56-
) {
57-
@for (
58-
media of playlist_thumbnail_media()[
59-
playlist.id
60-
];
61-
track media;
62-
let i = $index;
63-
let len = $count
54+
<div
55+
class="border-base-200 relative h-12 w-12 shrink-0 overflow-hidden rounded-md border"
56+
>
57+
@if (
58+
playlist_thumbnail_media()[playlist.id]
59+
?.length
6460
) {
65-
<img
66-
auth
67-
[source]="media"
68-
class="border-base-300 bg-base-200 absolute h-9 w-9 rounded-sm border object-cover shadow"
69-
[style.top]="
70-
0.3 -
71-
(len - 1) * 0.125 +
72-
(len - 1 - i) * 0.25 +
73-
'rem'
74-
"
75-
[style.left]="
76-
0.3 -
77-
(len - 1) * 0.125 +
78-
(len - 1 - i) * 0.25 +
79-
'rem'
80-
"
81-
[style.z-index]="i"
82-
/>
83-
}
84-
} @else {
85-
<div
86-
class="text-base-content/35 flex h-full w-full items-center justify-center"
87-
>
88-
<icon class="text-2xl">
89-
playlist_play
90-
</icon>
91-
</div>
92-
}
93-
</div>
94-
<div class="min-w-0 flex-1">
95-
<div class="truncate text-sm font-medium">
96-
{{ playlist.name }}
97-
</div>
98-
<div class="flex flex-wrap gap-1">
99-
@if (!playlist.enabled) {
100-
<span
101-
class="bg-warning text-warning-content shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase"
61+
@for (
62+
media of playlist_thumbnail_media()[
63+
playlist.id
64+
];
65+
track media;
66+
let i = $index;
67+
let len = $count
68+
) {
69+
<img
70+
auth
71+
[source]="media"
72+
class="border-base-300 bg-base-200 absolute h-9 w-9 rounded-sm border object-cover shadow"
73+
[style.top]="
74+
0.3 -
75+
(len - 1) * 0.125 +
76+
(len - 1 - i) * 0.25 +
77+
'rem'
78+
"
79+
[style.left]="
80+
0.3 -
81+
(len - 1) * 0.125 +
82+
(len - 1 - i) * 0.25 +
83+
'rem'
84+
"
85+
[style.z-index]="i"
86+
/>
87+
}
88+
} @else {
89+
<div
90+
class="text-base-content/35 flex h-full w-full items-center justify-center"
10291
>
103-
Disabled
104-
</span>
92+
<icon class="text-2xl">
93+
playlist_play
94+
</icon>
95+
</div>
10596
}
106-
@switch (getStatus(playlist)) {
107-
@case ('expired') {
108-
<span
109-
class="bg-error text-error-content shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase"
110-
>
111-
Expired
112-
</span>
113-
}
114-
@case ('pending') {
97+
</div>
98+
<div class="min-w-0 flex-1">
99+
<div class="truncate text-sm font-medium">
100+
{{ playlist.name }}
101+
</div>
102+
<div class="flex flex-wrap gap-1">
103+
@if (!playlist.enabled) {
115104
<span
116-
class="bg-info text-info-content shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase"
105+
class="bg-warning text-warning-content shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase"
117106
>
118-
Pending
107+
Disabled
119108
</span>
120109
}
121-
@case ('awaiting_approval') {
122-
<span
123-
class="bg-secondary text-secondary-content shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase"
124-
>
125-
Awaiting Approval
126-
</span>
110+
@switch (getStatus(playlist)) {
111+
@case ('expired') {
112+
<span
113+
class="bg-error text-error-content shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase"
114+
>
115+
Expired
116+
</span>
117+
}
118+
@case ('pending') {
119+
<span
120+
class="bg-info text-info-content shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase"
121+
>
122+
Pending
123+
</span>
124+
}
125+
@case ('awaiting_approval') {
126+
<span
127+
class="bg-secondary text-secondary-content shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase"
128+
>
129+
Awaiting Approval
130+
</span>
131+
}
127132
}
133+
</div>
134+
@if (playlist.description) {
135+
<div
136+
class="mt-0.5 truncate text-xs opacity-50"
137+
>
138+
{{ playlist.description }}
139+
</div>
128140
}
129141
</div>
130-
@if (playlist.description) {
131-
<div
132-
class="mt-0.5 truncate text-xs opacity-50"
133-
>
134-
{{ playlist.description }}
135-
</div>
136-
}
137142
</div>
138-
</div>
143+
</a>
139144
}
140145
} @else {
141146
<div
@@ -172,6 +177,8 @@ type PlaylistStatus = 'expired' | 'pending' | 'awaiting_approval' | null;
172177
MatInputModule,
173178
AuthenticatedImageDirective,
174179
IconComponent,
180+
RouterLink,
181+
MatRippleModule,
175182
],
176183
})
177184
export class PlaylistSidebarComponent {

apps/signage-manager/src/app/playlists/playlist-items.component.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,21 @@ import { SignageService } from '../signage.service';
3232
</div>
3333
@if (requires_approval()) {
3434
<button
35-
btn
35+
icon
3636
matRipple
37-
class="bg-warning text-warning-content h-10 shrink-0 rounded-lg px-3"
37+
class="border-base-200 hover:bg-base-200 hover:border-base-300 rounded-lg border hover:shadow-md"
3838
[disabled]="!is_admin()"
3939
[matTooltip]="approval_tooltip()"
4040
(click)="approvePlaylist()"
4141
>
42-
<icon class="mr-2 text-xl">order_approve</icon>
43-
<span>Approve</span>
42+
<icon class="text-warning">order_approve</icon>
4443
</button>
4544
}
4645
<button
4746
icon
4847
matRipple
4948
matTooltip="Edit playlist"
49+
class="border-base-200 hover:bg-base-200 hover:border-base-300 rounded-lg border hover:shadow-md"
5050
(click)="editPlaylist()"
5151
>
5252
<icon>edit</icon>
@@ -55,6 +55,7 @@ import { SignageService } from '../signage.service';
5555
icon
5656
matRipple
5757
matTooltip="Delete playlist"
58+
class="border-base-200 hover:bg-base-200 hover:border-base-300 rounded-lg border hover:shadow-md"
5859
(click)="removePlaylist()"
5960
>
6061
<icon class="text-error">delete</icon>

apps/signage-manager/src/app/playlists/playlists.component.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MatRippleModule } from '@angular/material/core';
44
import { MatTooltipModule } from '@angular/material/tooltip';
55
import { ActivatedRoute, Router } from '@angular/router';
66
import { IconComponent } from '@placeos/components';
7+
import { SignageMedia } from '@placeos/ts-client';
78
import { map } from 'rxjs/operators';
89
import { NavFooterComponent } from '../shared/nav-footer.component';
910
import { NavSidebarComponent } from '../shared/nav-sidebar.component';
@@ -14,6 +15,7 @@ import { PlaylistItemsComponent } from './playlist-items.component';
1415
import { PlaylistListComponent } from './playlist-list.component';
1516

1617
const TAB_QUERY_PARAM = 'tab';
18+
const ITEM_QUERY_PARAM = 'item';
1719

1820
function parsePlaylistTab(value: string | null): 'items' | 'details' {
1921
return value === 'details' ? 'details' : 'items';
@@ -204,6 +206,13 @@ export class PlaylistsSectionComponent {
204206
this._route.queryParamMap.pipe(map((p) => p.get(TAB_QUERY_PARAM))),
205207
{ initialValue: null as string | null },
206208
);
209+
private readonly _route_item_id = toSignal(
210+
this._route.queryParamMap.pipe(map((p) => p.get(ITEM_QUERY_PARAM))),
211+
{ initialValue: null as string | null },
212+
);
213+
private readonly _playlist_items = toSignal(this._service.playlist_media_items$, {
214+
initialValue: [] as SignageMedia[],
215+
});
207216

208217
private _route_resolved = false;
209218

@@ -235,6 +244,16 @@ export class PlaylistsSectionComponent {
235244
this._service.selected_playlist_item.set(null);
236245
}
237246
});
247+
248+
// Sync selected media item from query param
249+
effect(() => {
250+
const item_id = this._route_item_id();
251+
if (!item_id) return;
252+
const items = this._playlist_items();
253+
if (!items.length) return;
254+
const matched_item = items.find((item) => item.id === item_id);
255+
this._service.selected_playlist_item.set(matched_item || null);
256+
});
238257
}
239258

240259
public editPlaylist() {

apps/signage-manager/src/app/shared/media-preview-modal.component.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
44
import { MatRippleModule } from '@angular/material/core';
55
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
66
import { DomSanitizer } from '@angular/platform-browser';
7+
import { RouterLink } from '@angular/router';
78
import {
89
AuthenticatedImageDirective,
910
IconComponent,
@@ -153,8 +154,18 @@ import { SignageService } from '../signage.service';
153154
playlist of containing_playlists();
154155
track playlist.id
155156
) {
156-
<div
157-
class="flex items-center space-x-2 py-1"
157+
<a
158+
matRipple
159+
mat-dialog-close
160+
class="hover:bg-base-200 flex items-center space-x-2 rounded px-2 py-1 no-underline"
161+
[routerLink]="[
162+
'/playlists',
163+
playlist.id,
164+
]"
165+
[queryParams]="{
166+
tab: 'items',
167+
item: item.id,
168+
}"
158169
>
159170
<icon
160171
class="text-base-content/60 text-xl"
@@ -163,7 +174,7 @@ import { SignageService } from '../signage.service';
163174
<span class="text-sm">{{
164175
playlist.name
165176
}}</span>
166-
</div>
177+
</a>
167178
}
168179
</div>
169180
} @else {
@@ -190,6 +201,7 @@ import { SignageService } from '../signage.service';
190201
CommonModule,
191202
MatRippleModule,
192203
MatDialogModule,
204+
RouterLink,
193205
IconComponent,
194206
AuthenticatedImageDirective,
195207
MediaDurationPipe,

0 commit comments

Comments
 (0)