Skip to content

Commit 8dc66e3

Browse files
committed
side panel liquid glass and search improvements
Signed-off-by: Sertac Ozercan <sozercan@gmail.com>
1 parent 431ffd9 commit 8dc66e3

File tree

9 files changed

+181
-42
lines changed

9 files changed

+181
-42
lines changed

Core/Services/API/MockUITestYTMusicClient.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,28 @@ final class MockUITestYTMusicClient: YTMusicClientProtocol {
143143
)
144144
}
145145

146+
func searchFeaturedPlaylists(query _: String) async throws -> SearchResponse {
147+
try? await Task.sleep(for: .milliseconds(100))
148+
return SearchResponse(
149+
songs: [],
150+
albums: [],
151+
artists: [],
152+
playlists: self.searchResults.playlists,
153+
continuationToken: nil
154+
)
155+
}
156+
157+
func searchCommunityPlaylists(query _: String) async throws -> SearchResponse {
158+
try? await Task.sleep(for: .milliseconds(100))
159+
return SearchResponse(
160+
songs: [],
161+
albums: [],
162+
artists: [],
163+
playlists: self.searchResults.playlists,
164+
continuationToken: nil
165+
)
166+
}
167+
146168
func getSearchContinuation() async throws -> SearchResponse? {
147169
nil
148170
}

Core/Services/API/YTMusicClient.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ final class YTMusicClient: YTMusicClientProtocol {
259259
static let albums = "EgWKAQIYAWoMEA4QChADEAQQCRAF"
260260
static let artists = "EgWKAQIgAWoMEA4QChADEAQQCRAF"
261261
static let playlists = "EgWKAQIoAWoMEA4QChADEAQQCRAF"
262+
/// Featured playlists (first-party YouTube Music curated playlists)
263+
static let featuredPlaylists = "EgeKAQQoADgBagwQDhAKEAMQBBAJEAU="
264+
/// Community playlists (user-created playlists)
265+
static let communityPlaylists = "EgeKAQQoAEABagwQDhAKEAMQBBAJEAU="
262266
}
263267

264268
/// Continuation token for filtered search pagination.
@@ -320,6 +324,40 @@ final class YTMusicClient: YTMusicClientProtocol {
320324
return SearchResponse(songs: [], albums: [], artists: [], playlists: playlists, continuationToken: token)
321325
}
322326

327+
/// Searches for featured playlists only (YouTube Music curated playlists).
328+
func searchFeaturedPlaylists(query: String) async throws -> SearchResponse {
329+
self.logger.info("Searching featured playlists only for: \(query)")
330+
331+
let body: [String: Any] = [
332+
"query": query,
333+
"params": SearchFilterParams.featuredPlaylists,
334+
]
335+
336+
let data = try await request("search", body: body, ttl: APICache.TTL.search)
337+
let (playlists, token) = SearchResponseParser.parsePlaylistsOnly(data)
338+
self.searchContinuationToken = token
339+
340+
self.logger.info("Featured playlists search found \(playlists.count) playlists, hasMore: \(token != nil)")
341+
return SearchResponse(songs: [], albums: [], artists: [], playlists: playlists, continuationToken: token)
342+
}
343+
344+
/// Searches for community playlists only (user-created playlists).
345+
func searchCommunityPlaylists(query: String) async throws -> SearchResponse {
346+
self.logger.info("Searching community playlists only for: \(query)")
347+
348+
let body: [String: Any] = [
349+
"query": query,
350+
"params": SearchFilterParams.communityPlaylists,
351+
]
352+
353+
let data = try await request("search", body: body, ttl: APICache.TTL.search)
354+
let (playlists, token) = SearchResponseParser.parsePlaylistsOnly(data)
355+
self.searchContinuationToken = token
356+
357+
self.logger.info("Community playlists search found \(playlists.count) playlists, hasMore: \(token != nil)")
358+
return SearchResponse(songs: [], albums: [], artists: [], playlists: playlists, continuationToken: token)
359+
}
360+
323361
/// Searches for songs only with pagination support.
324362
func searchSongsWithPagination(query: String) async throws -> SearchResponse {
325363
self.logger.info("Searching songs with pagination for: \(query)")

Core/Services/Protocols.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ protocol YTMusicClientProtocol: Sendable {
109109
/// Searches for playlists only (filtered search with pagination).
110110
func searchPlaylists(query: String) async throws -> SearchResponse
111111

112+
/// Searches for featured playlists only (YouTube Music curated playlists).
113+
func searchFeaturedPlaylists(query: String) async throws -> SearchResponse
114+
115+
/// Searches for community playlists only (user-created playlists).
116+
func searchCommunityPlaylists(query: String) async throws -> SearchResponse
117+
112118
/// Fetches the next batch of search results via continuation.
113119
/// Returns nil if no more results are available.
114120
func getSearchContinuation() async throws -> SearchResponse?

Core/ViewModels/SearchViewModel.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ final class SearchViewModel {
6969
case songs = "Songs"
7070
case albums = "Albums"
7171
case artists = "Artists"
72-
case playlists = "Playlists"
72+
case featuredPlaylists = "Featured playlists"
73+
case communityPlaylists = "Community playlists"
7374

7475
var id: String { rawValue }
7576
}
@@ -85,7 +86,7 @@ final class SearchViewModel {
8586
self.results.albums.map { .album($0) }
8687
case .artists:
8788
self.results.artists.map { .artist($0) }
88-
case .playlists:
89+
case .featuredPlaylists, .communityPlaylists:
8990
self.results.playlists.map { .playlist($0) }
9091
}
9192
}
@@ -213,8 +214,10 @@ final class SearchViewModel {
213214
try await self.client.searchAlbums(query: currentQuery)
214215
case .artists:
215216
try await self.client.searchArtists(query: currentQuery)
216-
case .playlists:
217-
try await self.client.searchPlaylists(query: currentQuery)
217+
case .featuredPlaylists:
218+
try await self.client.searchFeaturedPlaylists(query: currentQuery)
219+
case .communityPlaylists:
220+
try await self.client.searchCommunityPlaylists(query: currentQuery)
218221
}
219222

220223
// Check cancellation and query change before updating results

Tests/KasetTests/Helpers/MockYTMusicClient.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,36 @@ final class MockYTMusicClient: YTMusicClientProtocol {
313313
)
314314
}
315315

316+
func searchFeaturedPlaylists(query: String) async throws -> SearchResponse {
317+
self.searchCalled = true
318+
self.searchQueries.append(query)
319+
self._searchContinuationIndex = 0
320+
if let error = shouldThrowError { throw error }
321+
let hasMore = !self.searchContinuationResponses.isEmpty
322+
return SearchResponse(
323+
songs: [],
324+
albums: [],
325+
artists: [],
326+
playlists: self.searchResponse.playlists,
327+
continuationToken: hasMore ? "mock-token" : nil
328+
)
329+
}
330+
331+
func searchCommunityPlaylists(query: String) async throws -> SearchResponse {
332+
self.searchCalled = true
333+
self.searchQueries.append(query)
334+
self._searchContinuationIndex = 0
335+
if let error = shouldThrowError { throw error }
336+
let hasMore = !self.searchContinuationResponses.isEmpty
337+
return SearchResponse(
338+
songs: [],
339+
albums: [],
340+
artists: [],
341+
playlists: self.searchResponse.playlists,
342+
continuationToken: hasMore ? "mock-token" : nil
343+
)
344+
}
345+
316346
func getSearchContinuation() async throws -> SearchResponse? {
317347
if let error = shouldThrowError { throw error }
318348
guard self._searchContinuationIndex < self.searchContinuationResponses.count else {

Views/macOS/LyricsView.swift

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,25 @@ struct LyricsView: View {
2121

2222
private let logger = DiagnosticsLogger.ai
2323

24+
/// Namespace for glass effect morphing.
25+
@Namespace private var lyricsNamespace
26+
2427
var body: some View {
25-
VStack(spacing: 0) {
26-
// Header
27-
self.headerView
28+
GlassEffectContainer(spacing: 0) {
29+
VStack(spacing: 0) {
30+
// Header
31+
self.headerView
2832

29-
Divider()
33+
Divider()
34+
.opacity(0.3)
3035

31-
// Content
32-
self.contentView
36+
// Content
37+
self.contentView
38+
}
39+
.frame(width: 280)
40+
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 20))
41+
.glassEffectID("lyricsPanel", in: self.lyricsNamespace)
3342
}
34-
.frame(minWidth: 280, maxWidth: 280)
35-
.background(.background.opacity(0.95))
3643
.glassEffectTransition(.materialize)
3744
.onChange(of: self.playerService.currentTrack?.videoId) { _, newVideoId in
3845
if let videoId = newVideoId, videoId != lastLoadedVideoId {
@@ -93,14 +100,16 @@ struct LyricsView: View {
93100
}
94101
}
95102
.padding(.horizontal, 16)
96-
.padding(.vertical, 12)
103+
.padding(.vertical, 14)
97104
}
98105

99106
// MARK: - Content
100107

101108
@ViewBuilder
102109
private var contentView: some View {
103-
if self.isLoading {
110+
if self.playerService.currentTrack == nil {
111+
self.noTrackPlayingView
112+
} else if self.isLoading {
104113
self.loadingView
105114
} else if let lyrics, lyrics.isAvailable {
106115
self.lyricsContentView(lyrics)
@@ -302,6 +311,25 @@ struct LyricsView: View {
302311
.frame(maxWidth: .infinity, maxHeight: .infinity)
303312
}
304313

314+
private var noTrackPlayingView: some View {
315+
VStack(spacing: 12) {
316+
Image(systemName: "play.circle")
317+
.font(.system(size: 40))
318+
.foregroundStyle(.tertiary)
319+
320+
Text("No Song Playing")
321+
.font(.headline)
322+
.foregroundStyle(.secondary)
323+
324+
Text("Play a song to view its lyrics here.")
325+
.font(.subheadline)
326+
.foregroundStyle(.tertiary)
327+
.multilineTextAlignment(.center)
328+
.padding(.horizontal, 24)
329+
}
330+
.frame(maxWidth: .infinity, maxHeight: .infinity)
331+
}
332+
305333
// MARK: - Data Loading
306334

307335
private func loadLyrics(for videoId: String) async {

Views/macOS/MainWindow.swift

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ struct MainWindow: View {
137137
@ViewBuilder
138138
private var mainContent: some View {
139139
if let client = ytMusicClient {
140-
HStack(spacing: 0) {
140+
ZStack(alignment: .trailing) {
141141
// Main navigation content
142142
NavigationSplitView(columnVisibility: self.$columnVisibility) {
143143
Sidebar(selection: self.$navigationSelection)
@@ -152,11 +152,11 @@ struct MainWindow: View {
152152
}
153153
}
154154

155-
// Right sidebar - either lyrics or queue (mutually exclusive)
156-
self.rightSidebarView(client: client)
155+
// Right sidebar overlay - either lyrics or queue (mutually exclusive)
156+
self.rightSidebarOverlay(client: client)
157157
}
158-
.animation(.easeInOut(duration: 0.2), value: self.playerService.showLyrics)
159-
.animation(.easeInOut(duration: 0.2), value: self.playerService.showQueue)
158+
.animation(.easeInOut(duration: 0.25), value: self.playerService.showLyrics)
159+
.animation(.easeInOut(duration: 0.25), value: self.playerService.showQueue)
160160
.frame(minWidth: 900, minHeight: 600)
161161
.toolbar {
162162
ToolbarItem(placement: .primaryAction) {
@@ -178,25 +178,31 @@ struct MainWindow: View {
178178
}
179179
}
180180

181-
/// Right sidebar showing either lyrics or queue (mutually exclusive).
181+
/// Right sidebar overlay showing either lyrics or queue as glass panels (mutually exclusive).
182182
@ViewBuilder
183-
private func rightSidebarView(client: any YTMusicClientProtocol) -> some View {
183+
private func rightSidebarOverlay(client: any YTMusicClientProtocol) -> some View {
184184
let showRightSidebar = self.playerService.showLyrics || self.playerService.showQueue
185185

186-
Divider()
187-
.opacity(showRightSidebar ? 1 : 0)
188-
.frame(width: showRightSidebar ? 1 : 0)
186+
if showRightSidebar {
187+
VStack {
188+
Spacer()
189189

190-
Group {
191-
if self.playerService.showLyrics {
192-
LyricsView(client: client)
193-
} else if self.playerService.showQueue {
194-
QueueView()
190+
Group {
191+
if self.playerService.showLyrics {
192+
LyricsView(client: client)
193+
} else if self.playerService.showQueue {
194+
QueueView()
195+
}
196+
}
197+
.frame(maxHeight: .infinity)
198+
.padding(.top, 12)
199+
.padding(.bottom, 76) // Space for PlayerBar
200+
.transition(.move(edge: .trailing).combined(with: .opacity))
201+
202+
Spacer()
195203
}
204+
.padding(.trailing, 16)
196205
}
197-
.frame(width: showRightSidebar ? 280 : 0)
198-
.opacity(showRightSidebar ? 1 : 0)
199-
.clipped()
200206
}
201207

202208
@ViewBuilder

Views/macOS/PlayerBar.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,6 @@ struct PlayerBar: View {
425425
.accessibilityIdentifier(AccessibilityID.PlayerBar.lyricsButton)
426426
.accessibilityLabel("Lyrics")
427427
.accessibilityValue(self.playerService.showLyrics ? "Showing" : "Hidden")
428-
.disabled(self.playerService.currentTrack == nil)
429428

430429
// Queue button
431430
Button {

Views/macOS/QueueView.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,25 @@ struct QueueView: View {
99
@Environment(FavoritesManager.self) private var favoritesManager
1010
@Environment(\.showCommandBar) private var showCommandBar
1111

12+
/// Namespace for glass effect morphing.
13+
@Namespace private var queueNamespace
14+
1215
var body: some View {
13-
VStack(spacing: 0) {
14-
// Header
15-
self.headerView
16+
GlassEffectContainer(spacing: 0) {
17+
VStack(spacing: 0) {
18+
// Header
19+
self.headerView
1620

17-
Divider()
21+
Divider()
22+
.opacity(0.3)
1823

19-
// Content
20-
self.contentView
24+
// Content
25+
self.contentView
26+
}
27+
.frame(width: 280)
28+
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 20))
29+
.glassEffectID("queuePanel", in: self.queueNamespace)
2130
}
22-
.frame(minWidth: 280, maxWidth: 280)
23-
.background(.background.opacity(0.95))
2431
.glassEffectTransition(.materialize)
2532
.accessibilityIdentifier(AccessibilityID.Queue.container)
2633
}
@@ -49,7 +56,7 @@ struct QueueView: View {
4956
}
5057
}
5158
.padding(.horizontal, 16)
52-
.padding(.vertical, 12)
59+
.padding(.vertical, 14)
5360
}
5461

5562
// MARK: - Content

0 commit comments

Comments
 (0)