diff --git a/data/genres/business-and-personal-finance.png b/data/genres/business-and-personal-finance.png new file mode 100644 index 0000000..789aadb Binary files /dev/null and b/data/genres/business-and-personal-finance.png differ diff --git a/data/genres/foreign-languages.png b/data/genres/foreign-languages.png new file mode 100644 index 0000000..5499f4e Binary files /dev/null and b/data/genres/foreign-languages.png differ diff --git a/data/genres/mystery-and-thriller.png b/data/genres/mystery-and-thriller.png new file mode 100644 index 0000000..d42cded Binary files /dev/null and b/data/genres/mystery-and-thriller.png differ diff --git a/data/genres/sci-fi.png b/data/genres/sci-fi.png new file mode 100644 index 0000000..959b6c8 Binary files /dev/null and b/data/genres/sci-fi.png differ diff --git a/data/genres/travel.png b/data/genres/travel.png new file mode 100644 index 0000000..41ba389 Binary files /dev/null and b/data/genres/travel.png differ diff --git a/data/vectors/business-and-personal-finance.svg b/data/vectors/business-and-personal-finance.svg new file mode 100644 index 0000000..7bb925c --- /dev/null +++ b/data/vectors/business-and-personal-finance.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/vectors/foreign-languages.svg b/data/vectors/foreign-languages.svg new file mode 100644 index 0000000..c423fb9 --- /dev/null +++ b/data/vectors/foreign-languages.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + Bonjour + Hello + + + + diff --git a/data/vectors/mystery-and-thriller.svg b/data/vectors/mystery-and-thriller.svg new file mode 100644 index 0000000..9b7dad0 --- /dev/null +++ b/data/vectors/mystery-and-thriller.svg @@ -0,0 +1,572 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/vectors/sci-fi.svg b/data/vectors/sci-fi.svg new file mode 100644 index 0000000..5b4161e --- /dev/null +++ b/data/vectors/sci-fi.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + diff --git a/data/vectors/travel.svg b/data/vectors/travel.svg new file mode 100644 index 0000000..d70a1bc --- /dev/null +++ b/data/vectors/travel.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/koto-action-bar.c b/src/components/action-bar.c similarity index 95% rename from src/components/koto-action-bar.c rename to src/components/action-bar.c index 3270d8a..698777c 100644 --- a/src/components/koto-action-bar.c +++ b/src/components/action-bar.c @@ -1,4 +1,4 @@ -/* koto-action-bar.c +/* action-bar.c * * Copyright 2021 Joshua Strobl * @@ -17,7 +17,6 @@ #include #include -#include "koto-action-bar.h" #include "../config/config.h" #include "../db/cartographer.h" #include "../indexer/album-playlist-funcs.h" @@ -25,9 +24,10 @@ #include "../playlist/add-remove-track-popover.h" #include "../playlist/current.h" #include "../playback/engine.h" -#include "../koto-button.h" #include "../koto-utils.h" #include "../koto-window.h" +#include "action-bar.h" +#include "button.h" extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup; extern KotoCartographer * koto_maps; @@ -263,12 +263,12 @@ void koto_action_bar_handle_play_track_button_clicked( KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->current_album_uuid); // Get the Album if (KOTO_IS_ALBUM(album)) { // Have an Album - playlist = koto_album_create_playlist(album); // Create our playlist dynamically for the Album + playlist = koto_album_get_playlist(album); // Create our playlist dynamically for the Album } } if (KOTO_IS_PLAYLIST(playlist)) { // Is a playlist - koto_current_playlist_set_playlist(current_playlist, playlist, FALSE); // Update our playlist to the one associated with the track we are playing + koto_current_playlist_set_playlist(current_playlist, playlist, FALSE, FALSE); // Update our playlist to the one associated with the track we are playing koto_playlist_set_track_as_current(playlist, koto_track_get_uuid(track)); // Get this track as the current track in the position } @@ -295,7 +295,7 @@ void koto_action_bar_handle_remove_from_playlist_button_clicked( goto doclose; } - if (!koto_utils_is_string_valid(self->current_playlist_uuid)) { // Not valid UUID + if (!koto_utils_string_is_valid(self->current_playlist_uuid)) { // Not valid UUID goto doclose; } @@ -325,11 +325,11 @@ void koto_action_bar_set_tracks_in_album_selection( return; } - if (koto_utils_is_string_valid(self->current_album_uuid)) { // Album UUID currently set + if (koto_utils_string_is_valid(self->current_album_uuid)) { // Album UUID currently set g_free(self->current_album_uuid); } - if (koto_utils_is_string_valid(self->current_playlist_uuid)) { // Playlist UUID currently set + if (koto_utils_string_is_valid(self->current_playlist_uuid)) { // Playlist UUID currently set g_free(self->current_playlist_uuid); } @@ -359,11 +359,11 @@ void koto_action_bar_set_tracks_in_playlist_selection( return; } - if (koto_utils_is_string_valid(self->current_album_uuid)) { // Album UUID currently set + if (koto_utils_string_is_valid(self->current_album_uuid)) { // Album UUID currently set g_free(self->current_album_uuid); } - if (koto_utils_is_string_valid(self->current_playlist_uuid)) { // Playlist UUID currently set + if (koto_utils_string_is_valid(self->current_playlist_uuid)) { // Playlist UUID currently set g_free(self->current_playlist_uuid); } diff --git a/src/components/koto-action-bar.h b/src/components/action-bar.h similarity index 99% rename from src/components/koto-action-bar.h rename to src/components/action-bar.h index d8c4572..d52281b 100644 --- a/src/components/koto-action-bar.h +++ b/src/components/action-bar.h @@ -1,4 +1,4 @@ -/* koto-action-bar.h +/* action-bar.h * * Copyright 2021 Joshua Strobl * diff --git a/src/components/album-info.c b/src/components/album-info.c new file mode 100644 index 0000000..94f0eab --- /dev/null +++ b/src/components/album-info.c @@ -0,0 +1,289 @@ +/* album-info.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 +#include +#include "../config/config.h" +#include "../db/cartographer.h" +#include "../indexer/structs.h" +#include "../koto-utils.h" +#include "album-info.h" + +extern KotoCartographer * koto_maps; +extern KotoConfig * config; + +enum { + PROP_0, + PROP_TYPE, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +struct _KotoAlbumInfo { + GtkBox parent_instance; + KotoAlbumInfoType type; + + KotoAlbum * album; + + GtkWidget * name_year_box; + GtkWidget * genres_tags_list; + + GtkWidget * name_label; + GtkWidget * year_badge; + GtkWidget * narrator_label; + + GtkWidget * description_label; +}; + +struct _KotoAlbumInfoClass { + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE(KotoAlbumInfo, koto_album_info, GTK_TYPE_BOX); + +static void koto_album_info_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_album_info_class_init(KotoAlbumInfoClass * c) { + GObjectClass * gobject_class; + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_album_info_set_property; + + props[PROP_TYPE] = g_param_spec_string( + "type", + "Type of AlbumInfo component", + "Type of AlbumInfo component", + "album", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_album_info_init(KotoAlbumInfo * self) { + gtk_widget_add_css_class(GTK_WIDGET(self), "album-info"); + self->type = KOTO_ALBUM_INFO_TYPE_ALBUM; // Default to Album here + + self->name_year_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create out name box for the name and year + gtk_widget_add_css_class(self->name_year_box, "album-title-year-combo"); + self->name_label = gtk_label_new(NULL); // Create our name label + gtk_widget_set_halign(self->name_label, GTK_ALIGN_START); + gtk_widget_set_valign(self->name_label, GTK_ALIGN_START); + gtk_widget_add_css_class(self->name_label, "album-title"); + + self->year_badge = gtk_label_new(NULL); // Create our year badge + gtk_widget_add_css_class(self->year_badge, "album-year"); + gtk_widget_add_css_class(self->year_badge, "label-badge"); + gtk_widget_set_valign(self->year_badge, GTK_ALIGN_CENTER); // Center vertically align the year badge + + self->narrator_label = gtk_label_new(NULL); // Create our narrator label + gtk_widget_add_css_class(self->narrator_label, "album-narrator"); + gtk_widget_set_halign(self->narrator_label, GTK_ALIGN_START); + + self->description_label = gtk_label_new(NULL); // Create our description label + gtk_widget_add_css_class(self->description_label, "album-description"); + gtk_widget_set_halign(self->description_label, GTK_ALIGN_START); + + gtk_box_append(GTK_BOX(self->name_year_box), self->name_label); // Add the name label to the name + year box + gtk_box_append(GTK_BOX(self->name_year_box), self->year_badge); // Add the year badge to the name + year box + + gtk_box_append(GTK_BOX(self), self->name_year_box); + gtk_box_append(GTK_BOX(self), self->narrator_label); + gtk_box_append(GTK_BOX(self), self->description_label); + + g_signal_connect(config, "notify::ui-info-show-description", G_CALLBACK(koto_album_info_apply_configuration_state), self); + g_signal_connect(config, "notify::ui-info-show-genres", G_CALLBACK(koto_album_info_apply_configuration_state), self); + g_signal_connect(config, "notify::ui-info-show-narrator", G_CALLBACK(koto_album_info_apply_configuration_state), self); + g_signal_connect(config, "notify::ui-info-show-year", G_CALLBACK(koto_album_info_apply_configuration_state), self); +} + +static void koto_album_info_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoAlbumInfo * self = KOTO_ALBUM_INFO(obj); + + switch (prop_id) { + case PROP_TYPE: + koto_album_info_set_type(self, g_value_get_string(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +void koto_album_info_apply_configuration_state( + KotoConfig * c, + guint prop_id, + KotoAlbumInfo * self +) { + (void) c; + (void) prop_id; + + gboolean show_description = TRUE; + gboolean show_genres = TRUE; + gboolean show_narrator = TRUE; + gboolean show_year = TRUE; + + g_object_get( + config, + "ui-album-info-show-description", + &show_description, + "ui-album-info-show-genres", + &show_genres, + "ui-album-info-show-narrator", + &show_narrator, + "ui-album-info-show-year", + &show_year, + NULL + ); + + if (self->type != KOTO_ALBUM_INFO_TYPE_AUDIOBOOK) { // If the type is NOT an audiobook + show_narrator = FALSE; // Narrator should never be shown. Only really applicable to audiobook + } + + if (self->type == KOTO_ALBUM_INFO_TYPE_PODCAST) { // If the type is podcast + show_year = FALSE; // Year isn't really applicable to podcast, at least not the "global" year + } + + if (koto_utils_string_is_valid(koto_album_get_description(self->album)) && show_description) { // Have description to show in the first place, and should show it + gtk_widget_show(self->description_label); + } else { // Don't have content, just hide it + gtk_widget_hide(self->description_label); + } + + if (!KOTO_IS_ALBUM(self->album)) { // Don't have an album defined + return; + } + + if (GTK_IS_FLOW_BOX(self->genres_tags_list)) { + if ((g_list_length(koto_album_get_genres(self->album)) > 0) && show_genres) { // Have genres to show in the first place, and should show it + gtk_widget_show(self->genres_tags_list); + } else { + gtk_widget_hide(self->genres_tags_list); + } + } + + if (koto_utils_string_is_valid(koto_album_get_narrator(self->album)) && show_narrator) { // Have narrator and should show it + gtk_widget_show(self->narrator_label); + } else { + gtk_widget_hide(self->narrator_label); + } + + if ((koto_album_get_year(self->album) != 0) && show_year) { // Have year and should show it + gtk_widget_show(self->year_badge); + } else { + gtk_widget_hide(self->year_badge); + } +} + +void koto_album_info_set_album_uuid( + KotoAlbumInfo * self, + gchar * album_uuid +) { + if (!KOTO_IS_ALBUM_INFO(self)) { + return; + } + + if (!koto_utils_string_is_valid(album_uuid)) { + return; + } + + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); // Get the album + + if (!KOTO_IS_ALBUM(album)) { // Is not an album + return; + } + + self->album = album; + + gtk_label_set_text(GTK_LABEL(self->name_label), koto_album_get_name(self->album)); // Set the name label text to the album + gtk_label_set_text(GTK_LABEL(self->year_badge), g_strdup_printf("%li", koto_album_get_year(self->album))); // Set the year label text to any year for the album + + gchar * narrator = koto_album_get_narrator(self->album); + + if (koto_utils_string_is_valid(narrator)) { // Have a narrator + gtk_label_set_text(GTK_LABEL(self->narrator_label), g_strdup_printf("Narrated by %s", koto_album_get_narrator(self->album))); // Set narrated by string + } + + gchar * description = koto_album_get_description(self->album); + + if (koto_utils_string_is_valid(description)) { // Have a description + gtk_label_set_markup(GTK_LABEL(self->description_label), description); // Set the markup so we treat it more like HTML and let pango do its thing + } + + if (GTK_IS_BOX(self->genres_tags_list)) { // Genres Flow + gtk_box_remove(GTK_BOX(self), self->genres_tags_list); // Remove the genres flow from the box + } + + self->genres_tags_list = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create our flow box for the genres + gtk_widget_add_css_class(self->genres_tags_list, "genres-tag-list"); + + GList * album_genres = koto_album_get_genres(self->album); + if (g_list_length(album_genres) != 0) { // Have genres + GList * cur_list; + for (cur_list = album_genres; cur_list != NULL; cur_list = cur_list->next) { // Iterate over each genre + GtkWidget * genre_label = gtk_label_new(koto_utils_string_title(cur_list->data)); + gtk_widget_add_css_class(genre_label, "label-badge"); // Add label badge styling + gtk_box_append(GTK_BOX(self->genres_tags_list), genre_label); // Append to the genre tags list + } + } else { + gtk_widget_hide(self->genres_tags_list); // Hide the genres tag list + } + + gtk_box_insert_child_after(GTK_BOX(self), self->genres_tags_list, self->name_year_box); // Add after the name+year flowbox + + koto_album_info_apply_configuration_state(NULL, 0, self); // Apply our configuration state immediately so we know what to show and hide for this album based on the config +} + +void koto_album_info_set_type( + KotoAlbumInfo * self, + const gchar * type +) { + if (!KOTO_IS_ALBUM_INFO(self)) { + return; + } + + if (g_strcmp0(type, "audiobook") == 0) { // If this is an audiobook + self->type = KOTO_ALBUM_INFO_TYPE_AUDIOBOOK; + } else if (g_strcmp0(type, "podcast") == 0) { // If this is a podcast + self->type = KOTO_ALBUM_INFO_TYPE_PODCAST; + } else { + self->type = KOTO_ALBUM_INFO_TYPE_ALBUM; + } +} + +KotoAlbumInfo * koto_album_info_new(gchar * type) { + return g_object_new( + KOTO_TYPE_ALBUM_INFO, + "orientation", + GTK_ORIENTATION_VERTICAL, + "type", + type, + NULL + ); +} \ No newline at end of file diff --git a/src/components/album-info.h b/src/components/album-info.h new file mode 100644 index 0000000..fe2e86c --- /dev/null +++ b/src/components/album-info.h @@ -0,0 +1,52 @@ +/* album-info.h + * + * 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. + */ + +#pragma once + +#include +#include +#include "../config/config.h" + +G_BEGIN_DECLS + +typedef enum { + KOTO_ALBUM_INFO_TYPE_ALBUM, + KOTO_ALBUM_INFO_TYPE_AUDIOBOOK, + KOTO_ALBUM_INFO_TYPE_PODCAST +} KotoAlbumInfoType; + +#define KOTO_TYPE_ALBUM_INFO (koto_album_info_get_type()) +G_DECLARE_FINAL_TYPE(KotoAlbumInfo, koto_album_info, KOTO, ALBUM_INFO, GtkBox); +#define KOTO_IS_ALBUM_INFO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ALBUM_INFO)) + +void koto_album_info_apply_configuration_state( + KotoConfig * c, + guint prop_id, + KotoAlbumInfo * self +); + +void koto_album_info_set_album_uuid( + KotoAlbumInfo * self, + gchar * album_uuid +); + +void koto_album_info_set_type( + KotoAlbumInfo * self, + const gchar * type +); + +KotoAlbumInfo * koto_album_info_new(gchar * type); \ No newline at end of file diff --git a/src/koto-button.c b/src/components/button.c similarity index 87% rename from src/koto-button.c rename to src/components/button.c index 5a40f04..f04f545 100644 --- a/src/koto-button.c +++ b/src/components/button.c @@ -16,9 +16,12 @@ */ #include -#include "koto-button.h" #include "config/config.h" -#include "koto-utils.h" +#include "button.h" +#include "../koto-window.h" +#include "../koto-utils.h" + +extern KotoWindow * main_window; struct _PixbufSize { guint size; @@ -70,6 +73,8 @@ struct _KotoButton { GtkBox parent_instance; guint pix_size; + gpointer arbitrary_data; + GtkWidget * button_pic; GtkWidget * badge_label; GtkWidget * button_label; @@ -313,6 +318,37 @@ void koto_button_flip(KotoButton * self) { koto_button_show_image(self, !self->currently_showing_alt); } +gpointer koto_button_get_data(KotoButton * self) { + return self->arbitrary_data; +} + +void koto_button_global_page_nav_callback( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +) { + (void) gesture; + (void) n_press; + (void) x; + (void) y; + + KotoButton * btn = KOTO_BUTTON(user_data); // Cast our user data as a button + + if (!KOTO_IS_BUTTON(btn)) { // Not a button + return; + } + + gchar * btn_nav_uuid = (gchar*) koto_button_get_data(btn); // Get the data + + if (!koto_utils_string_is_valid(btn_nav_uuid)) { // Not a valid string + return; + } + + koto_window_go_to_page(main_window, btn_nav_uuid); +} + void koto_button_handle_mouse_enter( GtkEventControllerMotion * controller, double x, @@ -357,6 +393,17 @@ void koto_button_hide_image(KotoButton * self) { } } +void koto_button_set_data( + KotoButton * self, + gpointer data +) { + if (!KOTO_IS_BUTTON(self)) { // Not a button + return; + } + + self->arbitrary_data = data; +} + void koto_button_set_pseudoactive_styling(KotoButton * self) { if (!KOTO_IS_BUTTON(self)) { // Not a button return; @@ -408,14 +455,19 @@ void koto_button_set_file_path( return; } - if (!koto_utils_is_string_valid(file_path)) { // file path is invalid + if (!koto_utils_string_is_valid(file_path)) { // file path is invalid return; } - if (koto_utils_is_string_valid(self->image_file_path)) { // image file path is valid + if (g_strcmp0(self->image_file_path, file_path) == 0) { // Request setting as same file + return; + } + + if (koto_utils_string_is_valid(self->image_file_path)) { // image file path is valid g_free(self->image_file_path); } + self->use_from_file = TRUE; self->image_file_path = g_strdup(file_path); koto_button_show_image(self, FALSE); } @@ -429,7 +481,7 @@ void koto_button_set_icon_name( return; } - if (!koto_utils_is_string_valid(icon_name)) { // Not a valid icon + if (!koto_utils_string_is_valid(icon_name)) { // Not a valid icon return; } @@ -517,18 +569,18 @@ void koto_button_set_text( return; } - if (!koto_utils_is_string_valid(text)) { // Invalid text + if (!koto_utils_string_is_valid(text)) { // Invalid text return; } - if (koto_utils_is_string_valid(self->text)) { // Text defined + if (koto_utils_string_is_valid(self->text)) { // Text defined g_free(self->text); // Free existing text } self->text = g_strdup(text); if (GTK_IS_LABEL(self->button_label)) { // If we have a button label - if (koto_utils_is_string_valid(self->text)) { // Have text set + if (koto_utils_string_is_valid(self->text)) { // Have text set gtk_label_set_text(GTK_LABEL(self->button_label), self->text); gtk_widget_show(self->button_label); // Show the label } else { // Have a label but no longer text @@ -536,8 +588,9 @@ void koto_button_set_text( g_free(self->button_label); } } else { // If we do not have a button label - if (koto_utils_is_string_valid(self->text)) { // If we have text + if (koto_utils_string_is_valid(self->text)) { // If we have text self->button_label = gtk_label_new(self->text); // Create our label + gtk_widget_set_hexpand(self->button_label, TRUE); gtk_label_set_xalign(GTK_LABEL(self->button_label), 0); if (GTK_IS_IMAGE(self->button_pic)) { // If we have an image @@ -551,6 +604,45 @@ void koto_button_set_text( g_object_notify_by_pspec(G_OBJECT(self), btn_props[PROP_TEXT]); } +void koto_button_set_text_justify( + KotoButton * self, + GtkJustification j +) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (!GTK_IS_LABEL(self->button_label)) { // If we do not have a button label + return; + } + + if (j == GTK_JUSTIFY_CENTER) { // Center text + gtk_label_set_xalign(GTK_LABEL(self->button_label), 0.5); + } else if (j == GTK_JUSTIFY_RIGHT) { // Right align + gtk_label_set_xalign(GTK_LABEL(self->button_label), 1.0); + } else { + gtk_label_set_xalign(GTK_LABEL(self->button_label), 0); + } + + gtk_label_set_justify(GTK_LABEL(self->button_label), j); +} + +void koto_button_set_text_wrap( + KotoButton * self, + gboolean wrap +) { + if (!KOTO_IS_BUTTON(self)) { + return; + } + + if (!GTK_IS_LABEL(self->button_label)) { // If we do not have a button label + return; + } + + gtk_label_set_single_line_mode(GTK_LABEL(self->button_label), !wrap); + gtk_label_set_wrap(GTK_LABEL(self->button_label), wrap); +} + void koto_button_show_image( KotoButton * self, gboolean use_alt @@ -560,7 +652,7 @@ void koto_button_show_image( } if (self->use_from_file) { // Use from a file instead of icon name - if (!koto_utils_is_string_valid(self->image_file_path)) { // Not set + if (!koto_utils_string_is_valid(self->image_file_path)) { // Not set return; } diff --git a/src/koto-button.h b/src/components/button.h similarity index 88% rename from src/koto-button.h rename to src/components/button.h index 88825ba..f41359b 100644 --- a/src/koto-button.h +++ b/src/components/button.h @@ -1,4 +1,4 @@ -/* koto-button.h +/* button.h * * Copyright 2021 Joshua Strobl * @@ -75,6 +75,16 @@ void koto_button_add_click_handler( void koto_button_flip(KotoButton * self); +gpointer koto_button_get_data(KotoButton * self); + +void koto_button_global_page_nav_callback( + GtkGestureClick * gesture, + int n_press, + double x, + double y, + gpointer user_data +); + void koto_button_handle_mouse_enter( GtkEventControllerMotion * controller, double x, @@ -89,6 +99,11 @@ void koto_button_handle_mouse_leave( void koto_button_hide_image(KotoButton * self); +void koto_button_set_data( + KotoButton * self, + gpointer data +); + void koto_button_set_pseudoactive_styling(KotoButton * self); void koto_button_set_badge_text( @@ -127,6 +142,16 @@ void koto_button_set_text( gchar * text ); +void koto_button_set_text_justify( + KotoButton * self, + GtkJustification j +); + +void koto_button_set_text_wrap( + KotoButton * self, + gboolean wrap +); + void koto_button_show_image( KotoButton * self, gboolean use_alt diff --git a/src/components/koto-cover-art-button.c b/src/components/cover-art-button.c similarity index 94% rename from src/components/koto-cover-art-button.c rename to src/components/cover-art-button.c index 3d872b9..e94c2fd 100644 --- a/src/components/koto-cover-art-button.c +++ b/src/components/cover-art-button.c @@ -1,4 +1,4 @@ -/* koto-cover-art-button.c +/* cover-art-button.c * * Copyright 2021 Joshua Strobl * @@ -17,9 +17,9 @@ #include #include -#include "koto-cover-art-button.h" -#include "../koto-button.h" #include "../koto-utils.h" +#include "button.h" +#include "cover-art-button.h" struct _KotoCoverArtButton { GObject parent_instance; @@ -29,6 +29,8 @@ struct _KotoCoverArtButton { GtkWidget * revealer; KotoButton * play_button; + gchar * art_path; + guint height; guint width; }; @@ -119,6 +121,8 @@ static void koto_cover_art_button_init(KotoCoverArtButton * self) { g_signal_connect(motion_controller, "enter", G_CALLBACK(koto_cover_art_button_show_overlay_controls), self); g_signal_connect(motion_controller, "leave", G_CALLBACK(koto_cover_art_button_hide_overlay_controls), self); gtk_widget_add_controller(self->main, motion_controller); + + self->art_path = NULL; } static void koto_cover_art_button_get_property( @@ -194,13 +198,16 @@ void koto_cover_art_button_set_art_path( return; } - gboolean defined_artwork = koto_utils_is_string_valid(art_path); + gboolean defined_artwork = koto_utils_string_is_valid(art_path); if (GTK_IS_IMAGE(self->art)) { // Already have an image if (!defined_artwork) { // No art path or empty string gtk_image_set_from_icon_name(GTK_IMAGE(self->art), "audio-x-generic-symbolic"); } else { // Have an art path - gtk_image_set_from_file(GTK_IMAGE(self->art), g_strdup(art_path)); // Set from the file + if (g_strcmp0(self->art_path, art_path) != 0) { + self->art_path = g_strdup(art_path); // Set our art path + gtk_image_set_from_file(GTK_IMAGE(self->art), self->art_path); // Set from the file + } } } else { // If we don't have an image self->art = koto_utils_create_image_from_filepath(defined_artwork ? g_strdup(art_path) : NULL, "audio-x-generic-symbolic", self->width, self->height); diff --git a/src/components/koto-cover-art-button.h b/src/components/cover-art-button.h similarity index 96% rename from src/components/koto-cover-art-button.h rename to src/components/cover-art-button.h index 9d9b843..d1f7d44 100644 --- a/src/components/koto-cover-art-button.h +++ b/src/components/cover-art-button.h @@ -1,4 +1,4 @@ -/* koto-cover-art-button.h +/* cover-art-button.h * * Copyright 2021 Joshua Strobl * @@ -17,7 +17,7 @@ #include #include -#include "../koto-button.h" +#include "button.h" G_BEGIN_DECLS diff --git a/src/koto-track-item.c b/src/components/track-item.c similarity index 97% rename from src/koto-track-item.c rename to src/components/track-item.c index ca42dfe..368e79f 100644 --- a/src/koto-track-item.c +++ b/src/components/track-item.c @@ -1,4 +1,4 @@ -/* koto-track-item.c +/* track-item.c * * Copyright 2021 Joshua Strobl * @@ -18,8 +18,8 @@ #include #include "indexer/structs.h" #include "playlist/add-remove-track-popover.h" -#include "koto-button.h" -#include "koto-track-item.h" +#include "button.h" +#include "track-item.h" extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup; diff --git a/src/koto-track-item.h b/src/components/track-item.h similarity index 98% rename from src/koto-track-item.h rename to src/components/track-item.h index 757e307..762cf44 100644 --- a/src/koto-track-item.h +++ b/src/components/track-item.h @@ -1,4 +1,4 @@ -/* koto-track-item.h +/* track-item.h * * Copyright 2021 Joshua Strobl * diff --git a/src/components/track-table.c b/src/components/track-table.c index 0d3cd7f..0907c08 100644 --- a/src/components/track-table.c +++ b/src/components/track-table.c @@ -21,10 +21,10 @@ #include "../playback/engine.h" #include "../playlist/current.h" #include "../playlist/playlist.h" -#include "../koto-button.h" #include "../koto-utils.h" #include "../koto-window.h" -#include "koto-action-bar.h" +#include "action-bar.h" +#include "button.h" #include "track-table.h" extern KotoActionBar * action_bar; @@ -122,14 +122,12 @@ void koto_track_table_bind_track_item( return; } - gchar * track_name = NULL; + gchar * track_name = koto_track_get_name(track); gchar * album_uuid = NULL; gchar * artist_uuid = NULL; g_object_get( track, - "parsed-name", - &track_name, "album-uuid", &album_uuid, "artist-uuid", @@ -142,7 +140,7 @@ void koto_track_table_bind_track_item( gtk_label_set_label(GTK_LABEL(track_position_label), g_strdup_printf("%u", track_position)); // Set the track position gtk_label_set_label(GTK_LABEL(track_name_label), track_name); // Set our track name - if (koto_utils_is_string_valid(album_uuid)) { // Is associated with an album + if (koto_utils_string_is_valid(album_uuid)) { // Is associated with an album KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); if (KOTO_IS_ALBUM(album)) { @@ -150,10 +148,12 @@ void koto_track_table_bind_track_item( } } - KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); + if (koto_utils_string_is_valid(artist_uuid)) { // Is associated with an artist + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); - if (KOTO_IS_ARTIST(artist)) { - gtk_label_set_label(GTK_LABEL(track_artist_label), koto_artist_get_name(artist)); // Get the name of the artist and set it to the label + if (KOTO_IS_ARTIST(artist)) { + gtk_label_set_label(GTK_LABEL(track_artist_label), koto_artist_get_name(artist)); // Get the name of the artist and set it to the label + } } GList * data = NULL; @@ -229,7 +229,7 @@ void koto_track_table_handle_track_album_clicked ( gtk_widget_add_css_class(GTK_WIDGET(self->track_album_button), "active"); koto_button_hide_image(self->track_num_button); // Go back to hiding the image - koto_track_table_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM); + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM); } void koto_track_table_handle_track_artist_clicked( @@ -247,7 +247,7 @@ void koto_track_table_handle_track_artist_clicked( gtk_widget_add_css_class(GTK_WIDGET(self->track_artist_button), "active"); koto_button_hide_image(self->track_num_button); // Go back to hiding the image - koto_track_table_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST); + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST); } void koto_track_table_item_handle_clicked( @@ -276,7 +276,7 @@ void koto_track_table_item_handle_clicked( KotoTrackTable * self = g_list_nth_data(data, 0); gchar * track_uuid = g_list_nth_data(data, 1); - if (!koto_utils_is_string_valid(track_uuid)) { // Not a valid string + if (!koto_utils_string_is_valid(track_uuid)) { // Not a valid string return; } @@ -284,7 +284,7 @@ void koto_track_table_item_handle_clicked( gtk_widget_grab_focus(GTK_WIDGET(main_window)); // Focus on the window koto_action_bar_toggle_reveal(action_bar, FALSE); koto_action_bar_close(action_bar); // Close the action bar - koto_current_playlist_set_playlist(current_playlist, self->playlist, FALSE); // Set the current playlist to the artist's playlist but do not play immediately + koto_current_playlist_set_playlist(current_playlist, self->playlist, FALSE, FALSE); // Set the current playlist to the artist's playlist but do not play immediately koto_playlist_set_track_as_current(self->playlist, track_uuid); // Set this track as the current one for the playlist koto_playback_engine_set_track_by_uuid(playback_engine, track_uuid, FALSE); // Tell our playback engine to start playing at this track } @@ -304,7 +304,7 @@ void koto_track_table_handle_track_name_clicked( gtk_widget_add_css_class(GTK_WIDGET(self->track_title_button), "active"); koto_button_hide_image(self->track_num_button); // Go back to hiding the image - koto_track_table_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME); + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME); } void koto_track_table_handle_track_num_clicked( @@ -320,13 +320,13 @@ void koto_track_table_handle_track_num_clicked( (void) y; KotoTrackTable * self = user_data; - KotoPreferredModelType current_model = koto_playlist_get_current_model(self->playlist); + KotoPreferredPlaylistSortType current_model = koto_playlist_get_current_model(self->playlist); - if (current_model == KOTO_PREFERRED_MODEL_TYPE_DEFAULT) { // Set to newest currently - koto_track_table_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST); // Sort reversed (oldest) + if (current_model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT) { // Set to newest currently + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST); // Sort reversed (oldest) koto_button_show_image(self->track_num_button, TRUE); // Use inverted value (pan-up-symbolic) } else { - koto_track_table_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // Sort newest + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT); // Sort newest koto_button_show_image(self->track_num_button, FALSE); // Use pan down default } } @@ -385,7 +385,7 @@ void koto_track_table_handle_tracks_selected( void koto_track_table_set_model( KotoTrackTable * self, - KotoPreferredModelType model + KotoPreferredPlaylistSortType model ) { if (!KOTO_IS_TRACK_TABLE(self)) { return; @@ -394,15 +394,15 @@ void koto_track_table_set_model( koto_playlist_apply_model(self->playlist, model); // Apply our new model self->model = G_LIST_MODEL(koto_playlist_get_store(self->playlist)); // Get the latest generated model / store and cast it as a GListModel - if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM) { // Not sorting by album currently + if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM) { // Not sorting by album currently gtk_widget_remove_css_class(GTK_WIDGET(self->track_album_button), "active"); } - if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST) { // Not sorting by artist currently + if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST) { // Not sorting by artist currently gtk_widget_remove_css_class(GTK_WIDGET(self->track_artist_button), "active"); } - if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME) { // Not sorting by track name + if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME) { // Not sorting by track name gtk_widget_remove_css_class(GTK_WIDGET(self->track_title_button), "active"); } @@ -414,7 +414,7 @@ void koto_track_table_set_model( void koto_track_table_set_playlist_model ( KotoTrackTable * self, - KotoPreferredModelType model + KotoPreferredPlaylistSortType model ) { if (!KOTO_IS_TRACK_TABLE(self)) { // Not a track table return; @@ -427,15 +427,15 @@ void koto_track_table_set_playlist_model ( koto_playlist_apply_model(self->playlist, model); // Apply our new model self->model = G_LIST_MODEL(koto_playlist_get_store(self->playlist)); // Get the latest generated model / store and cast it as a GListModel - if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM) { // Not sorting by album currently + if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM) { // Not sorting by album currently gtk_widget_remove_css_class(GTK_WIDGET(self->track_album_button), "active"); } - if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST) { // Not sorting by artist currently + if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST) { // Not sorting by artist currently gtk_widget_remove_css_class(GTK_WIDGET(self->track_artist_button), "active"); } - if (model != KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME) { // Not sorting by track name + if (model != KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME) { // Not sorting by track name gtk_widget_remove_css_class(GTK_WIDGET(self->track_title_button), "active"); } @@ -444,9 +444,9 @@ void koto_track_table_set_playlist_model ( gtk_list_view_set_model(GTK_LIST_VIEW(self->track_list_view), self->selection_model); // Set our multi selection model to our provided model - KotoPreferredModelType current_model = koto_playlist_get_current_model(self->playlist); // Get the current model + KotoPreferredPlaylistSortType current_model = koto_playlist_get_current_model(self->playlist); // Get the current model - if (current_model == KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST) { + if (current_model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST) { koto_button_show_image(self->track_num_button, TRUE); // Immediately use pan-up-symbolic } } @@ -464,7 +464,7 @@ void koto_track_table_set_playlist( } self->playlist = playlist; - koto_track_table_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // TODO: Enable this to be changed + koto_track_table_set_playlist_model(self, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT); // TODO: Enable this to be changed } void koto_track_table_setup_track_item( diff --git a/src/components/track-table.h b/src/components/track-table.h index 287d9b4..613cbf3 100644 --- a/src/components/track-table.h +++ b/src/components/track-table.h @@ -20,7 +20,7 @@ #include #include #include "../playlist/playlist.h" -#include "koto-action-bar.h" +#include "action-bar.h" G_BEGIN_DECLS @@ -98,12 +98,12 @@ void koto_track_table_handle_tracks_selected( void koto_track_table_set_model( KotoTrackTable * self, - KotoPreferredModelType model + KotoPreferredPlaylistSortType model ); void koto_track_table_set_playlist_model( KotoTrackTable * self, - KotoPreferredModelType model + KotoPreferredPlaylistSortType model ); void koto_track_table_set_playlist( diff --git a/src/config/config.c b/src/config/config.c index 21624b8..d97403f 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -37,6 +37,11 @@ enum { PROP_PLAYBACK_CONTINUE_ON_PLAYLIST, PROP_PLAYBACK_LAST_USED_VOLUME, PROP_PLAYBACK_MAINTAIN_SHUFFLE, + PROP_PREFERRED_ALBUM_SORT_TYPE, + PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION, + PROP_UI_ALBUM_INFO_SHOW_GENRES, + PROP_UI_ALBUM_INFO_SHOW_NARRATOR, + PROP_UI_ALBUM_INFO_SHOW_YEAR, PROP_UI_THEME_DESIRED, PROP_UI_THEME_OVERRIDE, N_PROPS, @@ -69,8 +74,17 @@ struct _KotoConfig { gdouble playback_last_used_volume; gboolean playback_maintain_shuffle; + /* Misc Prefs */ + + KotoPreferredAlbumSortType preferred_album_sort_type; + /* UI Settings */ + gboolean ui_album_info_show_description; + gboolean ui_album_info_show_genres; + gboolean ui_album_info_show_narrator; + gboolean ui_album_info_show_year; + gchar * ui_theme_desired; gboolean ui_theme_override; }; @@ -132,6 +146,46 @@ static void koto_config_class_init(KotoConfigClass * c) { G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); + config_props[PROP_PREFERRED_ALBUM_SORT_TYPE] = g_param_spec_string( + "artist-preferred-album-sort-type", + "Preferred album sort type (chronological or alphabetical-only)", + "Preferred album sort type (chronological or alphabetical-only)", + "default", + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION] = g_param_spec_boolean( + "ui-album-info-show-description", + "Show Description in Album Info", + "Show Description in Album Info", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_ALBUM_INFO_SHOW_GENRES] = g_param_spec_boolean( + "ui-album-info-show-genres", + "Show Genres in Album Info", + "Show Genres in Album Info", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_ALBUM_INFO_SHOW_NARRATOR] = g_param_spec_boolean( + "ui-album-info-show-narrator", + "Show Narrator in Album Info", + "Show Narrator in Album Info", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + config_props[PROP_UI_ALBUM_INFO_SHOW_YEAR] = g_param_spec_boolean( + "ui-album-info-show-year", + "Show Year in Album Info", + "Show Year in Album Info", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + config_props[PROP_UI_THEME_DESIRED] = g_param_spec_string( "ui-theme-desired", "Desired Theme", @@ -181,6 +235,18 @@ static void koto_config_get_property( case PROP_PLAYBACK_MAINTAIN_SHUFFLE: g_value_set_boolean(val, self->playback_maintain_shuffle); break; + case PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION: + g_value_set_boolean(val, self->ui_album_info_show_description); + break; + case PROP_UI_ALBUM_INFO_SHOW_GENRES: + g_value_set_boolean(val, self->ui_album_info_show_genres); + break; + case PROP_UI_ALBUM_INFO_SHOW_NARRATOR: + g_value_set_boolean(val, self->ui_album_info_show_narrator); + break; + case PROP_UI_ALBUM_INFO_SHOW_YEAR: + g_value_set_boolean(val, self->ui_album_info_show_year); + break; case PROP_UI_THEME_DESIRED: g_value_set_string(val, g_strdup(self->ui_theme_desired)); break; @@ -211,6 +277,21 @@ static void koto_config_set_property( case PROP_PLAYBACK_MAINTAIN_SHUFFLE: self->playback_maintain_shuffle = g_value_get_boolean(val); break; + case PROP_PREFERRED_ALBUM_SORT_TYPE: + self->preferred_album_sort_type = g_strcmp0(g_value_get_string(val), "alphabetical") ? KOTO_PREFERRED_ALBUM_ALWAYS_ALPHABETICAL : KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT; + break; + case PROP_UI_ALBUM_INFO_SHOW_DESCRIPTION: + self->ui_album_info_show_description = g_value_get_boolean(val); + break; + case PROP_UI_ALBUM_INFO_SHOW_GENRES: + self->ui_album_info_show_genres = g_value_get_boolean(val); + break; + case PROP_UI_ALBUM_INFO_SHOW_NARRATOR: + self->ui_album_info_show_narrator = g_value_get_boolean(val); + break; + case PROP_UI_ALBUM_INFO_SHOW_YEAR: + self->ui_album_info_show_year = g_value_get_boolean(val); + break; case PROP_UI_THEME_DESIRED: self->ui_theme_desired = g_strdup(g_value_get_string(val)); break; @@ -238,7 +319,7 @@ toml_table_t * koto_config_get_library( gchar * lib_uuid = (uuid_datum.ok) ? (gchar*) uuid_datum.u.s : NULL; - if (koto_utils_is_string_valid(lib_uuid) && (g_strcmp0(library_uuid, lib_uuid) == 0)) { // Is a valid string and the libraries match + if (koto_utils_string_is_valid(lib_uuid) && (g_strcmp0(library_uuid, lib_uuid) == 0)) { // Is a valid string and the libraries match return library; } } @@ -253,7 +334,7 @@ void koto_config_load( KotoConfig * self, gchar * path ) { - if (!koto_utils_is_string_valid(path)) { // Path is not valid + if (!koto_utils_string_is_valid(path)) { // Path is not valid return; } @@ -374,6 +455,30 @@ void koto_config_load( toml_table_t * ui_section = toml_table_in(conf, "ui"); if (ui_section) { // Have UI section + toml_datum_t album_info_show_description = toml_bool_in(ui_section, "album-info-show-description"); + + if (album_info_show_description.ok && (album_info_show_description.u.b != self->ui_album_info_show_description)) { // Changed if we are disabling description + g_object_set(self, "ui-album-info-show-description", album_info_show_description.u.b, NULL); + } + + toml_datum_t album_info_show_genres = toml_bool_in(ui_section, "album-info-show-genres"); + + if (album_info_show_genres.ok && (album_info_show_genres.u.b != self->ui_album_info_show_genres)) { // Changed if we are disabling description + g_object_set(self, "ui-album-info-show-genres", album_info_show_genres.u.b, NULL); + } + + toml_datum_t album_info_show_narrator = toml_bool_in(ui_section, "album-info-show-narrator"); + + if (album_info_show_narrator.ok && (album_info_show_narrator.u.b != self->ui_album_info_show_narrator)) { // Changed if we are disabling description + g_object_set(self, "ui-album-info-show-narrator", album_info_show_description.u.b, NULL); + } + + toml_datum_t album_info_show_year = toml_bool_in(ui_section, "album-info-show-year"); + + if (album_info_show_year.ok && (album_info_show_year.u.b != self->ui_album_info_show_year)) { // Changed if we are disabling description + g_object_set(self, "ui-album-info-show-year", album_info_show_year.u.b, NULL); + } + toml_datum_t name = toml_string_in(ui_section, "theme-desired"); if (name.ok && (g_strcmp0(name.u.s, self->ui_theme_desired) != 0)) { // Have a name specified and they are different @@ -472,6 +577,10 @@ void koto_config_monitor_handle_changed( } } +KotoPreferredAlbumSortType koto_config_get_preferred_album_sort_type(KotoConfig * self) { + return self->preferred_album_sort_type; +} + /** * Refresh will handle any FS notify change on our Koto config file and call load **/ @@ -568,7 +677,7 @@ void koto_config_save(KotoConfig * self) { } gchar * key_name = g_strdup(current_section_keyname->data); - gchar * key_name_replaced = koto_utils_replace_string_all(key_name, g_strdup_printf("%s-", (gchar*) section_name), ""); // Remove SECTIONNAME- + gchar * key_name_replaced = koto_utils_string_replace_all(key_name, g_strdup_printf("%s-", (gchar*) section_name), ""); // Remove SECTIONNAME- const gchar * line = g_strdup_printf("\t%s = %s", key_name_replaced, prop_val); diff --git a/src/config/config.h b/src/config/config.h index 2c68b18..5aa5f02 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -18,6 +18,7 @@ #pragma once #include #include +#include "../indexer/misc-types.h" G_BEGIN_DECLS @@ -45,6 +46,8 @@ void koto_config_monitor_handle_changed( gpointer user_data ); +KotoPreferredAlbumSortType koto_config_get_preferred_album_sort_type(KotoConfig * self); + void koto_config_refresh(KotoConfig * self); void koto_config_save(KotoConfig * self); diff --git a/src/db/cartographer.c b/src/db/cartographer.c index 87421dc..829a5c6 100644 --- a/src/db/cartographer.c +++ b/src/db/cartographer.c @@ -259,7 +259,7 @@ void koto_cartographer_add_album( gchar * album_uuid = koto_album_get_uuid(album); // Get the album UUID - if (!koto_utils_is_string_valid(album_uuid) || koto_cartographer_has_album_by_uuid(self, album_uuid)) { // Have the album or invalid UUID + if (!koto_utils_string_is_valid(album_uuid) || koto_cartographer_has_album_by_uuid(self, album_uuid)) { // Have the album or invalid UUID return; } @@ -287,7 +287,7 @@ void koto_cartographer_add_artist( gchar * artist_uuid = koto_artist_get_uuid(artist); - if (!koto_utils_is_string_valid(artist_uuid) || koto_cartographer_has_artist_by_uuid(self, artist_uuid)) { // Have the artist or invalid UUID + if (!koto_utils_string_is_valid(artist_uuid) || koto_cartographer_has_artist_by_uuid(self, artist_uuid)) { // Have the artist or invalid UUID return; } @@ -316,7 +316,7 @@ void koto_cartographer_add_library( gchar * library_uuid = koto_library_get_uuid(library); - if (!koto_utils_is_string_valid(library_uuid) || koto_cartographer_has_library_by_uuid(self, library_uuid)) { // Have the library or invalid UUID + if (!koto_utils_string_is_valid(library_uuid) || koto_cartographer_has_library_by_uuid(self, library_uuid)) { // Have the library or invalid UUID return; } @@ -387,7 +387,7 @@ void koto_cartographer_add_track( gchar * track_uuid = koto_track_get_uuid(track); - if (!koto_utils_is_string_valid(track_uuid) || koto_cartographer_has_track_by_uuid(self, track_uuid)) { // Have the track or invalid UUID + if (!koto_utils_string_is_valid(track_uuid) || koto_cartographer_has_track_by_uuid(self, track_uuid)) { // Have the track or invalid UUID return; } @@ -424,7 +424,7 @@ KotoArtist * koto_cartographer_get_artist_by_name( return NULL; } - if (!koto_utils_is_string_valid(artist_name)) { // Not a valid name + if (!koto_utils_string_is_valid(artist_name)) { // Not a valid name return NULL; } @@ -439,7 +439,7 @@ KotoArtist * koto_cartographer_get_artist_by_uuid( return NULL; } - if (!koto_utils_is_string_valid(artist_uuid)) { + if (!koto_utils_string_is_valid(artist_uuid)) { return NULL; } @@ -454,7 +454,7 @@ KotoLibrary * koto_cartographer_get_library_by_uuid( return NULL; } - if (!koto_utils_is_string_valid(library_uuid)) { // Not a valid string + if (!koto_utils_string_is_valid(library_uuid)) { // Not a valid string return NULL; } @@ -469,7 +469,7 @@ KotoLibrary * koto_cartographer_get_library_containing_path( return NULL; } - if (!koto_utils_is_string_valid(relative_path)) { // Not a valid string + if (!koto_utils_string_is_valid(relative_path)) { // Not a valid string return NULL; } @@ -503,7 +503,7 @@ GList * koto_cartographer_get_libraries_for_storage_uuid( GList * libraries = NULL; // Initialize our list - if (!koto_utils_is_string_valid(storage_uuid)) { // Not a valid storage UUID string + if (!koto_utils_string_is_valid(storage_uuid)) { // Not a valid storage UUID string return libraries; } @@ -544,6 +544,10 @@ KotoTrack * koto_cartographer_get_track_by_uuid( return NULL; } + if (!koto_utils_string_is_valid(track_uuid)) { + return NULL; + } + return g_hash_table_lookup(self->tracks, track_uuid); } @@ -555,7 +559,7 @@ KotoTrack * koto_cartographer_get_track_by_uniqueish_key( return NULL; } - if (!koto_utils_is_string_valid(key)) { + if (!koto_utils_string_is_valid(key)) { return NULL; } @@ -588,7 +592,7 @@ gboolean koto_cartographer_has_album_by_uuid( return FALSE; } - if (!koto_utils_is_string_valid(album_uuid)) { // Not a valid UUID + if (!koto_utils_string_is_valid(album_uuid)) { // Not a valid UUID return FALSE; } @@ -618,7 +622,7 @@ gboolean koto_cartographer_has_artist_by_uuid( return FALSE; } - if (!koto_utils_is_string_valid(artist_uuid)) { // Not a valid UUID + if (!koto_utils_string_is_valid(artist_uuid)) { // Not a valid UUID return FALSE; } @@ -649,7 +653,7 @@ gboolean koto_cartographer_has_library_by_uuid( return FALSE; } - if (!koto_utils_is_string_valid(library_uuid)) { // Not a valid UUID + if (!koto_utils_string_is_valid(library_uuid)) { // Not a valid UUID return FALSE; } @@ -679,7 +683,7 @@ gboolean koto_cartographer_has_playlist_by_uuid( return FALSE; } - if (!koto_utils_is_string_valid(playlist_uuid)) { // Not a valid UUID + if (!koto_utils_string_is_valid(playlist_uuid)) { // Not a valid UUID return FALSE; } @@ -709,7 +713,7 @@ gboolean koto_cartographer_has_track_by_uuid( return FALSE; } - if (!koto_utils_is_string_valid(track_uuid)) { // Not a valid UUID + if (!koto_utils_string_is_valid(track_uuid)) { // Not a valid UUID return FALSE; } @@ -739,7 +743,7 @@ void koto_cartographer_remove_album_by_uuid( return; } - if (!koto_utils_is_string_valid(album_uuid)) { + if (!koto_utils_string_is_valid(album_uuid)) { return; } @@ -792,7 +796,7 @@ void koto_cartographer_remove_artist_by_uuid( return; } - if (!koto_utils_is_string_valid(artist_uuid)) { // Artist UUID not valid + if (!koto_utils_string_is_valid(artist_uuid)) { // Artist UUID not valid return; } @@ -842,7 +846,7 @@ void koto_cartographer_remove_playlist_by_uuid( return; } - if (!koto_utils_is_string_valid(playlist_uuid)) { // Not a valid playlist UUID string + if (!koto_utils_string_is_valid(playlist_uuid)) { // Not a valid playlist UUID string return; } @@ -883,7 +887,7 @@ void koto_cartographer_remove_track_by_uuid( return; } - if (!koto_utils_is_string_valid(track_uuid)) { + if (!koto_utils_string_is_valid(track_uuid)) { return; } diff --git a/src/db/db.c b/src/db/db.c index 1c40b82..045d067 100644 --- a/src/db/db.c +++ b/src/db/db.c @@ -39,13 +39,13 @@ void close_db() { int create_db_tables() { gchar * tables_creation_queries = "CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, name string, art_path string);" - "CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, artist_id string, name string, art_path string, genres string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" + "CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, artist_id string, name string, description string, narrator string, art_path string, genres string, year int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" "CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, artist_id string, album_id string, name string, disc int, position int, duration int, genres string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" "CREATE TABLE IF NOT EXISTS libraries_albums(id string, album_id string, path string, PRIMARY KEY (id, album_id) FOREIGN KEY(album_id) REFERENCES albums(id) ON DELETE CASCADE);" "CREATE TABLE IF NOT EXISTS libraries_artists(id string, artist_id string, path string, PRIMARY KEY(id, artist_id) FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);" "CREATE TABLE IF NOT EXISTS libraries_tracks(id string, track_id string, path string, PRIMARY KEY(id, track_id) FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);" - "CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int);" - "CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, current int, FOREIGN KEY(playlist_id) REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"; + "CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int, album_id string, track_id string, playback_position_of_track int);" + "CREATE TABLE IF NOT EXISTS playlist_tracks(position INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id string, track_id string, FOREIGN KEY(playlist_id) REFERENCES playlist_meta(id), FOREIGN KEY(track_id) REFERENCES tracks(id) ON DELETE CASCADE);"; return (new_transaction(tables_creation_queries, "Failed to create required tables", TRUE) == SQLITE_OK) ? KOTO_DB_SUCCESS : KOTO_DB_FAIL; } diff --git a/src/db/loaders.c b/src/db/loaders.c index f12855f..ebbee80 100644 --- a/src/db/loaders.c +++ b/src/db/loaders.c @@ -18,6 +18,7 @@ #include "cartographer.h" #include "db.h" #include "loaders.h" +#include "../indexer/album-playlist-funcs.h" #include "../indexer/structs.h" #include "../koto-utils.h" @@ -34,8 +35,8 @@ int process_artists( (void) num_columns; (void) column_names; // Don't need any of the params - gchar * artist_uuid = g_strdup(koto_utils_unquote_string(fields[0])); // First column is UUID - gchar * artist_name = g_strdup(koto_utils_unquote_string(fields[1])); // Second column is artist name + gchar * artist_uuid = g_strdup(koto_utils_string_unquote(fields[0])); // First column is UUID + gchar * artist_name = g_strdup(koto_utils_string_unquote(fields[1])); // Second column is artist name KotoArtist * artist = koto_artist_new_with_uuid(artist_uuid); // Create our artist with the UUID @@ -63,7 +64,7 @@ int process_artists( koto_artist_set_as_finalized(artist); // Indicate it is finalized - int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE artist_id=\"%s\"", artist_uuid), process_tracks, NULL, NULL); // Process our tracks by artist uuid + int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE artist_id=\"%s\" AND album_id=''", artist_uuid), process_tracks, NULL, NULL); // Load all tracks for an artist that are NOT in an album (e.g. artists without albums) if (tracks_rc != SQLITE_OK) { // Failed to get our tracks g_critical("Failed to read our tracks: %s", sqlite3_errmsg(koto_db)); @@ -87,8 +88,8 @@ int process_artist_paths( KotoArtist * artist = (KotoArtist*) data; - gchar * library_uuid = g_strdup(koto_utils_unquote_string(fields[0])); - gchar * relative_path = g_strdup(koto_utils_unquote_string(fields[2])); + gchar * library_uuid = g_strdup(koto_utils_string_unquote(fields[0])); + gchar * relative_path = g_strdup(koto_utils_string_unquote(fields[2])); KotoLibrary * lib = koto_cartographer_get_library_by_uuid(koto_maps, library_uuid); // Get the library for this artist @@ -112,11 +113,14 @@ int process_albums( KotoArtist * artist = (KotoArtist*) data; - gchar * album_uuid = g_strdup(koto_utils_unquote_string(fields[0])); - gchar * artist_uuid = g_strdup(koto_utils_unquote_string(fields[1])); - gchar * album_name = g_strdup(koto_utils_unquote_string(fields[2])); - gchar * album_art = (fields[3] != NULL) ? g_strdup(koto_utils_unquote_string(fields[3])) : NULL; - gchar * album_genres = (fields[4] != NULL) ? g_strdup(koto_utils_unquote_string(fields[4])) : NULL; + gchar * album_uuid = g_strdup(koto_utils_string_unquote(fields[0])); + gchar * artist_uuid = g_strdup(koto_utils_string_unquote(fields[1])); + gchar * album_name = g_strdup(koto_utils_string_unquote(fields[2])); + gchar * album_description = (fields[3] != NULL) ? g_strdup(koto_utils_string_unquote(fields[3])) : NULL; + gchar * album_narrator = (fields[4] != NULL) ? g_strdup(koto_utils_string_unquote(fields[4])) : NULL; + gchar * album_art = (fields[5] != NULL) ? g_strdup(koto_utils_string_unquote(fields[5])) : NULL; + gchar * album_genres = (fields[6] != NULL) ? g_strdup(koto_utils_string_unquote(fields[6])) : NULL; + guint64 * album_year = (guint64*) g_ascii_strtoull(fields[7], NULL, 10); KotoAlbum * album = koto_album_new_with_uuid(artist, album_uuid); // Create our album @@ -124,16 +128,31 @@ int process_albums( album, "name", album_name, // Set name + "description", + album_description, + "narrator", + album_narrator, "art-path", album_art, // Set art path if any "preparsed-genres", album_genres, + "year", + album_year, NULL ); koto_cartographer_add_album(koto_maps, album); // Add the album to our global cartographer koto_artist_add_album(artist, album); // Add the album + int tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM tracks WHERE album_id=\"%s\"", album_uuid), process_tracks, NULL, NULL); // Process all the tracks for this specific album + + if (tracks_rc != SQLITE_OK) { // Failed to get our tracks + g_critical("Failed to read our tracks: %s", sqlite3_errmsg(koto_db)); + return 1; + } + + koto_album_mark_as_finalized(album); // Mark the album as finalized now that all tracks have been loaded, allowing our internal album playlist to re-sort itself + g_free(album_uuid); g_free(artist_uuid); g_free(album_name); @@ -156,26 +175,69 @@ int process_playlists( (void) num_columns; (void) column_names; // Don't need any of the params - gchar * playlist_uuid = g_strdup(koto_utils_unquote_string(fields[0])); // First column is UUID - gchar * playlist_name = g_strdup(koto_utils_unquote_string(fields[1])); // Second column is playlist name - gchar * playlist_art_path = g_strdup(koto_utils_unquote_string(fields[2])); // Third column is any art path + gchar * playlist_uuid = g_strdup(koto_utils_string_unquote(fields[0])); // First column is UUID + gchar * playlist_name = g_strdup(koto_utils_string_unquote(fields[1])); // Second column is playlist name + gchar * playlist_art_path = g_strdup(koto_utils_string_unquote(fields[2])); // Third column is any art path + guint64 playlist_preferred_sort = g_ascii_strtoull(koto_utils_string_unquote(fields[3]), NULL, 10); // Fourth column is preferred model which is an int + gchar * playlist_album_id = g_strdup(koto_utils_string_unquote(fields[4])); // Fifth column is any album ID + gchar * playlist_current_track_id = g_strdup(koto_utils_string_unquote(fields[5])); // Sixth column is any track ID + guint64 playlist_track_current_playback_pos = g_ascii_strtoull(koto_utils_string_unquote(fields[6]), NULL, 10); // Seventh column is any playback position for the track - KotoPlaylist * playlist = koto_playlist_new_with_uuid(playlist_uuid); // Create a playlist using the existing UUID + KotoPreferredPlaylistSortType sort_type = (KotoPreferredPlaylistSortType) playlist_preferred_sort; - koto_playlist_set_name(playlist, playlist_name); // Add the playlist name - koto_playlist_set_artwork(playlist, playlist_art_path); // Add the playlist art path + KotoPlaylist * playlist = NULL; - koto_cartographer_add_playlist(koto_maps, playlist); // Add to cartographer + gboolean for_album = FALSE; - int playlist_tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM playlist_tracks WHERE playlist_id=\"%s\" ORDER BY position ASC", playlist_uuid), process_playlists_tracks, playlist, NULL); // Process our playlist tracks + if (koto_utils_string_is_valid(playlist_album_id)) { // Album UUID is set + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, playlist_album_id); // Get the album based on the album ID set for the playlist - if (playlist_tracks_rc != SQLITE_OK) { // Failed to get our playlist tracks - g_critical("Failed to read our playlist tracks: %s", sqlite3_errmsg(koto_db)); - return 1; + if (KOTO_IS_ALBUM(album)) { // Is an album + playlist = koto_album_get_playlist(album); // Get the playlist + for_album = TRUE; + } + } else { + playlist = koto_playlist_new_with_uuid(playlist_uuid); // Create a playlist using the existing UUID + koto_playlist_apply_model(playlist, sort_type); // Set the model based on what is stored + koto_cartographer_add_playlist(koto_maps, playlist); // Add to cartographer + } + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist yet, in this scenario it is likely the album no longer exists + goto free; + } + + g_object_set( + playlist, + "name", + playlist_name, + "art-path", + playlist_art_path, + "ephemeral", + for_album, + NULL + ); + + if (!for_album) { // Isn't for an album + int playlist_tracks_rc = sqlite3_exec(koto_db, g_strdup_printf("SELECT * FROM playlist_tracks WHERE playlist_id=\"%s\" ORDER BY position ASC", playlist_uuid), process_playlists_tracks, playlist, NULL); // Process our playlist tracks + + if (playlist_tracks_rc != SQLITE_OK) { // Failed to get our playlist tracks + g_critical("Failed to read our playlist tracks: %s", sqlite3_errmsg(koto_db)); + goto free; + } + } + + if (koto_utils_string_is_valid(playlist_current_track_id)) { // If we have a track UUID (probably) + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, playlist_current_track_id); // Get the track UUID + + if (KOTO_IS_TRACK(track)) { // If this is a track + koto_track_set_playback_position(track, playlist_track_current_playback_pos); // Set the playback position of the track + koto_playlist_set_track_as_current(playlist, playlist_current_track_id); // Ensure we have this track set as the current one in the playlist + } } koto_playlist_mark_as_finalized(playlist); // Mark as finalized since loading should be complete +free: g_free(playlist_uuid); g_free(playlist_name); g_free(playlist_art_path); @@ -193,9 +255,8 @@ int process_playlists_tracks( (void) num_columns; (void) column_names; // Don't need these - gchar * playlist_uuid = g_strdup(koto_utils_unquote_string(fields[1])); - gchar * track_uuid = g_strdup(koto_utils_unquote_string(fields[2])); - gboolean current = g_strcmp0(koto_utils_unquote_string(fields[3]), "0"); + gchar * playlist_uuid = g_strdup(koto_utils_string_unquote(fields[1])); + gchar * track_uuid = g_strdup(koto_utils_string_unquote(fields[2])); KotoPlaylist * playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid); // Get the playlist KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); // Get the track @@ -204,7 +265,7 @@ int process_playlists_tracks( goto freeforret; } - koto_playlist_add_track(playlist, track, current, FALSE); // Add the track to the playlist but don't re-commit to the table + koto_playlist_add_track(playlist, track, FALSE, FALSE); // Add the track to the playlist but don't re-commit to the table freeforret: g_free(playlist_uuid); @@ -223,14 +284,22 @@ int process_tracks( (void) num_columns; (void) column_names; // Don't need these - gchar * track_uuid = g_strdup(koto_utils_unquote_string(fields[0])); - gchar * artist_uuid = g_strdup(koto_utils_unquote_string(fields[1])); - gchar * album_uuid = g_strdup(koto_utils_unquote_string(fields[2])); - gchar * name = g_strdup(koto_utils_unquote_string(fields[3])); + gchar * track_uuid = g_strdup(koto_utils_string_unquote(fields[0])); + + KotoTrack * existing_track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); + + if (KOTO_IS_TRACK(existing_track)) { // Already have track + g_free(track_uuid); + return 0; + } + + gchar * artist_uuid = g_strdup(koto_utils_string_unquote(fields[1])); + gchar * album_uuid = g_strdup(koto_utils_string_unquote(fields[2])); + gchar * name = g_strdup(koto_utils_string_unquote(fields[3])); guint * disc_num = (guint*) g_ascii_strtoull(fields[4], NULL, 10); guint64 * position = (guint64*) g_ascii_strtoull(fields[5], NULL, 10); guint64 * duration = (guint64*) g_ascii_strtoull(fields[6], NULL, 10); - gchar * genres = g_strdup(koto_utils_unquote_string(fields[7])); + gchar * genres = g_strdup(koto_utils_string_unquote(fields[7])); KotoTrack * track = koto_track_new_with_uuid(track_uuid); // Create our file @@ -265,10 +334,12 @@ int process_tracks( return 1; } + koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); // Get the artist koto_artist_add_track(artist, track); // Add the track for the artist - if (koto_utils_is_string_valid(album_uuid)) { // If we have an album UUID + if (koto_utils_string_is_valid(album_uuid)) { // If we have an album UUID KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); // Attempt to get album if (KOTO_IS_ALBUM(album)) { // This is an album @@ -293,13 +364,13 @@ int process_track_paths( (void) num_columns; (void) column_names; // Don't need these - KotoLibrary * library = koto_cartographer_get_library_by_uuid(koto_maps, koto_utils_unquote_string(fields[0])); + KotoLibrary * library = koto_cartographer_get_library_by_uuid(koto_maps, koto_utils_string_unquote(fields[0])); if (!KOTO_IS_LIBRARY(library)) { // Not a library return 1; } - koto_track_set_path(track, library, koto_utils_unquote_string(fields[1])); + koto_track_set_path(track, library, koto_utils_string_unquote(fields[1])); return 0; } diff --git a/src/indexer/album-playlist-funcs.h b/src/indexer/album-playlist-funcs.h index 083f284..3a795cc 100644 --- a/src/indexer/album-playlist-funcs.h +++ b/src/indexer/album-playlist-funcs.h @@ -20,6 +20,6 @@ G_BEGIN_DECLS -KotoPlaylist * koto_album_create_playlist(KotoAlbum * self); +KotoPlaylist * koto_album_get_playlist(KotoAlbum * self); G_END_DECLS \ No newline at end of file diff --git a/src/indexer/album.c b/src/indexer/album.c index 06b9f73..0011601 100644 --- a/src/indexer/album.c +++ b/src/indexer/album.c @@ -36,10 +36,13 @@ enum { PROP_0, PROP_UUID, PROP_DO_INITIAL_INDEX, - PROP_ALBUM_NAME, + PROP_NAME, PROP_ART_PATH, PROP_ARTIST_UUID, PROP_ALBUM_PREPARED_GENRES, + PROP_DESCRIPTION, + PROP_NARRATOR, + PROP_YEAR, N_PROPERTIES }; @@ -62,16 +65,20 @@ struct _KotoAlbum { gchar * uuid; gchar * name; + guint64 year; + gchar * description; + gchar * narrator; gchar * art_path; gchar * artist_uuid; GList * genres; - GList * tracks; + KotoPlaylist * playlist; GHashTable * paths; gboolean has_album_art; gboolean do_initial_index; + gboolean finalized; }; struct _KotoAlbumClass { @@ -126,7 +133,7 @@ static void koto_album_class_init(KotoAlbumClass * c) { G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); - props[PROP_ALBUM_NAME] = g_param_spec_string( + props[PROP_NAME] = g_param_spec_string( "name", "Name", "Name of Album", @@ -158,6 +165,32 @@ static void koto_album_class_init(KotoAlbumClass * c) { G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE ); + props[PROP_DESCRIPTION] = g_param_spec_string( + "description", + "Description of Album, typically for an audiobook or podcast", + "Description of Album, typically for an audiobook or podcast", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + props[PROP_NARRATOR] = g_param_spec_string( + "narrator", + "Narrator of audiobook", + "Narrator of audiobook", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + + props[PROP_YEAR] = g_param_spec_uint64( + "year", + "Year", + "Year of release", + 0, + G_MAXSHORT, + 0, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_WRITABLE + ); + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); album_signals[SIGNAL_TRACK_ADDED] = g_signal_new( @@ -188,10 +221,19 @@ static void koto_album_class_init(KotoAlbumClass * c) { } static void koto_album_init(KotoAlbum * self) { - self->has_album_art = FALSE; + self->description = NULL; self->genres = NULL; - self->tracks = NULL; + self->has_album_art = FALSE; + self->narrator = NULL; self->paths = g_hash_table_new(g_str_hash, g_str_equal); + + self->playlist = koto_playlist_new_with_uuid(NULL); // Create a playlist, with UUID set to NULL temporarily (until we know the album UUID) + koto_playlist_apply_model(self->playlist, KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_POS); // Sort by track position + koto_playlist_set_album_uuid(self->playlist, self->uuid); // Set the playlist UUID to be the same as the album + g_object_set(self->playlist, "ephemeral", TRUE, NULL); // Set as ephemeral / temporary + koto_cartographer_add_playlist(koto_maps, self->playlist); // Add to cartographer + + self->year = 0; } void koto_album_add_track( @@ -206,14 +248,11 @@ void koto_album_add_track( return; } - gchar * track_uuid = koto_track_get_uuid(track); - - if (g_list_index(self->tracks, track_uuid) != -1) { // Have added it already + if (koto_playlist_get_position_of_track(self->playlist, track) != -1) { // Have added it already return; } koto_cartographer_add_track(koto_maps, track); // Add the track to cartographer if necessary - self->tracks = g_list_insert_sorted_with_data(self->tracks, track_uuid, koto_track_helpers_sort_tracks_by_uuid, NULL); GList * track_genres = koto_track_get_genres(track); // Get the genres for the track GList * current_genre_list; @@ -234,6 +273,25 @@ void koto_album_add_track( self->genres = g_list_append(self->genres, g_strdup(track_genre)); // Duplicate the genre and add it to our list } + if (self->year == 0) { // Don't have a year set yet + guint64 track_year = koto_track_get_year(track); // Get the year from this track + + if (track_year > 0) { // Have a probably valid year set + self->year = track_year; // Set our track + } + } + + if (!koto_utils_string_is_valid(self->narrator)) { // No narrator set yet + gchar * track_narrator = koto_track_get_narrator(track); // Get the narrator for the track + + if (koto_utils_string_is_valid(track_narrator)) { // If this track has a narrator + self->narrator = g_strdup(track_narrator); + g_free(track_narrator); + } + } + + koto_playlist_add_track(self->playlist, track, FALSE, FALSE); // Add the track to our internal playlist + g_signal_emit( self, album_signals[SIGNAL_TRACK_ADDED], @@ -250,14 +308,17 @@ void koto_album_commit(KotoAlbum * self) { gchar * genres_string = koto_utils_join_string_list(self->genres, ";"); gchar * commit_op = g_strdup_printf( - "INSERT INTO albums(id, artist_id, name, art_path, genres)" - "VALUES('%s', '%s', quote(\"%s\"), quote(\"%s\"), '%s')" - "ON CONFLICT(id) DO UPDATE SET artist_id=excluded.artist_id, name=excluded.name, art_path=excluded.art_path, genres=excluded.genres;", + "INSERT INTO albums(id, artist_id, name, description, narrator, art_path, genres, year)" + "VALUES('%s', '%s', quote(\"%s\"), quote(\"%s\"), quote(\"%s\"), quote(\"%s\"), '%s', %ld)" + "ON CONFLICT(id) DO UPDATE SET artist_id=excluded.artist_id, name=excluded.name, description=excluded.description, narrator=excluded.narrator, art_path=excluded.art_path, genres=excluded.genres, year=excluded.year;", self->uuid, self->artist_uuid, - self->name, - self->art_path, - genres_string + koto_utils_string_get_valid(self->name), + koto_utils_string_get_valid(self->description), + koto_utils_string_get_valid(self->narrator), + koto_utils_string_get_valid(self->art_path), + koto_utils_string_get_valid(genres_string), + self->year ); new_transaction(commit_op, "Failed to write our album to the database", FALSE); @@ -284,42 +345,6 @@ void koto_album_commit(KotoAlbum * self) { } } -KotoPlaylist * koto_album_create_playlist(KotoAlbum * self) { - if (!KOTO_IS_ALBUM(self)) { // Not an album - return NULL; - } - - if (self->tracks == NULL) { // No files to add to the playlist - return NULL; - } - - KotoPlaylist * new_album_playlist = koto_playlist_new(); // Create a new playlist - - g_object_set(new_album_playlist, "ephemeral", TRUE, NULL); // Set as ephemeral / temporary - - // The following section effectively reverses our tracks, so the first is now last. - // It then adds them in reverse order, since our playlist add function will prepend to our queue - // This enables the preservation and defaulting of "newest" first everywhere else, while in albums preserving the rightful order of the album - // e.g. first track (0) being added last is actually first in the playlist's tracks - GList * reversed_tracks = g_list_copy(self->tracks); // Copy our tracks so we can reverse the order - - reversed_tracks = g_list_reverse(reversed_tracks); // Actually reverse it - - GList * t; - - for (t = reversed_tracks; t != NULL; t = t->next) { // For each of the tracks - gchar * track_uuid = t->data; - koto_playlist_add_track_by_uuid(new_album_playlist, track_uuid, FALSE, FALSE); // Add the UUID, skip commit to table since it is temporary - } - - g_list_free(t); - g_list_free(reversed_tracks); - - koto_playlist_apply_model(new_album_playlist, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // Ensure we are using our default model - - return new_album_playlist; -} - void koto_album_find_album_art(KotoAlbum * self) { if (self->has_album_art) { // If we already have album art return; @@ -376,14 +401,6 @@ void koto_album_find_album_art(KotoAlbum * self) { closedir(dir); } -GList * koto_album_get_genres(KotoAlbum * self) { - if (!KOTO_IS_ALBUM(self)) { // Not an album - return NULL; - } - - return self->genres; -} - static void koto_album_get_property( GObject * obj, guint prop_id, @@ -399,7 +416,7 @@ static void koto_album_get_property( case PROP_DO_INITIAL_INDEX: g_value_set_boolean(val, self->do_initial_index); break; - case PROP_ALBUM_NAME: + case PROP_NAME: g_value_set_string(val, self->name); break; case PROP_ART_PATH: @@ -424,13 +441,12 @@ static void koto_album_set_property( switch (prop_id) { case PROP_UUID: - self->uuid = g_strdup(g_value_get_string(val)); - g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); + koto_album_set_uuid(self, g_value_get_string(val)); break; case PROP_DO_INITIAL_INDEX: self->do_initial_index = g_value_get_boolean(val); break; - case PROP_ALBUM_NAME: // Name of album + case PROP_NAME: // Name of album koto_album_set_album_name(self, g_value_get_string(val)); break; case PROP_ART_PATH: // Path to art @@ -442,6 +458,15 @@ static void koto_album_set_property( case PROP_ALBUM_PREPARED_GENRES: koto_album_set_preparsed_genres(self, g_strdup(g_value_get_string(val))); break; + case PROP_DESCRIPTION: + koto_album_set_description(self, g_value_get_string(val)); + break; + case PROP_NARRATOR: + koto_album_set_narrator(self, g_value_get_string(val)); + break; + case PROP_YEAR: + koto_album_set_year(self, g_value_get_uint64(val)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); break; @@ -453,7 +478,27 @@ gchar * koto_album_get_art(KotoAlbum * self) { return g_strdup(""); } - return g_strdup((self->has_album_art && koto_utils_is_string_valid(self->art_path)) ? self->art_path : ""); + return g_strdup((self->has_album_art && koto_utils_string_is_valid(self->art_path)) ? self->art_path : ""); +} + +gchar * koto_album_get_artist_uuid(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->artist_uuid : NULL; +} + +gchar * koto_album_get_description(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->description : NULL; +} + +GList * koto_album_get_genres(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->genres : NULL; +} + +KotoLibraryType koto_album_get_lib_type(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return KOTO_LIBRARY_TYPE_UNKNOWN; + } + + return koto_artist_get_lib_type(koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid)); // Get the lib type for the artist. If artist isn't valid, we just return UNKNOWN } gchar * koto_album_get_name(KotoAlbum * self) { @@ -461,23 +506,15 @@ gchar * koto_album_get_name(KotoAlbum * self) { return NULL; } - if (!koto_utils_is_string_valid(self->name)) { // Not set + if (!koto_utils_string_is_valid(self->name)) { // Not set return NULL; } - return g_strdup(self->name); // Return duplicate of the name + return self->name; // Return name of the album } -gchar * koto_album_get_album_uuid(KotoAlbum * self) { - if (!KOTO_IS_ALBUM(self)) { // Not an album - return NULL; - } - - if (!koto_utils_is_string_valid(self->uuid)) { // Not set - return NULL; - } - - return g_strdup(self->uuid); // Return a duplicate of the UUID +gchar * koto_album_get_narrator(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->narrator : NULL; } gchar * koto_album_get_path(KotoAlbum * self) { @@ -492,7 +529,7 @@ gchar * koto_album_get_path(KotoAlbum * self) { KotoLibrary * cur_library = libs->data; // Get this as a KotoLibrary gchar * library_relative_path = g_hash_table_lookup(self->paths, koto_library_get_uuid(cur_library)); // Get any relative path in our paths based on the current UUID - if (!koto_utils_is_string_valid(library_relative_path)) { // Not a valid path + if (!koto_utils_string_is_valid(library_relative_path)) { // Not a valid path continue; } @@ -502,12 +539,26 @@ gchar * koto_album_get_path(KotoAlbum * self) { return NULL; } -GList * koto_album_get_tracks(KotoAlbum * self) { +KotoPlaylist * koto_album_get_playlist(KotoAlbum * self) { if (!KOTO_IS_ALBUM(self)) { // Not an album return NULL; } - return self->tracks; // Return the tracks + if (!KOTO_IS_PLAYLIST(self->playlist)) { // Not a playlist + return NULL; + } + + return self->playlist; +} + +GListStore * koto_album_get_store(KotoAlbum * self) { + KotoPlaylist * playlist = koto_album_get_playlist(self); + + if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist + return NULL; + } + + return koto_playlist_get_store(playlist); // Return the store from the playlist } gchar * koto_album_get_uuid(KotoAlbum * self) { @@ -518,6 +569,89 @@ gchar * koto_album_get_uuid(KotoAlbum * self) { return self->uuid; // Return the UUID } +guint64 koto_album_get_year(KotoAlbum * self) { + return KOTO_IS_ALBUM(self) ? self->year : 0; +} + +void koto_album_mark_as_finalized(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (self->finalized) { // Already finalized + return; + } + + self->finalized = TRUE; + koto_playlist_mark_as_finalized(self->playlist); + //koto_playlist_apply_model(self->playlist, self->model); // Resort our playlist +} + +void koto_album_remove_track( + KotoAlbum * self, + KotoTrack * track +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + koto_playlist_remove_track_by_uuid( + self->playlist, + koto_track_get_uuid(track) + ); + + g_signal_emit( + self, + album_signals[SIGNAL_TRACK_REMOVED], + 0, + track + ); +} + +void koto_album_set_album_name( + KotoAlbum * self, + const gchar * album_name +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (album_name == NULL) { // Not valid album name + return; + } + + if (self->name != NULL) { + g_free(self->name); + } + + self->name = g_strdup(album_name); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NAME]); +} + +void koto_album_set_artist_uuid( + KotoAlbum * self, + const gchar * artist_uuid +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (artist_uuid == NULL) { + return; + } + + if (self->artist_uuid != NULL) { + g_free(self->artist_uuid); + } + + self->artist_uuid = g_strdup(artist_uuid); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ARTIST_UUID]); +} + void koto_album_set_album_art( KotoAlbum * self, const gchar * album_art @@ -537,6 +671,56 @@ void koto_album_set_album_art( self->art_path = g_strdup(album_art); self->has_album_art = TRUE; + + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ART_PATH]); +} + +void koto_album_set_as_current_playlist(KotoAlbum * self) { + if (!KOTO_IS_ALBUM(self)) { + return; + } + + if (!KOTO_IS_CURRENT_PLAYLIST(current_playlist)) { + return; + } + + if (!KOTO_IS_PLAYLIST(self->playlist)) { // Don't have a playlist for the album for some reason + return; + } + + koto_current_playlist_set_playlist(current_playlist, self->playlist, TRUE, FALSE); // Set our new current playlist and start playing immediately +} + +void koto_album_set_description( + KotoAlbum * self, + const gchar * description +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!koto_utils_string_is_valid(description)) { + return; + } + + self->description = g_strdup(description); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DESCRIPTION]); +} + +void koto_album_set_narrator( + KotoAlbum * self, + const gchar * narrator +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!koto_utils_string_is_valid(narrator)) { + return; + } + + self->narrator = g_strdup(narrator); + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NARRATOR]); } void koto_album_set_path( @@ -564,83 +748,6 @@ void koto_album_set_path( self->do_initial_index = FALSE; } -void koto_album_remove_track( - KotoAlbum * self, - KotoTrack * track -) { - if (!KOTO_IS_ALBUM(self)) { // Not an album - return; - } - - if (!KOTO_IS_TRACK(track)) { // Not a track - return; - } - - self->tracks = g_list_remove(self->tracks, koto_track_get_uuid(track)); - g_signal_emit( - self, - album_signals[SIGNAL_TRACK_REMOVED], - 0, - track - ); -} - -void koto_album_set_album_name( - KotoAlbum * self, - const gchar * album_name -) { - if (!KOTO_IS_ALBUM(self)) { // Not an album - return; - } - - if (album_name == NULL) { // Not valid album name - return; - } - - if (self->name != NULL) { - g_free(self->name); - } - - self->name = g_strdup(album_name); -} - -void koto_album_set_artist_uuid( - KotoAlbum * self, - const gchar * artist_uuid -) { - if (!KOTO_IS_ALBUM(self)) { // Not an album - return; - } - - if (artist_uuid == NULL) { - return; - } - - if (self->artist_uuid != NULL) { - g_free(self->artist_uuid); - } - - self->artist_uuid = g_strdup(artist_uuid); -} - -void koto_album_set_as_current_playlist(KotoAlbum * self) { - if (!KOTO_IS_ALBUM(self)) { - return; - } - - if (!KOTO_IS_CURRENT_PLAYLIST(current_playlist)) { - return; - } - - KotoPlaylist * album_playlist = koto_album_create_playlist(self); - - if (!KOTO_IS_PLAYLIST(album_playlist)) { - return; - } - - koto_current_playlist_set_playlist(current_playlist, album_playlist, TRUE); // Set our new current playlist and start playing immediately -} - void koto_album_set_preparsed_genres( KotoAlbum * self, gchar * genrelist @@ -649,7 +756,7 @@ void koto_album_set_preparsed_genres( return; } - if (!koto_utils_is_string_valid(genrelist)) { // If it is an empty string + if (!koto_utils_string_is_valid(genrelist)) { // If it is an empty string return; } @@ -665,8 +772,49 @@ void koto_album_set_preparsed_genres( self->genres = preparsed_genres_list; }; +void koto_album_set_uuid( + KotoAlbum * self, + const gchar * uuid +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (!koto_utils_string_is_valid(uuid)) { + return; + } + + self->uuid = g_strdup(uuid); + g_object_set( + self->playlist, + "album-uuid", + self->uuid, + "uuid", + self->uuid, // Ensure the playlist has the same UUID as the album + NULL + ); + + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UUID]); +} + +void koto_album_set_year( + KotoAlbum * self, + guint64 year +) { + if (!KOTO_IS_ALBUM(self)) { // Not an album + return; + } + + if (year <= 0) { + return; + } + + self->year = year; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_YEAR]); +} + KotoAlbum * koto_album_new(gchar * artist_uuid) { - if (!koto_utils_is_string_valid(artist_uuid)) { // Invalid artist UUID provided + if (!koto_utils_string_is_valid(artist_uuid)) { // Invalid artist UUID provided return NULL; } @@ -688,14 +836,12 @@ KotoAlbum * koto_album_new_with_uuid( KotoArtist * artist, const gchar * uuid ) { - gchar * artist_uuid = NULL; - - g_object_get(artist, "uuid", &artist_uuid, NULL); + gchar * artist_uuid = koto_artist_get_uuid(artist); return g_object_new( KOTO_TYPE_ALBUM, "artist-uuid", - artist, + artist_uuid, "uuid", g_strdup(uuid), "do-initial-index", diff --git a/src/indexer/artist.c b/src/indexer/artist.c index 47def9f..0bf30e4 100644 --- a/src/indexer/artist.c +++ b/src/indexer/artist.c @@ -17,6 +17,7 @@ #include #include +#include "../config/config.h" #include "../db/db.h" #include "../db/cartographer.h" #include "../playlist/playlist.h" @@ -26,6 +27,7 @@ #include "track-helpers.h" extern KotoCartographer * koto_maps; +extern KotoConfig * config; enum { PROP_0, @@ -60,10 +62,12 @@ struct _KotoArtist { gboolean finalized; gboolean has_artist_art; gchar * artist_name; - GList * albums; GList * tracks; GHashTable * paths; KotoLibraryType type; + + GQueue * albums; + GListStore * albums_store; }; struct _KotoArtistClass { @@ -195,7 +199,8 @@ static void koto_artist_class_init(KotoArtistClass * c) { } static void koto_artist_init(KotoArtist * self) { - self->albums = NULL; // Create a new GList + self->albums = g_queue_new(); // Create a new GQueue + self->albums_store = g_list_store_new(KOTO_TYPE_ALBUM); // Create our GListStore of type KotoAlbum self->content_playlist = koto_playlist_new(); // Create our playlist g_object_set( @@ -223,7 +228,7 @@ void koto_artist_commit(KotoArtist * self) { "VALUES ('%s', quote(\"%s\"), NULL)" "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;", self->uuid, - self->artist_name + koto_utils_string_get_valid(self->artist_name) ); new_transaction(commit_op, "Failed to write our artist to the database", FALSE); @@ -248,14 +253,6 @@ void koto_artist_commit(KotoArtist * self) { } } -KotoPlaylist * koto_artist_get_playlist(KotoArtist * self) { - if (!KOTO_IS_ARTIST(self)) { - return NULL; - } - - return self->content_playlist; -} - static void koto_artist_get_property( GObject * obj, guint prop_id, @@ -311,13 +308,21 @@ void koto_artist_add_album( return; } - gchar * album_uuid = koto_album_get_uuid(album); + GList * found_albums = g_queue_find(self->albums, album); // Try finding the album - if (g_list_index(self->albums, album_uuid) != -1) { // If we have already added the album + if (found_albums != NULL) { // Already has been added + g_list_free(found_albums); return; } - self->albums = g_list_append(self->albums, album_uuid); // Push to our album's UUID to the end of the list + g_list_free(found_albums); + + g_queue_push_tail(self->albums, album); // Add the album to end of albums GQueue + g_list_store_append(self->albums_store, album); // Add the album to th estore as well + + if (self->finalized) { // Is already finalized + koto_artist_apply_model(self, koto_config_get_preferred_album_sort_type(config)); // Apply our model to sort our albums gqueue and store + } g_signal_emit( self, @@ -358,12 +363,32 @@ void koto_artist_add_track( ); } -GList * koto_artist_get_albums(KotoArtist * self) { +void koto_artist_apply_model( + KotoArtist * self, + KotoPreferredAlbumSortType model +) { + if (!KOTO_IS_ARTIST(self)) { + return; + } + + g_queue_sort(self->albums, koto_artist_model_sort_albums, &model); + g_list_store_sort(self->albums_store, koto_artist_model_sort_albums, &model); +} + +GQueue * koto_artist_get_albums(KotoArtist * self) { if (!KOTO_IS_ARTIST(self)) { // Not an artist return NULL; } - return g_list_copy(self->albums); + return self->albums; +} + +GListStore * koto_artist_get_albums_store(KotoArtist * self) { + if (!KOTO_IS_ARTIST(self)) { // Not an artist + return NULL; + } + + return self->albums_store; } KotoAlbum * koto_artist_get_album_by_name( @@ -377,15 +402,15 @@ KotoAlbum * koto_artist_get_album_by_name( KotoAlbum * album = NULL; GList * cur_list_iter; - for (cur_list_iter = self->albums; cur_list_iter != NULL; cur_list_iter = cur_list_iter->next) { // Iterate through our albums by their UUIDs - KotoAlbum * album_for_uuid = koto_cartographer_get_album_by_uuid(koto_maps, cur_list_iter->data); // Get the album + for (cur_list_iter = self->albums->head; cur_list_iter != NULL; cur_list_iter = cur_list_iter->next) { // Iterate through our albums by their UUIDs + KotoAlbum * this_album = cur_list_iter->data; - if (!KOTO_IS_ALBUM(album_for_uuid)) { // Not an album + if (!KOTO_IS_ALBUM(this_album)) { // Not an album continue; } - if (g_strcmp0(koto_album_get_name(album_for_uuid), album_name) == 0) { // These album names match - album = album_for_uuid; + if (g_strcmp0(koto_album_get_name(this_album), album_name) == 0) { // These album names match + album = this_album; break; } } @@ -398,7 +423,15 @@ gchar * koto_artist_get_name(KotoArtist * self) { return g_strdup(""); } - return g_strdup(koto_utils_is_string_valid(self->artist_name) ? self->artist_name : ""); // Return artist name if set + return g_strdup(koto_utils_string_is_valid(self->artist_name) ? self->artist_name : ""); // Return artist name if set +} + +KotoPlaylist * koto_artist_get_playlist(KotoArtist * self) { + if (!KOTO_IS_ARTIST(self)) { + return NULL; + } + + return self->content_playlist; } GList * koto_artist_get_tracks(KotoArtist * self) { @@ -425,7 +458,12 @@ void koto_artist_remove_album( return; } - self->albums = g_list_remove(self->albums, koto_album_get_uuid(album)); + g_queue_remove(self->albums, album); // Remove the album + + guint position = 0; + if (g_list_store_find(self->albums_store, album, &position)) { // Found the album in the list store + g_list_store_remove(self->albums_store, position); // Remove from the list store + } g_signal_emit( self, @@ -468,11 +506,11 @@ void koto_artist_set_artist_name( return; } - if (!koto_utils_is_string_valid(artist_name)) { // No artist name + if (!koto_utils_string_is_valid(artist_name)) { // No artist name return; } - if (koto_utils_is_string_valid(self->artist_name)) { // Has artist name + if (koto_utils_string_is_valid(self->artist_name)) { // Has artist name g_free(self->artist_name); } @@ -487,7 +525,7 @@ void koto_artist_set_as_finalized(KotoArtist * self) { self->finalized = TRUE; - if (g_list_length(self->albums) == 0) { // Have no albums + if (g_queue_get_length(self->albums) == 0) { // Have no albums g_signal_emit_by_name(self, "has-no-albums"); } } @@ -515,6 +553,36 @@ void koto_artist_set_path( self->type = koto_library_get_lib_type(lib); // Define our artist type as the type from the library } +gint koto_artist_model_sort_albums( + gconstpointer first_item, + gconstpointer second_item, + gpointer user_data +) { + KotoPreferredAlbumSortType * preferred_model = (KotoPreferredAlbumSortType*) user_data; + + KotoAlbum * first_album = KOTO_ALBUM(first_item); + KotoAlbum * second_album = KOTO_ALBUM(second_item); + + if (KOTO_IS_ALBUM(first_album) && !KOTO_IS_ALBUM(second_album)) { // First album is valid, second isn't + return -1; + } else if (!KOTO_IS_ALBUM(first_album) && KOTO_IS_ALBUM(second_album)) { // Second album is valid, first isn't + return 1; + } + + if (preferred_model == KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT) { // Sort chronological before alphabetical + guint64 fa_year = koto_album_get_year(first_album); + guint64 sa_year = koto_album_get_year(second_album); + + if (fa_year > sa_year) { // First album is newer than second + return -1; + } else if (fa_year < sa_year) { // Second album is newer than first + return 1; + } + } + + return g_utf8_collate(koto_album_get_name(first_album), koto_album_get_name(second_album)); +} + KotoArtist * koto_artist_new(gchar * artist_name) { KotoArtist * artist = g_object_new( KOTO_TYPE_ARTIST, diff --git a/src/indexer/file-indexer.c b/src/indexer/file-indexer.c index 4cfa0b1..9bd9e34 100644 --- a/src/indexer/file-indexer.c +++ b/src/indexer/file-indexer.c @@ -92,12 +92,12 @@ void index_folder( gchar * artist_name = g_strdup(split[split_len - 3]); g_strfreev(split); - if (!koto_utils_is_string_valid(album_name)) { + if (!koto_utils_string_is_valid(album_name)) { g_free(album_name); continue; } - if (!koto_utils_is_string_valid(artist_name)) { + if (!koto_utils_string_is_valid(artist_name)) { g_free(album_name); g_free(artist_name); continue; @@ -143,14 +143,17 @@ void index_file( return; } + gboolean for_audiobook = (koto_library_get_lib_type(lib) == KOTO_LIBRARY_TYPE_AUDIOBOOK); + gchar * relative_path_to_file = koto_library_get_relative_path_to_file(lib, g_strdup(path)); // Strip out library path so we have a relative path to the file + gchar * file_basename = g_path_get_basename(relative_path_to_file); + gchar ** split_on_relative_slashes = g_strsplit(relative_path_to_file, G_DIR_SEPARATOR_S, -1); // Split based on separator (e.g. / ) guint slash_sep_count = g_strv_length(split_on_relative_slashes); gchar * artist_author_podcast_name = g_strdup(split_on_relative_slashes[0]); // No matter what, artist should be first gchar * album_or_audiobook_name = NULL; - gchar * file_name = koto_track_helpers_get_name_for_file(path, artist_author_podcast_name); // Get the name of the file - guint cd = (guint) 1; + guint cd = (guint) 0; if (slash_sep_count >= 3) { // If this it is at least "artist" + "album" + "file" (or with CD) album_or_audiobook_name = g_strdup(split_on_relative_slashes[1]); // Duplicate the second item as the album or audiobook name @@ -159,22 +162,41 @@ void index_file( // #region CD parsing logic if ((slash_sep_count == 4)) { // If is at least "artist" + "album" + "cd" + "file" - gchar * cd_str = g_strdup(g_strstrip(koto_utils_replace_string_all(g_utf8_strdown(split_on_relative_slashes[2], -1), "cd", ""))); // Replace a lowercased version of our CD ("cd") and trim any whitespace + gchar * cd_str = g_strdup(g_strstrip(koto_utils_string_replace_all(g_utf8_strdown(split_on_relative_slashes[2], -1), "cd", ""))); // Replace a lowercased version of our CD ("cd") and trim any whitespace - cd = (guint) g_ascii_strtoull(cd_str, NULL, 10); // Attempt to convert + cd = (guint) g_ascii_strtoull(cd_str, NULL, 10); // Attempt to convert} + } - if (cd == 0) { // Had an error during conversion - cd = 1; // Set back to 1 - } + if (for_audiobook && (cd == 0)) { // No CD yet and is for an audiobook + cd = koto_track_helpers_get_cd_based_on_file_name(file_basename); // Base on file name + } else { + cd = 1; } // #endregion + gchar * file_name = NULL; + + if (for_audiobook) { // If this is for an audiobook library + guint64 pos = koto_track_helpers_get_position_based_on_file_name(file_basename); + + if (cd != 1) { // On a non "Part 1" audiobook + file_name = g_strdup_printf("%s - Part %u - %lu", album_or_audiobook_name, cd, pos); + } else { // Part 1 / CD 1 + file_name = g_strdup_printf("%s - %lu", album_or_audiobook_name, pos); + } + } + + if (!koto_utils_string_is_valid(file_name)) { // No valid file name yet + file_name = koto_track_helpers_get_name_for_file(path, artist_author_podcast_name); // Get the name of the file + } + g_strfreev(split_on_relative_slashes); + g_free(file_basename); gchar * sorta_uniqueish_key = NULL; - if (koto_utils_is_string_valid(album_or_audiobook_name)) { // Have audiobook or album name + if (koto_utils_string_is_valid(album_or_audiobook_name)) { // Have audiobook or album name sorta_uniqueish_key = g_strdup_printf("%s-%s-%s", artist_author_podcast_name, album_or_audiobook_name, file_name); } else { // No audiobook or album name sorta_uniqueish_key = g_strdup_printf("%s-%s", artist_author_podcast_name, file_name); @@ -193,7 +215,7 @@ void index_file( KotoAlbum * album = NULL; - if (koto_utils_is_string_valid(album_or_audiobook_name)) { // Have an album or audiobook name + if (koto_utils_string_is_valid(album_or_audiobook_name)) { // Have an album or audiobook name KotoAlbum * possible_album = koto_artist_get_album_by_name(artist, album_or_audiobook_name); album = KOTO_IS_ALBUM(possible_album) ? possible_album : NULL; } diff --git a/src/indexer/library.c b/src/indexer/library.c index 97a1d67..1365e8d 100644 --- a/src/indexer/library.c +++ b/src/indexer/library.c @@ -245,7 +245,7 @@ gchar * koto_library_get_relative_path_to_file( } gchar * appended_slash_to_library_path = g_str_has_suffix(self->path, G_DIR_SEPARATOR_S) ? g_strdup(self->path) : g_strdup_printf("%s%s", g_strdup(self->path), G_DIR_SEPARATOR_S); - gchar * cleaned_path = koto_utils_replace_string_all(full_path, appended_slash_to_library_path, ""); // Replace the full path + gchar * cleaned_path = koto_utils_string_replace_all(full_path, appended_slash_to_library_path, ""); // Replace the full path g_free(appended_slash_to_library_path); return cleaned_path; @@ -299,11 +299,11 @@ void koto_library_set_name( return; } - if (!koto_utils_is_string_valid(library_name)) { // Not a string + if (!koto_utils_string_is_valid(library_name)) { // Not a string return; } - if (koto_utils_is_string_valid(self->name)) { // Name already set + if (koto_utils_string_is_valid(self->name)) { // Name already set g_free(self->name); // Free the existing value } @@ -319,15 +319,15 @@ void koto_library_set_path( return; } - if (!koto_utils_is_string_valid(path)) { // Not a valid string + if (!koto_utils_string_is_valid(path)) { // Not a valid string return; } - if (koto_utils_is_string_valid(self->path)) { + if (koto_utils_string_is_valid(self->path)) { g_free(self->path); } - self->relative_path = g_path_is_absolute(path) ? koto_utils_replace_string_all(path, self->mount_path, "") : path; // Ensure path is relative to our mount, even if the mount is really our own system partition + self->relative_path = g_path_is_absolute(path) ? koto_utils_string_replace_all(path, self->mount_path, "") : path; // Ensure path is relative to our mount, even if the mount is really our own system partition self->path = g_build_path(G_DIR_SEPARATOR_S, self->mount_path, self->relative_path, NULL); // Ensure our path is to whatever the current path of the mount + relative path is } @@ -345,7 +345,7 @@ void koto_library_set_storage_uuid( g_free(self->mount_path); } - if (!koto_utils_is_string_valid(storage_uuid)) { // Not a valid string, which actually is allowed for built-ins + if (!koto_utils_string_is_valid(storage_uuid)) { // Not a valid string, which actually is allowed for built-ins self->mount = NULL; self->mount_path = g_strdup_printf("%s%s", g_get_home_dir(), G_DIR_SEPARATOR_S); // Set mount path to user's home directory self->storage_uuid = NULL; @@ -378,11 +378,11 @@ gchar * koto_library_to_config_string(KotoLibrary * self) { g_strv_builder_add(lib_builder, g_strdup_printf("\tdirectory=\"%s\"", self->relative_path)); // Add the directory - if (koto_utils_is_string_valid(self->name)) { // Have a library name + if (koto_utils_string_is_valid(self->name)) { // Have a library name g_strv_builder_add(lib_builder, g_strdup_printf("\tname=\"%s\"", self->name)); // Add the name } - if (koto_utils_is_string_valid(self->storage_uuid)) { // Have a storage UUID (not applicable to built-ins) + if (koto_utils_string_is_valid(self->storage_uuid)) { // Have a storage UUID (not applicable to built-ins) g_strv_builder_add(lib_builder, g_strdup_printf("\tstorage_uuid=\"%s\"", self->storage_uuid)); // Add the storage UUID } diff --git a/src/indexer/misc-types.h b/src/indexer/misc-types.h new file mode 100644 index 0000000..e3963e6 --- /dev/null +++ b/src/indexer/misc-types.h @@ -0,0 +1,22 @@ +/* misc-types.h + * + * 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. + */ + +#pragma once +typedef enum { + KOTO_PREFERRED_ALBUM_SORT_TYPE_DEFAULT, // Chronological is considered default + KOTO_PREFERRED_ALBUM_ALWAYS_ALPHABETICAL, // Prefer sorting alphabetically +} KotoPreferredAlbumSortType; \ No newline at end of file diff --git a/src/indexer/structs.h b/src/indexer/structs.h index c4b3951..786e4c7 100644 --- a/src/indexer/structs.h +++ b/src/indexer/structs.h @@ -16,9 +16,11 @@ */ #pragma once -#include +#include +#include #include #include +#include "misc-types.h" typedef enum { KOTO_LIBRARY_TYPE_AUDIOBOOK = 1, @@ -148,9 +150,16 @@ void koto_artist_add_track( KotoTrack * track ); +void koto_artist_apply_model( + KotoArtist * self, + KotoPreferredAlbumSortType model +); + void koto_artist_commit(KotoArtist * self); -GList * koto_artist_get_albums(KotoArtist * self); +GQueue * koto_artist_get_albums(KotoArtist * self); + +GListStore * koto_artist_get_albums_store(KotoArtist * self); KotoAlbum * koto_artist_get_album_by_name( KotoArtist * self, @@ -159,12 +168,20 @@ KotoAlbum * koto_artist_get_album_by_name( gchar * koto_artist_get_name(KotoArtist * self); +gchar * koto_artist_get_path(KotoArtist * self); + GList * koto_artist_get_tracks(KotoArtist * self); gchar * koto_artist_get_uuid(KotoArtist * self); KotoLibraryType koto_artist_get_lib_type(KotoArtist * self); +gint koto_artist_model_sort_albums( + gconstpointer first_item, + gconstpointer second_item, + gpointer user_data +); + void koto_artist_remove_album( KotoArtist * self, KotoAlbum * album @@ -211,16 +228,28 @@ void koto_album_find_album_art(KotoAlbum * self); gchar * koto_album_get_art(KotoAlbum * self); +gchar * koto_album_get_artist_uuid(KotoAlbum * self); + +gchar * koto_album_get_description(KotoAlbum * self); + +GList * koto_album_get_genres(KotoAlbum * self); + +KotoLibraryType koto_album_get_lib_type(KotoAlbum * self); + gchar * koto_album_get_name(KotoAlbum * self); -gchar * koto_album_get_album_uuid(KotoAlbum * self); +gchar * koto_album_get_narrator(KotoAlbum * self); gchar * koto_album_get_path(KotoAlbum * self); -GList * koto_album_get_tracks(KotoAlbum * self); +GListStore * koto_album_get_store(KotoAlbum * self); gchar * koto_album_get_uuid(KotoAlbum * self); +guint64 koto_album_get_year(KotoAlbum * self); + +void koto_album_mark_as_finalized(KotoAlbum * self); + void koto_album_remove_track( KotoAlbum * self, KotoTrack * track @@ -241,8 +270,17 @@ void koto_album_set_artist_uuid( const gchar * artist_uuid ); +void koto_album_set_description( + KotoAlbum * self, + const char * description +); + void koto_album_set_as_current_playlist(KotoAlbum * self); +void koto_album_set_narrator( + KotoAlbum * self, + const char * narrator +); void koto_album_set_path( KotoAlbum * self, @@ -255,6 +293,16 @@ void koto_album_set_preparsed_genres( gchar * genrelist ); +void koto_album_set_uuid( + KotoAlbum * self, + const gchar * uuid +); + +void koto_album_set_year( + KotoAlbum * self, + guint64 year +); + /** * File / Track Functions **/ @@ -270,6 +318,8 @@ KotoTrack * koto_track_new_with_uuid(const gchar * uuid); void koto_track_commit(KotoTrack * self); +gchar * koto_track_get_description(KotoTrack * self); + guint koto_track_get_disc_number(KotoTrack * self); guint64 koto_track_get_duration(KotoTrack * self); @@ -282,12 +332,18 @@ gchar * koto_track_get_path(KotoTrack * self); gchar * koto_track_get_name(KotoTrack * self); +gchar * koto_track_get_narrator(KotoTrack * self); + +guint64 koto_track_get_playback_position(KotoTrack * self); + guint64 koto_track_get_position(KotoTrack * self); gchar * koto_track_get_uniqueish_key(KotoTrack * self); gchar * koto_track_get_uuid(KotoTrack * self); +guint64 koto_track_get_year(KotoTrack * self); + void koto_track_remove_from_playlist( KotoTrack * self, gchar * playlist_uuid @@ -300,13 +356,7 @@ void koto_track_set_album_uuid( void koto_track_save_to_playlist( KotoTrack * self, - gchar * playlist_uuid, - gint current -); - -void koto_track_set_file_name( - KotoTrack * self, - gchar * new_file_name + gchar * playlist_uuid ); void koto_track_set_cd( @@ -314,16 +364,31 @@ void koto_track_set_cd( guint cd ); +void koto_track_set_description( + KotoTrack * self, + const gchar * description +); + void koto_track_set_duration( KotoTrack * self, guint64 duration ); +void koto_track_set_file_name( + KotoTrack * self, + gchar * new_file_name +); + void koto_track_set_genres( KotoTrack * self, char * genrelist ); +void koto_track_set_narrator( + KotoTrack * self, + const gchar * narrator +); + void koto_track_set_parsed_name( KotoTrack * self, gchar * new_parsed_name @@ -335,6 +400,11 @@ void koto_track_set_path( gchar * fixed_path ); +void koto_track_set_playback_position( + KotoTrack * self, + guint64 position +); + void koto_track_set_position( KotoTrack * self, guint64 pos @@ -345,6 +415,11 @@ void koto_track_set_preparsed_genres( gchar * genrelist ); +void koto_track_set_year( + KotoTrack * self, + guint64 year +); + void koto_track_update_metadata(KotoTrack * self); G_END_DECLS diff --git a/src/indexer/track-helpers.c b/src/indexer/track-helpers.c index 5a65599..076212e 100644 --- a/src/indexer/track-helpers.c +++ b/src/indexer/track-helpers.c @@ -17,8 +17,8 @@ #include #include +#include "../components/track-item.h" #include "../db/cartographer.h" -#include "../koto-track-item.h" #include "../koto-utils.h" #include "structs.h" @@ -40,7 +40,40 @@ void koto_track_helpers_init() { gchar * koto_track_helpers_get_corrected_genre(gchar * original_genre) { gchar * lookedup_genre = g_hash_table_lookup(genre_replacements, original_genre); // Look up the genre - return koto_utils_is_string_valid(lookedup_genre) ? lookedup_genre : original_genre; + return koto_utils_string_is_valid(lookedup_genre) ? lookedup_genre : original_genre; +} + +guint64 koto_track_helpers_get_cd_based_on_file_name(const gchar * file_name) { + gchar ** part_split = g_strsplit(file_name, "Part", -1); + gchar * part_str = NULL; + + for (guint i = 0; i < g_strv_length(part_split); i++) { // Iterate on the parts + gchar * stripped_part = g_strdup(g_strstrip(part_split[i])); // Trim the whitespace around this part + gchar ** split = g_regex_split_simple("^([\\d]+)", stripped_part, G_REGEX_JAVASCRIPT_COMPAT, 0); + + g_free(stripped_part); // Free the stripped part + + if (g_strv_length(split) > 1) { // Has positional info at the beginning of the string + part_str = g_strdup(split[1]); + g_strfreev(split); + break; + } else { + g_strfreev(split); + } + } + + guint64 cd = 0; + + if (koto_utils_string_is_valid(part_str)) { // Have a valid string for the part + cd = g_ascii_strtoull(part_str, NULL, 10); + g_free(part_str); + } + + if (cd == 0) { + cd = 1; // Should be first CD, not 0 + } + + return cd; } gchar * koto_track_helpers_get_name_for_file( @@ -58,19 +91,19 @@ gchar * koto_track_helpers_get_name_for_file( taglib_tag_free_strings(); // Free strings taglib_file_free(t_file); // Free the file - if (koto_utils_is_string_valid(file_name)) { // File name not set yet + if (koto_utils_string_is_valid(file_name)) { // File name not set yet return file_name; } - gchar * name_without_hyphen_surround = koto_utils_replace_string_all(koto_utils_get_filename_without_extension((gchar*) path), " - ", ""); // Remove - surrounded by spaces + gchar * name_without_hyphen_surround = koto_utils_string_replace_all(koto_utils_get_filename_without_extension((gchar*) path), " - ", ""); // Remove - surrounded by spaces - gchar * name_without_hyphen = koto_utils_replace_string_all(name_without_hyphen_surround, "-", ""); // Remove just - + gchar * name_without_hyphen = koto_utils_string_replace_all(name_without_hyphen_surround, "-", ""); // Remove just - g_free(name_without_hyphen_surround); - file_name = koto_utils_replace_string_all(name_without_hyphen, "_", " "); // Replace underscore with whitespace + file_name = koto_utils_string_replace_all(name_without_hyphen, "_", " "); // Replace underscore with whitespace - if (koto_utils_is_string_valid(optional_artist_name)) { // Was provided an optional artist name - gchar * replaced_artist = koto_utils_replace_string_all(file_name, optional_artist_name, ""); // Remove the artist + if (koto_utils_string_is_valid(optional_artist_name)) { // Was provided an optional artist name + gchar * replaced_artist = koto_utils_string_replace_all(file_name, optional_artist_name, ""); // Remove the artist g_free(file_name); file_name = g_strdup(replaced_artist); g_free(replaced_artist); @@ -87,21 +120,67 @@ gchar * koto_track_helpers_get_name_for_file( } guint64 koto_track_helpers_get_position_based_on_file_name(const gchar * file_name) { - guint64 position = 0; // Default to position 0 - gchar ** split = g_regex_split_simple("^([\\d]+)", file_name, G_REGEX_JAVASCRIPT_COMPAT, 0); + GRegex * num_pat = g_regex_new("^([\\d]+)", G_REGEX_JAVASCRIPT_COMPAT, 0, NULL); + gchar ** split = g_regex_split(num_pat, file_name, 0); if (g_strv_length(split) > 1) { // Has positional info at the beginning of the file - gchar * num = split[1]; + gchar * num = g_strdup(split[1]); + g_strfreev(split); if ((strcmp(num, "0") != 0) && (strcmp(num, "00") != 0)) { // Is not zero guint64 potential_pos = g_ascii_strtoull(num, NULL, 10); // Attempt to convert if (potential_pos != 0) { // Got a legitimate position - position = potential_pos; // Update position + g_free(num_pat); // Free our regex + return potential_pos; // Return this position } } } + gchar * fn_no_ext = koto_utils_get_filename_without_extension((gchar*) file_name); // Get the filename without the extension + split = g_strsplit(fn_no_ext, ".", -1); // Split every time we see . + + guint len_of_extension_split = g_strv_length(split); + + gchar * fn_last_split_on_period = g_strdup(split[len_of_extension_split - 1]); + g_free(fn_no_ext); + + g_strfreev(split); // Free our split + + gchar ** whitespace_split = g_strsplit(fn_last_split_on_period, " ", -1); // Split on whitespace + g_free(fn_last_split_on_period); + + gchar * last_item = koto_utils_string_replace_all(whitespace_split[g_strv_length(split) - 1], "#", ""); // Get last item, removing any # from it + g_strfreev(whitespace_split); + + gchar ** hyphen_split = g_strsplit(last_item, "-", -1); // Split on hyphen + g_free(last_item); + + gchar * position_str = NULL; + for (guint i = 0; i < g_strv_length(hyphen_split); i++) { // Iterate over each item + gchar * pos_str = hyphen_split[i]; + + if (!g_regex_match(num_pat, pos_str, 0, NULL)) { // Is not a number + continue; + } + + position_str = g_strdup(pos_str); + break; + } + + g_strfreev(hyphen_split); + + guint64 position = 0; + + if (position_str != NULL) { // If we have a string defined + if (g_regex_match(num_pat, position_str, 0, NULL)) { // Matches being a number + position = g_ascii_strtoull(position_str, NULL, 10); // Attempt to convert + } + + g_free(position_str); + } + + g_free(num_pat); return position; } diff --git a/src/indexer/track-helpers.h b/src/indexer/track-helpers.h index 299131d..e918766 100644 --- a/src/indexer/track-helpers.h +++ b/src/indexer/track-helpers.h @@ -19,6 +19,8 @@ void koto_track_helpers_init(); +guint64 koto_track_helpers_get_cd_based_on_file_name(const gchar * file_name); + gchar * koto_track_helpers_get_corrected_genre(gchar * original_genre); gchar * koto_track_helpers_get_name_for_file( diff --git a/src/indexer/track.c b/src/indexer/track.c index 8e5fb1d..f5eb42b 100644 --- a/src/indexer/track.c +++ b/src/indexer/track.c @@ -39,7 +39,11 @@ struct _KotoTrack { guint cd; guint64 position; guint64 duration; - guint64 * playback_position; + gchar * description; + gchar * narrator; + guint64 playback_position; + guint64 year; + GList * genres; gboolean do_initial_index; @@ -57,6 +61,9 @@ enum { PROP_CD, PROP_POSITION, PROP_DURATION, + PROP_DESCRIPTION, + PROP_NARRATOR, + PROP_YEAR, PROP_PLAYBACK_POSITION, PROP_PREPARSED_GENRES, N_PROPERTIES @@ -157,6 +164,32 @@ static void koto_track_class_init(KotoTrackClass * c) { G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); + props[PROP_DESCRIPTION] = g_param_spec_string( + "description", + "Description of Track, typically show notes for a podcast episode", + "Description of Track, typically show notes for a podcast episode", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_NARRATOR] = g_param_spec_string( + "narrator", + "Narrator, typically of an Audiobook", + "Narrator, typically of an Audiobook", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + props[PROP_YEAR] = g_param_spec_uint64( + "year", + "Year", + "Year", + 0, + G_MAXUINT64, + 0, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + props[PROP_PLAYBACK_POSITION] = g_param_spec_uint64( "playback-position", "Current playback position", @@ -179,10 +212,13 @@ static void koto_track_class_init(KotoTrackClass * c) { } static void koto_track_init(KotoTrack * self) { + self->description = NULL; // Initialize our description self->duration = 0; // Initialize our duration self->genres = NULL; // Initialize our genres list + self->narrator = NULL, // Initialize our narrator self->paths = g_hash_table_new(g_str_hash, g_str_equal); // Create our hash table of paths self->position = 0; // Initialize our duration + self->year = 0; // Initialize our year } static void koto_track_get_property( @@ -209,11 +245,20 @@ static void koto_track_get_property( case PROP_CD: g_value_set_uint(val, self->cd); break; + case PROP_DESCRIPTION: + g_value_set_string(val, self->description); + break; + case PROP_NARRATOR: + g_value_set_string(val, self->narrator); + break; + case PROP_YEAR: + g_value_set_uint64(val, self->year); + break; case PROP_POSITION: - g_value_set_uint(val, self->position); + g_value_set_uint64(val, self->position); break; case PROP_PLAYBACK_POSITION: - g_value_set_uint(val, GPOINTER_TO_UINT(self->playback_position)); + g_value_set_uint64(val, koto_track_get_playback_position(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); @@ -254,11 +299,20 @@ static void koto_track_set_property( koto_track_set_position(self, g_value_get_uint64(val)); break; case PROP_PLAYBACK_POSITION: - self->playback_position = GUINT_TO_POINTER(g_value_get_uint64(val)); + koto_track_set_playback_position(self, g_value_get_uint64(val)); break; case PROP_DURATION: koto_track_set_duration(self, g_value_get_uint64(val)); break; + case PROP_DESCRIPTION: + koto_track_set_description(self, g_value_get_string(val)); + break; + case PROP_NARRATOR: + koto_track_set_narrator(self, g_strdup(g_value_get_string(val))); + break; + case PROP_YEAR: + koto_track_set_year(self, g_value_get_uint64(val)); + break; case PROP_PREPARSED_GENRES: koto_track_set_preparsed_genres(self, g_strdup(g_value_get_string(val))); break; @@ -273,14 +327,10 @@ void koto_track_commit(KotoTrack * self) { return; } - if (!koto_utils_is_string_valid(self->artist_uuid)) { // No valid required artist UUID + if (!koto_utils_string_is_valid(self->artist_uuid)) { // No valid required artist UUID return; } - if (!koto_utils_is_string_valid(self->album_uuid)) { // If we do not have a valid album UUID - self->album_uuid = g_strdup(""); - } - gchar * commit_msg = "INSERT INTO tracks(id, artist_id, album_id, name, disc, position, duration, genres)" \ "VALUES('%s', '%s', '%s', quote(\"%s\"), %d, %d, %d, '%s')" \ "ON CONFLICT(id) DO UPDATE SET album_id=excluded.album_id, artist_id=excluded.artist_id, name=excluded.name, disc=excluded.disc, position=excluded.position, duration=excluded.duration, genres=excluded.genres;"; @@ -292,7 +342,7 @@ void koto_track_commit(KotoTrack * self) { commit_msg, self->uuid, self->artist_uuid, - self->album_uuid, + koto_utils_string_get_valid(self->album_uuid), g_strescape(self->parsed_name, NULL), (int) self->cd, (int) self->position, @@ -326,6 +376,10 @@ void koto_track_commit(KotoTrack * self) { } } +gchar * koto_track_get_description(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? g_strdup(self->description) : NULL; +} + guint koto_track_get_disc_number(KotoTrack * self) { return KOTO_IS_TRACK(self) ? self->cd : 1; } @@ -347,14 +401,14 @@ GVariant * koto_track_get_metadata_vardict(KotoTrack * self) { KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, self->artist_uuid); gchar * artist_name = koto_artist_get_name(artist); - if (koto_utils_is_string_valid(self->album_uuid)) { // Have an album associated + if (koto_utils_string_is_valid(self->album_uuid)) { // Have an album associated KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); if (KOTO_IS_ALBUM(album)) { gchar * album_art_path = koto_album_get_art(album); gchar * album_name = koto_album_get_name(album); - if (koto_utils_is_string_valid(album_art_path)) { // Valid album art path + if (koto_utils_string_is_valid(album_art_path)) { // Valid album art path album_art_path = g_strconcat("file://", album_art_path, NULL); // Prepend with file:// g_variant_builder_add(builder, "{sv}", "mpris:artUrl", g_variant_new_string(album_art_path)); } @@ -366,7 +420,7 @@ GVariant * koto_track_get_metadata_vardict(KotoTrack * self) { g_variant_builder_add(builder, "{sv}", "mpris:trackid", g_variant_new_string(self->uuid)); - if (koto_utils_is_string_valid(artist_name)) { // Valid artist name + if (koto_utils_string_is_valid(artist_name)) { // Valid artist name GVariant * artist_name_variant; GVariantBuilder * artist_list_builder = g_variant_builder_new(G_VARIANT_TYPE("as")); g_variant_builder_add(artist_list_builder, "s", artist_name); @@ -388,11 +442,11 @@ GVariant * koto_track_get_metadata_vardict(KotoTrack * self) { } gchar * koto_track_get_name(KotoTrack * self) { - if (!KOTO_IS_TRACK(self)) { // Not a track - return NULL; - } + return KOTO_IS_TRACK(self) ? g_strdup(self->parsed_name) : NULL; +} - return g_strdup(self->parsed_name); +gchar * koto_track_get_narrator(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? g_strdup(self->narrator) : NULL; } gchar * koto_track_get_path(KotoTrack * self) { @@ -420,6 +474,10 @@ gchar * koto_track_get_path(KotoTrack * self) { return path; } +guint64 koto_track_get_playback_position(KotoTrack * self) { + return KOTO_IS_TRACK(self) ? self->playback_position : 0; +} + guint64 koto_track_get_position(KotoTrack * self) { return KOTO_IS_TRACK(self) ? self->position : 0; } @@ -433,13 +491,13 @@ gchar * koto_track_get_uniqueish_key(KotoTrack * self) { gchar * artist_name = koto_artist_get_name(artist); // Get the artist name - if (koto_utils_is_string_valid(self->album_uuid)) { // If we have an album associated with this track (not necessarily guaranteed) + if (koto_utils_string_is_valid(self->album_uuid)) { // If we have an album associated with this track (not necessarily guaranteed) KotoAlbum * possible_album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); if (KOTO_IS_ALBUM(possible_album)) { // Album exists gchar * album_name = koto_album_get_name(possible_album); // Get the name of the album - if (koto_utils_is_string_valid(album_name)) { + if (koto_utils_string_is_valid(album_name)) { return g_strdup_printf("%s-%s-%s", artist_name, album_name, self->parsed_name); // Create a key of (ARTIST/WRITER)-(ALBUM/AUDIOBOOK)-(CHAPTER/TRACK) } } @@ -456,6 +514,14 @@ gchar * koto_track_get_uuid(KotoTrack * self) { return self->uuid; // Do not return a duplicate since otherwise comparison refs fail due to pointer positions being different } +guint64 koto_track_get_year(KotoTrack * self) { + if (!KOTO_IS_TRACK(self)) { + return 0; + } + + return self->year; +} + void koto_track_remove_from_playlist( KotoTrack * self, gchar * playlist_uuid @@ -487,7 +553,7 @@ void koto_track_set_album_uuid( gchar * uuid = g_strdup(album_uuid); - if (!koto_utils_is_string_valid(uuid)) { // If this is not a valid string + if (!koto_utils_string_is_valid(uuid)) { // If this is not a valid string return; } @@ -497,19 +563,17 @@ void koto_track_set_album_uuid( void koto_track_save_to_playlist( KotoTrack * self, - gchar * playlist_uuid, - gint current + gchar * playlist_uuid ) { if (!KOTO_IS_TRACK(self)) { return; } gchar * commit_op = g_strdup_printf( - "INSERT INTO playlist_tracks(playlist_id, track_id, current)" - "VALUES('%s', '%s', quote(\"%d\"))", + "INSERT INTO playlist_tracks(playlist_id, track_id)" + "VALUES('%s', '%s')", playlist_uuid, - self->uuid, - current + self->uuid ); new_transaction(commit_op, "Failed to save track to playlist", FALSE); @@ -531,6 +595,30 @@ void koto_track_set_cd( g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CD]); } +void koto_track_set_description( + KotoTrack * self, + const gchar * description +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid(description)) { + return; + } + + if (g_strcmp0(self->description, description) == 0) { // Same description + return; + } + + if (koto_utils_string_is_valid(self->description)) { + g_free(self->description); // Free the existing narrator + } + + self->description = g_strdup(description); // Duplicate our description + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DESCRIPTION]); +} + void koto_track_set_duration( KotoTrack * self, guint64 duration @@ -544,6 +632,7 @@ void koto_track_set_duration( } self->duration = duration; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DURATION]); } void koto_track_set_genres( @@ -554,7 +643,7 @@ void koto_track_set_genres( return; } - if (!koto_utils_is_string_valid((gchar*) genrelist)) { // If it is an empty string + if (!koto_utils_string_is_valid((gchar*) genrelist)) { // If it is an empty string return; } @@ -570,7 +659,7 @@ void koto_track_set_genres( gchar * genre = genres[i]; // Get the genre gchar * lowercased_genre = g_utf8_strdown(g_strstrip(genre), -1); // Lowercase the genre - gchar * lowercased_hyphenated_genre = koto_utils_replace_string_all(lowercased_genre, " ", "-"); + gchar * lowercased_hyphenated_genre = koto_utils_string_replace_all(lowercased_genre, " ", "-"); g_free(lowercased_genre); // Free the lowercase genre string since we no longer need it gchar * corrected_genre = koto_track_helpers_get_corrected_genre(lowercased_hyphenated_genre); // Get any corrected genre @@ -585,6 +674,30 @@ void koto_track_set_genres( g_strfreev(genres); // Free the list of genres locally } +void koto_track_set_narrator( + KotoTrack * self, + const gchar * narrator +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + if (!koto_utils_string_is_valid(narrator)) { + return; + } + + if (g_strcmp0(self->narrator, narrator) == 0) { // Same narrator + return; + } + + if (koto_utils_string_is_valid(self->narrator)) { + g_free(self->narrator); // Free the existing narrator + } + + self->narrator = g_strdup(narrator); // Duplicate our narrator + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_NARRATOR]); +} + void koto_track_set_parsed_name( KotoTrack * self, gchar * new_parsed_name @@ -593,11 +706,11 @@ void koto_track_set_parsed_name( return; } - if (!koto_utils_is_string_valid(new_parsed_name)) { + if (!koto_utils_string_is_valid(new_parsed_name)) { return; } - gboolean have_existing_name = koto_utils_is_string_valid(self->parsed_name); + gboolean have_existing_name = koto_utils_string_is_valid(self->parsed_name); if (have_existing_name && (strcmp(self->parsed_name, new_parsed_name) == 0)) { // Have existing name that matches one provided return; // Don't do anything @@ -620,7 +733,7 @@ void koto_track_set_path( return; } - if (!koto_utils_is_string_valid(fixed_path)) { // Not a valid path + if (!koto_utils_string_is_valid(fixed_path)) { // Not a valid path return; } @@ -635,10 +748,25 @@ void koto_track_set_path( } } +void koto_track_set_playback_position( + KotoTrack * self, + guint64 position +) { + if (!KOTO_IS_TRACK(self)) { // Not a track + return; + } + + self->playback_position = position; +} + void koto_track_set_position( KotoTrack * self, guint64 pos ) { + if (!KOTO_IS_TRACK(self)) { // Not a track + return; + } + if (pos == 0) { // No position change really return; } @@ -647,29 +775,48 @@ void koto_track_set_position( g_object_notify_by_pspec(G_OBJECT(self), props[PROP_POSITION]); } +void koto_track_set_year( + KotoTrack * self, + guint64 year +) { + if (!KOTO_IS_TRACK(self)) { + return; + } + + self->year = year; + g_object_notify_by_pspec(G_OBJECT(self), props[PROP_YEAR]); +} + void koto_track_update_metadata(KotoTrack * self) { if (!KOTO_IS_TRACK(self)) { // Not a track return; } gchar * optimal_track_path = koto_track_get_path(self); // Check all the libraries associated with this track, based on priority, return a built path using lib path + relative file path + + if (!koto_utils_string_is_valid(optimal_track_path)) { // Not a valid string + return; + } + TagLib_File * t_file = taglib_file_new(optimal_track_path); // Get a taglib file for this file - g_free(optimal_track_path); if ((t_file != NULL) && taglib_file_is_valid(t_file)) { // If we got the taglib file and it is valid TagLib_Tag * tag = taglib_file_tag(t_file); // Get our tag koto_track_set_genres(self, taglib_tag_genre(tag)); // Set our genres to any genres listed for the track koto_track_set_position(self, (uint) taglib_tag_track(tag)); // Get the track, convert to uint and cast as a pointer - + koto_track_set_year(self, (guint64) taglib_tag_year(tag)); // Get the track year and convert it to guint64 const TagLib_AudioProperties * tag_props = taglib_file_audioproperties(t_file); // Get the audio properties of the file koto_track_set_duration(self, taglib_audioproperties_length(tag_props)); // Get the length of the track and set it as our duration - } else { // Failed to get tag info + } + + if (self->position == 0) { // Failed to get tag info or got the tag info but position is zero guint64 position = koto_track_helpers_get_position_based_on_file_name(g_path_get_basename(optimal_track_path)); // Get the likely position koto_track_set_position(self, position); // Set our position } taglib_tag_free_strings(); // Free strings taglib_file_free(t_file); // Free the file + g_free(optimal_track_path); } void koto_track_set_preparsed_genres( @@ -680,7 +827,7 @@ void koto_track_set_preparsed_genres( return; } - if (!koto_utils_is_string_valid(genrelist)) { // If it is an empty string + if (!koto_utils_string_is_valid(genrelist)) { // If it is an empty string return; } diff --git a/src/koto-dialog-container.c b/src/koto-dialog-container.c index 41049d0..fb7c2ac 100644 --- a/src/koto-dialog-container.c +++ b/src/koto-dialog-container.c @@ -17,7 +17,7 @@ #include #include -#include "koto-button.h" +#include "components/button.h" #include "koto-dialog-container.h" struct _KotoDialogContainer { diff --git a/src/koto-expander.c b/src/koto-expander.c index 14fb48a..f95af87 100644 --- a/src/koto-expander.c +++ b/src/koto-expander.c @@ -17,8 +17,8 @@ #include #include +#include "components/button.h" #include "config/config.h" -#include "koto-button.h" #include "koto-expander.h" #include "koto-utils.h" @@ -212,7 +212,7 @@ void koto_expander_set_icon_name( return; } - if (!koto_utils_is_string_valid(icon)) { // Not a valid string + if (!koto_utils_string_is_valid(icon)) { // Not a valid string return; } diff --git a/src/koto-expander.h b/src/koto-expander.h index 48f6b40..558ebcf 100644 --- a/src/koto-expander.h +++ b/src/koto-expander.h @@ -18,7 +18,7 @@ #pragma once #include -#include "koto-button.h" +#include "components/button.h" G_BEGIN_DECLS diff --git a/src/koto-nav.c b/src/koto-nav.c index 44f9b63..7c67785 100644 --- a/src/koto-nav.c +++ b/src/koto-nav.c @@ -16,11 +16,11 @@ */ #include +#include "components/button.h" +#include "config/config.h" #include "db/cartographer.h" #include "indexer/structs.h" #include "playlist/playlist.h" -#include "config/config.h" -#include "koto-button.h" #include "koto-expander.h" #include "koto-nav.h" #include "koto-utils.h" @@ -112,12 +112,17 @@ void koto_nav_create_audiobooks_section(KotoNav * self) { koto_expander_set_content(a_expander, new_content); self->audiobooks_local = koto_button_new_plain("Library"); + koto_button_set_data(self->audiobooks_local, &"audiobooks.library"); + self->audiobooks_audible = koto_button_new_plain("Audible"); self->audiobooks_librivox = koto_button_new_plain("LibriVox"); gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_local)); gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_audible)); gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->audiobooks_librivox)); + + koto_button_add_click_handler(self->audiobooks_local, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), self->audiobooks_local); + } void koto_nav_create_music_section(KotoNav * self) { @@ -129,13 +134,15 @@ void koto_nav_create_music_section(KotoNav * self) { GtkWidget * new_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); self->music_local = koto_button_new_plain("Library"); + koto_button_set_data(self->music_local, &"music.library"); + self->music_radio = koto_button_new_plain("Radio"); gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->music_local)); gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->music_radio)); koto_expander_set_content(m_expander, new_content); - koto_button_add_click_handler(self->music_local, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_nav_handle_local_music_click), NULL); + koto_button_add_click_handler(self->music_local, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), self->music_local); } void koto_nav_create_playlist_section(KotoNav * self) { @@ -187,37 +194,6 @@ void koto_nav_handle_playlist_add_click( koto_window_show_dialog(main_window, "create-modify-playlist"); } -void koto_nav_handle_local_music_click( - GtkGestureClick * gesture, - int n_press, - double x, - double y, - gpointer user_data -) { - (void) gesture; - (void) n_press; - (void) x; - (void) y; - (void) user_data; - koto_window_go_to_page(main_window, "music.local"); // Go to the playlist page -} - -void koto_nav_handle_playlist_button_click( - GtkGestureClick * gesture, - int n_press, - double x, - double y, - gpointer user_data -) { - (void) gesture; - (void) n_press; - (void) x; - (void) y; - gchar * playlist_uuid = user_data; - - koto_window_go_to_page(main_window, playlist_uuid); // Go to the playlist page -} - void koto_nav_handle_playlist_added( KotoCartographer * carto, KotoPlaylist * playlist, @@ -228,6 +204,10 @@ void koto_nav_handle_playlist_added( return; } + if (koto_playlist_get_is_hidden(playlist)) { // Should be hidden from nav + return; + } + KotoNav * self = user_data; if (!KOTO_IS_NAV(self)) { @@ -245,25 +225,28 @@ void koto_nav_handle_playlist_added( gchar * playlist_art_path = koto_playlist_get_artwork(playlist); // Get any file path for it KotoButton * playlist_button = NULL; - if (koto_utils_is_string_valid(playlist_art_path)) { // Have a file associated + if (koto_utils_string_is_valid(playlist_art_path)) { // Have a file associated playlist_button = koto_button_new_with_file(playlist_name, playlist_art_path, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); } else { // No file associated playlist_button = koto_button_new_with_icon(playlist_name, "audio-x-generic-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); } - if (KOTO_IS_BUTTON(playlist_button)) { - g_hash_table_insert(self->playlist_buttons, playlist_uuid, playlist_button); // Add the button + if (!KOTO_IS_BUTTON(playlist_button)) { // Failed to create the playlist button + return; + } - // TODO: Make this a ListBox and sort the playlists alphabetically - GtkBox * playlist_expander_content = GTK_BOX(koto_expander_get_content(self->playlists_expander)); + koto_button_set_data(playlist_button, playlist_uuid); // Set our data to the playlist UUID string + g_hash_table_insert(self->playlist_buttons, playlist_uuid, playlist_button); // Add the button - if (GTK_IS_BOX(playlist_expander_content)) { - gtk_box_append(playlist_expander_content, GTK_WIDGET(playlist_button)); + // TODO: Make this a ListBox and sort the playlists alphabetically + GtkBox * playlist_expander_content = GTK_BOX(koto_expander_get_content(self->playlists_expander)); - koto_button_add_click_handler(playlist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_nav_handle_playlist_button_click), playlist_uuid); - koto_window_handle_playlist_added(koto_maps, playlist, main_window); // TODO: MOVE THIS - g_signal_connect(playlist, "modified", G_CALLBACK(koto_nav_handle_playlist_modified), self); - } + if (GTK_IS_BOX(playlist_expander_content)) { + gtk_box_append(playlist_expander_content, GTK_WIDGET(playlist_button)); + + koto_button_add_click_handler(playlist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), playlist_button); + koto_window_handle_playlist_added(koto_maps, playlist, main_window); // TODO: MOVE THIS + g_signal_connect(playlist, "modified", G_CALLBACK(koto_nav_handle_playlist_modified), self); } } @@ -291,13 +274,13 @@ void koto_nav_handle_playlist_modified( gchar * artwork = koto_playlist_get_artwork(playlist); // Get the artwork - if (koto_utils_is_string_valid(artwork)) { // Have valid artwork + if (koto_utils_string_is_valid(artwork)) { // Have valid artwork koto_button_set_file_path(playlist_button, artwork); // Update the artwork path } gchar * name = koto_playlist_get_name(playlist); // Get the name - if (koto_utils_is_string_valid(name)) { // Have valid name + if (koto_utils_string_is_valid(name)) { // Have valid name koto_button_set_text(playlist_button, name); // Update the button text } } diff --git a/src/koto-nav.h b/src/koto-nav.h index d69d7bf..6583eb4 100644 --- a/src/koto-nav.h +++ b/src/koto-nav.h @@ -60,14 +60,6 @@ void koto_nav_handle_playlist_removed( gpointer user_data ); -void koto_nav_handle_local_music_click( - GtkGestureClick * gesture, - int n_press, - double x, - double y, - gpointer user_data -); - GtkWidget * koto_nav_get_nav(KotoNav * self); G_END_DECLS diff --git a/src/koto-playerbar.c b/src/koto-playerbar.c index 6625ef9..7070e30 100644 --- a/src/koto-playerbar.c +++ b/src/koto-playerbar.c @@ -17,13 +17,13 @@ #include #include +#include "components/button.h" #include "config/config.h" #include "db/cartographer.h" #include "playlist/add-remove-track-popover.h" #include "playlist/current.h" #include "playlist/playlist.h" #include "playback/engine.h" -#include "koto-button.h" #include "config/config.h" #include "koto-playerbar.h" #include "koto-utils.h" @@ -64,6 +64,9 @@ struct _KotoPlayerBar { GtkWidget * playback_album; GtkWidget * playback_artist; + /* Misc Widgets */ + GtkWidget * playback_position_label; + gint64 last_recorded_duration; gboolean is_progressbar_seeking; @@ -177,98 +180,104 @@ void koto_playerbar_apply_configuration_state( gtk_scale_button_set_value(GTK_SCALE_BUTTON(self->volume_button), config_last_used_volume * 100); } -void koto_playerbar_create_playback_details(KotoPlayerBar * bar) { - bar->playback_details_section = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); +void koto_playerbar_create_playback_details(KotoPlayerBar * self) { + self->playback_details_section = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); GtkIconTheme * default_icon_theme = gtk_icon_theme_get_for_display(gdk_display_get_default()); // Get the icon theme for this display if (default_icon_theme != NULL) { - gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(bar->main)); + gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self->main)); GtkIconPaintable * audio_paintable = gtk_icon_theme_lookup_icon(default_icon_theme, "audio-x-generic-symbolic", NULL, 96, scale_factor, GTK_TEXT_DIR_NONE, GTK_ICON_LOOKUP_PRELOAD); if (GTK_IS_ICON_PAINTABLE(audio_paintable)) { - if (GTK_IS_IMAGE(bar->artwork)) { - gtk_image_set_from_paintable(GTK_IMAGE(bar->artwork), GDK_PAINTABLE(audio_paintable)); + if (GTK_IS_IMAGE(self->artwork)) { + gtk_image_set_from_paintable(GTK_IMAGE(self->artwork), GDK_PAINTABLE(audio_paintable)); } else { // Not an image - bar->artwork = gtk_image_new_from_paintable(GDK_PAINTABLE(audio_paintable)); - gtk_widget_add_css_class(bar->artwork, "circular"); - gtk_widget_set_size_request(bar->artwork, 96, 96); - gtk_box_append(GTK_BOX(bar->playback_section), bar->artwork); + self->artwork = gtk_image_new_from_paintable(GDK_PAINTABLE(audio_paintable)); + gtk_widget_add_css_class(self->artwork, "circular"); + gtk_widget_set_size_request(self->artwork, 96, 96); + gtk_box_append(GTK_BOX(self->playback_section), self->artwork); } } } - bar->playback_title = gtk_label_new("Title"); - gtk_label_set_xalign(GTK_LABEL(bar->playback_title), 0); + self->playback_title = gtk_label_new("Title"); + gtk_label_set_xalign(GTK_LABEL(self->playback_title), 0); - bar->playback_album = gtk_label_new("Album"); - gtk_label_set_xalign(GTK_LABEL(bar->playback_album), 0); + self->playback_album = gtk_label_new("Album"); + gtk_label_set_xalign(GTK_LABEL(self->playback_album), 0); - bar->playback_artist = gtk_label_new("Artist"); - gtk_label_set_xalign(GTK_LABEL(bar->playback_artist), 0); + self->playback_artist = gtk_label_new("Artist"); + gtk_label_set_xalign(GTK_LABEL(self->playback_artist), 0); - gtk_box_append(GTK_BOX(bar->playback_details_section), GTK_WIDGET(bar->playback_title)); - gtk_box_append(GTK_BOX(bar->playback_details_section), GTK_WIDGET(bar->playback_album)); - gtk_box_append(GTK_BOX(bar->playback_details_section), GTK_WIDGET(bar->playback_artist)); + gtk_box_append(GTK_BOX(self->playback_details_section), GTK_WIDGET(self->playback_title)); + gtk_box_append(GTK_BOX(self->playback_details_section), GTK_WIDGET(self->playback_album)); + gtk_box_append(GTK_BOX(self->playback_details_section), GTK_WIDGET(self->playback_artist)); - gtk_box_append(GTK_BOX(bar->playback_section), GTK_WIDGET(bar->playback_details_section)); + gtk_box_append(GTK_BOX(self->playback_section), GTK_WIDGET(self->playback_details_section)); } -void koto_playerbar_create_primary_controls(KotoPlayerBar * bar) { - bar->back_button = koto_button_new_with_icon("", "media-skip-backward-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); - bar->play_pause_button = koto_button_new_with_icon("", "media-playback-start-symbolic", "media-playback-pause-symbolic", KOTO_BUTTON_PIXBUF_SIZE_LARGE); // TODO: Have this take in a state and switch to a different icon if necessary - bar->forward_button = koto_button_new_with_icon("", "media-skip-forward-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); +void koto_playerbar_create_primary_controls(KotoPlayerBar * self) { + self->back_button = koto_button_new_with_icon("", "media-skip-backward-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->play_pause_button = koto_button_new_with_icon("", "media-playback-start-symbolic", "media-playback-pause-symbolic", KOTO_BUTTON_PIXBUF_SIZE_LARGE); // TODO: Have this take in a state and switch to a different icon if necessary + self->forward_button = koto_button_new_with_icon("", "media-skip-forward-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); - if (KOTO_IS_BUTTON(bar->back_button)) { - gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->back_button)); - koto_button_add_click_handler(bar->back_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_backwards), bar); + if (KOTO_IS_BUTTON(self->back_button)) { + gtk_box_append(GTK_BOX(self->primary_controls_section), GTK_WIDGET(self->back_button)); + koto_button_add_click_handler(self->back_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_backwards), self); } - if (KOTO_IS_BUTTON(bar->play_pause_button)) { - gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->play_pause_button)); - koto_button_add_click_handler(bar->play_pause_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_play_pause), bar); + if (KOTO_IS_BUTTON(self->play_pause_button)) { + gtk_box_append(GTK_BOX(self->primary_controls_section), GTK_WIDGET(self->play_pause_button)); + koto_button_add_click_handler(self->play_pause_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_play_pause), self); } - if (KOTO_IS_BUTTON(bar->forward_button)) { - gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->forward_button)); - koto_button_add_click_handler(bar->forward_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_forwards), bar); + if (KOTO_IS_BUTTON(self->forward_button)) { + gtk_box_append(GTK_BOX(self->primary_controls_section), GTK_WIDGET(self->forward_button)); + koto_button_add_click_handler(self->forward_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_forwards), self); } } -void koto_playerbar_create_secondary_controls(KotoPlayerBar * bar) { - bar->repeat_button = koto_button_new_with_icon("", "media-playlist-repeat-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); - bar->shuffle_button = koto_button_new_with_icon("", "media-playlist-shuffle-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); - bar->playlist_button = koto_button_new_with_icon("", "playlist-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); - bar->eq_button = koto_button_new_with_icon("", "multimedia-equalizer-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); +void koto_playerbar_create_secondary_controls(KotoPlayerBar * self) { + self->playback_position_label = gtk_label_new(NULL); // Create our label to communicate position and duration of a track - bar->volume_button = gtk_volume_button_new(); // Have this take in a state and switch to a different icon if necessary + self->repeat_button = koto_button_new_with_icon("", "media-playlist-repeat-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->shuffle_button = koto_button_new_with_icon("", "media-playlist-shuffle-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->playlist_button = koto_button_new_with_icon("", "playlist-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); + self->eq_button = koto_button_new_with_icon("", "multimedia-equalizer-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL); - if (KOTO_IS_BUTTON(bar->repeat_button)) { - gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->repeat_button)); - koto_button_add_click_handler(bar->repeat_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_track_repeat), bar); + self->volume_button = gtk_volume_button_new(); // Have this take in a state and switch to a different icon if necessary + + if (GTK_IS_LABEL(self->playback_position_label)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), self->playback_position_label); } - if (KOTO_IS_BUTTON(bar->shuffle_button)) { - gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->shuffle_button)); - koto_button_add_click_handler(bar->shuffle_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_playlist_shuffle), bar); + if (KOTO_IS_BUTTON(self->repeat_button)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), GTK_WIDGET(self->repeat_button)); + koto_button_add_click_handler(self->repeat_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_track_repeat), self); } - if (KOTO_IS_BUTTON(bar->playlist_button)) { - gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->playlist_button)); - koto_button_add_click_handler(bar->playlist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_handle_playlist_button_clicked), bar); + if (KOTO_IS_BUTTON(self->shuffle_button)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), GTK_WIDGET(self->shuffle_button)); + koto_button_add_click_handler(self->shuffle_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_playlist_shuffle), self); } - if (KOTO_IS_BUTTON(bar->eq_button)) { - gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->eq_button)); + if (KOTO_IS_BUTTON(self->playlist_button)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), GTK_WIDGET(self->playlist_button)); + koto_button_add_click_handler(self->playlist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_handle_playlist_button_clicked), self); } - if (GTK_IS_VOLUME_BUTTON(bar->volume_button)) { + if (KOTO_IS_BUTTON(self->eq_button)) { + gtk_box_append(GTK_BOX(self->secondary_controls_section), GTK_WIDGET(self->eq_button)); + } + + if (GTK_IS_VOLUME_BUTTON(self->volume_button)) { GtkAdjustment * granular_volume_change = gtk_adjustment_new(50.0, 0, 100.0, 1.0, 1.0, 1.0); - g_object_set(bar->volume_button, "use-symbolic", TRUE, NULL); - gtk_scale_button_set_adjustment(GTK_SCALE_BUTTON(bar->volume_button), granular_volume_change); // Set our adjustment - gtk_box_append(GTK_BOX(bar->secondary_controls_section), bar->volume_button); + g_object_set(self->volume_button, "use-symbolic", TRUE, NULL); + gtk_scale_button_set_adjustment(GTK_SCALE_BUTTON(self->volume_button), granular_volume_change); // Set our adjustment + gtk_box_append(GTK_BOX(self->secondary_controls_section), self->volume_button); - g_signal_connect(GTK_SCALE_BUTTON(bar->volume_button), "value-changed", G_CALLBACK(koto_playerbar_handle_volume_button_change), bar); + g_signal_connect(GTK_SCALE_BUTTON(self->volume_button), "value-changed", G_CALLBACK(koto_playerbar_handle_volume_button_change), self); } } @@ -312,13 +321,13 @@ void koto_playerbar_handle_is_playing( return; } - KotoPlayerBar * bar = user_data; + KotoPlayerBar * self = user_data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - koto_button_show_image(bar->play_pause_button, TRUE); // Set to TRUE to show pause as the next action + koto_button_show_image(self->play_pause_button, TRUE); // Set to TRUE to show pause as the next action } void koto_playerbar_handle_is_paused( @@ -329,13 +338,13 @@ void koto_playerbar_handle_is_paused( return; } - KotoPlayerBar * bar = user_data; + KotoPlayerBar * self = user_data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - koto_button_show_image(bar->play_pause_button, FALSE); // Set to FALSE to show play as the next action + koto_button_show_image(self->play_pause_button, FALSE); // Set to FALSE to show play as the next action } void koto_playerbar_handle_playlist_button_clicked( @@ -366,13 +375,13 @@ void koto_playerbar_handle_progressbar_gesture_begin( ) { (void) gesture; (void) seq; - KotoPlayerBar * bar = data; + KotoPlayerBar * self = data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - bar->is_progressbar_seeking = TRUE; + self->is_progressbar_seeking = TRUE; } void koto_playerbar_handle_progressbar_gesture_end( @@ -382,12 +391,12 @@ void koto_playerbar_handle_progressbar_gesture_end( ) { (void) gesture; (void) seq; - KotoPlayerBar * bar = data; + KotoPlayerBar * self = data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - bar->is_progressbar_seeking = FALSE; + self->is_progressbar_seeking = FALSE; } void koto_playerbar_handle_progressbar_pressed( @@ -401,26 +410,26 @@ void koto_playerbar_handle_progressbar_pressed( (void) n_press; (void) x; (void) y; - KotoPlayerBar * bar = data; + KotoPlayerBar * self = data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - bar->is_progressbar_seeking = TRUE; + self->is_progressbar_seeking = TRUE; } void koto_playerbar_handle_progressbar_value_changed( GtkRange * progress_bar, gpointer data ) { - KotoPlayerBar * bar = data; + KotoPlayerBar * self = data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - if (!bar->is_progressbar_seeking) { + if (!self->is_progressbar_seeking) { return; } @@ -437,13 +446,13 @@ void koto_playerbar_handle_tick_duration( return; } - KotoPlayerBar * bar = user_data; + KotoPlayerBar * self = user_data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - koto_playerbar_set_progressbar_duration(bar, koto_playback_engine_get_duration(engine)); + koto_playerbar_set_progressbar_duration(self, koto_playback_engine_get_duration(engine)); } void koto_playerbar_handle_tick_track( @@ -454,15 +463,27 @@ void koto_playerbar_handle_tick_track( return; } - KotoPlayerBar * bar = user_data; + KotoPlayerBar * self = user_data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - if (!bar->is_progressbar_seeking) { - koto_playerbar_set_progressbar_value(bar, koto_playback_engine_get_progress(engine)); + if (self->is_progressbar_seeking) { // Currently seeking + return; } + + KotoTrack * current_track = koto_playback_engine_get_current_track(engine); + + gtk_label_set_text( + GTK_LABEL(self->playback_position_label), + g_strdup_printf( + "%s / %s", + koto_utils_seconds_to_time_format(koto_track_get_playback_position(current_track)), + koto_utils_seconds_to_time_format(koto_track_get_duration(current_track)) + )); + + koto_playerbar_set_progressbar_value(self, koto_playback_engine_get_progress(engine)); } void koto_playerbar_handle_track_repeat( @@ -473,16 +494,16 @@ void koto_playerbar_handle_track_repeat( return; } - KotoPlayerBar * bar = user_data; + KotoPlayerBar * self = user_data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } if (koto_playback_engine_get_track_repeat(engine)) { // Is repeating - gtk_widget_add_css_class(GTK_WIDGET(bar->repeat_button), "active"); // Add active CSS class + gtk_widget_add_css_class(GTK_WIDGET(self->repeat_button), "active"); // Add active CSS class } else { // Is not repeating - gtk_widget_remove_css_class(GTK_WIDGET(bar->repeat_button), "active"); // Remove active CSS class + gtk_widget_remove_css_class(GTK_WIDGET(self->repeat_button), "active"); // Remove active CSS class } } @@ -494,16 +515,16 @@ void koto_playerbar_handle_track_shuffle( return; } - KotoPlayerBar * bar = user_data; + KotoPlayerBar * self = user_data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } if (koto_playback_engine_get_track_shuffle(engine)) { // Is repeating - gtk_widget_add_css_class(GTK_WIDGET(bar->shuffle_button), "active"); // Add active CSS class + gtk_widget_add_css_class(GTK_WIDGET(self->shuffle_button), "active"); // Add active CSS class } else { // Is not repeating - gtk_widget_remove_css_class(GTK_WIDGET(bar->shuffle_button), "active"); // Remove active CSS class + gtk_widget_remove_css_class(GTK_WIDGET(self->shuffle_button), "active"); // Remove active CSS class } } @@ -517,28 +538,28 @@ void koto_playerbar_handle_volume_button_change( koto_playback_engine_set_volume(playback_engine, (double) value / 100); } -void koto_playerbar_reset_progressbar(KotoPlayerBar * bar) { - if (!KOTO_IS_PLAYERBAR(bar)) { +void koto_playerbar_reset_progressbar(KotoPlayerBar * self) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - if (!GTK_IS_RANGE(bar->progress_bar)) { + if (!GTK_IS_RANGE(self->progress_bar)) { return; } - gtk_range_set_range(GTK_RANGE(bar->progress_bar), 0, 0); // Reset range - gtk_range_set_value(GTK_RANGE(bar->progress_bar), 0); // Set value to 0 + gtk_range_set_range(GTK_RANGE(self->progress_bar), 0, 0); // Reset range + gtk_range_set_value(GTK_RANGE(self->progress_bar), 0); // Set value to 0 } void koto_playerbar_set_progressbar_duration( - KotoPlayerBar * bar, + KotoPlayerBar * self, gint64 duration ) { - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - if (!GTK_IS_RANGE(bar->progress_bar)) { + if (!GTK_IS_RANGE(self->progress_bar)) { return; } @@ -546,25 +567,25 @@ void koto_playerbar_set_progressbar_duration( return; } - if (duration != bar->last_recorded_duration) { // Duration is different than what we recorded - bar->last_recorded_duration = duration; - gtk_range_set_range(GTK_RANGE(bar->progress_bar), 0, bar->last_recorded_duration); + if (duration != self->last_recorded_duration) { // Duration is different than what we recorded + self->last_recorded_duration = duration; + gtk_range_set_range(GTK_RANGE(self->progress_bar), 0, self->last_recorded_duration); } } void koto_playerbar_set_progressbar_value( - KotoPlayerBar * bar, + KotoPlayerBar * self, double progress ) { - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } - if (!GTK_IS_RANGE(bar->progress_bar)) { + if (!GTK_IS_RANGE(self->progress_bar)) { return; } - gtk_range_set_value(GTK_RANGE(bar->progress_bar), progress); + gtk_range_set_value(GTK_RANGE(self->progress_bar), progress); } void koto_playerbar_toggle_play_pause( @@ -622,9 +643,9 @@ void koto_playerbar_update_track_info( return; } - KotoPlayerBar * bar = user_data; + KotoPlayerBar * self = user_data; - if (!KOTO_IS_PLAYERBAR(bar)) { + if (!KOTO_IS_PLAYERBAR(self)) { return; } @@ -644,8 +665,8 @@ void koto_playerbar_update_track_info( g_free(artist_uuid); - if (koto_utils_is_string_valid(track_name)) { // Have a track name - gtk_label_set_text(GTK_LABEL(bar->playback_title), track_name); // Set the label + if (koto_utils_string_is_valid(track_name)) { // Have a track name + gtk_label_set_text(GTK_LABEL(self->playback_title), track_name); // Set the label } if (KOTO_IS_ARTIST(artist)) { @@ -653,16 +674,17 @@ void koto_playerbar_update_track_info( g_object_get(artist, "name", &artist_name, NULL); if ((artist_name != NULL) && (strcmp(artist_name, "") != 0)) { // Have an artist name - gtk_label_set_text(GTK_LABEL(bar->playback_artist), artist_name); - gtk_widget_show(bar->playback_artist); + gtk_label_set_text(GTK_LABEL(self->playback_artist), artist_name); + gtk_widget_show(self->playback_artist); } else { // Don't have an artist name somehow - gtk_widget_hide(bar->playback_artist); + gtk_widget_hide(self->playback_artist); } } gchar * art_path = NULL; - if (koto_utils_is_string_valid(album_uuid)) { // Have a valid album UUID + gboolean set_album_label = FALSE; + if (koto_utils_string_is_valid(album_uuid)) { // Have a valid album UUID KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid); g_free(album_uuid); @@ -671,21 +693,22 @@ void koto_playerbar_update_track_info( g_object_get(album, "name", &album_name, "art-path", &art_path, NULL); // Get album name and art path if ((album_name != NULL) && (strcmp(album_name, "") != 0)) { // Have an album name - gtk_label_set_text(GTK_LABEL(bar->playback_album), album_name); - gtk_widget_show(bar->playback_album); - } else { - gtk_widget_hide(bar->playback_album); + gtk_label_set_text(GTK_LABEL(self->playback_album), album_name); + set_album_label = TRUE; + gtk_widget_show(self->playback_album); } } } + (set_album_label) ? gtk_widget_show(self->playback_album) : gtk_widget_hide(self->playback_album); + if ((art_path != NULL) && g_path_is_absolute(art_path)) { // Have an album artist path - gtk_image_set_from_file(GTK_IMAGE(bar->artwork), art_path); // Update the art + gtk_image_set_from_file(GTK_IMAGE(self->artwork), art_path); // Update the art } else { - gtk_image_set_from_icon_name(GTK_IMAGE(bar->artwork), "audio-x-generic-symbolic"); // Use generic instead + gtk_image_set_from_icon_name(GTK_IMAGE(self->artwork), "audio-x-generic-symbolic"); // Use generic instead } } -GtkWidget * koto_playerbar_get_main(KotoPlayerBar * bar) { - return bar->main; +GtkWidget * koto_playerbar_get_main(KotoPlayerBar * self) { + return self->main; } diff --git a/src/koto-playerbar.h b/src/koto-playerbar.h index a542502..a3b3d2e 100644 --- a/src/koto-playerbar.h +++ b/src/koto-playerbar.h @@ -27,7 +27,7 @@ G_DECLARE_FINAL_TYPE(KotoPlayerBar, koto_playerbar, KOTO, PLAYERBAR, GObject) #define KOTO_IS_PLAYERBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYERBAR)) KotoPlayerBar * koto_playerbar_new(void); -GtkWidget * koto_playerbar_get_main(KotoPlayerBar * bar); +GtkWidget * koto_playerbar_get_main(KotoPlayerBar * self); void koto_playerbar_apply_configuration_state( KotoConfig * config, @@ -35,11 +35,11 @@ void koto_playerbar_apply_configuration_state( KotoPlayerBar * self ); -void koto_playerbar_create_playback_details(KotoPlayerBar * bar); +void koto_playerbar_create_playback_details(KotoPlayerBar * self); -void koto_playerbar_create_primary_controls(KotoPlayerBar * bar); +void koto_playerbar_create_primary_controls(KotoPlayerBar * self); -void koto_playerbar_create_secondary_controls(KotoPlayerBar * bar); +void koto_playerbar_create_secondary_controls(KotoPlayerBar * self); void koto_playerbar_go_backwards( GtkGestureClick * gesture, @@ -131,15 +131,15 @@ void koto_playerbar_handle_volume_button_change( gpointer user_data ); -void koto_playerbar_reset_progressbar(KotoPlayerBar * bar); +void koto_playerbar_reset_progressbar(KotoPlayerBar * self); void koto_playerbar_set_progressbar_duration( - KotoPlayerBar * bar, + KotoPlayerBar * self, gint64 duration ); void koto_playerbar_set_progressbar_value( - KotoPlayerBar * bar, + KotoPlayerBar * self, gdouble progress ); diff --git a/src/koto-utils.c b/src/koto-utils.c index 8b22649..3a536f5 100644 --- a/src/koto-utils.c +++ b/src/koto-utils.c @@ -100,6 +100,7 @@ gchar * koto_utils_get_filename_without_extension(gchar * filename) { gchar * stripped_file_name = g_strstrip(g_strdup(trimmed_file_name)); // Strip leading and trailing whitespace g_free(trimmed_file_name); + g_strfreev(split); return stripped_file_name; } @@ -111,13 +112,13 @@ gchar * koto_utils_join_string_list ( GList * cur_list; for (cur_list = list; cur_list != NULL; cur_list = cur_list->next) { // For each item in the list gchar * current_item = cur_list->data; - if (!koto_utils_is_string_valid(current_item)) { // Not a valid string + if (!koto_utils_string_is_valid(current_item)) { // Not a valid string continue; } gchar * item_plus_sep = g_strdup_printf("%s%s", current_item, sep); - if (koto_utils_is_string_valid(liststring)) { // Is a valid string + if (koto_utils_string_is_valid(liststring)) { // Is a valid string gchar * new_string = g_strconcat(liststring, item_plus_sep, NULL); g_free(liststring); liststring = new_string; @@ -128,11 +129,6 @@ gchar * koto_utils_join_string_list ( return (liststring == NULL) ? g_strdup("") : liststring; } - -gboolean koto_utils_is_string_valid(gchar * str) { - return ((str != NULL) && (g_strcmp0(str, "") != 0)); -} - void koto_utils_mkdir(gchar * path) { mkdir(path, 0755); chown(path, getuid(), getgid()); @@ -145,12 +141,37 @@ void koto_utils_push_queue_element_to_store( g_list_store_append(G_LIST_STORE(user_data), data); } -gchar * koto_utils_replace_string_all( +gchar * koto_utils_seconds_to_time_format(guint64 seconds) { + GDateTime * date = g_date_time_new_from_unix_utc(seconds); // Add our seconds after UTC + gchar * date_string = g_date_time_format(date, (g_date_time_get_hour(date) != 0) ? "%H:%M:%S" : "%M:%S"); + g_date_time_unref(date); + return date_string; +} + +gboolean koto_utils_string_contains_substring( + gchar * s, + gchar * sub +) { + gchar ** separated_string = g_strsplit(s, sub, -1); // Split on our substring + gboolean contains = (g_strv_length(separated_string) > 1); + g_strfreev(separated_string); + return contains; +} + +gchar * koto_utils_string_get_valid(gchar * str) { + return koto_utils_string_is_valid(str) ? str : g_strdup(""); // Return string if a string, otherwise return an empty string +} + +gboolean koto_utils_string_is_valid(const gchar * str) { + return ((str != NULL) && (g_strcmp0(str, "") != 0)); +} + +gchar * koto_utils_string_replace_all( gchar * str, gchar * find, gchar * repl ) { - if (!koto_utils_is_string_valid(str)) { // Not a valid string + if (!koto_utils_string_is_valid(str)) { // Not a valid string return g_strdup(""); } @@ -166,22 +187,12 @@ gchar * koto_utils_replace_string_all( return g_strdup(g_strjoinv(repl, split)); } -gboolean koto_utils_string_contains_substring( - gchar * s, - gchar * sub -) { - gchar ** separated_string = g_strsplit(s, sub, -1); // Split on our substring - gboolean contains = (g_strv_length(separated_string) > 1); - g_strfreev(separated_string); - return contains; -} - GList * koto_utils_string_to_string_list( gchar * s, gchar * sep ) { GList * list = NULL; - if (!koto_utils_is_string_valid(s)) { // Provided string is not valid + if (!koto_utils_string_is_valid(s)) { // Provided string is not valid return list; } @@ -189,17 +200,41 @@ GList * koto_utils_string_to_string_list( for (guint i = 0; i < g_strv_length(separated_strings); i++) { // Iterate over each item gchar * item = separated_strings[i]; - list = g_list_append(list, g_strdup(item)); + if (g_strcmp0(item, "") != 0) { // Not an empty string + list = g_list_append(list, g_strdup(item)); + } } g_strfreev(separated_strings); // Free our strings return list; } -gchar * koto_utils_unquote_string(gchar * s) { +gchar * koto_utils_string_title(const gchar * s) { + if (!koto_utils_string_is_valid(s)) { // Not a valid string + return NULL; + } + + glong len = g_utf8_strlen(s, -1); + + if (len == 0) { // Empty string + return g_strdup(s); // Just duplicate itself + } else if (len == 1) { // One char + return g_utf8_strup(s, -1); // Uppercase all relevant cases + } else { + gchar * first_char = g_utf8_substring(s, 0, 1); + gchar * rest_of_string = g_utf8_substring(s, 1, len); // Rest of string + gchar * titled_string = g_strdup_printf("%s%s", g_utf8_strup(first_char, -1), rest_of_string); + + g_free(first_char); + g_free(rest_of_string); + return titled_string; + } +} + +gchar * koto_utils_string_unquote(gchar * s) { gchar * new_s = NULL; - if (!koto_utils_is_string_valid(s)) { // Not a valid string + if (!koto_utils_string_is_valid(s)) { // Not a valid string new_s = g_strdup(""); return new_s; } diff --git a/src/koto-utils.h b/src/koto-utils.h index cff0b46..131fe36 100644 --- a/src/koto-utils.h +++ b/src/koto-utils.h @@ -39,8 +39,6 @@ gchar * koto_utils_join_string_list( gchar * sep ); -gboolean koto_utils_is_string_valid(gchar * str); - void koto_utils_mkdir(gchar * path); void koto_utils_push_queue_element_to_store( @@ -48,22 +46,30 @@ void koto_utils_push_queue_element_to_store( gpointer user_data ); -gchar * koto_utils_replace_string_all( - gchar * str, - gchar * find, - gchar * repl -); +gchar * koto_utils_seconds_to_time_format(guint64 seconds); gboolean koto_utils_string_contains_substring( gchar * s, gchar * sub ); +gchar * koto_utils_string_get_valid(gchar * str); + +gboolean koto_utils_string_is_valid(const gchar * str); + +gchar * koto_utils_string_replace_all( + gchar * str, + gchar * find, + gchar * repl +); + GList * koto_utils_string_to_string_list( gchar * s, gchar * sep ); -gchar * koto_utils_unquote_string(gchar * s); +gchar * koto_utils_string_title(const gchar * s); + +gchar * koto_utils_string_unquote(gchar * s); G_END_DECLS diff --git a/src/koto-window.c b/src/koto-window.c index 67be588..dcf014d 100644 --- a/src/koto-window.c +++ b/src/koto-window.c @@ -16,9 +16,10 @@ */ #include -#include "components/koto-action-bar.h" +#include "components/action-bar.h" #include "db/cartographer.h" #include "indexer/structs.h" +#include "pages/audiobooks/library.h" #include "pages/music/music-local.h" #include "pages/playlist/list.h" #include "playback/engine.h" @@ -33,6 +34,7 @@ extern KotoActionBar * action_bar; extern KotoAddRemoveTrackPopover * koto_add_remove_track_popup; +extern KotoAudiobooksLibraryPage * audiobooks_library_page; extern KotoCartographer * koto_maps; extern KotoCreateModifyPlaylistDialog * playlist_create_modify_dialog; extern KotoConfig * config; @@ -138,11 +140,13 @@ static void koto_window_init (KotoWindow * self) { gtk_widget_queue_draw(self->content_layout); + audiobooks_library_page = koto_audiobooks_library_page_new(); // Create our audiobooks library page music_local_page = koto_page_music_local_new(); // TODO: Remove and do some fancy state loading - koto_window_add_page(self, "music.local", GTK_WIDGET(music_local_page)); - koto_window_go_to_page(self, "music.local"); + koto_window_add_page(self, "audiobooks.library", GTK_WIDGET(koto_audiobooks_library_page_get_main(audiobooks_library_page))); + koto_window_add_page(self, "music.library", GTK_WIDGET(music_local_page)); + koto_window_go_to_page(self, "audiobooks.library"); gtk_widget_show(self->pages); // Do not remove this. Will cause sporadic hiding of the local page content otherwise. } @@ -174,7 +178,7 @@ void koto_window_manage_style( (void) prop_id; if (!KOTO_IS_WINDOW(self)) { // Not a Koto Window - g_warning("Not a window"); + return; } gchar * desired_theme = NULL; @@ -188,7 +192,7 @@ void koto_window_manage_style( NULL ); - if (!koto_utils_is_string_valid(desired_theme)) { // Theme not valid + if (!koto_utils_string_is_valid(desired_theme)) { // Theme not valid desired_theme = "dark"; } diff --git a/src/koto.gresource.xml b/src/koto.gresource.xml index 60a81d4..b09893c 100644 --- a/src/koto.gresource.xml +++ b/src/koto.gresource.xml @@ -1,6 +1,11 @@ + ../data/genres/business-and-personal-finance.png + ../data/genres/foreign-languages.png + ../data/genres/mystery-and-thriller.png + ../data/genres/sci-fi.png + ../data/genres/travel.png ../theme/koto-builtin-dark.css ../theme/koto-builtin-gruvbox.css ../theme/koto-builtin-light.css diff --git a/src/main.c b/src/main.c index e1cc3b6..01e9637 100644 --- a/src/main.c +++ b/src/main.c @@ -28,6 +28,7 @@ #include "playback/mimes.h" #include "playback/mpris.h" #include "paths.h" +#include "playlist/current.h" #include "config/config.h" #include "koto-paths.h" @@ -40,6 +41,7 @@ extern GDBusNodeInfo * introspection_data; extern KotoPlaybackEngine * playback_engine; extern KotoCartographer * koto_maps; +extern KotoCurrentPlaylist * current_playlist; extern sqlite3 * koto_db; extern GHashTable * supported_mimes_hash; @@ -74,6 +76,7 @@ static void on_activate (GtkApplication * app) { static void on_shutdown(GtkApplication * app) { (void) app; + koto_current_playlist_save_playlist_state(current_playlist); // Save the current playlist state if necessary before closure koto_config_save(config); // Save our config close_db(); // Close the database g_bus_unown_name(mpris_bus_id); diff --git a/src/meson.build b/src/meson.build index 7087ead..01ac94b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,8 +5,11 @@ add_global_arguments([ ], language: 'c') koto_sources = [ - 'components/koto-action-bar.c', - 'components/koto-cover-art-button.c', + 'components/album-info.c', + 'components/action-bar.c', + 'components/button.c', + 'components/cover-art-button.c', + 'components/track-item.c', 'components/track-table.c', 'config/config.c', 'db/cartographer.c', @@ -18,6 +21,11 @@ koto_sources = [ 'indexer/library.c', 'indexer/track-helpers.c', 'indexer/track.c', + 'pages/audiobooks/audiobook-view.c', + 'pages/audiobooks/genres-banner.c', + 'pages/audiobooks/genre-button.c', + 'pages/audiobooks/library.c', + 'pages/audiobooks/writer-page.c', 'pages/music/album-view.c', 'pages/music/artist-view.c', 'pages/music/disc-view.c', @@ -32,13 +40,11 @@ koto_sources = [ 'playlist/current.c', 'playlist/playlist.c', 'main.c', - 'koto-button.c', 'koto-dialog-container.c', 'koto-expander.c', 'koto-nav.c', 'koto-playerbar.c', 'koto-paths.c', - 'koto-track-item.c', 'koto-utils.c', 'koto-window.c', ] diff --git a/src/pages/audiobooks/audiobook-view.c b/src/pages/audiobooks/audiobook-view.c new file mode 100644 index 0000000..be2b1bc --- /dev/null +++ b/src/pages/audiobooks/audiobook-view.c @@ -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 +#include +#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 + ); +} \ No newline at end of file diff --git a/src/pages/audiobooks/audiobook-view.h b/src/pages/audiobooks/audiobook-view.h new file mode 100644 index 0000000..fe5cee4 --- /dev/null +++ b/src/pages/audiobooks/audiobook-view.h @@ -0,0 +1,56 @@ +/* audiobook-view.h + * + * 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. + */ + +#pragma once + +#include +#include +#include "../../indexer/structs.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_AUDIOBOOK_VIEW (koto_audiobook_view_get_type()) +G_DECLARE_FINAL_TYPE(KotoAudiobookView, koto_audiobook_view, KOTO, AUDIOBOOK_VIEW, GtkBox); +#define KOTO_IS_AUDIOBOOK_VIEW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_AUDIOBOOK_VIEW)) + +void koto_audiobook_view_set_album( + KotoAudiobookView * self, + KotoAlbum * album +); + +GtkWidget * koto_audiobook_view_create_track_item( + gpointer item, + gpointer user_data +); + +void koto_audiobook_view_handle_play_clicked( + GtkButton * button, + gpointer user_data +); + +void koto_audiobook_view_handle_playlist_updated( + KotoPlaylist * playlist, + gpointer user_data +); + +void koto_audiobook_view_update_side_info(KotoAudiobookView * self); + +void koto_audiobook_view_destroy_associated_user_data(gpointer user_data); + +KotoAudiobookView * koto_audiobook_view_new(); + +G_END_DECLS \ No newline at end of file diff --git a/src/pages/audiobooks/genre-button.c b/src/pages/audiobooks/genre-button.c new file mode 100644 index 0000000..a706a5e --- /dev/null +++ b/src/pages/audiobooks/genre-button.c @@ -0,0 +1,216 @@ +/* genres-button.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 +#include +#include "../../components/button.h" +#include "../../koto-utils.h" +#include "genre-button.h" + +enum { + PROP_0, + PROP_GENRE_ID, + PROP_GENRE_NAME, + N_PROPS +}; + +static GParamSpec * genre_button_props[N_PROPS] = { + NULL, +}; + +struct _KotoAudiobooksGenreButton { + GtkBox parent_instance; + + gchar * genre_id; + gchar * genre_name; + + GtkWidget * bg_image; + KotoButton * button; +}; + +struct _KotoAudiobooksGenreButtonClass { + GtkBoxClass parent_class; +}; + +G_DEFINE_TYPE(KotoAudiobooksGenreButton, koto_audiobooks_genre_button, GTK_TYPE_BOX); + +static void koto_audiobooks_genre_button_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_audiobooks_genre_button_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_audiobooks_genre_button_class_init(KotoAudiobooksGenreButtonClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_audiobooks_genre_button_set_property; + gobject_class->get_property = koto_audiobooks_genre_button_get_property; + + genre_button_props[PROP_GENRE_ID] = g_param_spec_string( + "genre-id", + "Genre ID", + "Genre ID", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + genre_button_props[PROP_GENRE_NAME] = g_param_spec_string( + "genre-name", + "Genre Name", + "Genre Name", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPS, genre_button_props); +} + +static void koto_audiobooks_genre_button_init(KotoAudiobooksGenreButton * self) { + gtk_widget_add_css_class(GTK_WIDGET(self), "audiobook-genre-button"); + gtk_widget_set_hexpand(GTK_WIDGET(self), FALSE); + gtk_widget_set_vexpand(GTK_WIDGET(self), FALSE); + + gtk_widget_set_size_request(GTK_WIDGET(self), 260, 120); + GtkWidget * overlay = gtk_overlay_new(); // Create our overlay + self->bg_image = gtk_picture_new(); // Create our new image + + self->button = koto_button_new_plain(NULL); // Create a new button + koto_button_set_text_wrap(self->button, TRUE); // Enable text wrapping for long genre lines + gtk_widget_set_valign(GTK_WIDGET(self->button), GTK_ALIGN_END); // Align towards bottom of button + + gtk_widget_set_hexpand(overlay, TRUE); + + gtk_overlay_set_child(GTK_OVERLAY(overlay), self->bg_image); + gtk_overlay_add_overlay(GTK_OVERLAY(overlay), GTK_WIDGET(self->button)); + + gtk_box_append(GTK_BOX(self), overlay); // Add the overlay to self +} + +static void koto_audiobooks_genre_button_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoAudiobooksGenreButton * self = KOTO_AUDIOBOOKS_GENRE_BUTTON(obj); + + switch (prop_id) { + case PROP_GENRE_ID: + g_value_set_string(val, self->genre_id); + break; + case PROP_GENRE_NAME: + g_value_set_string(val, self->genre_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_audiobooks_genre_button_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoAudiobooksGenreButton * self = KOTO_AUDIOBOOKS_GENRE_BUTTON(obj); + + switch (prop_id) { + case PROP_GENRE_ID: + koto_audiobooks_genre_button_set_id(self, g_strdup(g_value_get_string(val))); + break; + case PROP_GENRE_NAME: + koto_audiobooks_genre_button_set_name(self, g_strdup(g_value_get_string(val))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +KotoButton * koto_audiobooks_genre_button_get_button(KotoAudiobooksGenreButton * self) { + return KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self) ? self->button : NULL; +} + +gchar * koto_audiobooks_genre_button_get_id(KotoAudiobooksGenreButton * self) { + return KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self) ? self->genre_id : NULL; +} + +gchar * koto_audiobooks_genre_button_get_name(KotoAudiobooksGenreButton * self) { + return KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self) ? self->genre_name : NULL; +} + +void koto_audiobooks_genre_button_set_id( + KotoAudiobooksGenreButton * self, + gchar * genre_id +) { + if (!KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self)) { + return; + } + + if (!koto_utils_string_is_valid(genre_id)) { + return; + } + + self->genre_id = genre_id; + + gtk_picture_set_resource(GTK_PICTURE(self->bg_image), g_strdup_printf("/com/github/joshstrobl/koto/%s.png", genre_id)); + gtk_widget_set_size_request(self->bg_image, 260, 120); +} + +void koto_audiobooks_genre_button_set_name( + KotoAudiobooksGenreButton * self, + gchar * genre_name +) { + if (!KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(self)) { + return; + } + + if (!koto_utils_string_is_valid(genre_name)) { + return; + } + + if (koto_utils_string_is_valid(self->genre_name)) { // Already have a genre name + g_free(self->genre_name); + } + + self->genre_name = genre_name; + koto_button_set_text(self->button, genre_name); +} + +KotoAudiobooksGenreButton * koto_audiobooks_genre_button_new( + gchar * genre_id, + gchar * genre_name +) { + return g_object_new( + KOTO_TYPE_AUDIOBOOKS_GENRE_BUTTON, + "genre-id", + genre_id, + "genre-name", + genre_name, + NULL + ); +} \ No newline at end of file diff --git a/src/pages/audiobooks/genre-button.h b/src/pages/audiobooks/genre-button.h new file mode 100644 index 0000000..dc98a4b --- /dev/null +++ b/src/pages/audiobooks/genre-button.h @@ -0,0 +1,49 @@ +/* genres-button.h + * + * 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 +#include +#include "../../components/button.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_AUDIOBOOKS_GENRE_BUTTON koto_audiobooks_genre_button_get_type() +G_DECLARE_FINAL_TYPE(KotoAudiobooksGenreButton, koto_audiobooks_genre_button, KOTO, AUDIOBOOKS_GENRE_BUTTON, GtkBox) +#define KOTO_IS_AUDIOBOOKS_GENRE_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_AUDIOBOOKS_GENRE_BUTTON)) + +KotoButton * koto_audiobooks_genre_button_get_button(KotoAudiobooksGenreButton * self); + +gchar * koto_audiobooks_genre_button_get_id(KotoAudiobooksGenreButton * self); + +gchar * koto_audiobooks_genre_button_get_name(KotoAudiobooksGenreButton * self); + +void koto_audiobooks_genre_button_set_id( + KotoAudiobooksGenreButton * self, + gchar * genre_id +); + +void koto_audiobooks_genre_button_set_name( + KotoAudiobooksGenreButton * self, + gchar * genre_name +); + +KotoAudiobooksGenreButton * koto_audiobooks_genre_button_new( + gchar * genre_id, + gchar * genre_name +); + +G_END_DECLS \ No newline at end of file diff --git a/src/pages/audiobooks/genres-banner.c b/src/pages/audiobooks/genres-banner.c new file mode 100644 index 0000000..72d6374 --- /dev/null +++ b/src/pages/audiobooks/genres-banner.c @@ -0,0 +1,153 @@ +/* genres-banner.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 +#include +#include "../../components/button.h" +#include "../../koto-utils.h" +#include "genre-button.h" +#include "genres-banner.h" + +enum { + SIGNAL_GENRE_CLICKED, + N_SIGNALS +}; + +static guint banner_signals[N_SIGNALS] = { + 0 +}; + +struct _KotoAudiobooksGenresBanner { + GObject parent_instance; + + GtkWidget * main; // Our main content + + GtkWidget * banner_revealer; // Our GtkRevealer for the strip + GtkWidget * banner_viewport; // Our banner viewport + GtkWidget * banner_content; // Our banner content as a horizontal box + + GtkWidget * strip_revealer; // Our GtkRevealer for the strip + GtkWidget * strip_viewport; // Our strip viewport + GtkWidget * strip_content; // Our strip content + + GHashTable * genre_ids_to_names; // Our genre "ids" to the name + GHashTable * genre_ids_to_ptrs; // Our HashTable of genre IDs to our GPtrArray with pointers to banner and strip buttons +}; + +struct _KotoAudiobooksGenresBannerClass { + GObjectClass parent_instance; + void (* genre_clicked) (gchar * genre); +}; + +G_DEFINE_TYPE(KotoAudiobooksGenresBanner, koto_audiobooks_genres_banner, G_TYPE_OBJECT); + +static void koto_audiobooks_genres_banner_class_init(KotoAudiobooksGenresBannerClass * c) { + GObjectClass * gobject_class = G_OBJECT_CLASS(c); + + banner_signals[SIGNAL_GENRE_CLICKED] = g_signal_new( + "genre-clicked", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET(KotoAudiobooksGenresBannerClass, genre_clicked), + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_CHAR + ); +} + +static void koto_audiobooks_genres_banner_init(KotoAudiobooksGenresBanner * self) { + self->genre_ids_to_ptrs = g_hash_table_new(g_str_hash, g_str_equal); + self->genre_ids_to_names = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(self->genre_ids_to_names, g_strdup("hip-hop"), g_strdup("Hip-hop")); + g_hash_table_insert(self->genre_ids_to_names, g_strdup("indie"), g_strdup("Indie")); + g_hash_table_insert(self->genre_ids_to_names, g_strdup("sci-fi"), g_strdup("Science Fiction")); + + self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // Create the main box as a vertical box + gtk_widget_add_css_class(self->main, "genres-banner"); + gtk_widget_set_halign(self->main, GTK_ALIGN_START); + gtk_widget_set_vexpand(self->main, FALSE); + + self->banner_revealer = gtk_revealer_new(); // Create our revealer for the banner + self->strip_revealer = gtk_revealer_new(); // Create a revealer for the strip + + gtk_revealer_set_transition_type(GTK_REVEALER(self->strip_revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + gtk_revealer_set_transition_type(GTK_REVEALER(self->banner_revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP); + + self->banner_viewport = gtk_viewport_new(NULL, NULL); // Create our viewport for enabling the banner content to be scrollable + self->strip_viewport = gtk_viewport_new(NULL, NULL); // Create our viewport for enabling the strip content to be scrollable + + self->banner_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create our horizontal box for the content + gtk_widget_add_css_class(self->banner_content, "large-banner"); + + self->strip_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create the horizontal box for the strip content + gtk_widget_add_css_class(self->strip_content, "strip"); + + gtk_revealer_set_child(GTK_REVEALER(self->banner_revealer), self->banner_viewport); // Set the viewport to be the content which gets revealed + gtk_revealer_set_child(GTK_REVEALER(self->strip_revealer), self->strip_viewport); // Set the viewport to be the cont ent which gets revealed + gtk_revealer_set_reveal_child(GTK_REVEALER(self->banner_revealer), TRUE); // Show the banner by default + + gtk_viewport_set_child(GTK_VIEWPORT(self->banner_viewport), self->banner_content); // Set the banner content for this viewport + gtk_viewport_set_child(GTK_VIEWPORT(self->strip_viewport), self->strip_content); // Set the strip content for this viewport + + gtk_box_append(GTK_BOX(self->main), self->strip_revealer); + gtk_box_append(GTK_BOX(self->main), self->banner_revealer); +} + +void koto_audiobooks_genres_banner_add_genre( + KotoAudiobooksGenresBanner * self, + gchar * genre_id +) { + if (!KOTO_IS_AUDIOBOOKS_GENRES_BANNER(self)) { // Not a banner + return; + } + + if (!koto_utils_string_is_valid(genre_id)) { // Not a valid genre ID + return; + } + + if (g_hash_table_contains(self->genre_ids_to_ptrs, genre_id)) { // Already have added this + return; + } + + gchar * name = g_hash_table_lookup(self->genre_ids_to_names, genre_id); // Get the named equivelant for this ID + + if (name == NULL) { // Didn't find a name equivelant + name = g_strdup(genre_id); // Just duplicate the genre ID for now + } + + KotoButton * genre_strip_button = koto_button_new_plain(name); // Create a new button with the name + gtk_box_append(GTK_BOX(self->strip_content), GTK_WIDGET(genre_strip_button)); // Add our KotoButton to the strip content + KotoAudiobooksGenreButton * genre_banner_button = koto_audiobooks_genre_button_new(genre_id, name); // Create our big button + gtk_box_append(GTK_BOX(self->banner_content), GTK_WIDGET(genre_banner_button)); + + GPtrArray * buttons = g_ptr_array_new(); + g_ptr_array_add(buttons, (gpointer) genre_strip_button); // Add our KotoButton as a gpointer to our GPtrArray as the first item + g_ptr_array_add(buttons, (gpointer) genre_banner_button); // Add our KotoAudiobooksGenresButton as a gpointer to our GPtrArray as the second item + + g_hash_table_replace(self->genre_ids_to_ptrs, genre_id, buttons); // Add our GPtrArray to our genre_ids_to_ptrs +} + +GtkWidget * koto_audiobooks_genres_banner_get_main(KotoAudiobooksGenresBanner * self) { + return KOTO_IS_AUDIOBOOKS_GENRES_BANNER(self) ? self->main : NULL; +} + +KotoAudiobooksGenresBanner * koto_audiobooks_genres_banner_new() { + return g_object_new(KOTO_TYPE_AUDIOBOOKS_GENRE_BANNER, NULL); +} \ No newline at end of file diff --git a/src/pages/audiobooks/genres-banner.h b/src/pages/audiobooks/genres-banner.h new file mode 100644 index 0000000..7a4e1b7 --- /dev/null +++ b/src/pages/audiobooks/genres-banner.h @@ -0,0 +1,44 @@ +/* genres-banner.h + * + * 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. + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define KOTO_TYPE_AUDIOBOOKS_GENRE_BANNER koto_audiobooks_genres_banner_get_type() +#define KOTO_AUDIOBOOKS_GENRES_BANNER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_AUDIOBOOKS_GENRE_BANNER, KotoAudiobooksGenresBanner)) +typedef struct _KotoAudiobooksGenresBanner KotoAudiobooksGenresBanner; +typedef struct _KotoAudiobooksGenresBannerClass KotoAudiobooksGenresBannerClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_audiobooks_genres_banner_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_AUDIOBOOKS_GENRES_BANNER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_AUDIOBOOKS_GENRE_BANNER)) + +void koto_audiobooks_genres_banner_add_genre( + KotoAudiobooksGenresBanner * self, + gchar * genre_id +); + +GtkWidget * koto_audiobooks_genres_banner_get_main(KotoAudiobooksGenresBanner * self); + +KotoAudiobooksGenresBanner * koto_audiobooks_genres_banner_new(); + +G_END_DECLS \ No newline at end of file diff --git a/src/pages/audiobooks/library.c b/src/pages/audiobooks/library.c new file mode 100644 index 0000000..b6c9864 --- /dev/null +++ b/src/pages/audiobooks/library.c @@ -0,0 +1,213 @@ +/* library.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 +#include +#include "../../components/button.h" +#include "../../db/cartographer.h" +#include "../../indexer/structs.h" +#include "../../koto-utils.h" +#include "../../koto-window.h" +#include "genres-banner.h" +#include "library.h" +#include "writer-page.h" + +extern KotoCartographer * koto_maps; +extern KotoWindow * main_window; + +struct _KotoAudiobooksLibraryPage { + GObject parent_instance; + + GtkWidget * main; // Our main content, contains banner and scrolled window + GtkWidget * content_scroll; // Our Scrolled Window + GtkWidget * content; // Content inside scrolled window + + KotoAudiobooksGenresBanner * banner; + + GtkWidget * writers_flow; + GHashTable * writers_to_buttons; // HashTable of UUIDs of "Artists" to their KotoButton + GHashTable * writers_to_pages; // HashTable of UUIDs of "Artists" to their KotoAudiobooksWritersPage +}; + +struct _KotoAudiobooksLibraryPageClass { + GObjectClass parent_instance; +}; + +G_DEFINE_TYPE(KotoAudiobooksLibraryPage, koto_audiobooks_library_page, G_TYPE_OBJECT); + +KotoAudiobooksLibraryPage * audiobooks_library_page; + +static void koto_audiobooks_library_page_init(KotoAudiobooksLibraryPage * self) { + self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(self->main, "audiobook-library"); + self->content_scroll = gtk_scrolled_window_new(); // Create our GtkScrolledWindow + self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_vexpand(self->content, TRUE); // Ensure content expands vertically to allow flowboxchild to take up as much space as it requests + gtk_widget_add_css_class(self->content, "content"); + + self->banner = koto_audiobooks_genres_banner_new(); // Create our banner + + self->writers_flow = gtk_flow_box_new(); // Create our flow box + //gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(self->writers_flow), TRUE); + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(self->writers_flow), 100); // Set to a random amount that is not realistic, however GTK sets a default to 7 which is too small. + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->writers_flow), GTK_SELECTION_NONE); + gtk_widget_add_css_class(self->writers_flow, "writers-button-flow"); + + self->writers_to_buttons = g_hash_table_new(g_str_hash, g_str_equal); + self->writers_to_pages = g_hash_table_new(g_str_hash, g_str_equal); + + g_signal_connect(koto_maps, "album-added", G_CALLBACK(koto_audiobooks_library_page_handle_add_album), self); // Notify when we have a new Album + g_signal_connect(koto_maps, "artist-added", G_CALLBACK(koto_audiobooks_library_page_handle_add_artist), self); // Notify when we have a new Artist + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->content_scroll), self->content); // Add our content to the content scroll + gtk_box_append(GTK_BOX(self->main), koto_audiobooks_genres_banner_get_main(self->banner)); // Add the banner to the content + gtk_box_append(GTK_BOX(self->main), self->content_scroll); // Add our scroll window to the main content + gtk_box_append(GTK_BOX(self->content), self->writers_flow); // Add our flowbox to the content +} + +static void koto_audiobooks_library_page_class_init(KotoAudiobooksLibraryPageClass * c) { + (void) c; +} + +void koto_audiobooks_library_page_create_artist_ux( + KotoAudiobooksLibraryPage * self, + KotoArtist * artist +) { + if (!KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self)) { // Not a AudiobooksLibraryPage + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + gchar * artist_uuid = koto_artist_get_uuid(artist); // Get the artist UUID + + if (!g_hash_table_contains(self->writers_to_pages, artist_uuid)) { // Don't have the page + KotoWriterPage * writers_page = koto_writer_page_new(artist); + koto_window_add_page(main_window, artist_uuid, koto_writer_page_get_main(writers_page)); // Add the page to the stack + } + + if (!g_hash_table_contains(self->writers_to_buttons, artist_uuid)) { // Don't have the button for this + KotoButton * artist_button = koto_button_new_plain(koto_artist_get_name(artist)); // Create a KotoButton for this artist + koto_button_set_data(artist_button, artist_uuid); // Set the artist_uuid as the data for the button + + koto_button_set_text_justify(artist_button, GTK_JUSTIFY_CENTER); // Center the text + koto_button_set_text_wrap(artist_button, TRUE); + gtk_widget_add_css_class(GTK_WIDGET(artist_button), "writer-button"); + gtk_widget_set_size_request(GTK_WIDGET(artist_button), 260, 120); + + koto_button_add_click_handler(artist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_button_global_page_nav_callback), artist_button); + + g_hash_table_replace(self->writers_to_buttons, artist_uuid, artist_button); // Add the button to the Writers to Buttons hashtable + gtk_flow_box_insert(GTK_FLOW_BOX(self->writers_flow), GTK_WIDGET(artist_button), -1); // Append the button to our flowbox + + GtkWidget * button_parent = gtk_widget_get_parent(GTK_WIDGET(artist_button)); // This is the GtkFlowboxChild that the button is in + gtk_widget_set_halign(button_parent, GTK_ALIGN_START); + } +} + +void koto_audiobooks_library_page_handle_add_album( + KotoCartographer * carto, + KotoAlbum * album, + KotoAudiobooksLibraryPage * self +) { + + if (!KOTO_IS_CARTOGRAPHER(carto)) { // Not cartographer + return; + } + + if (!KOTO_IS_ALBUM(album)) { // Not an album + return; + } + + if (!KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self)) { // Not a AudiobooksLibraryPage + return; + } + + gchar * artist_uuid = koto_album_get_artist_uuid(album); // Get the Album's artist UUID + KotoArtist * artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); + + if (!KOTO_IS_ARTIST(artist)) { // Failed to get artist + return; + } + + if (koto_artist_get_lib_type(artist) != KOTO_LIBRARY_TYPE_AUDIOBOOK) { // Not in an Audiobook library + return; + } + + koto_audiobooks_library_page_add_genres(self, koto_album_get_genres(album)); // Add all the genres necessary for this album +} + +void koto_audiobooks_library_page_handle_add_artist( + KotoCartographer * carto, + KotoArtist * artist, + KotoAudiobooksLibraryPage * self +) { + if (!KOTO_IS_CARTOGRAPHER(carto)) { // Not cartographer + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + if (!KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self)) { // Not a AudiobooksLibraryPage + return; + } + + if (koto_artist_get_lib_type(artist) != KOTO_LIBRARY_TYPE_AUDIOBOOK) { // Not in an Audiobook library + return; + } + + koto_audiobooks_library_page_create_artist_ux(self, artist); // Create the UX for the artist if necessary +} + +void koto_audiobooks_library_page_add_genres( + KotoAudiobooksLibraryPage * self, + GList * genres +) { + if (!KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self)) { // Not a AudiobooksLibraryPage + return; + } + + if (g_list_length(genres) == 0) { // No contents + return; + } + + GList * cur_list; + for (cur_list = genres; cur_list != NULL; cur_list = cur_list->next) { // Iterate over each genre + gchar * genre = (gchar*) cur_list->data; + if (!koto_utils_string_is_valid(genre)) { + continue; + } + + if (g_strcmp0(genre, "audiobook") == 0) { // Is generic + continue; + } + + koto_audiobooks_genres_banner_add_genre(self->banner, genre); // Add this genre + } +} + +GtkWidget * koto_audiobooks_library_page_get_main(KotoAudiobooksLibraryPage * self) { + return KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(self) ? self->main : NULL; +} + +KotoAudiobooksLibraryPage * koto_audiobooks_library_page_new() { + return g_object_new(KOTO_TYPE_AUDIOBOOKS_LIBRARY_PAGE, NULL); +} \ No newline at end of file diff --git a/src/pages/audiobooks/library.h b/src/pages/audiobooks/library.h new file mode 100644 index 0000000..88e633f --- /dev/null +++ b/src/pages/audiobooks/library.h @@ -0,0 +1,56 @@ +/* library.h + * + * 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. + */ + +#pragma once + +#include +#include +#include "../../db/cartographer.h" +#include "../../indexer/structs.h" + +G_BEGIN_DECLS + +#define KOTO_TYPE_AUDIOBOOKS_LIBRARY_PAGE koto_audiobooks_library_page_get_type() +#define KOTO_AUDIOBOOKS_LIBRARY_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), KOTO_TYPE_AUDIOBOOKS_LIBRARY_PAGE, KotoAudiobooksLibraryPage)) +typedef struct _KotoAudiobooksLibraryPage KotoAudiobooksLibraryPage; +typedef struct _KotoAudiobooksLibraryPageClass KotoAudiobooksLibraryPageClass; + +GLIB_AVAILABLE_IN_ALL +GType koto_audiobooks_library_page_get_type(void) G_GNUC_CONST; + +#define KOTO_IS_AUDIOBOOKS_LIBRARY_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_AUDIOBOOKS_LIBRARY_PAGE)) + +void koto_audiobooks_library_page_add_genres( + KotoAudiobooksLibraryPage * self, + GList * genres +); + +void koto_audiobooks_library_page_handle_add_album( + KotoCartographer * carto, + KotoAlbum * album, + KotoAudiobooksLibraryPage * self +); + +void koto_audiobooks_library_page_handle_add_artist( + KotoCartographer * carto, + KotoArtist * artist, + KotoAudiobooksLibraryPage * self +); + +GtkWidget * koto_audiobooks_library_page_get_main(KotoAudiobooksLibraryPage * self); + +KotoAudiobooksLibraryPage * koto_audiobooks_library_page_new(); \ No newline at end of file diff --git a/src/pages/audiobooks/writer-page.c b/src/pages/audiobooks/writer-page.c new file mode 100644 index 0000000..f0944ce --- /dev/null +++ b/src/pages/audiobooks/writer-page.c @@ -0,0 +1,206 @@ +/* writers-page.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 +#include +#include "../../db/cartographer.h" +#include "../../indexer/structs.h" +#include "../../koto-utils.h" +#include "../../koto-window.h" +#include "audiobook-view.h" +#include "writer-page.h" + +extern KotoCartographer * koto_maps; +extern KotoWindow * main_window; + +enum { + PROP_0, + PROP_ARTIST, + N_PROPERTIES +}; + +static GParamSpec * props[N_PROPERTIES] = { + NULL, +}; + +struct _KotoWriterPage { + GObject parent_instance; + + KotoArtist * artist; + + GtkWidget * main; // Our main content will be a scrolled Scrolled Window + GtkWidget * content; // Content inside scrolled window + + GtkWidget * writers_header; // Header GtkLabel for the writer + GListModel * model; + + GtkWidget * audiobooks_flow; +}; + +struct _KotoWriterPageClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE(KotoWriterPage, koto_writer_page, G_TYPE_OBJECT); + +static void koto_writer_page_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +); + +static void koto_writer_page_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +); + +static void koto_writer_page_class_init(KotoWriterPageClass * c) { + GObjectClass * gobject_class; + + gobject_class = G_OBJECT_CLASS(c); + gobject_class->set_property = koto_writer_page_set_property; + gobject_class->get_property = koto_writer_page_get_property; + + props[PROP_ARTIST] = g_param_spec_object( + "artist", + "Artist", + "Artist", + KOTO_TYPE_ARTIST, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + + g_object_class_install_properties(gobject_class, N_PROPERTIES, props); +} + +static void koto_writer_page_init(KotoWriterPage * self) { + self->main = gtk_scrolled_window_new(); // Set main to be a scrolled window + self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(self->content, "writer-page"); + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->main), self->content); // Set content to be the child of the scrolled window + + self->writers_header = gtk_label_new(NULL); // Create an empty label + gtk_widget_add_css_class(self->writers_header, "writer-header"); + gtk_widget_set_halign(self->writers_header, GTK_ALIGN_START); + + self->audiobooks_flow = gtk_flow_box_new(); // Create our flowbox of the audiobooks views + gtk_widget_add_css_class(self->audiobooks_flow, "audiobooks-flow"); + + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(self->audiobooks_flow), 2); // Allow 2 to ensure adequate spacing for description + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->audiobooks_flow), GTK_SELECTION_NONE); // Do not allow selection + + gtk_widget_set_hexpand(self->audiobooks_flow, TRUE); // Expand horizontally + gtk_widget_set_vexpand(self->audiobooks_flow, TRUE); // Expand vertically + + gtk_box_append(GTK_BOX(self->content), self->writers_header); + gtk_box_append(GTK_BOX(self->content), self->audiobooks_flow); // Add the audiobooks flow to the content +} + +static void koto_writer_page_get_property( + GObject * obj, + guint prop_id, + GValue * val, + GParamSpec * spec +) { + KotoWriterPage * self = KOTO_WRITER_PAGE(obj); + + switch (prop_id) { + case PROP_ARTIST: + g_value_set_object(val, self->artist); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +static void koto_writer_page_set_property( + GObject * obj, + guint prop_id, + const GValue * val, + GParamSpec * spec +) { + KotoWriterPage * self = KOTO_WRITER_PAGE(obj); + + switch (prop_id) { + case PROP_ARTIST: + koto_writer_page_set_artist(self, (KotoArtist*) g_value_get_object(val)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec); + break; + } +} + +GtkWidget * koto_writer_page_create_item( + gpointer item, + gpointer user_data +) { + (void) user_data; + + KotoAlbum * album = KOTO_ALBUM(item); // Cast the model data as an album + + if (!KOTO_IS_ALBUM(album)) { // Fetched item from list is not album + return NULL; + } + + KotoAudiobookView * view = koto_audiobook_view_new(); // Create our KotoAudiobookView + koto_audiobook_view_set_album(view, album); // Set the item for this set up audiobook view + return GTK_WIDGET(view); +} + +GtkWidget * koto_writer_page_get_main(KotoWriterPage * self) { + return self->main; +} + +void koto_writer_page_set_artist( + KotoWriterPage * self, + KotoArtist * artist +) { + if (!KOTO_IS_WRITER_PAGE(self)) { // Not a writers page + return; + } + + if (!KOTO_IS_ARTIST(artist)) { // Not an artist + return; + } + + self->artist = artist; + gtk_label_set_text(GTK_LABEL(self->writers_header), koto_artist_get_name(self->artist)); // Get the label for the writers header + + self->model = G_LIST_MODEL(koto_artist_get_albums_store(artist)); // Get the store and cast it as a list model for our model + + gtk_flow_box_bind_model( + GTK_FLOW_BOX(self->audiobooks_flow), + self->model, + koto_writer_page_create_item, + NULL, + NULL + ); +} + +KotoWriterPage * koto_writer_page_new(KotoArtist * artist) { + return g_object_new( + KOTO_TYPE_WRITER_PAGE, + "artist", + artist, + NULL + ); +} \ No newline at end of file diff --git a/src/pages/audiobooks/writer-page.h b/src/pages/audiobooks/writer-page.h new file mode 100644 index 0000000..2a2a913 --- /dev/null +++ b/src/pages/audiobooks/writer-page.h @@ -0,0 +1,41 @@ +/* writers-page.h + * + * 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. + */ + +#pragma once +#include +#include + +G_BEGIN_DECLS + +#define KOTO_TYPE_WRITER_PAGE (koto_writer_page_get_type()) +G_DECLARE_FINAL_TYPE(KotoWriterPage, koto_writer_page, KOTO, WRITER_PAGE, GObject) + +GtkWidget* koto_writer_page_create_item( + gpointer item, + gpointer user_data +); + +GtkWidget * koto_writer_page_get_main(KotoWriterPage * self); + +void koto_writer_page_set_artist( + KotoWriterPage * self, + KotoArtist * artist +); + +KotoWriterPage * koto_writer_page_new(KotoArtist * artist); + +G_END_DECLS \ No newline at end of file diff --git a/src/pages/music/album-view.c b/src/pages/music/album-view.c index d733dc2..678f23b 100644 --- a/src/pages/music/album-view.c +++ b/src/pages/music/album-view.c @@ -17,10 +17,11 @@ #include #include -#include "../../components/koto-cover-art-button.h" +#include "../../components/album-info.h" +#include "../../components/button.h" +#include "../../components/cover-art-button.h" #include "../../db/cartographer.h" #include "../../indexer/structs.h" -#include "../../koto-button.h" #include "album-view.h" #include "disc-view.h" #include "config/config.h" @@ -37,7 +38,7 @@ struct _KotoAlbumView { KotoCoverArtButton * album_cover; - GtkWidget * album_label; + KotoAlbumInfo * album_info; GHashTable * cd_to_disc_views; }; @@ -90,6 +91,8 @@ static void koto_album_view_init(KotoAlbumView * self) { gtk_widget_add_css_class(self->main, "album-view"); gtk_widget_set_can_focus(self->main, FALSE); self->album_tracks_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + self->album_info = koto_album_info_new(KOTO_ALBUM_INFO_TYPE_ALBUM); // Create an Album-type Album Info + gtk_box_prepend(GTK_BOX(self->album_tracks_box), GTK_WIDGET(self->album_info)); // Add our Album Info to the album_tracks box self->discs = gtk_list_box_new(); // Create our list of our tracks gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->discs), GTK_SELECTION_NONE); @@ -228,11 +231,7 @@ void koto_album_view_set_album( gchar * album_art = koto_album_get_art(self->album); // Get the art for the album koto_cover_art_button_set_art_path(self->album_cover, album_art); - - self->album_label = gtk_label_new(koto_album_get_name(album)); - gtk_widget_set_halign(self->album_label, GTK_ALIGN_START); - gtk_box_prepend(GTK_BOX(self->album_tracks_box), self->album_label); // Prepend our new label to the album + tracks box - + koto_album_info_set_album_uuid(self->album_info, koto_album_get_uuid(album)); // Set the album we should be displaying info about on the Album Info g_signal_connect(self->album, "track-added", G_CALLBACK(koto_album_view_handle_track_added), self); // Handle track added on our album } diff --git a/src/pages/music/artist-view.c b/src/pages/music/artist-view.c index 314487f..d0c4215 100644 --- a/src/pages/music/artist-view.c +++ b/src/pages/music/artist-view.c @@ -17,7 +17,7 @@ #include #include -#include "../../components/koto-cover-art-button.h" +#include "../../components/cover-art-button.h" #include "../../components/track-table.h" #include "../../db/cartographer.h" #include "../../indexer/artist-playlist-funcs.h" @@ -323,7 +323,7 @@ void koto_artist_view_toggle_playback( return; } - koto_current_playlist_set_playlist(current_playlist, artist_playlist, TRUE); // Set our playlist to the one associated with the Artist and start playback immediately + koto_current_playlist_set_playlist(current_playlist, artist_playlist, TRUE, FALSE); // Set our playlist to the one associated with the Artist and start playback immediately } KotoArtistView * koto_artist_view_new(KotoArtist * artist) { diff --git a/src/pages/music/disc-view.c b/src/pages/music/disc-view.c index 4425baa..01a8971 100644 --- a/src/pages/music/disc-view.c +++ b/src/pages/music/disc-view.c @@ -16,11 +16,11 @@ */ #include -#include "../../components/koto-action-bar.h" +#include "../../components/action-bar.h" +#include "../../components/track-item.h" #include "../../db/cartographer.h" #include "../../indexer/track-helpers.h" #include "../../indexer/structs.h" -#include "../../koto-track-item.h" #include "../../koto-utils.h" #include "disc-view.h" @@ -194,7 +194,6 @@ void koto_disc_view_add_track( KotoTrackItem * track_item = koto_track_item_new(track); // Create our new track item if (!KOTO_IS_TRACK_ITEM(track_item)) { // Failed to create a track item for this track - g_warning("Failed to build track item for track with UUID %s", track_uuid); return; } @@ -212,9 +211,9 @@ void koto_disc_view_handle_selected_rows_changed( return; } - gchar * album_uuid = koto_album_get_album_uuid(self->album); // Get the UUID + gchar * album_uuid = koto_album_get_uuid(self->album); // Get the UUID - if (!koto_utils_is_string_valid(album_uuid)) { // Not set + if (!koto_utils_string_is_valid(album_uuid)) { // Not set return; } @@ -247,7 +246,7 @@ void koto_disc_view_remove_track( return; } - if (!koto_utils_is_string_valid(track_uuid)) { // If our UUID is not a valid + if (!koto_utils_string_is_valid(track_uuid)) { // If our UUID is not a valid return; } diff --git a/src/pages/music/music-local.c b/src/pages/music/music-local.c index fae261c..b979817 100644 --- a/src/pages/music/music-local.c +++ b/src/pages/music/music-local.c @@ -17,9 +17,9 @@ #include #include +#include "../../components/button.h" #include "../../db/cartographer.h" #include "../../indexer/structs.h" -#include "koto-button.h" #include "config/config.h" #include "../../koto-utils.h" #include "music-local.h" @@ -154,7 +154,7 @@ void koto_page_music_local_go_to_artist_by_uuid( NULL ); - if (!koto_utils_is_string_valid(artist_name)) { // Failed to get the artist name + if (!koto_utils_string_is_valid(artist_name)) { // Failed to get the artist name return; } diff --git a/src/pages/playlist/list.c b/src/pages/playlist/list.c index f35f3e2..b49b330 100644 --- a/src/pages/playlist/list.c +++ b/src/pages/playlist/list.c @@ -17,14 +17,14 @@ #include #include -#include "../../components/koto-action-bar.h" -#include "../../components/koto-cover-art-button.h" +#include "../../components/action-bar.h" +#include "../../components/button.h" +#include "../../components/cover-art-button.h" #include "../../components/track-table.h" #include "../../db/cartographer.h" #include "../../indexer/structs.h" #include "../../playlist/current.h" #include "../../playlist/playlist.h" -#include "../../koto-button.h" #include "../../koto-window.h" #include "../../playlist/create-modify-dialog.h" #include "list.h" @@ -209,7 +209,7 @@ void koto_playlist_page_handle_cover_art_clicked( return; } - koto_current_playlist_set_playlist(current_playlist, self->playlist, TRUE); // Switch to this playlist and start playing immediately + koto_current_playlist_set_playlist(current_playlist, self->playlist, TRUE, FALSE); // Switch to this playlist and start playing immediately } void koto_playlist_page_handle_edit_button_clicked( @@ -245,13 +245,13 @@ void koto_playlist_page_handle_playlist_modified( gchar * artwork = koto_playlist_get_artwork(playlist); // Get the artwork - if (koto_utils_is_string_valid(artwork)) { // Have valid artwork + if (koto_utils_string_is_valid(artwork)) { // Have valid artwork koto_cover_art_button_set_art_path(self->playlist_image, artwork); // Update our Koto Cover Art Button } gchar * name = koto_playlist_get_name(playlist); // Get the name - if (koto_utils_is_string_valid(name)) { // Have valid name + if (koto_utils_string_is_valid(name)) { // Have valid name gtk_label_set_label(GTK_LABEL(self->name_label), name); // Update the name label } } @@ -264,7 +264,7 @@ void koto_playlist_page_set_playlist_uuid( return; } - if (!koto_utils_is_string_valid(playlist_uuid)) { // Provided UUID string is not valid + if (!koto_utils_string_is_valid(playlist_uuid)) { // Provided UUID string is not valid return; } @@ -309,7 +309,7 @@ void koto_playlist_page_update_header(KotoPlaylistPage * self) { gchar * artwork = koto_playlist_get_artwork(self->playlist); - if (koto_utils_is_string_valid(artwork)) { // Have artwork + if (koto_utils_string_is_valid(artwork)) { // Have artwork koto_cover_art_button_set_art_path(self->playlist_image, artwork); // Update our artwork } } diff --git a/src/pages/playlist/list.h b/src/pages/playlist/list.h index eb263c0..6822a26 100644 --- a/src/pages/playlist/list.h +++ b/src/pages/playlist/list.h @@ -19,7 +19,7 @@ #include #include -#include "../../components/koto-action-bar.h" +#include "../../components/action-bar.h" #include "../../playlist/playlist.h" #include "../../koto-utils.h" diff --git a/src/playback/engine.c b/src/playback/engine.c index e449842..3ece126 100644 --- a/src/playback/engine.c +++ b/src/playback/engine.c @@ -73,11 +73,14 @@ struct _KotoPlaybackEngine { gboolean tick_duration_timer_running; gboolean tick_track_timer_running; + gboolean tick_update_playlist_state; gboolean via_config_continue_on_playlist; // Pulls from our Koto Config gboolean via_config_maintain_shuffle; // Pulls from our Koto Config guint playback_position; + gint requested_playback_position; + gdouble volume; }; @@ -214,7 +217,7 @@ static void koto_playback_engine_init(KotoPlaybackEngine * self) { self->monitor = gst_bus_new(); // Get the bus for the playbin self->duration_query = gst_query_new_duration(GST_FORMAT_TIME); // Create our re-usable query for querying the duration - self->position_query = gst_query_new_position(GST_FORMAT_DEFAULT); // Create our re-usable query for querying the position + self->position_query = gst_query_new_position(GST_FORMAT_TIME); // Create our re-usable query for querying the position if (GST_IS_BUS(self->monitor)) { gst_bus_add_watch(self->monitor, koto_playback_engine_monitor_changed, self); @@ -226,8 +229,10 @@ static void koto_playback_engine_init(KotoPlaybackEngine * self) { self->is_playing_specific_track = FALSE; self->is_repeat_enabled = FALSE; self->is_shuffle_enabled = FALSE; + self->requested_playback_position = -1; self->tick_duration_timer_running = FALSE; self->tick_track_timer_running = FALSE; + self->tick_update_playlist_state = FALSE; self->via_config_continue_on_playlist = FALSE; self->via_config_maintain_shuffle = TRUE; @@ -247,6 +252,10 @@ void koto_playback_engine_apply_configuration_state( guint prop_id, KotoPlaybackEngine * self ) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return; + } + (void) c; (void) prop_id; @@ -274,6 +283,10 @@ void koto_playback_engine_apply_configuration_state( } void koto_playback_engine_backwards(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return; + } + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); // Get the current playlist if (!KOTO_IS_PLAYLIST(playlist)) { // If we do not have a playlist currently @@ -284,6 +297,7 @@ void koto_playback_engine_backwards(KotoPlaybackEngine * self) { return; } + koto_playback_engine_set_track_playback_position(self, 0); // Reset the current track position to 0 koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_previous(playlist), FALSE); } @@ -291,6 +305,7 @@ void koto_playback_engine_handle_current_playlist_changed( KotoCurrentPlaylist * current_pl, KotoPlaylist * playlist, gboolean play_immediately, + gboolean play_current, gpointer user_data ) { (void) current_pl; @@ -305,11 +320,29 @@ void koto_playback_engine_handle_current_playlist_changed( } if (play_immediately) { // Should play immediately - koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_next(playlist), FALSE); // Go to "next" which is the first track + gchar * play_uuid = NULL; + + if (play_current) { // Play the current track + KotoTrack * track = koto_playlist_get_current_track(playlist); // Get the current track + + if (KOTO_IS_TRACK(track)) { + play_uuid = koto_track_get_uuid(track); + } + } + + if (!koto_utils_string_is_valid(play_uuid)) { + play_uuid = koto_playlist_go_to_next(playlist); + } + + koto_playback_engine_set_track_by_uuid(self, play_uuid, FALSE); // Go to "next" which is the first track } } void koto_playback_engine_forwards(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return; + } + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); // Get the current playlist if (!KOTO_IS_PLAYLIST(playlist)) { // If we do not have a playlist currently @@ -323,16 +356,21 @@ void koto_playback_engine_forwards(KotoPlaybackEngine * self) { (self->via_config_continue_on_playlist && self->is_playing_specific_track) || // Playing a specific track and wanting to continue on the playlist (!self->is_playing_specific_track) // Not playing a specific track ) { + koto_playback_engine_set_track_playback_position(self, 0); // Reset the current track position to 0 koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_next(playlist), FALSE); } } } KotoTrack * koto_playback_engine_get_current_track(KotoPlaybackEngine * self) { - return self->current_track; + return KOTO_IS_PLAYBACK_ENGINE(self) ? self->current_track : NULL; } gint64 koto_playback_engine_get_duration(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return 0; + } + guint64 track_duration = koto_track_get_duration(self->current_track); // Get the current track's duration if (track_duration != 0) { // Have a duration stored @@ -350,6 +388,10 @@ gint64 koto_playback_engine_get_duration(KotoPlaybackEngine * self) { } gdouble koto_playback_engine_get_progress(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { // Not a playback engine + return 0; + } + gdouble progress = 0.0; gint64 gstprog = 0; @@ -360,7 +402,7 @@ gdouble koto_playback_engine_get_progress(KotoPlaybackEngine * self) { return 0.0; } - progress = gstprog / 100000; + progress = gstprog / GST_SECOND; // Devide by NS to get seconds } return progress; @@ -371,15 +413,15 @@ GstState koto_playback_engine_get_state(KotoPlaybackEngine * self) { } gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine * self) { - return self->is_repeat_enabled; + return KOTO_IS_PLAYBACK_ENGINE(self) ? self->is_repeat_enabled : FALSE; } gboolean koto_playback_engine_get_track_shuffle(KotoPlaybackEngine * self) { - return self->is_shuffle_enabled; + return KOTO_IS_PLAYBACK_ENGINE(self) ? self->is_shuffle_enabled : FALSE; } gdouble koto_playback_engine_get_volume(KotoPlaybackEngine * self) { - return self->volume; + return KOTO_IS_PLAYBACK_ENGINE(self) ? self->volume : 0; } gboolean koto_playback_engine_monitor_changed( @@ -390,6 +432,10 @@ gboolean koto_playback_engine_monitor_changed( (void) bus; KotoPlaybackEngine * self = user_data; + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return FALSE; + } + switch (GST_MESSAGE_TYPE(msg)) { case GST_MESSAGE_DURATION_CHANGED: { // Duration changed koto_playback_engine_tick_duration(self); @@ -402,6 +448,15 @@ gboolean koto_playback_engine_monitor_changed( GstState pending_state; gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state); + gboolean possibly_buffered = (old_state == GST_STATE_READY) && (new_state == GST_STATE_PAUSED) && (pending_state == GST_STATE_PLAYING); + + if (possibly_buffered && (self->requested_playback_position != -1)) { // Have a requested playback position + koto_playback_engine_set_position(self, self->requested_playback_position); // Set the position + self->requested_playback_position = -1; + koto_playback_engine_play(self); // Start playback + return TRUE; + } + if (new_state == GST_STATE_PLAYING) { // Now playing koto_playback_engine_tick_duration(self); g_signal_emit(self, playback_engine_signals[SIGNAL_IS_PLAYING], 0); // Emit our is playing state signal @@ -426,6 +481,10 @@ gboolean koto_playback_engine_monitor_changed( } void koto_playback_engine_play(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + self->is_playing = TRUE; gst_element_set_state(self->player, GST_STATE_PLAYING); // Set our state to play @@ -439,10 +498,19 @@ void koto_playback_engine_play(KotoPlaybackEngine * self) { g_timeout_add(100, koto_playback_engine_tick_track, self); // Create a 100ms track tick } + if (!self->tick_update_playlist_state) { + self->tick_update_playlist_state = TRUE; + g_timeout_add(60000, koto_playback_engine_tick_update_playlist_state, self); // Update the current playlist state every minute + } + koto_update_mpris_playback_state(GST_STATE_PLAYING); } void koto_playback_engine_pause(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + self->is_playing = FALSE; gst_element_change_state(self->player, GST_STATE_CHANGE_PLAYING_TO_PAUSED); koto_update_mpris_playback_state(GST_STATE_PAUSED); @@ -452,6 +520,10 @@ void koto_playback_engine_set_position( KotoPlaybackEngine * self, int position ) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + gst_element_seek( self->playbin, 1.0, @@ -464,6 +536,21 @@ void koto_playback_engine_set_position( ); } +void koto_playback_engine_set_track_playback_position( + KotoPlaybackEngine * self, + guint64 pos +) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + if (!KOTO_IS_TRACK(self->current_track) || self->is_repeat_enabled) { // If we do not have a track or repeating + return; + } + + koto_track_set_playback_position(self->current_track, pos); // Set the playback position of the track +} + void koto_playback_engine_set_track_repeat( KotoPlaybackEngine * self, gboolean enable_repeat @@ -476,6 +563,10 @@ void koto_playback_engine_set_track_shuffle( KotoPlaybackEngine * self, gboolean enable_shuffle ) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); if (!KOTO_IS_PLAYLIST(playlist)) { // Don't have a playlist currently @@ -496,7 +587,7 @@ void koto_playback_engine_set_track_by_uuid( return; } - if (!koto_utils_is_string_valid(track_uuid)) { // If this is not a valid track uuid string + if (!koto_utils_string_is_valid(track_uuid)) { // If this is not a valid track uuid string return; } @@ -519,10 +610,8 @@ void koto_playback_engine_set_track_by_uuid( g_object_set(self->playbin, "uri", gst_filename, NULL); g_free(gst_filename); // Free the filename + self->requested_playback_position = koto_track_get_playback_position(self->current_track); // Set our requested playback position koto_playback_engine_play(self); // Play the new track - - // TODO: Add prior position state setting here, like picking up at a specific part of an audiobook or podcast - koto_playback_engine_set_position(self, 0); koto_playback_engine_set_volume(self, self->volume); // Re-enforce our volume on the updated playbin GVariant * metadata = koto_track_get_metadata_vardict(track); // Get the GVariantBuilder variable dict for the metadata @@ -540,14 +629,14 @@ void koto_playback_engine_set_track_by_uuid( GVariant * artist_name_var = g_variant_dict_lookup_value(metadata_dict, "playbackengine:artist", NULL); // Get the GVariant for the name of the artist gchar * artist_name = g_strdup(g_variant_get_string(artist_name_var, NULL)); // Get the string for the artist name - gchar * artist_album_combo = koto_utils_is_string_valid(album_name) ? g_strjoin(" - ", artist_name, album_name, NULL) : artist_name; // Join artist and album name separated by " - " + gchar * artist_album_combo = koto_utils_string_is_valid(album_name) ? g_strjoin(" - ", artist_name, album_name, NULL) : artist_name; // Join artist and album name separated by " - " gchar * icon_name = "audio-x-generic-symbolic"; if (g_variant_dict_contains(metadata_dict, "mpris:artUrl")) { // If we have artwork specified GVariant * art_url_var = g_variant_dict_lookup_value(metadata_dict, "mpris:artUrl", NULL); // Get the GVariant for the art URL const gchar * art_uri = g_variant_get_string(art_url_var, NULL); // Get the string for the artwork - icon_name = koto_utils_replace_string_all(g_strdup(art_uri), "file://", ""); + icon_name = koto_utils_string_replace_all(g_strdup(art_uri), "file://", ""); } // Super important note: We are not using libnotify directly because the synchronous nature of notify_notification_send seems to result in dbus timeouts @@ -577,11 +666,19 @@ void koto_playback_engine_set_volume( KotoPlaybackEngine * self, gdouble volume ) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + self->volume = volume; g_object_set(self->playbin, "volume", self->volume, NULL); } void koto_playback_engine_stop(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + gst_element_set_state(self->player, GST_STATE_NULL); GstPad * pad = gst_element_get_static_pad(self->player, "sink"); // Get the static pad of the audio element @@ -594,16 +691,26 @@ void koto_playback_engine_stop(KotoPlaybackEngine * self) { } void koto_playback_engine_toggle(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + if (self->is_playing) { // Currently playing koto_playback_engine_pause(self); // Pause } else { koto_playback_engine_play(self); // Play } + + koto_current_playlist_save_playlist_state(current_playlist); // Save the playlist state during pause and play in case we are doing that remotely } gboolean koto_playback_engine_tick_duration(gpointer user_data) { KotoPlaybackEngine * self = user_data; + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return FALSE; + } + if (self->is_playing) { // Is playing g_signal_emit(self, playback_engine_signals[SIGNAL_TICK_DURATION], 0); // Emit our 1s track tick } else { // Not playing so exiting timer @@ -616,23 +723,67 @@ gboolean koto_playback_engine_tick_duration(gpointer user_data) { gboolean koto_playback_engine_tick_track(gpointer user_data) { KotoPlaybackEngine * self = user_data; + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return FALSE; + } + if (self->is_playing) { // Is playing + KotoPlaylist * playlist = koto_current_playlist_get_playlist(current_playlist); + + if (KOTO_IS_PLAYLIST(playlist)) { + koto_playlist_emit_modified(playlist); // Emit modified on the playlist to update UI in realtime (ish, okay? every 100ms is enough geez) + } + g_signal_emit(self, playback_engine_signals[SIGNAL_TICK_TRACK], 0); // Emit our 100ms track tick } else { self->tick_track_timer_running = FALSE; } + koto_playback_engine_update_track_position(self); // Update track position if needed + + return self->is_playing; +} + +gboolean koto_playback_engine_tick_update_playlist_state(gpointer user_data) { + KotoPlaybackEngine * self = user_data; + + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return FALSE; + } + + if (self->is_playing) { // Is playing + koto_current_playlist_save_playlist_state(current_playlist); // Save the state of the current playlist + } else { + self->tick_update_playlist_state = FALSE; + } + return self->is_playing; } void koto_playback_engine_toggle_track_repeat(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + koto_playback_engine_set_track_repeat(self, !self->is_repeat_enabled); } void koto_playback_engine_toggle_track_shuffle(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + koto_playback_engine_set_track_shuffle(self, !self->is_shuffle_enabled); // Invert the currently shuffling vale } +void koto_playback_engine_update_track_position(KotoPlaybackEngine * self) { + if (!KOTO_IS_PLAYBACK_ENGINE(self)) { + return; + } + + koto_playback_engine_set_track_playback_position(self, (guint64) koto_playback_engine_get_progress(self)); // Set to current progress +} + KotoPlaybackEngine * koto_playback_engine_new() { return g_object_new(KOTO_TYPE_PLAYBACK_ENGINE, NULL); } diff --git a/src/playback/engine.h b/src/playback/engine.h index 88889fb..ecf9d45 100644 --- a/src/playback/engine.h +++ b/src/playback/engine.h @@ -56,6 +56,7 @@ void koto_playback_engine_handle_current_playlist_changed( KotoCurrentPlaylist * current_pl, KotoPlaylist * playlist, gboolean play_immediately, + gboolean play_current, gpointer user_data ); @@ -94,6 +95,11 @@ void koto_playback_engine_set_position( int position ); +void koto_playback_engine_set_track_playback_position( + KotoPlaybackEngine * self, + guint64 pos +); + void koto_playback_engine_set_track_repeat( KotoPlaybackEngine * self, gboolean enable_repeat @@ -123,6 +129,10 @@ void koto_playback_engine_toggle_track_shuffle(KotoPlaybackEngine * self); void koto_playback_engine_update_duration(KotoPlaybackEngine * self); +void koto_playback_engine_update_track_position(KotoPlaybackEngine * self); + gboolean koto_playback_engine_tick_duration(gpointer user_data); gboolean koto_playback_engine_tick_track(gpointer user_data); + +gboolean koto_playback_engine_tick_update_playlist_state(gpointer user_data); diff --git a/src/playback/mimes.c b/src/playback/mimes.c index eabaecd..8c3f0ea 100644 --- a/src/playback/mimes.c +++ b/src/playback/mimes.c @@ -41,7 +41,7 @@ gboolean koto_playback_engine_gst_caps_iter( g_hash_table_add(supported_mimes_hash, g_strdup(caps_name)); supported_mimes = g_list_prepend(supported_mimes, g_strdup(caps_name)); - supported_mimes = g_list_prepend(supported_mimes, g_strdup(koto_utils_replace_string_all(caps_name, "x-", ""))); + supported_mimes = g_list_prepend(supported_mimes, g_strdup(koto_utils_string_replace_all(caps_name, "x-", ""))); return TRUE; } diff --git a/src/playlist/add-remove-track-popover.c b/src/playlist/add-remove-track-popover.c index b730769..01c08a6 100644 --- a/src/playlist/add-remove-track-popover.c +++ b/src/playlist/add-remove-track-popover.c @@ -150,11 +150,10 @@ void koto_add_remove_track_popover_handle_checkbutton_toggle( continue; // Skip this } - gchar * track_uuid = koto_track_get_uuid(track); // Get the track - if (should_add) { // Should be adding - koto_playlist_add_track_by_uuid(playlist, track_uuid, FALSE, TRUE); // Add the track to the playlist + koto_playlist_add_track(playlist, track, FALSE, TRUE); // Add the track to the playlist } else { // Should be removing + gchar * track_uuid = koto_track_get_uuid(track); // Get the track koto_playlist_remove_track_by_uuid(playlist, track_uuid); // Remove the track from the playlist } } diff --git a/src/playlist/create-modify-dialog.c b/src/playlist/create-modify-dialog.c index e8201dd..32c2ee9 100644 --- a/src/playlist/create-modify-dialog.c +++ b/src/playlist/create-modify-dialog.c @@ -205,7 +205,7 @@ void koto_create_modify_playlist_dialog_handle_create_click( } KotoPlaylist * playlist = NULL; - gboolean modify_existing_playlist = koto_utils_is_string_valid(self->playlist_uuid); + gboolean modify_existing_playlist = koto_utils_string_is_valid(self->playlist_uuid); if (modify_existing_playlist) { // Modifying an existing playlist playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->playlist_uuid); @@ -313,13 +313,14 @@ void koto_create_modify_playlist_dialog_reset(KotoCreateModifyPlaylistDialog * s gtk_entry_set_placeholder_text(GTK_ENTRY(self->name_entry), "Name of playlist"); // Reset placeholder gtk_image_set_from_icon_name(GTK_IMAGE(self->playlist_image), "insert-image-symbolic"); // Reset the image gtk_button_set_label(GTK_BUTTON(self->create_button), "Create"); + self->playlist_uuid = NULL; // Reset playlist UUID } void koto_create_modify_playlist_dialog_set_playlist_uuid( KotoCreateModifyPlaylistDialog * self, gchar * playlist_uuid ) { - if (!koto_utils_is_string_valid(playlist_uuid)) { // Not a valid playlist UUID string + if (!koto_utils_string_is_valid(playlist_uuid)) { // Not a valid playlist UUID string return; } @@ -335,7 +336,7 @@ void koto_create_modify_playlist_dialog_set_playlist_uuid( gchar * art = koto_playlist_get_artwork(playlist); - if (!koto_utils_is_string_valid(art)) { // If art is not defined + if (!koto_utils_string_is_valid(art)) { // If art is not defined gtk_image_set_from_icon_name(GTK_IMAGE(self->playlist_image), "insert-image-symbolic"); // Reset the image } else { gtk_image_set_from_file(GTK_IMAGE(self->playlist_image), art); diff --git a/src/playlist/current.c b/src/playlist/current.c index 4b6a980..ba27fa5 100644 --- a/src/playlist/current.c +++ b/src/playlist/current.c @@ -58,8 +58,9 @@ static void koto_current_playlist_class_init(KotoCurrentPlaylistClass * c) { NULL, NULL, G_TYPE_NONE, - 2, + 3, KOTO_TYPE_PLAYLIST, + G_TYPE_BOOLEAN, G_TYPE_BOOLEAN ); } @@ -71,13 +72,26 @@ static void koto_current_playlist_init(KotoCurrentPlaylist * self) { } KotoPlaylist * koto_current_playlist_get_playlist(KotoCurrentPlaylist * self) { - return self->current_playlist; + return KOTO_IS_CURRENT_PLAYLIST(self) ? self->current_playlist : NULL; +} + +void koto_current_playlist_save_playlist_state(KotoCurrentPlaylist * self) { + if (!KOTO_IS_CURRENT_PLAYLIST(self)) { // Not a CurrentPlaylist + return; + } + + if (!KOTO_IS_PLAYLIST(self->current_playlist)) { // No playlist + return; + } + + koto_playlist_save_current_playback_state(self->current_playlist); // Save the current playlist state } void koto_current_playlist_set_playlist( KotoCurrentPlaylist * self, KotoPlaylist * playlist, - gboolean play_immediately + gboolean play_immediately, + gboolean play_current ) { if (!KOTO_IS_CURRENT_PLAYLIST(self)) { return; @@ -88,6 +102,8 @@ void koto_current_playlist_set_playlist( } if (KOTO_IS_PLAYLIST(self->current_playlist)) { + koto_current_playlist_save_playlist_state(self); // Save the current playlist state if needed + gboolean * is_temp = FALSE; g_object_get(self->current_playlist, "ephemeral", &is_temp, NULL); // Get the current ephemeral value @@ -101,15 +117,14 @@ void koto_current_playlist_set_playlist( } self->current_playlist = playlist; - // TODO: Saved state - koto_playlist_set_position(self->current_playlist, -1); // Reset our position, use -1 since "next" song is then 0 g_object_ref(playlist); // Increment the reference g_signal_emit( self, signals[SIGNAL_PLAYLIST_CHANGED], 0, playlist, - play_immediately + play_immediately, + play_current ); } diff --git a/src/playlist/current.h b/src/playlist/current.h index 09409e5..57b8cd2 100644 --- a/src/playlist/current.h +++ b/src/playlist/current.h @@ -43,10 +43,13 @@ KotoCurrentPlaylist * koto_current_playlist_new(); KotoPlaylist * koto_current_playlist_get_playlist(KotoCurrentPlaylist * self); +void koto_current_playlist_save_playlist_state(KotoCurrentPlaylist * self); + void koto_current_playlist_set_playlist( KotoCurrentPlaylist * self, KotoPlaylist * playlist, - gboolean play_immediately + gboolean play_immediately, + gboolean play_current ); G_END_DECLS diff --git a/src/playlist/playlist.c b/src/playlist/playlist.c index 58a99d8..7e3bb39 100644 --- a/src/playlist/playlist.c +++ b/src/playlist/playlist.c @@ -30,6 +30,7 @@ enum { PROP_0, PROP_UUID, PROP_NAME, + PROP_ALBUM_UUID, PROP_ART_PATH, PROP_EPHEMERAL, PROP_IS_SHUFFLE_ENABLED, @@ -49,10 +50,11 @@ struct _KotoPlaylist { gchar * uuid; gchar * name; gchar * art_path; + gchar * album_uuid; gint current_position; gchar * current_uuid; - KotoPreferredModelType model; + KotoPreferredPlaylistSortType model; gboolean ephemeral; gboolean is_shuffle_enabled; @@ -127,6 +129,14 @@ static void koto_playlist_class_init(KotoPlaylistClass * c) { G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE ); + props[PROP_ALBUM_UUID] = g_param_spec_string( + "album-uuid", + "UUID of an Album associated with this Playlist", + "UUID of an Album associated with this Playlist. Useful for Audiobooks.", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE + ); + props[PROP_ART_PATH] = g_param_spec_string( "art-path", "Path to any associated artwork of the Playlist", @@ -219,6 +229,9 @@ static void koto_playlist_get_property( case PROP_NAME: g_value_set_string(val, self->name); break; + case PROP_ALBUM_UUID: + g_value_set_string(val, self->album_uuid); + break; case PROP_ART_PATH: g_value_set_string(val, self->art_path); break; @@ -249,6 +262,9 @@ static void koto_playlist_set_property( case PROP_NAME: koto_playlist_set_name(self, g_value_get_string(val)); break; + case PROP_ALBUM_UUID: + koto_playlist_set_album_uuid(self, g_value_get_string(val)); + break; case PROP_ART_PATH: koto_playlist_set_artwork(self, g_value_get_string(val)); break; @@ -267,7 +283,7 @@ static void koto_playlist_set_property( static void koto_playlist_init(KotoPlaylist * self) { self->current_position = -1; // Default to -1 so first time incrementing puts it at 0 self->current_uuid = NULL; - self->model = KOTO_PREFERRED_MODEL_TYPE_DEFAULT; // Default to default model + self->model = KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT; // Default to default model self->is_shuffle_enabled = FALSE; self->ephemeral = FALSE; self->finalized = FALSE; @@ -282,7 +298,17 @@ void koto_playlist_add_to_played_tracks( KotoPlaylist * self, gchar * uuid ) { - if (g_queue_index(self->played_tracks, uuid) != -1) { // Already added + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, uuid); + + if (!KOTO_IS_TRACK(track)) { + return; + } + + if (koto_playlist_get_position_of_track(self, track) != -1) { // Already added return; } @@ -295,32 +321,22 @@ void koto_playlist_add_track( gboolean current, gboolean commit_to_table ) { - koto_playlist_add_track_by_uuid(self, koto_track_get_uuid(track), current, commit_to_table); -} - -void koto_playlist_add_track_by_uuid( - KotoPlaylist * self, - gchar * uuid, - gboolean current, - gboolean commit_to_table -) { - KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, uuid); // Get the track + if (!KOTO_IS_PLAYLIST(self)) { + return; + } if (!KOTO_IS_TRACK(track)) { return; } - GList * found_tracks_uuids = g_queue_find_custom(self->tracks, uuid, koto_playlist_compare_track_uuids); + gchar * track_uuid = koto_track_get_uuid(track); - if (found_tracks_uuids != NULL) { // Is somewhere in the tracks already - g_list_free(found_tracks_uuids); + if (koto_playlist_get_position_of_track(self, track) != -1) { // Found already return; } - g_list_free(found_tracks_uuids); - - g_queue_push_tail(self->tracks, uuid); // Prepend the UUID to the tracks - g_queue_push_tail(self->sorted_tracks, uuid); // Also add to our sorted tracks + g_queue_push_tail(self->tracks, track_uuid); // Prepend the UUID to the tracks + g_queue_push_tail(self->sorted_tracks, track_uuid); // Also add to our sorted tracks g_list_store_append(self->store, track); // Add to the store if (self->finalized) { // Is already finalized @@ -328,24 +344,25 @@ void koto_playlist_add_track_by_uuid( } if (commit_to_table) { - koto_track_save_to_playlist(track, self->uuid, (g_strcmp0(self->current_uuid, uuid) == 0) ? 1 : 0); // Call to save the playlist to the track + koto_track_save_to_playlist(track, self->uuid); // Call to save the playlist to the track } if (current && (g_queue_get_length(self->tracks) > 1)) { // Is current and NOT the first item - self->current_uuid = uuid; // Mark this as current UUID + self->current_uuid = track_uuid; // Mark this as current UUID + koto_playlist_apply_model(self, self->model); // Re-apply our model to enforce mass sort } g_signal_emit( self, playlist_signals[SIGNAL_TRACK_ADDED], 0, - uuid + track_uuid ); } void koto_playlist_apply_model( KotoPlaylist * self, - KotoPreferredModelType preferred_model + KotoPreferredPlaylistSortType preferred_model ) { GList * sort_user_data = NULL; @@ -359,68 +376,97 @@ void koto_playlist_apply_model( } void koto_playlist_commit(KotoPlaylist * self) { - if (self->ephemeral) { // Temporary playlist + if (!KOTO_IS_PLAYLIST(self)) { return; } + gboolean album_acceptable_type = FALSE; + + if (koto_utils_string_is_valid(self->album_uuid)) { + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); // Get the album + + if (KOTO_IS_ALBUM(album)) { // Got the album + KotoLibraryType album_type = koto_album_get_lib_type(album); + album_acceptable_type = ((album_type == KOTO_LIBRARY_TYPE_AUDIOBOOK) || (album_type == KOTO_LIBRARY_TYPE_PODCAST)); + } + } + + if (self->ephemeral && !album_acceptable_type) { // Is a temporary playlist and NOT an acceptable type + return; + } + + guint64 track_playback_pos = 0; + + if (koto_utils_string_is_valid(self->current_uuid)) { // Have a track UUID + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); // Get the track + + if (KOTO_IS_TRACK(track)) { // Is a track + track_playback_pos = koto_track_get_playback_position(track); // Get the track playback position and set it to our track_playback_pos + } + } + gchar * commit_op = g_strdup_printf( - "INSERT INTO playlist_meta(id, name, art_path, preferred_model)" - "VALUES('%s', quote(\"%s\"), quote(\"%s\"), 0)" - "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;", + "INSERT INTO playlist_meta(id, name, art_path, preferred_model, album_id, track_id, playback_position_of_track)" + "VALUES('%s', quote(\"%s\"), quote(\"%s\"), %li, '%s', '%s', '%li')" + "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path, preferred_model=excluded.preferred_model, album_id=excluded.album_id, track_id=excluded.track_id;", self->uuid, - self->name, - self->art_path + koto_utils_string_get_valid(self->name), + koto_utils_string_get_valid(self->art_path), + (guint64) self->model, + koto_utils_string_get_valid(self->album_uuid), + koto_utils_string_get_valid(self->current_uuid), + track_playback_pos ); new_transaction(commit_op, "Failed to save playlist", FALSE); } -void koto_playlist_commit_tracks( - gpointer data, - gpointer user_data -) { - KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, data); // Get the track - - if (track == NULL) { // Not a track - KotoPlaylist * self = user_data; - gchar * playlist_uuid = self->uuid; // Get the playlist UUID - - gchar * current_track = g_queue_peek_nth(self->tracks, self->current_position); // Get the UUID of the current track - //koto_track_save_to_playlist(track, playlist_uuid, (data == current_track) ? 1 : 0); // Call to save the playlist to the track - g_free(playlist_uuid); - g_free(current_track); +void koto_playlist_emit_modified(KotoPlaylist * self) { + if (!KOTO_IS_PLAYLIST(self)) { + return; } -} -gint koto_playlist_compare_track_uuids( - gconstpointer a, - gconstpointer b -) { - return g_strcmp0(a, b); + g_signal_emit( + self, + playlist_signals[SIGNAL_MODIFIED], + 0 + ); } gchar * koto_playlist_get_artwork(KotoPlaylist * self) { - return (self->art_path == NULL) ? NULL : g_strdup(self->art_path); // Return a duplicate of our art path + return (KOTO_IS_PLAYLIST(self) && koto_utils_string_is_valid(self->art_path)) ? g_strdup(self->art_path) : NULL; // Return a duplicate of our art path } -KotoPreferredModelType koto_playlist_get_current_model(KotoPlaylist * self) { - return self->model; +KotoPreferredPlaylistSortType koto_playlist_get_current_model(KotoPlaylist * self) { + return KOTO_IS_PLAYLIST(self) ? self->model : KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT; } -guint koto_playlist_get_current_position(KotoPlaylist * self) { - return self->current_position; +gint koto_playlist_get_current_position(KotoPlaylist * self) { + return KOTO_IS_PLAYLIST(self) ? self->current_position : -1; +} + +KotoTrack * koto_playlist_get_current_track(KotoPlaylist * self) { + if (!KOTO_IS_PLAYLIST(self)) { // Not a playlist + return NULL; + } + + return koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); // Get the current track } gboolean koto_playlist_get_is_finalized(KotoPlaylist * self) { - return self->finalized; + return KOTO_IS_PLAYLIST(self) ? self->finalized : FALSE; +} + +gboolean koto_playlist_get_is_hidden(KotoPlaylist * self) { + return (self->ephemeral && koto_utils_string_is_valid(self->album_uuid)); // If the playlist is ephemeral and associated with an album, it should be hidden from nav } guint koto_playlist_get_length(KotoPlaylist * self) { - return g_queue_get_length(self->tracks); // Get the length of the tracks + return KOTO_IS_PLAYLIST(self) ? g_queue_get_length(self->tracks) : 0; // Get the length of the tracks } gchar * koto_playlist_get_name(KotoPlaylist * self) { - return (self->name == NULL) ? NULL : g_strdup(self->name); + return (KOTO_IS_PLAYLIST(self) && koto_utils_string_is_valid(self->name)) ? g_strdup(self->name) : NULL; } gint koto_playlist_get_position_of_track( @@ -501,11 +547,15 @@ gchar * koto_playlist_go_to_next(KotoPlaylist * self) { if (self->is_shuffle_enabled) { // Shuffling enabled gchar * random_track_uuid = koto_playlist_get_random_track(self); // Get a random track + self->current_uuid = random_track_uuid; // Update our current UUID koto_playlist_add_to_played_tracks(self, random_track_uuid); + + koto_playlist_emit_modified(self); + return random_track_uuid; } - if (!koto_utils_is_string_valid(self->current_uuid)) { // No valid UUID yet + if (!koto_utils_string_is_valid(self->current_uuid)) { // No valid UUID yet self->current_position++; } else { // Have a UUID currently KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); @@ -525,6 +575,7 @@ gchar * koto_playlist_go_to_next(KotoPlaylist * self) { self->current_uuid = g_queue_peek_nth(self->sorted_tracks, self->current_position); koto_playlist_add_to_played_tracks(self, self->current_uuid); + koto_playlist_emit_modified(self); return self->current_uuid; } @@ -534,7 +585,7 @@ gchar * koto_playlist_go_to_previous(KotoPlaylist * self) { return koto_playlist_get_random_track(self); // Get a random track } - if (!koto_utils_is_string_valid(self->current_uuid)) { // No valid UUID + if (!koto_utils_string_is_valid(self->current_uuid)) { // No valid UUID return NULL; } @@ -553,11 +604,13 @@ gchar * koto_playlist_go_to_previous(KotoPlaylist * self) { self->current_position = pos_of_song - 1; // Decrement our position based on position of song self->current_uuid = g_queue_peek_nth(self->sorted_tracks, self->current_position); + koto_playlist_emit_modified(self); + return self->current_uuid; } void koto_playlist_mark_as_finalized(KotoPlaylist * self) { - if (self->finalized) { // Already finalized + if (!KOTO_IS_PLAYLIST(self)) { // Not a playlist return; } @@ -592,11 +645,11 @@ gint koto_playlist_model_sort_by_track( GList * ptr_list = data_list; KotoPlaylist * self = g_list_nth_data(ptr_list, 0); // First item in the GPtrArray is a pointer to our playlist - KotoPreferredModelType model = GPOINTER_TO_UINT(g_list_nth_data(ptr_list, 1)); // Second item in the GPtrArray is a pointer to our KotoPreferredModelType + KotoPreferredPlaylistSortType model = GPOINTER_TO_UINT(g_list_nth_data(ptr_list, 1)); // Second item in the GPtrArray is a pointer to our KotoPreferredPlaylistSortType if ( - (model == KOTO_PREFERRED_MODEL_TYPE_DEFAULT) || // Newest first model - (model == KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST) // Oldest first + (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT) || // Newest first model + (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST) // Oldest first ) { gint first_track_pos = g_queue_index(self->tracks, koto_track_get_uuid(first_track)); gint second_track_pos = g_queue_index(self->tracks, koto_track_get_uuid(second_track)); @@ -609,14 +662,14 @@ gint koto_playlist_model_sort_by_track( return -1; } - if (model == KOTO_PREFERRED_MODEL_TYPE_DEFAULT) { // Newest first + if (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT) { // Newest first return (first_track_pos < second_track_pos) ? 1 : -1; // Display first at end, not beginning } else { return (first_track_pos < second_track_pos) ? -1 : 1; // Display at beginning, not end } } - if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM) { // Sort by album name + if (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM) { // Sort by album name gchar * first_album_uuid = NULL; gchar * second_album_uuid = NULL; @@ -653,7 +706,7 @@ gint koto_playlist_model_sort_by_track( return g_utf8_collate(koto_album_get_name(first_album), koto_album_get_name(second_album)); } - if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST) { // Sort by artist name + if (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST) { // Sort by artist name gchar * first_artist_uuid = NULL; gchar * second_artist_uuid = NULL; @@ -682,31 +735,27 @@ gint koto_playlist_model_sort_by_track( } return g_utf8_collate(koto_artist_get_name(first_artist), koto_artist_get_name(second_artist)); - } + } else if (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME) { // Track name + return g_utf8_collate(koto_track_get_name(first_track), koto_track_get_name(second_track)); + } else if (model == KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_POS) { // Sort by track position - if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME) { // Track name - gchar * first_track_name = NULL; - gchar * second_track_name = NULL; + guint first_track_disc = koto_track_get_disc_number(first_track); + guint second_track_disc = koto_track_get_disc_number(second_track); - g_object_get( - first_track, - "parsed-name", - &first_track_name, - NULL - ); + if (first_track_disc < second_track_disc) { // First is in an earlier CD / Disc / Part + return -1; + } else if (first_track_disc > second_track_disc) { // First is in later CD / Disc / Part + return 1; + } // Continue on to same CD - g_object_get( - second_track, - "parsed-name", - &second_track_name, - NULL - ); + guint64 first_track_pos = koto_track_get_position(first_track); + guint64 second_track_pos = koto_track_get_position(second_track); - gint ret = g_utf8_collate(first_track_name, second_track_name); - g_free(first_track_name); - g_free(second_track_name); - - return ret; + if (first_track_pos < second_track_pos) { + return -1; + } else if (first_track_pos > second_track_pos) { + return 1; + } } return 0; @@ -761,10 +810,73 @@ void koto_playlist_remove_track_by_uuid( ); } +void koto_playlist_save_current_playback_state(KotoPlaylist * self) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + if (!koto_utils_string_is_valid(self->album_uuid)) { // No album associated with this playlist + return; + } + + KotoAlbum * album = koto_cartographer_get_album_by_uuid(koto_maps, self->album_uuid); // Get the album + + if (!KOTO_IS_ALBUM(album)) { // Failed to get the album + return; + } + + KotoLibraryType album_type = koto_album_get_lib_type(album); + + if ((album_type == KOTO_LIBRARY_TYPE_MUSIC) || (album_type == KOTO_LIBRARY_TYPE_UNKNOWN)) { // Not an Audiobook or Podcast + return; + } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid); // Get the track + + if (!KOTO_IS_TRACK(track) || !koto_utils_string_is_valid(self->current_uuid)) { // If we don't have a track we are currently playing + gchar * commit_op = g_strdup_printf("UPDATE playlist_meta SET track_id='', playback_position_of_track=0 WHERE id='%s';", self->uuid); + new_transaction(commit_op, "Failed to update our playlist meta", FALSE); + return; + } + + gchar * commit_op = g_strdup_printf( + "UPDATE playlist_meta SET track_id='%s', playback_position_of_track=%li WHERE id='%s';", + koto_utils_string_get_valid(self->current_uuid), + koto_track_get_playback_position(track), + self->uuid + ); + + new_transaction(commit_op, "Failed to update our playlist state", FALSE); + koto_playlist_emit_modified(self); +} + +void koto_playlist_set_album_uuid( + KotoPlaylist * self, + const gchar * album_uuid +) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + + if (!koto_utils_string_is_valid(album_uuid)) { + return; + } + + self->album_uuid = g_strdup(album_uuid); // Update the album UUID + + if (self->finalized) { // Has already been loaded + koto_playlist_emit_modified(self); + } +} + void koto_playlist_set_artwork( KotoPlaylist * self, const gchar * path ) { + if (!KOTO_IS_PLAYLIST(self)) { + return; + } + if (path == NULL) { return; } @@ -792,11 +904,7 @@ void koto_playlist_set_artwork( self->art_path = g_strdup(path); // Update our art path to a duplicate of provided path if (self->finalized) { // Has already been loaded - g_signal_emit( - self, - playlist_signals[SIGNAL_MODIFIED], - 0 - ); + koto_playlist_emit_modified(self); } free_cookie: @@ -818,11 +926,7 @@ void koto_playlist_set_name( self->name = g_strdup(name); if (self->finalized) { // Has already been loaded - g_signal_emit( - self, - playlist_signals[SIGNAL_MODIFIED], - 0 - ); + koto_playlist_emit_modified(self); } } @@ -830,19 +934,39 @@ void koto_playlist_set_position( KotoPlaylist * self, gint position ) { + if (!KOTO_IS_PLAYLIST(self)) { // Not a playlist + return; + } + self->current_position = position; + + if (self->finalized) { // Has already been loaded + koto_playlist_emit_modified(self); + } } void koto_playlist_set_track_as_current( KotoPlaylist * self, gchar * track_uuid ) { - gint position_of_track = g_queue_index(self->sorted_tracks, track_uuid); // Get the position of the UUID in our tracks - - if (position_of_track != -1) { // In tracks - self->current_uuid = track_uuid; - self->current_position = position_of_track; + if (!KOTO_IS_PLAYLIST(self)) { // Not a playlist + return; } + + KotoTrack * track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); // Get the track + + if (!KOTO_IS_TRACK(track)) { // Not a track + return; + } + + gint position_of_track = koto_playlist_get_position_of_track(self, track); + + if (position_of_track == -1) { + return; + } + + self->current_uuid = track_uuid; + self->current_position = position_of_track; // Set accurate position } void koto_playlist_set_uuid( diff --git a/src/playlist/playlist.h b/src/playlist/playlist.h index 997aa54..69f708e 100644 --- a/src/playlist/playlist.h +++ b/src/playlist/playlist.h @@ -23,12 +23,13 @@ G_BEGIN_DECLS typedef enum { - KOTO_PREFERRED_MODEL_TYPE_DEFAULT, // Considered to be newest first - KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST, - KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM, - KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST, - KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME -} KotoPreferredModelType; + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_DEFAULT, // Considered to be newest first + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_OLDEST_FIRST, + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ALBUM, + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_ARTIST, + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_NAME, + KOTO_PREFERRED_PLAYLIST_SORT_TYPE_SORT_BY_TRACK_POS +} KotoPreferredPlaylistSortType; /** * Type Definition @@ -64,40 +65,29 @@ void koto_playlist_add_track( gboolean commit_to_table ); -void koto_playlist_add_track_by_uuid( - KotoPlaylist * self, - gchar * uuid, - gboolean current, - gboolean commit_to_table -); - void koto_playlist_apply_model( KotoPlaylist * self, - KotoPreferredModelType preferred_model + KotoPreferredPlaylistSortType preferred_model ); void koto_playlist_commit(KotoPlaylist * self); -void koto_playlist_commit_tracks( - gpointer data, - gpointer user_data -); - -gint koto_playlist_compare_track_uuids( - gconstpointer a, - gconstpointer b -); +void koto_playlist_emit_modified(KotoPlaylist * self); gchar * koto_playlist_get_artwork(KotoPlaylist * self); -KotoPreferredModelType koto_playlist_get_current_model(KotoPlaylist * self); +KotoPreferredPlaylistSortType koto_playlist_get_current_model(KotoPlaylist * self); -guint koto_playlist_get_current_position(KotoPlaylist * self); +gint koto_playlist_get_current_position(KotoPlaylist * self); + +KotoTrack * koto_playlist_get_current_track(KotoPlaylist * self); guint koto_playlist_get_length(KotoPlaylist * self); gboolean koto_playlist_get_is_finalized(KotoPlaylist * self); +gboolean koto_playlist_get_is_hidden(KotoPlaylist * self); + gchar * koto_playlist_get_name(KotoPlaylist * self); gint koto_playlist_get_position_of_track( @@ -139,12 +129,17 @@ void koto_playlist_remove_track_by_uuid( gchar * uuid ); +void koto_playlist_set_album_uuid( + KotoPlaylist * self, + const gchar * album_uuid +); + void koto_playlist_set_artwork( KotoPlaylist * self, const gchar * path ); -void koto_playlist_save_state(KotoPlaylist * self); +void koto_playlist_save_current_playback_state(KotoPlaylist * self); void koto_playlist_set_name( KotoPlaylist * self, diff --git a/theme/_disc-view.scss b/theme/_disc-view.scss index 2b613e4..dc3a487 100644 --- a/theme/_disc-view.scss +++ b/theme/_disc-view.scss @@ -10,21 +10,5 @@ color: $text-color-faded; margin: 10px 0; } - - & .track-list { - & > row { - &:not(:active):not(:selected) { // Neither active nor selected, see gtk overrides - color: $text-color-bright; - - &:nth-child(odd):not(:hover) { - background-color: $bg-primary; - } - - &:nth-child(even), &:hover { - background-color: $bg-secondary; - } - } - } - } } } \ No newline at end of file diff --git a/theme/_main.scss b/theme/_main.scss index 7f5234f..a02659b 100644 --- a/theme/_main.scss +++ b/theme/_main.scss @@ -1,6 +1,12 @@ +@import 'components/album-info'; +@import 'components/audiobook-view'; +@import 'components/badge'; @import 'components/cover-art-button'; @import 'components/gtk-overrides'; +@import 'components/track-list'; @import 'components/track-table'; +@import 'components/writer-page'; +@import 'pages/audiobook-library'; @import 'pages/artist-view'; @import 'pages/music-local'; @import 'pages/playlist-page'; @@ -39,7 +45,8 @@ window { // All the classes we want consistent padding applied to for its primary content .artist-view-content, // Has the albums - .playlist-page { // Individual playlists + .playlist-page, // Individual playlists + .writer-page { // Writer page in Audiobook padding: $itempadding; } } diff --git a/theme/_player-bar.scss b/theme/_player-bar.scss index 99e9338..9c4f1ba 100644 --- a/theme/_player-bar.scss +++ b/theme/_player-bar.scss @@ -46,4 +46,11 @@ } } } + + .playerbar-secondary-controls { // Secondary controls + label { // Inner playback position label + font-size: large; + margin-right: $halvedpadding; + } + } } diff --git a/theme/_vars.scss b/theme/_vars.scss index 28ea9b1..2bf2ba5 100644 --- a/theme/_vars.scss +++ b/theme/_vars.scss @@ -5,4 +5,5 @@ $palewhite: #cccccc; $red : #FF4652; $itempadding: 40px; -$halvedpadding: $itempadding / 2; \ No newline at end of file +$halvedpadding: $itempadding / 2; +$quarterpadding: $itempadding / 4; \ No newline at end of file diff --git a/theme/components/_album-info.scss b/theme/components/_album-info.scss new file mode 100644 index 0000000..f0c646c --- /dev/null +++ b/theme/components/_album-info.scss @@ -0,0 +1,30 @@ +// This file contain the styling for the Album Info section + +.album-info { + .album-description, + .album-narrator, + .album-title-year-combo, + .genres-tag-list { + margin-bottom: $quarterpadding + } + + .album-title-year-combo { + padding-top: $halvedpadding; + } + + .album-title { // Title of album + color: $text-color-faded; + font-size: 2.5em; + font-weight: bold; + } + + .album-year { + margin-left: $halvedpadding; + } + + .genres-tag-list { // Genres Tag List + & .label-badge:not(:last-child) { + margin-right: $halvedpadding; + } + } +} \ No newline at end of file diff --git a/theme/components/_audiobook-view.scss b/theme/components/_audiobook-view.scss new file mode 100644 index 0000000..dafa68a --- /dev/null +++ b/theme/components/_audiobook-view.scss @@ -0,0 +1,34 @@ +// This is the styling for the Audiobook VIew + +@import '../vars'; + +.audiobook-view { + .side-info { // Side Info + margin-right: $halvedpadding; + + button, + image { + margin-bottom: $halvedpadding; + } + + button { // Play / Continue Playback button + font-size: large; + font-weight: bold; + } + + & > label { + font-size: large; + + &:last-child { + margin-bottom: $halvedpadding; + } + } + } + + .chapters-label { // Chapters label after album info + color: $text-color-faded; + font-size: x-large; + font-weight: bold; + padding: $halvedpadding 0; // Top / bottom padding + } +} \ No newline at end of file diff --git a/theme/components/_badge.scss b/theme/components/_badge.scss new file mode 100644 index 0000000..1f28024 --- /dev/null +++ b/theme/components/_badge.scss @@ -0,0 +1,12 @@ + // This file contains the styling for our label badge + +@import '../vars'; + +.label-badge { + color: $text-color-faded; + font-size: large; + font-weight: 900; + background-color: $bg-secondary; + border-radius: 10px; + padding: 5px 20px; +} \ No newline at end of file diff --git a/theme/components/_track-list.scss b/theme/components/_track-list.scss new file mode 100644 index 0000000..217089b --- /dev/null +++ b/theme/components/_track-list.scss @@ -0,0 +1,19 @@ +// Track List styling + +@import '../vars'; + +.track-list { + & > row { + &:not(:active):not(:selected) { // Neither active nor selected, see gtk overrides + color: $text-color-bright; + + &:nth-child(odd):not(:hover) { + background-color: $bg-primary; + } + + &:nth-child(even), &:hover { + background-color: $bg-secondary; + } + } + } +} \ No newline at end of file diff --git a/theme/components/_writer-page.scss b/theme/components/_writer-page.scss new file mode 100644 index 0000000..c37f3cf --- /dev/null +++ b/theme/components/_writer-page.scss @@ -0,0 +1,12 @@ +// This is the styling for the writer page + +@import '../vars'; + +.writer-page { + .writer-header { // Our writer / artist header label + color: $text-color-faded; + font-size: 4em; + font-weight: bold; + padding-bottom: $itempadding; + } +} \ No newline at end of file diff --git a/theme/pages/_audiobook-library.scss b/theme/pages/_audiobook-library.scss new file mode 100644 index 0000000..6de0139 --- /dev/null +++ b/theme/pages/_audiobook-library.scss @@ -0,0 +1,36 @@ +// This file contains the styling for our Audiobook Library + +@import '../vars'; + +.audiobook-library { // Library page + .genres-banner { // Banner for genres list + .large-banner { // Large banner with art for each genre + padding: $itempadding; + + .audiobook-genre-button { // Genre buttons + .koto-button { + font-size: 2em; + margin: 0.5em; + } + } + } + } + + .writers-button-flow { // Flowbox of buttons for writers + padding: 0 $itempadding; // Horizontal padding of our standard item padding + + flowboxchild { + padding: 0; + + &:nth-child(even) { + margin: 0 0.5em; + } + + .writer-button { // Writer button + color: $text-color-bright; + font-size: 1.4em; + background-color: $bg-secondary; + } + } + } +} \ No newline at end of file diff --git a/theme/pages/_music-local.scss b/theme/pages/_music-local.scss index ac257fd..de5cda9 100644 --- a/theme/pages/_music-local.scss +++ b/theme/pages/_music-local.scss @@ -21,14 +21,6 @@ & > overlay { margin-right: $itempadding; } - - & > box { - & > label { - font-size: xx-large; - font-weight: 900; - padding: $halvedpadding 0; - } - } } } }