Start implementation of database functionality, starting with table generation.

This database will be stored in the user's XDG_DATA_HOME directory under a folder called `com.github.joshstrobl.koto`.

Refactored Koto Track Listing into a dedicated Disc View component, break apart out CDs / Discs into relevant Disc View components, sorting them by the disc and showing a label if there are more than one discs / CDs.

Started using the row-activated event for our Artist GtkListBox instead of having press events on the artist buttons. Makes switching between artists far more reliable. Added a slide left / right stack animation and set its animation to 400ms so it's in the goldilocks zone (IMO) of not being too fast or too slow.

Fix a warning related to scale factor fetching in the PlayerBar.
This commit is contained in:
Joshua Strobl 2021-02-27 17:26:24 +02:00
parent 134b4d79a8
commit eac4940c77
17 changed files with 434 additions and 61 deletions

View file

@ -19,8 +19,8 @@
#include <gtk-4.0/gtk/gtk.h>
#include "../../indexer/album.h"
#include "../../indexer/artist.h"
#include "../../koto-track-item.h"
#include "album-view.h"
#include "disc-view.h"
#include "koto-config.h"
#include "koto-utils.h"
@ -29,7 +29,7 @@ struct _KotoAlbumView {
KotoIndexedAlbum *album;
GtkWidget *main;
GtkWidget *album_tracks_box;
GtkWidget *tracks;
GtkWidget *discs;
GtkWidget *album_label;
GHashTable *cd_to_track_listbox;
@ -71,13 +71,15 @@ static void koto_album_view_init(KotoAlbumView *self) {
gtk_widget_set_can_focus(self->main, FALSE);
self->album_tracks_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
self->tracks = gtk_list_box_new(); // Create our list of our tracks
gtk_list_box_set_sort_func(GTK_LIST_BOX(self->tracks), koto_album_view_sort_tracks, NULL, NULL); // Ensure we can sort our tracks
gtk_widget_add_css_class(self->tracks, "track-list");
gtk_widget_set_size_request(self->tracks, 600, -1);
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);
gtk_list_box_set_sort_func(GTK_LIST_BOX(self->discs), koto_album_view_sort_discs, NULL, NULL); // Ensure we can sort our discs
gtk_widget_add_css_class(self->discs, "discs-list");
gtk_widget_set_can_focus(self->discs, FALSE);
gtk_widget_set_size_request(self->discs, 600, -1);
gtk_box_append(GTK_BOX(self->main), self->album_tracks_box); // Add the tracks box to the art info combo box
gtk_box_append(GTK_BOX(self->album_tracks_box), self->tracks); // Add the tracks list box to the albums tracks box
gtk_box_append(GTK_BOX(self->album_tracks_box), self->discs); // Add the discs list box to the albums tracks box
}
GtkWidget* koto_album_view_get_main(KotoAlbumView *self) {
@ -134,40 +136,49 @@ void koto_album_view_set_album(KotoAlbumView *self, KotoIndexedAlbum *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
GList *t;
for (t = koto_indexed_album_get_files(self->album); t != NULL; t = t->next) { // For each file / track
KotoIndexedFile *file = (KotoIndexedFile*) t->data;
KotoTrackItem *track_item = koto_track_item_new(file); // Create our new track item
gtk_list_box_prepend(GTK_LIST_BOX(self->tracks), GTK_WIDGET(track_item)); // Add to our tracks list box
GHashTable *discs = g_hash_table_new(g_str_hash, g_str_equal);
GList *tracks = koto_indexed_album_get_files(album); // Get the tracks for this album
for (guint i = 0; i < g_list_length(tracks); i++) {
KotoIndexedFile *file = (KotoIndexedFile*) g_list_nth_data(tracks, i); // Get the
guint *disc_number;
g_object_get(file, "cd", &disc_number, NULL);
gchar *disc_num_as_str = g_strdup_printf("%u", GPOINTER_TO_UINT(disc_number));
if (g_hash_table_contains(discs, disc_num_as_str)) { // Already have this added
continue; // Skip
}
g_hash_table_insert(discs, disc_num_as_str, "0"); // Mark this disc number in the hash table
KotoDiscView *disc_view = koto_disc_view_new(album, disc_number);
gtk_list_box_append(GTK_LIST_BOX(self->discs), GTK_WIDGET(disc_view)); // Add the Disc View to the List Box
}
if (g_list_length(g_hash_table_get_keys(discs)) == 1) { // Only have one album
GtkListBoxRow *row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->discs), 0); // Get the first item
if (GTK_IS_LIST_BOX_ROW(row)) { // Is a row
koto_disc_view_set_disc_label_visible((KotoDiscView*) gtk_list_box_row_get_child(row), FALSE); // Get
}
}
g_hash_table_destroy(discs);
}
int koto_album_view_sort_tracks(GtkListBoxRow *track1, GtkListBoxRow *track2, gpointer user_data) {
int koto_album_view_sort_discs(GtkListBoxRow *disc1, GtkListBoxRow *disc2, gpointer user_data) {
(void) user_data;
KotoTrackItem *track1_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(track1));
KotoTrackItem *track2_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(track2));
KotoDiscView *disc1_item = KOTO_DISC_VIEW(gtk_list_box_row_get_child(disc1));
KotoDiscView *disc2_item = KOTO_DISC_VIEW(gtk_list_box_row_get_child(disc2));
KotoIndexedFile *track1_file;
KotoIndexedFile *track2_file;
guint disc1_num;
guint disc2_num;
g_object_get(track1_item, "track", &track1_file, NULL);
g_object_get(track2_item, "track", &track2_file, NULL);
g_object_get(disc1_item, "disc", &disc1_num, NULL);
g_object_get(disc2_item, "disc", &disc2_num, NULL);
guint16 *track1_pos;
guint16 *track2_pos;
g_object_get(track1_file, "position", &track1_pos, NULL);
g_object_get(track2_file, "position", &track2_pos, NULL);
if (track1_pos == track2_pos) { // Identical positions (like reported as 0)
gchar *track1_name;
gchar *track2_name;
g_object_get(track1_file, "parsed-name", &track1_name, NULL);
g_object_get(track2_file, "parsed-name", &track2_name, NULL);
return g_utf8_collate(track1_name, track2_name);
} else if (track1_pos < track2_pos) {
if (disc1_num == disc2_num) { // Identical positions (like reported as 0)
return 0;
} else if (disc1_num < disc2_num) {
return -1;
} else {
return 1;

View file

@ -32,6 +32,6 @@ KotoAlbumView* koto_album_view_new(KotoIndexedAlbum *album);
GtkWidget* koto_album_view_get_main(KotoAlbumView *self);
void koto_album_view_add_track_to_listbox(KotoIndexedAlbum *self, KotoIndexedFile *file);
void koto_album_view_set_album(KotoAlbumView *self, KotoIndexedAlbum *album);
int koto_album_view_sort_tracks(GtkListBoxRow *track1, GtkListBoxRow *track2, gpointer user_data);
int koto_album_view_sort_discs(GtkListBoxRow *track1, GtkListBoxRow *track2, gpointer user_data);
G_END_DECLS

218
src/pages/music/disc-view.c Normal file
View file

@ -0,0 +1,218 @@
/* disc-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 <gtk-4.0/gtk/gtk.h>
#include "../../indexer/album.h"
#include "../../koto-track-item.h"
#include "disc-view.h"
struct _KotoDiscView {
GtkBox parent_instance;
KotoIndexedAlbum *album;
GtkWidget *header;
GtkWidget *label;
GtkWidget *list;
guint *disc_number;
};
G_DEFINE_TYPE(KotoDiscView, koto_disc_view, GTK_TYPE_BOX);
enum {
PROP_0,
PROP_DISC,
PROP_ALBUM,
N_PROPERTIES
};
static GParamSpec *props[N_PROPERTIES] = { NULL, };
static void koto_disc_view_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec);
static void koto_disc_view_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec);
static void koto_disc_view_class_init(KotoDiscViewClass *c) {
GObjectClass *gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_disc_view_set_property;
gobject_class->get_property = koto_disc_view_get_property;
props[PROP_DISC] = g_param_spec_uint(
"disc",
"Disc",
"Disc",
0,
G_MAXUINT16,
1,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE
);
props[PROP_ALBUM] = g_param_spec_object(
"album",
"Album",
"Album",
KOTO_TYPE_INDEXED_ALBUM,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_disc_view_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) {
KotoDiscView *self = KOTO_DISC_VIEW(obj);
switch (prop_id) {
case PROP_DISC:
g_value_set_uint(val, GPOINTER_TO_UINT(self->disc_number));
break;
case PROP_ALBUM:
g_value_set_object(val, self->album);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_disc_view_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) {
KotoDiscView *self = KOTO_DISC_VIEW(obj);
switch (prop_id) {
case PROP_DISC:
koto_disc_view_set_disc_number(self, g_value_get_uint(val));
break;
case PROP_ALBUM:
koto_disc_view_set_album(self, (KotoIndexedAlbum*) g_value_get_object(val));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_disc_view_init(KotoDiscView *self) {
gtk_widget_add_css_class(GTK_WIDGET(self), "disc-view");
gtk_widget_set_can_focus(GTK_WIDGET(self), FALSE);
self->header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_hexpand(self->header, TRUE);
gtk_widget_set_size_request(self->header, 16, -1);
GtkWidget *ico = gtk_image_new_from_icon_name("drive-optical-symbolic");
gtk_box_prepend(GTK_BOX(self->header), ico);
self->label = gtk_label_new(NULL); // Create an empty label
gtk_widget_set_halign(self->label, GTK_ALIGN_START);
gtk_widget_set_valign(self->label, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(self->header), self->label);
gtk_box_append(GTK_BOX(self), self->header);
}
void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album) {
if (album == NULL) {
return;
}
if (self->album != NULL) {
g_free(self->album);
}
self->album = album;
if (GTK_IS_LIST_BOX(self->list)) { // Already have a listbox
gtk_box_remove(GTK_BOX(self), self->list); // Remove the box
g_object_unref(self->list); // Unref the list
}
self->list = gtk_list_box_new(); // Create our list of our tracks
gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->list), GTK_SELECTION_MULTIPLE);
gtk_list_box_set_sort_func(GTK_LIST_BOX(self->list), koto_album_view_sort_tracks, NULL, NULL); // Ensure we can sort our tracks
gtk_widget_add_css_class(self->list, "track-list");
gtk_widget_set_size_request(self->list, 600, -1);
gtk_box_append(GTK_BOX(self), self->list);
GList *t;
for (t = koto_indexed_album_get_files(self->album); t != NULL; t = t->next) { // For each file / track
KotoIndexedFile *file = (KotoIndexedFile*) t->data;
guint *disc_number;
g_object_get(file, "cd", &disc_number, NULL); // get the disc number
if (GPOINTER_TO_UINT(self->disc_number) != GPOINTER_TO_UINT(disc_number)) { // Track does not belong to this CD
continue;
}
KotoTrackItem *track_item = koto_track_item_new(file); // Create our new track item
gtk_list_box_prepend(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Add to our tracks list box
}
}
void koto_disc_view_set_disc_number(KotoDiscView *self, guint disc_number) {
if (disc_number == 0) {
return;
}
self->disc_number = GUINT_TO_POINTER(disc_number);
gchar *disc_label = g_strdup_printf("Disc %u", disc_number);
gtk_label_set_text(GTK_LABEL(self->label), disc_label); // Set the label
g_free(disc_label);
}
void koto_disc_view_set_disc_label_visible(KotoDiscView *self, gboolean visible) {
(visible) ? gtk_widget_show(self->header) : gtk_widget_hide(self->header);
}
int koto_album_view_sort_tracks(GtkListBoxRow *track1, GtkListBoxRow *track2, gpointer user_data) {
(void) user_data;
KotoTrackItem *track1_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(track1));
KotoTrackItem *track2_item = KOTO_TRACK_ITEM(gtk_list_box_row_get_child(track2));
KotoIndexedFile *track1_file;
KotoIndexedFile *track2_file;
g_object_get(track1_item, "track", &track1_file, NULL);
g_object_get(track2_item, "track", &track2_file, NULL);
guint16 *track1_pos;
guint16 *track2_pos;
g_object_get(track1_file, "position", &track1_pos, NULL);
g_object_get(track2_file, "position", &track2_pos, NULL);
if (track1_pos == track2_pos) { // Identical positions (like reported as 0)
gchar *track1_name;
gchar *track2_name;
g_object_get(track1_file, "parsed-name", &track1_name, NULL);
g_object_get(track2_file, "parsed-name", &track2_name, NULL);
return g_utf8_collate(track1_name, track2_name);
} else if (track1_pos < track2_pos) {
return -1;
} else {
return 1;
}
}
KotoDiscView* koto_disc_view_new(KotoIndexedAlbum *album, guint *disc_number) {
return g_object_new(KOTO_TYPE_DISC_VIEW,
"disc", disc_number,
"album", album,
"orientation", GTK_ORIENTATION_VERTICAL,
NULL
);
}

View file

@ -0,0 +1,36 @@
/* disc-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 <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../../indexer/album.h"
G_BEGIN_DECLS
#define KOTO_TYPE_DISC_VIEW (koto_disc_view_get_type())
G_DECLARE_FINAL_TYPE(KotoDiscView, koto_disc_view, KOTO, DISC_VIEW, GtkBox)
KotoDiscView* koto_disc_view_new(KotoIndexedAlbum *album, guint *disc);
void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album);
void koto_disc_view_set_disc_label_visible(KotoDiscView *self, gboolean visible);
void koto_disc_view_set_disc_number(KotoDiscView *self, guint disc_number);
int koto_album_view_sort_tracks(GtkListBoxRow *track1, GtkListBoxRow *track2, gpointer user_data);
G_END_DECLS

View file

@ -117,17 +117,12 @@ void koto_page_music_local_add_artist(KotoPageMusicLocal *self, KotoIndexedArtis
KotoArtistView *artist_view = koto_artist_view_new(); // Create our new artist view
koto_artist_view_add_artist(artist_view, artist); // Add the artist
gtk_stack_add_named(GTK_STACK(self->stack), koto_artist_view_get_main(artist_view), artist_name);
GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick
g_signal_connect(controller, "pressed", G_CALLBACK(koto_page_music_local_handle_artist_click), self);
gtk_widget_add_controller(GTK_WIDGET(artist_button), GTK_EVENT_CONTROLLER(controller));
}
void koto_page_music_local_handle_artist_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) {
(void) n_press; (void) x; (void) y;
void koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *row, gpointer data) {
(void) box;
KotoPageMusicLocal *self = (KotoPageMusicLocal*) data;
GtkWidget *btn_widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture)); // Get the widget that applied to this gesture
KotoButton *btn = KOTO_BUTTON(btn_widget);
KotoButton *btn = KOTO_BUTTON(gtk_list_box_row_get_child(row));
gchar *artist_name;
g_object_get(btn, "button-text", &artist_name, NULL);
@ -169,6 +164,7 @@ void koto_page_music_local_set_library(KotoPageMusicLocal *self, KotoIndexedLibr
gboolean list_created = GTK_IS_LIST_BOX(self->artist_list);
if (list_created) { // Successfully created our list
g_signal_connect(GTK_LIST_BOX(self->artist_list), "row-activated", G_CALLBACK(koto_page_music_local_handle_artist_click), self);
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(self->artist_list), TRUE);
gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->artist_list), GTK_SELECTION_BROWSE);
gtk_list_box_set_sort_func(GTK_LIST_BOX(self->artist_list), koto_page_music_local_sort_artists, NULL, NULL); // Add our sort function
@ -181,6 +177,8 @@ void koto_page_music_local_set_library(KotoPageMusicLocal *self, KotoIndexedLibr
gtk_widget_set_size_request(GTK_WIDGET(self->artist_list), 300, -1);
self->stack = gtk_stack_new(); // Create a new stack
gtk_stack_set_transition_duration(GTK_STACK(self->stack), 400);
gtk_stack_set_transition_type(GTK_STACK(self->stack), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT);
gtk_widget_set_hexpand(self->stack, TRUE);
gboolean stack_created = GTK_IS_STACK(self->stack);

View file

@ -31,7 +31,7 @@ G_DECLARE_FINAL_TYPE (KotoPageMusicLocal, koto_page_music_local, KOTO, PAGE_MUSI
KotoPageMusicLocal* koto_page_music_local_new();
void koto_page_music_local_add_artist(KotoPageMusicLocal *self, KotoIndexedArtist *artist);
void koto_page_music_local_handle_artist_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);
void koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *row, gpointer data);
void koto_page_music_local_set_library(KotoPageMusicLocal *self, KotoIndexedLibrary *lib);
int koto_page_music_local_sort_artists(GtkListBoxRow *artist1, GtkListBoxRow *artist2, gpointer user_data);