Implement initial audiobook UX (some of which is a bit of a WIP).

- Renamed various components and moved them to src/components.
- Renamed KOTO_PREFERRED_MODEL* to KOTO_PREFERRED_PLAYLIST*
- Renamed koto string utility functions to always be prefixed with koto_utils_string_ for consistency.
- Added configuration options for show / hiding various album information, as well as preferred sort type.
- Changed db schema to reflect various metadata changes (sorry).
- Implemented genre, narrator, year aggregation from KotoTrack to KotoAlbum for use in KotoAlbumInfo and audiobooks.
- Rearchitected our playlist functionality for KotoAlbums to always have an inner KotoPlaylist that is used.
- Added various getters / setters for new koto_album functionality.
- Implement aggregation of KotoAlbum pointer aggregation in the KotoArtist as a GQueue and GListStore instead of GList so we can get all the albums associated with an artist and use the GListStore for the audiobook view.
- Implement some initial album sorting in Artists (more work to do on this front).
- Many improvements to file indexing logic for CD and position detection, various new koto_track_helpers.
- Add new logic for knowing when to hide playlists given we generate them for each Album now.
- Fix missing updates of KotoPlaylist in KotoNav.
- Added playback position to KotoPlayerbar, renamed bar refs to self.
- New Playlist state saving.
- Updated track ticking logic for track in KotoPlaybackEngine.
- Fixed playback position detection in our KotoPlaybackEngine by swapping from GST_FORMAT_DEFAULT to GST_FORMAT_TIME.
- Changed our get_progress to divide by GST_SECOND.
- Fixed missing type checks in various KotoPlaybackEngine functions.

Fixes #13. Fixes #14. Fixes #15.
This commit is contained in:
Joshua Strobl 2021-08-10 19:18:46 +03:00
parent 93f3f45adf
commit 77b4e900e6
86 changed files with 4926 additions and 824 deletions

View file

@ -0,0 +1,255 @@
/* audiobook-view.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../../components/album-info.h"
#include "../../components/button.h"
#include "../../components/track-item.h"
#include "../../db/cartographer.h"
#include "../../indexer/album-playlist-funcs.h"
#include "../../indexer/structs.h"
#include "../../playlist/current.h"
#include "../../koto-utils.h"
#include "../../koto-window.h"
#include "audiobook-view.h"
extern KotoCartographer * koto_maps;
extern KotoCurrentPlaylist * current_playlist;
extern KotoWindow * main_window;
struct _KotoAudiobookView {
GtkBox parent_instance;
KotoAlbum * album; // Album associated with this view
GtkWidget * side_info; // Our side info (artwork, playback button, position)
GtkWidget * info_contents; // Our info and contents vertical box
KotoAlbumInfo * album_info; // Our "Album" (Audiobook) info
GtkWidget * audiobook_art; // Our GtkImage for the audiobook art
GtkWidget * playback_button; // Our GtkButton for playback
GtkWidget * chapter_info; // Our GtkLabel of the position, effectively
GtkWidget * playback_position; // Our GtkLabel for the track playback position
GtkWidget * tracks_list; // GtkListBox of tracks
};
struct _KotoAudiobookViewClass {
GtkBoxClass parent_class;
};
G_DEFINE_TYPE(KotoAudiobookView, koto_audiobook_view, GTK_TYPE_BOX);
static void koto_audiobook_view_class_init(KotoAudiobookViewClass * c) {
(void) c;
}
static void koto_audiobook_view_init(KotoAudiobookView * self) {
gtk_widget_add_css_class(GTK_WIDGET(self), "audiobook-view");
self->side_info = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(self->side_info, "side-info");
gtk_widget_set_size_request(self->side_info, 220, -1); // Match audiobook art size
self->info_contents = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
self->audiobook_art = gtk_image_new_from_icon_name("audio-x-generic-symbolic"); // Create an image with a symbolic icon
gtk_widget_set_size_request(self->audiobook_art, 220, 220); // Set to 220 like our cover art button
self->playback_button = gtk_button_new_with_label("Play"); // Default to our button saying Play instead of continue
gtk_widget_add_css_class(self->playback_button, "suggested-action");
g_signal_connect(self->playback_button, "clicked", G_CALLBACK(koto_audiobook_view_handle_play_clicked), self);
self->chapter_info = gtk_label_new(NULL);
self->playback_position = gtk_label_new(NULL);
gtk_widget_set_halign(self->chapter_info, GTK_ALIGN_START);
gtk_widget_set_halign(self->playback_position, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(self->side_info), self->audiobook_art); // Add our audiobook art to the side info
gtk_box_append(GTK_BOX(self->side_info), GTK_WIDGET(self->playback_button)); // Add our playback button to the side info
gtk_box_append(GTK_BOX(self->side_info), self->chapter_info);
gtk_box_append(GTK_BOX(self->side_info), self->playback_position);
self->album_info = koto_album_info_new("audiobook"); // Create an "audiobook" type KotoAlbumInfo
gtk_box_append(GTK_BOX(self->info_contents), GTK_WIDGET(self->album_info)); // Add the album info the info contents
GtkWidget * chapters_label = gtk_label_new("Chapters");
gtk_widget_add_css_class(chapters_label, "chapters-label");
gtk_widget_set_halign(chapters_label, GTK_ALIGN_START); // Align to the start
gtk_box_append(GTK_BOX(self->info_contents), chapters_label);
self->tracks_list = gtk_list_box_new(); // Create our list of our tracks
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(self->tracks_list), FALSE);
gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->tracks_list), GTK_SELECTION_MULTIPLE);
gtk_widget_add_css_class(self->tracks_list, "track-list");
gtk_box_append(GTK_BOX(self->info_contents), self->tracks_list); // Add our listbox to the info contents
gtk_box_append(GTK_BOX(self), self->side_info); // Add our side info to the box
gtk_box_append(GTK_BOX(self), self->info_contents); // Add our info contents to the box
}
GtkWidget * koto_audiobook_view_create_track_item(
gpointer item,
gpointer user_data
) {
(void) user_data;
if (!KOTO_IS_TRACK(item)) { // Not actually a track
return NULL;
}
KotoTrack * track = KOTO_TRACK(item); // Cast our item as a track
KotoTrackItem * track_item = koto_track_item_new(track); // Create our track item
if (!KOTO_IS_TRACK_ITEM(track_item)) { // Not a track item
return NULL;
}
return GTK_WIDGET(track_item); // Cast as a widget and return the track item
}
void koto_audiobook_view_handle_play_clicked(
GtkButton * button,
gpointer user_data
) {
(void) button;
KotoAudiobookView * self = user_data;
if (!KOTO_IS_AUDIOBOOK_VIEW(self)) {
return;
}
if (!KOTO_IS_ALBUM(self->album)) { // Don't have an album associated with this view
return;
}
koto_playlist_commit(koto_album_get_playlist(self->album)); // Ensure we commit the current playlist to the database. This is needed to ensure we are able to save the playlist state going forward
koto_current_playlist_set_playlist(current_playlist, koto_album_get_playlist(self->album), TRUE, TRUE); // Set this playlist to be played immediately, play current track
}
void koto_audiobook_view_handle_playlist_updated(
KotoPlaylist * playlist,
gpointer user_data
) {
(void) playlist;
KotoAudiobookView * self = user_data;
if (!KOTO_IS_AUDIOBOOK_VIEW(self)) {
return;
}
koto_audiobook_view_update_side_info(self); // Update the side info based on the playlist modification
}
void koto_audiobook_view_set_album(
KotoAudiobookView * self,
KotoAlbum * album
) {
if (!KOTO_IS_AUDIOBOOK_VIEW(self)) {
return;
}
if (!KOTO_IS_ALBUM(album)) { // Not an album
return;
}
self->album = album;
gchar * album_art_path = koto_album_get_art(album); // Get any artwork
if (koto_utils_string_is_valid(album_art_path)) { // Have album art
gtk_image_set_from_file(GTK_IMAGE(self->audiobook_art), album_art_path); // Set our album art
}
koto_album_info_set_album_uuid(self->album_info, koto_album_get_uuid(album)); // Apply our album info
gtk_list_box_bind_model(
// Apply our binding for the GtkListBox
GTK_LIST_BOX(self->tracks_list),
G_LIST_MODEL(koto_album_get_store(album)),
koto_audiobook_view_create_track_item,
NULL,
koto_audiobook_view_destroy_associated_user_data
);
KotoPlaylist * album_playlist = koto_album_get_playlist(self->album); // Get the album playlist
if (!KOTO_IS_PLAYLIST(album_playlist)) { // Not a playlist
return;
}
g_signal_connect(album_playlist, "modified", G_CALLBACK(koto_audiobook_view_handle_playlist_updated), self); // Handle modifications of a playlist
g_signal_connect(album_playlist, "track-load-finalized", G_CALLBACK(koto_audiobook_view_handle_playlist_updated), self); // Handle when a playlist is finalized
koto_audiobook_view_update_side_info(self); // Update our side info
}
void koto_audiobook_view_update_side_info(KotoAudiobookView * self) {
if (!KOTO_IS_AUDIOBOOK_VIEW(self)) {
return;
}
if (!KOTO_IS_ALBUM(self->album)) { // Not an album
return;
}
KotoPlaylist * album_playlist = koto_album_get_playlist(self->album); // Get the album playlist
if (!KOTO_IS_PLAYLIST(album_playlist)) { // Not a playlist
return;
}
gint playlist_position = koto_playlist_get_current_position(album_playlist); // Get the current position in the playlist
gint playlist_length = koto_playlist_get_length(album_playlist); // Get the length of the playlist
KotoTrack * current_track = koto_playlist_get_current_track(album_playlist); // Get the track for the playlist
if (!KOTO_IS_TRACK(current_track)) { // Not a track
return;
}
guint playback_position = koto_track_get_playback_position(current_track);
guint duration = koto_track_get_duration(current_track);
gboolean continuing = (playlist_position >= 1) || ((playlist_position <= 0) && (playback_position != 0));
gtk_button_set_label(GTK_BUTTON(self->playback_button), continuing ? "Resume" : "Play");
if (continuing) { // Have been playing track
gtk_label_set_text(GTK_LABEL(self->chapter_info), g_strdup_printf("Track %i of %i", (playlist_position <= 0) ? 1 : (playlist_position + 1), playlist_length));
gtk_label_set_text(GTK_LABEL(self->playback_position), g_strdup_printf("%s / %s", koto_utils_seconds_to_time_format(playback_position), koto_utils_seconds_to_time_format(duration)));
gtk_widget_show(self->chapter_info);
gtk_widget_show(self->playback_position);
} else {
gtk_widget_hide(self->chapter_info); // Hide by default
gtk_widget_hide(self->playback_position); // Hide by default
}
}
void koto_audiobook_view_destroy_associated_user_data(gpointer user_data) {
(void) user_data;
}
KotoAudiobookView * koto_audiobook_view_new() {
return g_object_new(
KOTO_TYPE_AUDIOBOOK_VIEW,
"orientation",
GTK_ORIENTATION_HORIZONTAL,
NULL
);
}