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:
11 files changed
+546
-109
No files matched your search
@@ -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))
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user