Show soundtrack covers and edit alternative names (#49)
Covers: statistics cards show the soundtrack's cover on the left, and the
statistics popup shows it large. Until it arrives, and without a cover, the
soundtrack's initials stand in. Covers load only for cards on or near the
screen, three at a time, and are cached in covers/ next to the settings
(CoverCache), downloaded again only when cover_modified changes. The index and
file handling are shared with CharacterCache in a new FileCache.
Alternative names: the popup shows them and has an editor (add, remove, save)
that sends PUT /soundtracks/{uuid}/alternative-names. The statistics search and
the Search popup find soundtracks by them; Search shows the matching name after
the real one. Search now loads /soundtracks, falling back to the plain list.
The API types are regenerated from the server's spec, with a SoundtrackCover
theme style in every theme.
Closes #49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a735c515bf
commit
ef6ac8f1fd
37 files changed
+8029
-1763
No files matched your search
@@ -0,0 +1,95 @@
|
||||
class_name CoverCache
|
||||
extends RefCounted
|
||||
## Soundtrack covers kept on disk in the [code]covers[/code] folder next to the settings,
|
||||
## by soundtrack UUID, so each one is downloaded only when it changed on the server (its
|
||||
## [code]cover_modified[/code] time). See [FileCache].
|
||||
##
|
||||
## Covers are downloaded a few at a time, in the order they are asked for, so opening the
|
||||
## statistics doesn't send a request for every soundtrack at once.
|
||||
|
||||
const FOLDER_NAME: String = "covers"
|
||||
## How many covers are downloaded at the same time.
|
||||
const MAX_DOWNLOADS: int = 3
|
||||
|
||||
# The covers on disk and their index
|
||||
var _files: FileCache = FileCache.new(FOLDER_NAME)
|
||||
# Covers waiting to be downloaded: [uuid, modified]
|
||||
var _waiting: Array[Array] = []
|
||||
# Who is waiting for each cover that is asked for or downloading: uuid -> Array[Callable]
|
||||
var _callbacks: Dictionary = {}
|
||||
var _downloading: int = 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_files.load_index()
|
||||
|
||||
|
||||
## The folder the covers are kept in.
|
||||
static func folder() -> String:
|
||||
return Settings.config_dir().path_join(FOLDER_NAME)
|
||||
|
||||
|
||||
## Deletes the covers of soundtracks that are not in [param covers] (UUID -> cover
|
||||
## time), and those whose soundtrack no longer has a cover.
|
||||
func keep_only(covers: Dictionary) -> void:
|
||||
var kept: Dictionary = {}
|
||||
for uuid: String in covers:
|
||||
if int(covers[uuid]) > 0:
|
||||
kept[uuid] = true
|
||||
_files.keep_only(kept)
|
||||
|
||||
|
||||
## Calls [param on_done] with the cover of the soundtrack [param uuid] as an [Image]:
|
||||
## from disk when the cached one has the server's time [param modified], otherwise once
|
||||
## it has been downloaded. Nothing is called when the soundtrack has no cover
|
||||
## ([param modified] 0) or the download fails.
|
||||
func fetch_cover(uuid: String, modified: int, on_done: Callable) -> void:
|
||||
if modified <= 0 || uuid.is_empty():
|
||||
return
|
||||
if _files.has_file(uuid) && _files.modified(uuid) == modified:
|
||||
var cached: Image = CharacterCard.image_from_bytes(_files.read(uuid))
|
||||
if cached != null:
|
||||
on_done.call(cached)
|
||||
return
|
||||
if _callbacks.has(uuid):
|
||||
_callbacks[uuid].append(on_done)
|
||||
return
|
||||
_callbacks[uuid] = [on_done]
|
||||
_waiting.append([uuid, modified])
|
||||
_download_next()
|
||||
|
||||
|
||||
## How many covers are waiting for their turn or downloading.
|
||||
func busy() -> int:
|
||||
return _callbacks.size()
|
||||
|
||||
|
||||
func _download_next() -> void:
|
||||
while _downloading < MAX_DOWNLOADS && !_waiting.is_empty():
|
||||
var next: Array = _waiting.pop_front()
|
||||
var uuid: String = next[0]
|
||||
_downloading += 1
|
||||
MusicServerClient.fetch_soundtrack_cover(uuid,
|
||||
_on_downloaded.bind(uuid, next[1]), _on_failed.bind(uuid))
|
||||
|
||||
|
||||
func _on_downloaded(bytes: PackedByteArray, uuid: String, modified: int) -> void:
|
||||
_downloading -= 1
|
||||
var cover: Image = CharacterCard.image_from_bytes(bytes)
|
||||
if cover == null:
|
||||
GameLog.warning("The cover of soundtrack ", uuid, " could not be read")
|
||||
else:
|
||||
_files.store(uuid, bytes, modified)
|
||||
for on_done: Callable in _callbacks.get(uuid, []):
|
||||
# A card may be gone by the time its cover arrives
|
||||
if cover != null && on_done.is_valid():
|
||||
on_done.call(cover)
|
||||
_callbacks.erase(uuid)
|
||||
_download_next()
|
||||
|
||||
|
||||
# A 404 means the server has no cover for it yet
|
||||
func _on_failed(uuid: String) -> void:
|
||||
_downloading -= 1
|
||||
_callbacks.erase(uuid)
|
||||
_download_next()
|
||||
Reference in new issue
Block a user