feat(award): use /award/getAlbums for the grid + follow award as entity

Two behavior fixes for AwardView based on user feedback.

1. Grid content. The previous implementation rendered the releases
   array from /award/page, which is user-scoped (only surfaces awards
   on albums the user already favorited), so the grid came up empty
   in almost every case. Switch to /award/getAlbums which is the
   public catalog listing with pagination. /award/page is still
   called, but only to hydrate the hero (name, image, magazine).

2. Follow-the-award. Awards themselves are followable on Qobuz mobile
   (it's what feeds the 'Awards' tab of Release Watch). Add:
   - favorite_awards table + store methods in favorites_cache.rs
   - 4 V2 commands (get/sync/cache/uncache) mirroring the label ones
   - awardFavoritesStore.ts — clone of labelFavoritesStore with string
     ids (Qobuz mobile API passes award_ids as strings)
   - Follow/Unfollow heart button in AwardView hero, matching the
     ArtistDetailView / LabelView visual
   - loadAwardFavorites() wired into the login bootstrap alongside
     loadArtistFavorites / loadLabelFavorites
   - i18n keys (award.follow / award.unfollow)

Also adds a Ver más button for paginated loading of the album grid.

cc
This commit is contained in:
vicrodh committed 2026-04-14 13:02:37 -06:00
1 parent e52e3e1fdc
commit a273174716
11 files changed
+546 -109

No files matched your search

+72
View File
@@ -427,3 +427,75 @@ pub async fn v2_uncache_favorite_label(
.remove_favorite_label(labelId)
.map_err(|e| RuntimeError::Internal(e))
}
// ============ Award favorites cache (mirrors label) ============
#[tauri::command]
pub async fn v2_get_cached_favorite_awards(
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
) -> Result<Vec<String>, RuntimeError> {
let guard = cache_state
.store
.lock()
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
let store = guard
.as_ref()
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
store
.get_favorite_award_ids()
.map_err(|e| RuntimeError::Internal(e))
}
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_sync_cached_favorite_awards(
awardIds: Vec<String>,
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
) -> Result<(), RuntimeError> {
let guard = cache_state
.store
.lock()
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
let store = guard
.as_ref()
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
store
.sync_favorite_awards(&awardIds)
.map_err(|e| RuntimeError::Internal(e))
}
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_cache_favorite_award(
awardId: String,
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
) -> Result<(), RuntimeError> {
let guard = cache_state
.store
.lock()
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
let store = guard
.as_ref()
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
store
.add_favorite_award(&awardId)
.map_err(|e| RuntimeError::Internal(e))
}
#[tauri::command]
#[allow(non_snake_case)]
pub async fn v2_uncache_favorite_award(
awardId: String,
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
) -> Result<(), RuntimeError> {
let guard = cache_state
.store
.lock()
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
let store = guard
.as_ref()
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
store
.remove_favorite_award(&awardId)
.map_err(|e| RuntimeError::Internal(e))
}
+83
View File
@@ -95,6 +95,18 @@ impl FavoritesCacheStore {
)
.map_err(|e| format!("Failed to create favorite_labels table: {}", e))?;
// Awards — added as part of the Follow Award feature. award_id
// is TEXT because /favorite/create?award_ids=... takes string
// identifiers and the Android DTO declares id as String?.
conn.execute(
"CREATE TABLE IF NOT EXISTS favorite_awards (
award_id TEXT PRIMARY KEY,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)",
[],
)
.map_err(|e| format!("Failed to create favorite_awards table: {}", e))?;
Ok(Self { conn })
}
@@ -382,6 +394,74 @@ impl FavoritesCacheStore {
Ok(())
}
// ============ Award favorites ============
pub fn get_favorite_award_ids(&self) -> Result<Vec<String>, String> {
let mut stmt = self
.conn
.prepare("SELECT award_id FROM favorite_awards")
.map_err(|e| format!("Failed to prepare query: {}", e))?;
let rows = stmt
.query_map([], |row| row.get::<_, String>(0))
.map_err(|e| format!("Failed to query favorite awards: {}", e))?;
let mut ids = Vec::new();
for row in rows {
ids.push(row.map_err(|e| format!("Failed to read row: {}", e))?);
}
Ok(ids)
}
pub fn is_award_favorite(&self, award_id: &str) -> Result<bool, String> {
let mut stmt = self
.conn
.prepare("SELECT 1 FROM favorite_awards WHERE award_id = ?1")
.map_err(|e| format!("Failed to prepare query: {}", e))?;
let exists = stmt
.exists(params![award_id])
.map_err(|e| format!("Failed to check favorite: {}", e))?;
Ok(exists)
}
pub fn add_favorite_award(&self, award_id: &str) -> Result<(), String> {
self.conn
.execute(
"INSERT OR IGNORE INTO favorite_awards (award_id) VALUES (?1)",
params![award_id],
)
.map_err(|e| format!("Failed to add favorite award: {}", e))?;
Ok(())
}
pub fn remove_favorite_award(&self, award_id: &str) -> Result<(), String> {
self.conn
.execute(
"DELETE FROM favorite_awards WHERE award_id = ?1",
params![award_id],
)
.map_err(|e| format!("Failed to remove favorite award: {}", e))?;
Ok(())
}
pub fn sync_favorite_awards(&self, award_ids: &[String]) -> Result<(), String> {
self.conn
.execute("DELETE FROM favorite_awards", [])
.map_err(|e| format!("Failed to clear favorite awards: {}", e))?;
for award_id in award_ids {
self.conn
.execute(
"INSERT INTO favorite_awards (award_id) VALUES (?1)",
params![award_id],
)
.map_err(|e| format!("Failed to insert favorite award: {}", e))?;
}
Ok(())
}
// ============ Clear all (for logout) ============
pub fn clear_all(&self) -> Result<(), String> {
@@ -397,6 +477,9 @@ impl FavoritesCacheStore {
self.conn
.execute("DELETE FROM favorite_labels", [])
.map_err(|e| format!("Failed to clear favorite labels: {}", e))?;
self.conn
.execute("DELETE FROM favorite_awards", [])
.map_err(|e| format!("Failed to clear favorite awards: {}", e))?;
Ok(())
}
}
+4
View File
@@ -1694,6 +1694,10 @@ pub fn run() {
commands_v2::v2_sync_cached_favorite_labels,
commands_v2::v2_cache_favorite_label,
commands_v2::v2_uncache_favorite_label,
commands_v2::v2_get_cached_favorite_awards,
commands_v2::v2_sync_cached_favorite_awards,
commands_v2::v2_cache_favorite_award,
commands_v2::v2_uncache_favorite_award,
commands_v2::v2_share_track_songlink,
commands_v2::v2_share_album_songlink,
commands_v2::v2_library_backfill_downloads,