Implement Playlist functionality. My god...

Too many changes to summarize.

- Fixes #2.
- Fixes #3.
- Fixes #5.
- Fixes #7.

Start work on uncrustify config.
This commit is contained in:
Joshua Strobl 2021-05-07 16:45:57 +03:00
parent ddf1987b50
commit 0e2244ba90
62 changed files with 6280 additions and 374 deletions

View file

@ -0,0 +1,355 @@
/* koto-action-bar.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "koto-action-bar.h"
#include "../db/cartographer.h"
#include "../pages/music/music-local.h"
#include "../playlist/add-remove-track-popover.h"
#include "../playback/engine.h"
#include "../koto-button.h"
#include "../koto-window.h"
extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup;
extern KotoCartographer *koto_maps;
extern KotoPageMusicLocal *music_local_page;
extern KotoPlaybackEngine *playback_engine;
extern KotoWindow *main_window;
enum {
SIGNAL_CLOSED,
N_SIGNALS
};
static guint actionbar_signals[N_SIGNALS] = { 0 };
struct _KotoActionBar {
GObject parent_instance;
GtkActionBar *main;
GtkWidget *center_box_content;
GtkWidget *start_box_content;
GtkWidget *stop_box_content;
KotoButton *close_button;
GtkWidget *go_to_artist;
GtkWidget *playlists;
GtkWidget *play_track;
GtkWidget *remove_from_playlist;
GList *current_list;
gchar *current_album_uuid;
gchar *current_playlist_uuid;
KotoActionBarRelative relative;
};
struct _KotoActionBarClass {
GObjectClass parent_class;
void (* closed) (KotoActionBar *self);
};
G_DEFINE_TYPE(KotoActionBar, koto_action_bar, G_TYPE_OBJECT);
KotoActionBar* action_bar;
static void koto_action_bar_class_init(KotoActionBarClass *c) {
GObjectClass *gobject_class = G_OBJECT_CLASS(c);
actionbar_signals[SIGNAL_CLOSED] = g_signal_new(
"closed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoActionBarClass, closed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
0
);
}
static void koto_action_bar_init(KotoActionBar *self) {
self->main = GTK_ACTION_BAR(gtk_action_bar_new()); // Create a new action bar
self->current_list = NULL;
self->close_button = koto_button_new_with_icon(NULL, "window-close-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL);
self->go_to_artist = gtk_button_new_with_label("Go to Artist");
self->playlists = gtk_button_new_with_label("Playlists");
self->play_track = gtk_button_new_with_label("Play");
self->remove_from_playlist = gtk_button_new_with_label("Remove from Playlist");
gtk_widget_add_css_class(self->playlists, "suggested-action");
gtk_widget_add_css_class(self->play_track, "suggested-action");
gtk_widget_add_css_class(self->remove_from_playlist, "destructive-action");
gtk_widget_set_size_request(self->play_track, 160, -1);
self->center_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
self->start_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
self->stop_box_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 20);
gtk_box_prepend(GTK_BOX(self->start_box_content), GTK_WIDGET(self->go_to_artist));
gtk_box_prepend(GTK_BOX(self->center_box_content), GTK_WIDGET(self->play_track));
gtk_box_prepend(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->playlists));
gtk_box_append(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->remove_from_playlist));
gtk_box_append(GTK_BOX(self->stop_box_content), GTK_WIDGET(self->close_button));
gtk_action_bar_pack_start(self->main, self->start_box_content);
gtk_action_bar_pack_end(self->main, self->stop_box_content);
gtk_action_bar_set_center_widget(self->main, self->center_box_content);
// Hide all the widgets by default
gtk_widget_hide(GTK_WIDGET(self->go_to_artist));
gtk_widget_hide(GTK_WIDGET(self->playlists));
gtk_widget_hide(GTK_WIDGET(self->play_track));
gtk_widget_hide(GTK_WIDGET(self->remove_from_playlist));
// Set up bindings
koto_button_add_click_handler(self->close_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_action_bar_handle_close_button_clicked), self);
g_signal_connect(self->go_to_artist, "clicked", G_CALLBACK(koto_action_bar_handle_go_to_artist_button_clicked), self);
g_signal_connect(self->playlists, "clicked", G_CALLBACK(koto_action_bar_handle_playlists_button_clicked), self);
g_signal_connect(self->play_track, "clicked", G_CALLBACK(koto_action_bar_handle_play_track_button_clicked), self);
g_signal_connect(self->remove_from_playlist, "clicked", G_CALLBACK(koto_action_bar_handle_remove_from_playlist_button_clicked), self);
koto_action_bar_toggle_reveal(self, FALSE); // Hide by default
}
void koto_action_bar_close(KotoActionBar *self) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
gtk_widget_hide(GTK_WIDGET(koto_add_remove_track_popup)); // Hide the Add / Remove Track Playlists popover
koto_action_bar_toggle_reveal(self, FALSE); // Hide the action bar
g_signal_emit(
self,
actionbar_signals[SIGNAL_CLOSED],
0,
NULL
);
}
GtkActionBar* koto_action_bar_get_main(KotoActionBar *self) {
if (!KOTO_IS_ACTION_BAR(self)) {
return NULL;
}
return self->main;
}
void koto_action_bar_handle_close_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) {
(void) gesture; (void) n_press; (void) x; (void) y;
koto_action_bar_close(data);
}
void koto_action_bar_handle_go_to_artist_button_clicked(GtkButton *button, gpointer data) {
(void) button;
KotoActionBar *self = data;
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_list == NULL || (g_list_length(self->current_list) != 1)) { // Not a list or not exactly one item
return;
}
KotoIndexedTrack *selected_track = g_list_nth_data(self->current_list, 0); // Get the first item
if (!KOTO_IS_INDEXED_TRACK(selected_track)) { // Not a track
return;
}
gchar *artist_uuid = NULL;
g_object_get(
selected_track,
"artist-uuid",
&artist_uuid,
NULL
);
koto_page_music_local_go_to_artist_by_uuid(music_local_page, artist_uuid); // Go to the artist
koto_window_go_to_page(main_window, "music.local"); // Navigate to the local music stack so we can see the substack page
koto_action_bar_close(self); // Close the action bar
}
void koto_action_bar_handle_playlists_button_clicked(GtkButton *button, gpointer data) {
(void) button;
KotoActionBar *self = data;
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_list == NULL || (g_list_length(self->current_list) == 0)) { // Not a list or no content
return;
}
koto_add_remove_track_popover_set_tracks(koto_add_remove_track_popup, self->current_list); // Set the popover tracks
koto_add_remove_track_popover_set_pointing_to_widget(koto_add_remove_track_popup, GTK_WIDGET(self->playlists), GTK_POS_TOP); // Show the popover above the button
gtk_widget_show(GTK_WIDGET(koto_add_remove_track_popup));
}
void koto_action_bar_handle_play_track_button_clicked(GtkButton *button, gpointer data) {
(void) button;
KotoActionBar *self = data;
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_list == NULL || (g_list_length(self->current_list) != 1)) { // Not a list or not exactly 1 item selected
goto doclose;
}
KotoIndexedTrack *track = g_list_nth_data(self->current_list, 0); // Get the first track
if (!KOTO_IS_INDEXED_TRACK(track)) { // Not a track
goto doclose;
}
koto_playback_engine_set_track_by_uuid(playback_engine, koto_indexed_track_get_uuid(track)); // Set the track to play
doclose:
koto_action_bar_close(self);
}
void koto_action_bar_handle_remove_from_playlist_button_clicked(GtkButton *button, gpointer data) {
(void) button;
KotoActionBar *self = data;
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_list == NULL || (g_list_length(self->current_list) == 0)) { // Not a list or no content
goto doclose;
}
if (self->current_playlist_uuid == NULL || (g_strcmp0(self->current_playlist_uuid, "") == 0)) { // Not valid UUID
goto doclose;
}
KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->current_playlist_uuid);
if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist
goto doclose;
}
GList *cur_list;
for (cur_list = self->current_list; cur_list != NULL; cur_list = cur_list->next) { // For each KotoIndexedTrack
KotoIndexedTrack *track = cur_list->data;
koto_playlist_remove_track_by_uuid(playlist, koto_indexed_track_get_uuid(track)); // Remove this track
}
doclose:
koto_action_bar_close(self);
}
void koto_action_bar_set_tracks_in_album_selection(KotoActionBar *self, gchar *album_uuid, GList *tracks) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_album_uuid != NULL && (g_strcmp0(self->current_album_uuid, "") != 0)) { // Album UUID currently set
g_free(self->current_album_uuid);
}
if (self->current_playlist_uuid != NULL && (g_strcmp0(self->current_playlist_uuid, "") != 0)) { // Playlist UUID currently set
g_free(self->current_playlist_uuid);
}
self->current_playlist_uuid = NULL;
self->current_album_uuid = g_strdup(album_uuid);
self->relative = KOTO_ACTION_BAR_IS_ALBUM_RELATIVE;
g_list_free(self->current_list);
self->current_list = g_list_copy(tracks);
koto_add_remove_track_popover_clear_tracks(koto_add_remove_track_popup); // Clear the current popover contents
koto_add_remove_track_popover_set_tracks(koto_add_remove_track_popup, self->current_list); // Set the associated tracks to remove
koto_action_bar_toggle_go_to_artist_visibility(self, FALSE);
koto_action_bar_toggle_play_button_visibility(self, g_list_length(self->current_list) == 1);
gtk_widget_show(GTK_WIDGET(self->playlists));
gtk_widget_hide(GTK_WIDGET(self->remove_from_playlist));
}
void koto_action_bar_set_tracks_in_playlist_selection(KotoActionBar *self, gchar *playlist_uuid, GList *tracks) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
if (self->current_album_uuid != NULL && (g_strcmp0(self->current_album_uuid, "") != 0)) { // Album UUID currently set
g_free(self->current_album_uuid);
}
if (self->current_playlist_uuid != NULL && (g_strcmp0(self->current_playlist_uuid, "") != 0)) { // Playlist UUID currently set
g_free(self->current_playlist_uuid);
}
self->current_album_uuid = NULL;
self->current_playlist_uuid = g_strdup(playlist_uuid);
self->relative = KOTO_ACTION_BAR_IS_PLAYLIST_RELATIVE;
g_list_free(self->current_list);
self->current_list = g_list_copy(tracks);
gboolean single_selected = g_list_length(tracks) == 1;
koto_action_bar_toggle_go_to_artist_visibility(self, single_selected);
koto_action_bar_toggle_play_button_visibility(self, single_selected);
gtk_widget_hide(GTK_WIDGET(self->playlists));
gtk_widget_show(GTK_WIDGET(self->remove_from_playlist));
}
void koto_action_bar_toggle_go_to_artist_visibility(KotoActionBar *self, gboolean visible) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
(visible) ? gtk_widget_show(GTK_WIDGET(self->go_to_artist)) : gtk_widget_hide(GTK_WIDGET(self->go_to_artist));
}
void koto_action_bar_toggle_play_button_visibility(KotoActionBar *self, gboolean visible) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
(visible) ? gtk_widget_show(GTK_WIDGET(self->play_track)) : gtk_widget_hide(GTK_WIDGET(self->play_track));
}
void koto_action_bar_toggle_reveal(KotoActionBar *self, gboolean state) {
if (!KOTO_IS_ACTION_BAR(self)) {
return;
}
gtk_action_bar_set_revealed(self->main, state);
}
KotoActionBar* koto_action_bar_new() {
return g_object_new(
KOTO_TYPE_ACTION_BAR,
NULL
);
}

View file

@ -0,0 +1,58 @@
/* koto-action-bar.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.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../indexer/structs.h"
G_BEGIN_DECLS
typedef enum {
KOTO_ACTION_BAR_IS_ALBUM_RELATIVE = 1,
KOTO_ACTION_BAR_IS_PLAYLIST_RELATIVE = 2
} KotoActionBarRelative;
#define KOTO_TYPE_ACTION_BAR (koto_action_bar_get_type())
#define KOTO_IS_ACTION_BAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ACTION_BAR))
typedef struct _KotoActionBar KotoActionBar;
typedef struct _KotoActionBarClass KotoActionBarClass;
GLIB_AVAILABLE_IN_ALL
GType koto_action_bar_get_type(void) G_GNUC_CONST;
/**
* Action Bar Functions
**/
KotoActionBar* koto_action_bar_new(void);
void koto_action_bar_close(KotoActionBar *self);
GtkActionBar* koto_action_bar_get_main(KotoActionBar *self);
void koto_action_bar_handle_close_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);
void koto_action_bar_handle_go_to_artist_button_clicked(GtkButton *button, gpointer data);
void koto_action_bar_handle_playlists_button_clicked(GtkButton *button, gpointer data);
void koto_action_bar_handle_play_track_button_clicked(GtkButton *button, gpointer data);
void koto_action_bar_handle_remove_from_playlist_button_clicked(GtkButton *button, gpointer data);
void koto_action_bar_set_tracks_in_album_selection(KotoActionBar *self, gchar *album_uuid, GList *tracks);
void koto_action_bar_set_tracks_in_playlist_selection(KotoActionBar *self, gchar *playlist_uuid, GList *tracks);
void koto_action_bar_toggle_go_to_artist_visibility(KotoActionBar *self, gboolean visible);
void koto_action_bar_toggle_play_button_visibility(KotoActionBar *self, gboolean visible);
void koto_action_bar_toggle_reveal(KotoActionBar *self, gboolean state);
G_END_DECLS

View file

@ -0,0 +1,217 @@
/* koto-cover-art-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 <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "koto-cover-art-button.h"
#include "../koto-button.h"
#include "../koto-utils.h"
struct _KotoCoverArtButton {
GObject parent_instance;
GtkWidget *art;
GtkWidget *main;
GtkWidget *revealer;
KotoButton *play_pause_button;
guint height;
guint width;
};
G_DEFINE_TYPE(KotoCoverArtButton, koto_cover_art_button, G_TYPE_OBJECT);
enum {
PROP_0,
PROP_DESIRED_HEIGHT,
PROP_DESIRED_WIDTH,
PROP_ART_PATH,
N_PROPERTIES
};
static GParamSpec *props[N_PROPERTIES] = { NULL, };
static void koto_cover_art_button_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec);
static void koto_cover_art_button_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec);
static void koto_cover_art_button_class_init(KotoCoverArtButtonClass *c) {
GObjectClass *gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->get_property = koto_cover_art_button_get_property;
gobject_class->set_property = koto_cover_art_button_set_property;
props[PROP_DESIRED_HEIGHT] = g_param_spec_uint(
"desired-height",
"Desired height",
"Desired height",
0,
G_MAXUINT,
0,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_WRITABLE
);
props[PROP_DESIRED_WIDTH] = g_param_spec_uint(
"desired-width",
"Desired width",
"Desired width",
0,
G_MAXUINT,
0,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_WRITABLE
);
props[PROP_ART_PATH] = g_param_spec_string(
"art-path",
"Path to art",
"Path to art",
NULL,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_WRITABLE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_cover_art_button_init(KotoCoverArtButton *self) {
self->main = gtk_overlay_new(); // Create our overlay container
gtk_widget_add_css_class(self->main, "cover-art-button");
self->revealer = gtk_revealer_new(); // Create a new revealer
gtk_revealer_set_transition_type(GTK_REVEALER(self->revealer), GTK_REVEALER_TRANSITION_TYPE_CROSSFADE);
gtk_revealer_set_transition_duration(GTK_REVEALER(self->revealer), 400);
GtkWidget *controls = gtk_center_box_new(); // Create a center box for the controls
self->play_pause_button = koto_button_new_with_icon("", "media-playback-start-symbolic", "media-playback-pause-symbolic", KOTO_BUTTON_PIXBUF_SIZE_NORMAL);
gtk_center_box_set_center_widget(GTK_CENTER_BOX(controls), GTK_WIDGET(self->play_pause_button));
gtk_revealer_set_child(GTK_REVEALER(self->revealer), controls);
koto_cover_art_button_hide_overlay_controls(NULL, self); // Hide by default
gtk_overlay_add_overlay(GTK_OVERLAY(self->main), self->revealer); // Add our revealer as the overlay
GtkEventController *motion_controller = gtk_event_controller_motion_new(); // Create our new motion event controller to track mouse leave and enter
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);
}
static void koto_cover_art_button_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) {
(void) val;
switch (prop_id) {
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_cover_art_button_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) {
KotoCoverArtButton *self = KOTO_COVER_ART_BUTTON(obj);
switch (prop_id) {
case PROP_ART_PATH:
koto_cover_art_button_set_art_path(self, g_value_get_string(val)); // Get the value and call our set_art_path with it
break;
case PROP_DESIRED_HEIGHT:
koto_cover_art_button_set_dimensions(self, g_value_get_uint(val), 0);
break;
case PROP_DESIRED_WIDTH:
koto_cover_art_button_set_dimensions(self, 0, g_value_get_uint(val));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
void koto_cover_art_button_hide_overlay_controls(GtkEventControllerFocus *controller, gpointer data) {
(void) controller;
KotoCoverArtButton* self = data;
gtk_revealer_set_reveal_child(GTK_REVEALER(self->revealer), FALSE);
}
KotoButton* koto_cover_art_button_get_button(KotoCoverArtButton *self) {
if (!KOTO_IS_COVER_ART_BUTTON(self)) {
return NULL;
}
return self->play_pause_button;
}
GtkWidget* koto_cover_art_button_get_main(KotoCoverArtButton *self) {
if (!KOTO_IS_COVER_ART_BUTTON(self)) {
return NULL;
}
return self->main;
}
void koto_cover_art_button_set_art_path(KotoCoverArtButton *self, const gchar *art_path) {
if (!KOTO_IS_COVER_ART_BUTTON(self)) {
return;
}
gboolean defined_artwork = (art_path != NULL || (g_strcmp0(art_path, "") != 0));
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
}
} 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);
gtk_overlay_set_child(GTK_OVERLAY(self->main), self->art); // Set the child
}
}
void koto_cover_art_button_set_dimensions(KotoCoverArtButton *self, guint height, guint width) {
if (!KOTO_IS_COVER_ART_BUTTON(self)) {
return;
}
if (height != 0) {
self->height = height;
}
if (width != 0) {
self->width = width;
}
if ((self->height != 0) && (self->width != 0)) { // Both height and width set
gtk_widget_set_size_request(self->main, self->width, self->height); // Update our widget
if (GTK_IS_IMAGE(self->art)) { // Art is defined
gtk_widget_set_size_request(self->art, self->width, self->height); // Update our image as well
}
}
}
void koto_cover_art_button_show_overlay_controls(GtkEventControllerFocus *controller, gpointer data) {
(void) controller;
KotoCoverArtButton* self = data;
gtk_revealer_set_reveal_child(GTK_REVEALER(self->revealer), TRUE);
}
KotoCoverArtButton* koto_cover_art_button_new(guint height, guint width, const gchar *art_path) {
return g_object_new(KOTO_TYPE_COVER_ART_BUTTON,
"desired-height",
height,
"desired-width",
width,
"art-path",
art_path,
NULL
);
}

View file

@ -0,0 +1,40 @@
/* koto-cover-art-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 <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../koto-button.h"
G_BEGIN_DECLS
#define KOTO_TYPE_COVER_ART_BUTTON (koto_cover_art_button_get_type())
G_DECLARE_FINAL_TYPE(KotoCoverArtButton, koto_cover_art_button, KOTO, COVER_ART_BUTTON, GObject);
#define KOTO_IS_COVER_ART_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_COVER_ART_BUTTON))
/**
* Cover Art Functions
**/
KotoCoverArtButton* koto_cover_art_button_new(guint height, guint width, const gchar *art_path);
KotoButton* koto_cover_art_button_get_button(KotoCoverArtButton *self);
GtkWidget* koto_cover_art_button_get_main(KotoCoverArtButton *self);
void koto_cover_art_button_hide_overlay_controls(GtkEventControllerFocus *controller, gpointer data);
void koto_cover_art_button_set_art_path(KotoCoverArtButton *self, const gchar *art_path);
void koto_cover_art_button_set_dimensions(KotoCoverArtButton *self, guint height, guint width);
void koto_cover_art_button_show_overlay_controls(GtkEventControllerFocus *controller, gpointer data);
G_END_DECLS

View file

@ -18,6 +18,20 @@
#include <glib-2.0/glib.h>
#include "cartographer.h"
enum {
SIGNAL_ALBUM_ADDED,
SIGNAL_ALBUM_REMOVED,
SIGNAL_ARTIST_ADDED,
SIGNAL_ARTIST_REMOVED,
SIGNAL_PLAYLIST_ADDED,
SIGNAL_PLAYLIST_REMOVED,
SIGNAL_TRACK_ADDED,
SIGNAL_TRACK_REMOVED,
N_SIGNALS
};
static guint cartographer_signals[N_SIGNALS] = { 0 };
struct _KotoCartographer {
GObject parent_instance;
@ -27,12 +41,130 @@ struct _KotoCartographer {
GHashTable *tracks;
};
struct _KotoCartographerClass {
GObjectClass parent_class;
void (* album_added) (KotoCartographer *cartographer, KotoIndexedAlbum *album);
void (* album_removed) (KotoCartographer *cartographer, KotoIndexedAlbum *album);
void (* artist_added) (KotoCartographer *cartographer, KotoIndexedArtist *artist);
void (* artist_removed) (KotoCartographer *cartographer, KotoIndexedArtist *artist);
void (* playlist_added) (KotoCartographer *cartographer, KotoPlaylist *playlist);
void (* playlist_removed) (KotoCartographer *cartographer, KotoPlaylist *playlist);
void (* track_added) (KotoCartographer *cartographer, KotoIndexedTrack *track);
void (* track_removed) (KotoCartographer *cartographer, KotoIndexedTrack *track);
};
G_DEFINE_TYPE(KotoCartographer, koto_cartographer, G_TYPE_OBJECT);
KotoCartographer *koto_maps = NULL;
static void koto_cartographer_class_init(KotoCartographerClass *c) {
(void) c;
GObjectClass *gobject_class;
gobject_class = G_OBJECT_CLASS(c);
cartographer_signals[SIGNAL_ALBUM_ADDED] = g_signal_new(
"album-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, album_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_INDEXED_ALBUM
);
cartographer_signals[SIGNAL_ALBUM_REMOVED] = g_signal_new(
"album-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, album_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
cartographer_signals[SIGNAL_ARTIST_ADDED] = g_signal_new(
"artist-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, artist_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_INDEXED_ARTIST
);
cartographer_signals[SIGNAL_ARTIST_REMOVED] = g_signal_new(
"artist-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, artist_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
cartographer_signals[SIGNAL_PLAYLIST_ADDED] = g_signal_new(
"playlist-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, playlist_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_PLAYLIST
);
cartographer_signals[SIGNAL_PLAYLIST_REMOVED] = g_signal_new(
"playlist-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, playlist_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
cartographer_signals[SIGNAL_TRACK_ADDED] = g_signal_new(
"track-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, track_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
KOTO_TYPE_INDEXED_TRACK
);
cartographer_signals[SIGNAL_TRACK_REMOVED] = g_signal_new(
"track-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoCartographerClass, track_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
}
static void koto_cartographer_init(KotoCartographer *self) {
@ -46,36 +178,80 @@ void koto_cartographer_add_album(KotoCartographer *self, KotoIndexedAlbum *album
gchar *album_uuid = NULL;
g_object_get(album, "uuid", &album_uuid, NULL);
if ((album_uuid != NULL) && (!koto_cartographer_has_album_by_uuid(self, album_uuid))) { // Don't have this album
g_hash_table_replace(self->albums, album_uuid, album);
if ((album_uuid == NULL) || koto_cartographer_has_album_by_uuid(self, album_uuid)) { // Have the album or invalid UUID
return;
}
g_hash_table_replace(self->albums, album_uuid, album);
g_signal_emit(
self,
cartographer_signals[SIGNAL_ALBUM_ADDED],
0,
album
);
}
void koto_cartographer_add_artist(KotoCartographer *self, KotoIndexedArtist *artist) {
gchar *artist_uuid = NULL;
g_object_get(artist, "uuid", &artist_uuid, NULL);
if ((artist_uuid != NULL) && (!koto_cartographer_has_artist_by_uuid(self, artist_uuid))) { // Don't have this album
g_hash_table_replace(self->artists, artist_uuid, artist);
if ((artist_uuid == NULL) || koto_cartographer_has_artist_by_uuid(self, artist_uuid)) { // Have the artist or invalid UUID
return;
}
g_hash_table_replace(self->artists, artist_uuid, artist);
g_signal_emit(
self,
cartographer_signals[SIGNAL_ARTIST_ADDED],
0,
artist
);
}
void koto_cartographer_add_playlist(KotoCartographer *self, KotoPlaylist *playlist) {
gchar *playlist_uuid = NULL;
g_object_get(playlist, "uuid", &playlist_uuid, NULL);
if ((playlist_uuid != NULL) && (!koto_cartographer_has_playlist_by_uuid(self, playlist_uuid))) { // Don't have this album
g_hash_table_replace(self->playlists, playlist_uuid, playlist);
if ((playlist_uuid == NULL) || koto_cartographer_has_playlist_by_uuid(self, playlist_uuid)) { // Have the playlist or invalid UUID
return;
}
g_hash_table_replace(self->playlists, playlist_uuid, playlist);
if (koto_playlist_get_is_finalized(playlist)) { // Already finalized
koto_cartographer_emit_playlist_added(playlist, self); // Emit playlist-added immediately
} else { // Not finalized
g_signal_connect(playlist, "track-load-finalized", G_CALLBACK(koto_cartographer_emit_playlist_added), self);
}
}
void koto_cartographer_emit_playlist_added(KotoPlaylist *playlist, KotoCartographer *self) {
g_signal_emit(
self,
cartographer_signals[SIGNAL_PLAYLIST_ADDED],
0,
playlist
);
}
void koto_cartographer_add_track(KotoCartographer *self, KotoIndexedTrack *track) {
gchar *track_uuid = NULL;
g_object_get(track, "uuid", &track_uuid, NULL);
if ((track_uuid != NULL) && (!koto_cartographer_has_playlist_by_uuid(self, track_uuid))) { // Don't have this album
g_hash_table_replace(self->tracks, track_uuid, track);
if ((track_uuid == NULL) || koto_cartographer_has_track_by_uuid(self, track_uuid)) { // Have the track or invalid UUID
return;
}
g_hash_table_replace(self->tracks, track_uuid, track);
g_signal_emit(
self,
cartographer_signals[SIGNAL_TRACK_ADDED],
0,
track
);
}
KotoIndexedAlbum* koto_cartographer_get_album_by_uuid(KotoCartographer *self, gchar* album_uuid) {
@ -86,6 +262,10 @@ KotoIndexedArtist* koto_cartographer_get_artist_by_uuid(KotoCartographer *self,
return g_hash_table_lookup(self->artists, artist_uuid);
}
GHashTable* koto_cartographer_get_playlists(KotoCartographer *self) {
return self->playlists;
}
KotoPlaylist* koto_cartographer_get_playlist_by_uuid(KotoCartographer *self, gchar* playlist_uuid) {
return g_hash_table_lookup(self->playlists, playlist_uuid);
}
@ -153,57 +333,77 @@ gboolean koto_cartographer_has_track_by_uuid(KotoCartographer *self, gchar* trac
void koto_cartographer_remove_album(KotoCartographer *self, KotoIndexedAlbum *album) {
gchar *album_uuid = NULL;
g_object_get(album, "uuid", &album_uuid, NULL);
return koto_cartographer_remove_album_by_uuid(self, album_uuid);
koto_cartographer_remove_album_by_uuid(self, album_uuid);
}
void koto_cartographer_remove_album_by_uuid(KotoCartographer *self, gchar* album_uuid) {
if (album_uuid != NULL) {
g_hash_table_remove(self->albums, album_uuid);
}
return;
g_signal_emit(
self,
cartographer_signals[SIGNAL_ALBUM_REMOVED],
0,
album_uuid
);
}
}
void koto_cartographer_remove_artist(KotoCartographer *self, KotoIndexedArtist *artist) {
gchar *artist_uuid = NULL;
g_object_get(artist, "uuid", &artist_uuid, NULL);
return koto_cartographer_remove_artist_by_uuid(self, artist_uuid);
koto_cartographer_remove_artist_by_uuid(self, artist_uuid);
}
void koto_cartographer_remove_artist_by_uuid(KotoCartographer *self, gchar* artist_uuid) {
if (artist_uuid == NULL) {
g_hash_table_remove(self->artists, artist_uuid);
}
return;
g_signal_emit(
self,
cartographer_signals[SIGNAL_ARTIST_REMOVED],
0,
artist_uuid
);
}
}
void koto_cartographer_remove_playlist(KotoCartographer *self, KotoPlaylist *playlist) {
gchar *playlist_uuid = NULL;
g_object_get(playlist, "uuid", &playlist_uuid, NULL);
return koto_cartographer_remove_playlist_by_uuid(self, playlist_uuid);
koto_cartographer_remove_playlist_by_uuid(self, playlist_uuid);
}
void koto_cartographer_remove_playlist_by_uuid(KotoCartographer *self, gchar* playlist_uuid) {
if (playlist_uuid != NULL) {
g_hash_table_remove(self->playlists, playlist_uuid);
}
return;
g_signal_emit(
self,
cartographer_signals[SIGNAL_PLAYLIST_REMOVED],
0,
playlist_uuid
);
}
}
void koto_cartographer_remove_track(KotoCartographer *self, KotoIndexedTrack *track) {
gchar *track_uuid = NULL;
g_object_get(track, "uuid", &track_uuid, NULL);
return koto_cartographer_remove_track_by_uuid(self, track_uuid);
koto_cartographer_remove_track_by_uuid(self, track_uuid);
}
void koto_cartographer_remove_track_by_uuid(KotoCartographer *self, gchar* track_uuid) {
if (track_uuid != NULL) {
g_hash_table_remove(self->tracks, track_uuid);
}
return;
g_signal_emit(
self,
cartographer_signals[SIGNAL_TRACK_REMOVED],
0,
track_uuid
);
}
}
KotoCartographer* koto_cartographer_new() {

View file

@ -27,7 +27,12 @@ G_BEGIN_DECLS
**/
#define KOTO_TYPE_CARTOGRAPHER koto_cartographer_get_type()
G_DECLARE_FINAL_TYPE(KotoCartographer, koto_cartographer, KOTO, CARTOGRAPHER, GObject);
typedef struct _KotoCartographer KotoCartographer;
typedef struct _KotoCartographerClass KotoCartographerClass;
GLIB_AVAILABLE_IN_ALL
GType koto_cartographer_get_type(void) G_GNUC_CONST;
/**
* Cartographer Functions
@ -40,9 +45,12 @@ void koto_cartographer_add_artist(KotoCartographer *self, KotoIndexedArtist *art
void koto_cartographer_add_playlist(KotoCartographer *self, KotoPlaylist *playlist);
void koto_cartographer_add_track(KotoCartographer *self, KotoIndexedTrack *track);
void koto_cartographer_emit_playlist_added(KotoPlaylist *playlist, KotoCartographer *self);
KotoIndexedAlbum* koto_cartographer_get_album_by_uuid(KotoCartographer *self, gchar* album_uuid);
KotoIndexedArtist* koto_cartographer_get_artist_by_uuid(KotoCartographer *self, gchar* artist_uuid);
KotoPlaylist* koto_cartographer_get_playlist_by_uuid(KotoCartographer *self, gchar* playlist_uuid);
GHashTable* koto_cartographer_get_playlists(KotoCartographer *self);
KotoIndexedTrack* koto_cartographer_get_track_by_uuid(KotoCartographer *self, gchar* track_uuid);
gboolean koto_cartographer_has_album(KotoCartographer *self, KotoIndexedAlbum *album);

View file

@ -38,8 +38,8 @@ int create_db_tables() {
char *tables_creation_queries = "CREATE TABLE IF NOT EXISTS artists(id string UNIQUE PRIMARY KEY, path string, type int, name string, art_path string);"
"CREATE TABLE IF NOT EXISTS albums(id string UNIQUE PRIMARY KEY, path string, artist_id string, name string, art_path string, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);"
"CREATE TABLE IF NOT EXISTS tracks(id string UNIQUE PRIMARY KEY, path string, type int, artist_id string, album_id string, file_name string, name string, disc int, position int, FOREIGN KEY(artist_id) REFERENCES artists(id) ON DELETE CASCADE);"
"CREATE TABLE IF NOT EXISTS playlist_meta(id string UNIQUE PRIMARY KEY, name string, art_path string);"
"CREATE TABLE IF NOT EXISTS playlist_tracks(playlist_id string PRIMARY KEY, track_id string, position int, 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);"
"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);";
gchar *create_tables_errmsg = NULL;
int rc = sqlite3_exec(koto_db, tables_creation_queries, 0,0, &create_tables_errmsg);

View file

@ -373,14 +373,46 @@ static void koto_indexed_album_set_property(GObject *obj, guint prop_id, const G
}
gchar* koto_indexed_album_get_album_art(KotoIndexedAlbum *self) {
return g_strdup((self->has_album_art && (self->art_path != NULL) && (strcmp(self->art_path, "") != 0)) ? self->art_path : "");
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return g_strdup("");
}
return g_strdup((self->has_album_art && (self->art_path != NULL) && (g_strcmp0(self->art_path, "") != 0)) ? self->art_path : "");
}
gchar *koto_indexed_album_get_album_name(KotoIndexedAlbum *self) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return NULL;
}
if ((self->name == NULL) || g_strcmp0(self->name, "") == 0) { // Not set
return NULL;
}
return g_strdup(self->name); // Return duplicate of the name
}
gchar* koto_indexed_album_get_album_uuid(KotoIndexedAlbum *self) {
if ((self->uuid == NULL) || g_strcmp0(self->uuid, "") == 0) { // Not set
return NULL;
}
return g_strdup(self->uuid); // Return a duplicate of the UUID
}
GList* koto_indexed_album_get_tracks(KotoIndexedAlbum *self) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return NULL;
}
return self->tracks; // Return
}
void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album_art) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return;
}
if (album_art == NULL) { // Not valid album art
return;
}
@ -395,6 +427,10 @@ void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album
}
void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *track) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return;
}
if (track == NULL) { // Not a file
return;
}
@ -405,6 +441,10 @@ void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *tr
}
void koto_indexed_album_set_album_name(KotoIndexedAlbum *self, const gchar *album_name) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return;
}
if (album_name == NULL) { // Not valid album name
return;
}
@ -417,6 +457,10 @@ void koto_indexed_album_set_album_name(KotoIndexedAlbum *self, const gchar *albu
}
void koto_indexed_album_set_artist_uuid(KotoIndexedAlbum *self, const gchar *artist_uuid) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return;
}
if (artist_uuid == NULL) {
return;
}
@ -429,6 +473,10 @@ void koto_indexed_album_set_artist_uuid(KotoIndexedAlbum *self, const gchar *art
}
void koto_indexed_album_set_as_current_playlist(KotoIndexedAlbum *self) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return;
}
if (self->tracks == NULL) { // No files to add to the playlist
return;
}
@ -436,11 +484,23 @@ void koto_indexed_album_set_as_current_playlist(KotoIndexedAlbum *self) {
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 = self->tracks; t != NULL; t = t->next) { // For each of the tracks
koto_playlist_add_track_by_uuid(new_album_playlist, (gchar*) t->data); // Add the UUID
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
koto_current_playlist_set_playlist(current_playlist, new_album_playlist); // Set our new current playlist
}
@ -491,16 +551,19 @@ gint koto_indexed_album_sort_tracks(gconstpointer track1_uuid, gconstpointer tra
}
void koto_indexed_album_update_path(KotoIndexedAlbum *self, const gchar* new_path) {
if (new_path == NULL) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return;
}
if (self->path != NULL) {
if ((new_path == NULL) || g_strcmp0(new_path, "") == 0) {
return;
}
if ((self->path != NULL) && g_strcmp0(self->path, "") != 0) {
g_free(self->path);
}
self->path = g_strdup(new_path);
koto_indexed_album_set_album_name(self, g_path_get_basename(self->path)); // Update our album name based on the base name
if (!self->do_initial_index) { // Not doing our initial index

View file

@ -151,7 +151,11 @@ static void koto_indexed_artist_set_property(GObject *obj, guint prop_id, const
}
void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid) {
if ((album_uuid == NULL) || (strcmp(album_uuid, "") == 0)) { // No album UUID really defined
if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist
return;
}
if ((album_uuid == NULL) || g_strcmp0(album_uuid, "") == 0) { // No album UUID really defined
return;
}
@ -163,11 +167,27 @@ void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid) {
}
GList* koto_indexed_artist_get_albums(KotoIndexedArtist *self) {
if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist
return NULL;
}
return g_list_copy(self->albums);
}
gchar* koto_indexed_artist_get_name(KotoIndexedArtist *self) {
if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist
return g_strdup("");
}
return g_strdup(g_strcmp0(self->artist_name, "") == 0 ? "" : self->artist_name); // Return artist name if set
}
void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum *album) {
if (album == NULL) { // No album defined
if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist
return;
}
if (!KOTO_INDEXED_ALBUM(album)) { // No album defined
return;
}
@ -177,11 +197,15 @@ void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum
}
void koto_indexed_artist_update_path(KotoIndexedArtist *self, const gchar *new_path) {
if (new_path == NULL) { // No path really
if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist
return;
}
if (self->path != NULL) { // Already have a path set
if ((new_path == NULL) || g_strcmp0(new_path, "") == 0) { // No path really
return;
}
if ((self->path != NULL) && g_strcmp0(self->path, "") != 0) { // Already have a path set
g_free(self->path); // Free
}
@ -190,11 +214,15 @@ void koto_indexed_artist_update_path(KotoIndexedArtist *self, const gchar *new_p
}
void koto_indexed_artist_set_artist_name(KotoIndexedArtist *self, const gchar *artist_name) {
if (artist_name == NULL) { // No artist name
if (!KOTO_IS_INDEXED_ARTIST(self)) { // Not an artist
return;
}
if (self->artist_name != NULL) { // Has artist name
if ((artist_name == NULL) || g_strcmp0(artist_name, "") == 0) { // No artist name
return;
}
if ((self->artist_name != NULL) && g_strcmp0(self->artist_name, "") != 0) { // Has artist name
g_free(self->artist_name);
}

View file

@ -22,6 +22,7 @@
#include <taglib/tag_c.h>
#include "../db/cartographer.h"
#include "../db/db.h"
#include "../playlist/playlist.h"
#include "../koto-utils.h"
#include "structs.h"
@ -242,6 +243,57 @@ int process_albums(void *data, int num_columns, char **fields, char **column_nam
return 0;
}
int process_playlists(void *data, int num_columns, char **fields, char **column_names) {
(void) data; (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
KotoPlaylist *playlist = koto_playlist_new_with_uuid(playlist_uuid); // Create a playlist using the existing UUID
koto_playlist_set_name(playlist, playlist_name); // Add the playlist name
koto_playlist_set_artwork(playlist, playlist_art_path); // Add the playlist art path
koto_cartographer_add_playlist(koto_maps, playlist); // Add to cartographer
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));
return 1;
}
koto_playlist_mark_as_finalized(playlist); // Mark as finalized since loading should be complete
g_free(playlist_uuid);
g_free(playlist_name);
g_free(playlist_art_path);
return 0;
}
int process_playlists_tracks(void *data, int num_columns, char **fields, char **column_names) {
(void) data; (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");
KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid); // Get the playlist
KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid); // Get the track
if (!KOTO_IS_PLAYLIST(playlist)) {
goto freeforret;
}
koto_playlist_add_track(playlist, track, current, FALSE); // Add the track to the playlist but don't re-commit to the table
freeforret:
g_free(playlist_uuid);
g_free(track_uuid);
return 0;
}
int process_tracks(void *data, int num_columns, char **fields, char **column_names) {
(void) num_columns; (void) column_names; // Don't need these
@ -287,6 +339,12 @@ void read_from_db(KotoIndexedLibrary *self) {
}
g_hash_table_foreach(self->music_artists, output_artists, NULL);
int playlist_rc = sqlite3_exec(koto_db, "SELECT * FROM playlist_meta", process_playlists, self, NULL); // Process our playlists
if (playlist_rc != SQLITE_OK) { // Failed to get our playlists
g_critical("Failed to read our playlists: %s", sqlite3_errmsg(koto_db));
return;
}
}
void start_indexing(KotoIndexedLibrary *self) {
@ -362,7 +420,6 @@ void index_folder(KotoIndexedLibrary *self, gchar *path, guint depth) {
KotoIndexedArtist *artist = koto_indexed_library_get_artist(self, artist_name); // Get the artist
if (artist == NULL) {
g_message("Failed to get artist by name of: %s", artist_name);
continue;
}
@ -422,7 +479,6 @@ void output_artists(gpointer artist_key, gpointer artist_ptr, gpointer data) {
void output_track(gpointer data, gpointer user_data) {
(void) user_data;
g_message("Track UUID: %s", g_strdup(data));
KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) data);
if (track == NULL) {

View file

@ -27,12 +27,15 @@ G_BEGIN_DECLS
#define KOTO_TYPE_INDEXED_LIBRARY koto_indexed_library_get_type()
G_DECLARE_FINAL_TYPE(KotoIndexedLibrary, koto_indexed_library, KOTO, INDEXED_LIBRARY, GObject);
#define KOTO_IS_INDEXED_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_INDEXED_LIBRARY))
#define KOTO_TYPE_INDEXED_ARTIST koto_indexed_artist_get_type()
G_DECLARE_FINAL_TYPE (KotoIndexedArtist, koto_indexed_artist, KOTO, INDEXED_ARTIST, GObject);
#define KOTO_IS_INDEXED_ARTIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_INDEXED_ARTIST))
#define KOTO_TYPE_INDEXED_ALBUM koto_indexed_album_get_type()
G_DECLARE_FINAL_TYPE (KotoIndexedAlbum, koto_indexed_album, KOTO, INDEXED_ALBUM, GObject);
#define KOTO_IS_INDEXED_ALBUM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_INDEXED_ALBUM))
#define KOTO_TYPE_INDEXED_TRACK koto_indexed_track_get_type()
G_DECLARE_FINAL_TYPE(KotoIndexedTrack, koto_indexed_track, KOTO, INDEXED_TRACK, GObject);
@ -51,6 +54,8 @@ void koto_indexed_library_remove_artist(KotoIndexedLibrary *self, KotoIndexedArt
void koto_indexed_library_set_path(KotoIndexedLibrary *self, gchar *path);
int process_artists(void *data, int num_columns, char **fields, char **column_names);
int process_albums(void *data, int num_columns, char **fields, char **column_names);
int process_playlists(void *data, int num_columns, char **fields, char **column_names);
int process_playlists_tracks(void *data, int num_columns, char **fields, char **column_names);
int process_tracks(void *data, int num_columns, char **fields, char **column_names);
void read_from_db(KotoIndexedLibrary *self);
void start_indexing(KotoIndexedLibrary *self);
@ -68,6 +73,7 @@ void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid);
void koto_indexed_artist_commit(KotoIndexedArtist *self);
guint koto_indexed_artist_find_album_with_name(gconstpointer *album_data, gconstpointer *album_name_data);
GList* koto_indexed_artist_get_albums(KotoIndexedArtist *self);
gchar* koto_indexed_artist_get_name(KotoIndexedArtist *self);
void koto_indexed_artist_remove_album(KotoIndexedArtist *self, KotoIndexedAlbum *album);
void koto_indexed_artist_remove_album_by_name(KotoIndexedArtist *self, gchar *album_name);
void koto_indexed_artist_set_artist_name(KotoIndexedArtist *self, const gchar *artist_name);
@ -86,6 +92,8 @@ void koto_indexed_album_commit(KotoIndexedAlbum *self);
void koto_indexed_album_find_album_art(KotoIndexedAlbum *self);
void koto_indexed_album_find_tracks(KotoIndexedAlbum *self, magic_t magic_cookie, const gchar *path);
gchar* koto_indexed_album_get_album_art(KotoIndexedAlbum *self);
gchar* koto_indexed_album_get_album_name(KotoIndexedAlbum *self);
gchar* koto_indexed_album_get_album_uuid(KotoIndexedAlbum *self);
GList* koto_indexed_album_get_tracks(KotoIndexedAlbum *self);
void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *track);
void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album_art);
@ -103,8 +111,10 @@ KotoIndexedTrack* koto_indexed_track_new(KotoIndexedAlbum *album, const gchar *p
KotoIndexedTrack* koto_indexed_track_new_with_uuid(const gchar *uuid);
void koto_indexed_track_commit(KotoIndexedTrack *self);
gchar* koto_indexed_track_get_uuid(KotoIndexedTrack *self);
void koto_indexed_track_parse_name(KotoIndexedTrack *self);
void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, guint position, gint current);
void koto_indexed_track_remove_from_playlist(KotoIndexedTrack *self, gchar *playlist_uuid);
void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, gint current);
void koto_indexed_track_set_file_name(KotoIndexedTrack *self, gchar *new_file_name);
void koto_indexed_track_set_cd(KotoIndexedTrack *self, guint cd);
void koto_indexed_track_set_parsed_name(KotoIndexedTrack *self, gchar *new_parsed_name);

View file

@ -278,6 +278,14 @@ void koto_indexed_track_commit(KotoIndexedTrack *self) {
g_free(commit_op_errmsg);
}
gchar* koto_indexed_track_get_uuid(KotoIndexedTrack *self) {
if (!KOTO_IS_INDEXED_TRACK(self)) {
return NULL;
}
return self->uuid; // Do not return a duplicate since otherwise comparison refs fail due to pointer positions being different
}
void koto_indexed_track_parse_name(KotoIndexedTrack *self) {
gchar *copied_file_name = g_strdelimit(g_strdup(self->file_name), "_", ' '); // Replace _ with whitespace for starters
@ -335,14 +343,37 @@ void koto_indexed_track_parse_name(KotoIndexedTrack *self) {
g_free(file_without_ext);
}
void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, guint position, gint current) {
void koto_indexed_track_remove_from_playlist(KotoIndexedTrack *self, gchar *playlist_uuid) {
if (!KOTO_IS_INDEXED_TRACK(self)) {
return;
}
gchar *commit_op = g_strdup_printf(
"INSERT INTO playlist_tracks(playlist_id, track_id, position, current)"
"VALUES('%s', '%s', quote(\"%d\"), quote(\"%d\")"
"ON CONFLICT(playlist_id, track_id) DO UPDATE SET position=excluded.position, current=excluded.current;",
"DELETE FROM playlist_tracks WHERE track_id='%s' AND playlist_id='%s'",
self->uuid,
playlist_uuid
);
gchar *commit_op_errmsg = NULL;
int rc = sqlite3_exec(koto_db, commit_op, 0, 0, &commit_op_errmsg);
if (rc != SQLITE_OK) {
g_warning("Failed to remove track from playlist: %s", commit_op_errmsg);
}
g_free(commit_op);
g_free(commit_op_errmsg);
}
void koto_indexed_track_save_to_playlist(KotoIndexedTrack *self, gchar *playlist_uuid, gint current) {
if (!KOTO_IS_INDEXED_TRACK(self)) {
return;
}
gchar *commit_op = g_strdup_printf(
"INSERT INTO playlist_tracks(playlist_id, track_id, current)"
"VALUES('%s', '%s', quote(\"%d\"))",
playlist_uuid,
self->uuid,
position,
current
);

View file

@ -51,10 +51,11 @@ guint koto_get_pixbuf_size(KotoButtonPixbufSize s) {
enum {
PROP_BTN_0,
PROP_USE_FROM_FILE,
PROP_PIX_SIZE,
PROP_TEXT,
PROP_BADGE_TEXT,
PROP_USE_FROM_FILE,
PROP_IMAGE_FILE_PATH,
PROP_ICON_NAME,
PROP_ALT_ICON_NAME,
N_BTN_PROPERTIES
@ -70,11 +71,16 @@ struct _KotoButton {
GtkWidget *badge_label;
GtkWidget *button_label;
GtkGesture *left_click_gesture;
GtkGesture *right_click_gesture;
gchar *image_file_path;
gchar *badge_text;
gchar *icon_name;
gchar *alt_icon_name;
gchar *text;
KotoButtonImagePosition image_position;
gboolean use_from_file;
gboolean currently_showing_alt;
};
@ -96,14 +102,6 @@ static void koto_button_class_init(KotoButtonClass *c) {
gobject_class->set_property = koto_button_set_property;
gobject_class->get_property = koto_button_get_property;
btn_props[PROP_USE_FROM_FILE] = g_param_spec_boolean(
"use-from-file",
"Use from a file / file name",
"Use from a file / file name",
FALSE,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE
);
btn_props[PROP_PIX_SIZE] = g_param_spec_uint(
"pixbuf-size",
"Pixbuf Size",
@ -130,6 +128,22 @@ static void koto_button_class_init(KotoButtonClass *c) {
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE
);
btn_props[PROP_USE_FROM_FILE] = g_param_spec_boolean(
"use-from-file",
"Use from a file / file name",
"Use from a file / file name",
FALSE,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE
);
btn_props[PROP_IMAGE_FILE_PATH] = g_param_spec_string(
"image-file-path",
"File path to image",
"File path to image",
NULL,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE
);
btn_props[PROP_ICON_NAME] = g_param_spec_string(
"icon-name",
"Icon Name",
@ -151,6 +165,16 @@ static void koto_button_class_init(KotoButtonClass *c) {
static void koto_button_init(KotoButton *self) {
self->currently_showing_alt = FALSE;
self->image_position = KOTO_BUTTON_IMAGE_POS_LEFT;
self->left_click_gesture = gtk_gesture_click_new(); // Set up our left click gesture
self->right_click_gesture = gtk_gesture_click_new(); // Set up our right click gesture
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->left_click_gesture), (int) KOTO_BUTTON_CLICK_TYPE_PRIMARY); // Only allow left clicks on left click gesture
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->right_click_gesture), (int) KOTO_BUTTON_CLICK_TYPE_SECONDARY); // Only allow right clicks on right click gesture
gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(self->left_click_gesture)); // Add our left click gesture
gtk_widget_add_controller(GTK_WIDGET(self), GTK_EVENT_CONTROLLER(self->right_click_gesture)); // Add our right click gesture
}
static void koto_button_constructed(GObject *obj) {
@ -165,6 +189,9 @@ static void koto_button_get_property(GObject *obj, guint prop_id, GValue *val, G
KotoButton *self = KOTO_BUTTON(obj);
switch (prop_id) {
case PROP_IMAGE_FILE_PATH:
g_value_set_string(val, self->image_file_path);
break;
case PROP_USE_FROM_FILE:
g_value_set_boolean(val, self->use_from_file);
break;
@ -193,23 +220,24 @@ static void koto_button_set_property(GObject *obj, guint prop_id, const GValue *
KotoButton *self = KOTO_BUTTON(obj);
switch (prop_id) {
case PROP_USE_FROM_FILE:
self->use_from_file = g_value_get_boolean(val);
break;
case PROP_PIX_SIZE:
koto_button_set_pixbuf_size(self, g_value_get_uint(val));
break;
case PROP_TEXT:
if (val == NULL) {
koto_button_set_text(self, NULL);
} else {
koto_button_set_text(self, g_strdup(g_value_get_string(val)));
if (val != NULL) {
koto_button_set_text(self, (gchar*) g_value_get_string(val));
}
break;
case PROP_BADGE_TEXT:
koto_button_set_badge_text(self, g_strdup(g_value_get_string(val)));
break;
case PROP_USE_FROM_FILE:
self->use_from_file = g_value_get_boolean(val);
break;
case PROP_IMAGE_FILE_PATH:
koto_button_set_file_path(self, (gchar*) g_value_get_string(val));
break;
case PROP_ICON_NAME:
koto_button_set_icon_name(self, g_strdup(g_value_get_string(val)), FALSE);
if (!self->currently_showing_alt) { // Not showing alt
@ -228,10 +256,32 @@ static void koto_button_set_property(GObject *obj, guint prop_id, const GValue *
}
}
void koto_button_add_click_handler(KotoButton *self, KotoButtonClickType button, GCallback handler, gpointer user_data) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
if ((button != KOTO_BUTTON_CLICK_TYPE_PRIMARY) && (button != KOTO_BUTTON_CLICK_TYPE_SECONDARY)) { // Not valid type
return;
}
g_signal_connect((button == KOTO_BUTTON_CLICK_TYPE_PRIMARY) ? self->left_click_gesture : self->right_click_gesture, "pressed", handler, user_data);
}
void koto_button_flip(KotoButton *self) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
koto_button_show_image(self, !self->currently_showing_alt);
}
void koto_button_hide_image(KotoButton *self) {
if (GTK_IS_WIDGET(self->button_pic)) { // Is a widget
gtk_widget_hide(self->button_pic);
}
}
void koto_button_set_badge_text(KotoButton *self, gchar *text) {
if ((text == NULL) || (strcmp(text, "") == 0)) { // If the text is empty
self->badge_text = g_strdup("");
@ -256,6 +306,23 @@ void koto_button_set_badge_text(KotoButton *self, gchar *text) {
g_object_notify_by_pspec(G_OBJECT(self), btn_props[PROP_BADGE_TEXT]);
}
void koto_button_set_file_path(KotoButton *self, gchar *file_path) {
if (!KOTO_IS_BUTTON(self)) { // Not a button
return;
}
if (file_path == NULL || (g_strcmp0(file_path, "") == 0)) { // Empty string or null
return;
}
if (self->image_file_path != NULL && (g_strcmp0(self->image_file_path, "") != 0)) { // Not null and not empty
g_free(self->image_file_path);
}
self->image_file_path = g_strdup(file_path);
koto_button_show_image(self, FALSE);
}
void koto_button_set_icon_name(KotoButton *self, gchar *icon_name, gboolean for_alt) {
gchar *copied_icon_name = g_strdup(icon_name);
@ -291,6 +358,22 @@ void koto_button_set_icon_name(KotoButton *self, gchar *icon_name, gboolean for_
g_object_notify_by_pspec(G_OBJECT(self), for_alt ? btn_props[PROP_ALT_ICON_NAME] : btn_props[PROP_ICON_NAME]);
}
void koto_button_set_image_position(KotoButton *self, KotoButtonImagePosition pos) {
if (self->image_position == pos) { // Is a different position that currently
return;
}
if (GTK_IS_WIDGET(self->button_pic)) { // Button is already defined
if (pos == KOTO_BUTTON_IMAGE_POS_RIGHT) { // If we want to move the image to the right
gtk_box_reorder_child_after(GTK_BOX(self), self->button_pic, self->button_label); // Move image to after label
} else { // Moving image to left
gtk_box_reorder_child_after(GTK_BOX(self), self->button_label, self->button_pic); // Move label to after image
}
}
self->image_position = pos;
}
void koto_button_set_pixbuf_size(KotoButton *self, guint size) {
g_return_if_fail(size != self->pix_size); // If the sizes aren't different, return
@ -305,16 +388,14 @@ void koto_button_set_text(KotoButton *self, gchar *text) {
return;
}
gchar *copied_text = g_strdup(text); // Copy our text
if (strcmp(copied_text, "") == 0) { // Clearing our text
if (self->text != NULL) { // Text defined
g_free(self->text); // Free existing text
}
self->text = copied_text;
self->text = g_strdup(text);
if (GTK_IS_LABEL(self->button_label)) { // If we have a button label
if (strcmp(self->text, "") != 0) { // Have text set
if (g_strcmp0(self->text, "") != 0) { // 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
@ -322,7 +403,7 @@ void koto_button_set_text(KotoButton *self, gchar *text) {
g_free(self->button_label);
}
} else { // If we do not have a button label
if (strcmp(self->text, "") != 0) { // If we have text
if ((self->text != NULL) && (g_strcmp0(self->text, "") != 0)) { // If we have text
self->button_label = gtk_label_new(self->text); // Create our label
gtk_label_set_xalign(GTK_LABEL(self->button_label), 0);
@ -342,15 +423,25 @@ void koto_button_show_image(KotoButton *self, gboolean use_alt) {
return;
}
if (use_alt && ((self->alt_icon_name == NULL) || (strcmp(self->alt_icon_name, "") == 0))) { // Don't have an alt icon set
return;
} else if (!use_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Don't have icon set
return;
}
if (self->use_from_file) { // Use from a file instead of icon name
// TODO: Add
if ((self->image_file_path == NULL) || g_strcmp0(self->image_file_path, "") == 0) { // Not set
return;
}
if (GTK_IS_IMAGE(self->button_pic)) { // Already have an icon
gtk_image_set_from_file(GTK_IMAGE(self->button_pic), self->image_file_path);
} else { // Don't have an image yet
self->button_pic = gtk_image_new_from_file(self->image_file_path); // Create a new image from the file
gtk_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box
}
} else { // From icon name
if (use_alt && ((self->alt_icon_name == NULL) || (strcmp(self->alt_icon_name, "") == 0))) { // Don't have an alt icon set
return;
} else if (!use_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Don't have icon set
return;
}
self->currently_showing_alt = use_alt;
gchar *name = use_alt ? self->alt_icon_name : self->icon_name;
@ -358,12 +449,21 @@ void koto_button_show_image(KotoButton *self, gboolean use_alt) {
gtk_image_set_from_icon_name(GTK_IMAGE(self->button_pic), name); // Just update the existing iamge
} else { // Not an image
self->button_pic = gtk_image_new_from_icon_name(name); // Get our new image
gtk_image_set_pixel_size(GTK_IMAGE(self->button_pic), self->pix_size);
gtk_box_prepend(GTK_BOX(self), self->button_pic); // Prepend to the box
}
gtk_image_set_icon_size(GTK_IMAGE(self->button_pic), GTK_ICON_SIZE_INHERIT); // Inherit height of parent widget
}
gtk_image_set_pixel_size(GTK_IMAGE(self->button_pic), self->pix_size);
gtk_image_set_icon_size(GTK_IMAGE(self->button_pic), GTK_ICON_SIZE_INHERIT); // Inherit height of parent widget
gtk_widget_show(self->button_pic); // Ensure we actually are showing the image
}
void koto_button_unflatten(KotoButton *self) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
gtk_widget_remove_css_class(GTK_WIDGET(self), "flat");
}
KotoButton* koto_button_new_plain(gchar *label) {
@ -382,3 +482,16 @@ KotoButton* koto_button_new_with_icon(gchar *label, gchar *icon_name, gchar *alt
NULL
);
}
KotoButton *koto_button_new_with_file(gchar *label, gchar *file_path, KotoButtonPixbufSize size) {
return g_object_new(KOTO_TYPE_BUTTON,
"button-text", label,
"use-from-file",
TRUE,
"image-file-path",
file_path,
"pixbuf-size",
koto_get_pixbuf_size(size),
NULL
);
}

View file

@ -23,6 +23,11 @@
G_BEGIN_DECLS
typedef enum {
KOTO_BUTTON_CLICK_TYPE_PRIMARY = 1,
KOTO_BUTTON_CLICK_TYPE_SECONDARY = 3
} KotoButtonClickType;
typedef enum {
KOTO_BUTTON_PIXBUF_SIZE_INVALID,
KOTO_BUTTON_PIXBUF_SIZE_TINY,
@ -33,6 +38,11 @@ typedef enum {
KOTO_BUTTON_PIXBUF_SIZE_GODLIKE
} KotoButtonPixbufSize;
typedef enum {
KOTO_BUTTON_IMAGE_POS_LEFT,
KOTO_BUTTON_IMAGE_POS_RIGHT
} KotoButtonImagePosition;
#define NUM_BUILTIN_SIZES 7
#define KOTO_TYPE_BUTTON (koto_button_get_type())
@ -43,14 +53,19 @@ guint koto_get_pixbuf_size(KotoButtonPixbufSize size);
KotoButton* koto_button_new_plain(gchar *label);
KotoButton* koto_button_new_with_icon(gchar *label, gchar *icon_name, gchar *alt_icon_name, KotoButtonPixbufSize size);
KotoButton* koto_button_new_with_pixbuf(gchar *label, GdkPixbuf *pix, KotoButtonPixbufSize size);
KotoButton *koto_button_new_with_file(gchar *label, gchar *file_path, KotoButtonPixbufSize size);
void koto_button_add_click_handler(KotoButton *self, KotoButtonClickType button, GCallback handler, gpointer user_data);
void koto_button_flip(KotoButton *self);
void koto_button_hide_image(KotoButton *self);
void koto_button_set_badge_text(KotoButton *self, gchar *text);
void koto_button_set_file_path(KotoButton *self, gchar *file_path);
void koto_button_set_icon_name(KotoButton *self, gchar *icon_name, gboolean for_alt);
void koto_button_set_image_position(KotoButton *self, KotoButtonImagePosition pos);
void koto_button_set_pixbuf(KotoButton *self, GdkPixbuf *pix);
void koto_button_set_pixbuf_size(KotoButton *self, guint size);
void koto_button_set_text(KotoButton *self, gchar *text);
void koto_button_show_image(KotoButton *self, gboolean use_alt);
void koto_button_unflatten(KotoButton *self);
G_END_DECLS

View file

@ -0,0 +1,99 @@
/* koto-dialog-container.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
#include "koto-button.h"
#include "koto-dialog-container.h"
struct _KotoDialogContainer {
GtkBox parent_instance;
KotoButton *close_button;
GtkWidget *dialogs;
};
G_DEFINE_TYPE(KotoDialogContainer, koto_dialog_container, GTK_TYPE_BOX);
static void koto_dialog_container_class_init(KotoDialogContainerClass *c) {
(void) c;
}
static void koto_dialog_container_init(KotoDialogContainer *self) {
gtk_widget_add_css_class(GTK_WIDGET(self), "koto-dialog-container");
g_object_set(GTK_WIDGET(self),
"hexpand",
TRUE,
"vexpand",
TRUE,
NULL);
self->close_button = koto_button_new_with_icon(NULL, "window-close-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_LARGE);
gtk_widget_set_halign(GTK_WIDGET(self->close_button), GTK_ALIGN_END);
gtk_box_prepend(GTK_BOX(self), GTK_WIDGET(self->close_button)); // Add our close button
self->dialogs = gtk_stack_new();
gtk_stack_set_transition_duration(GTK_STACK(self->dialogs), 0); // No transition timing
gtk_stack_set_transition_type(GTK_STACK(self->dialogs), GTK_STACK_TRANSITION_TYPE_NONE); // No transition
gtk_widget_set_halign(self->dialogs, GTK_ALIGN_CENTER);
gtk_widget_set_hexpand(self->dialogs, TRUE);
gtk_widget_set_valign(self->dialogs, GTK_ALIGN_CENTER);
gtk_widget_set_vexpand(self->dialogs, TRUE);
gtk_box_append(GTK_BOX(self), self->dialogs); // Add the dialogs stack
koto_button_add_click_handler(self->close_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_dialog_container_handle_close_click), self);
gtk_widget_hide(GTK_WIDGET(self)); // Hide by default
}
void koto_dialog_container_add_dialog(KotoDialogContainer *self, gchar *dialog_name, GtkWidget *dialog) {
if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container
return;
}
gtk_stack_add_named(GTK_STACK(self->dialogs), dialog, dialog_name); // Add the dialog to the stack
}
void koto_dialog_container_handle_close_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
koto_dialog_container_hide((KotoDialogContainer*) user_data);
}
void koto_dialog_container_hide(KotoDialogContainer *self) {
if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container
return;
}
gtk_widget_hide(GTK_WIDGET(self));
}
void koto_dialog_container_show_dialog(KotoDialogContainer *self, gchar *dialog_name) {
if (!KOTO_IS_DIALOG_CONTAINER(self)) { // Not a dialog container
return;
}
gtk_stack_set_visible_child_name(GTK_STACK(self->dialogs), dialog_name); // Set to the dialog name
gtk_widget_show(GTK_WIDGET(self)); // Ensure we show self
}
KotoDialogContainer* koto_dialog_container_new() {
return g_object_new(KOTO_TYPE_DIALOG_CONTAINER,
"orientation",
GTK_ORIENTATION_VERTICAL,
NULL
);
}

View file

@ -1,4 +1,4 @@
/* create-dialog.h
/* koto-dialog-container.h
*
* Copyright 2021 Joshua Strobl
*
@ -25,17 +25,18 @@ G_BEGIN_DECLS
* Type Definition
**/
#define KOTO_TYPE_CREATE_PLAYLIST_DIALOG koto_create_playlist_dialog_get_type()
G_DECLARE_FINAL_TYPE(KotoCreatePlaylistDialog, koto_create_playlist_dialog, KOTO, CREATE_PLAYLIST_DIALOG, GObject);
#define KOTO_TYPE_DIALOG_CONTAINER koto_dialog_container_get_type()
G_DECLARE_FINAL_TYPE(KotoDialogContainer, koto_dialog_container, KOTO, DIALOG_CONTAINER, GtkBox);
#define KOTO_IS_DIALOG_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_DIALOG_CONTAINER))
/**
* Create Dialog Functions
* Functions
**/
KotoCreatePlaylistDialog* koto_create_playlist_dialog_new();
GtkWidget* koto_create_playlist_dialog_get_content(KotoCreatePlaylistDialog *self);
void koto_create_playlist_dialog_handle_close(KotoCreatePlaylistDialog *self);
void koto_create_playlist_dialog_handle_create(KotoCreatePlaylistDialog *self);
void koto_create_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
KotoDialogContainer* koto_dialog_container_new();
void koto_dialog_container_add_dialog(KotoDialogContainer *self, gchar *dialog_name, GtkWidget *dialog);
void koto_dialog_container_handle_close_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_dialog_container_hide(KotoDialogContainer *self);
void koto_dialog_container_show_dialog(KotoDialogContainer *self, gchar *dialog_name);
G_END_DECLS

View file

@ -123,7 +123,7 @@ static void koto_expander_set_property(GObject *obj, guint prop_id, const GValue
KotoExpander *self = KOTO_EXPANDER(obj);
if (!GTK_IS_WIDGET(self->header_button)) { // Header Button is not a widget
KotoButton *new_button = koto_button_new_with_icon("Temporary Text", "emblem-favorite-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL);
KotoButton *new_button = koto_button_new_with_icon(NULL, "emblem-favorite-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL);
if (GTK_IS_WIDGET(new_button)) { // Created our widget successfully
self->header_button = new_button;
@ -175,9 +175,7 @@ static void koto_expander_init(KotoExpander *self) {
self->constructed = TRUE;
GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick
g_signal_connect(controller, "pressed", G_CALLBACK(koto_expander_toggle_content), self);
gtk_widget_add_controller(GTK_WIDGET(self->header_expand_button), GTK_EVENT_CONTROLLER(controller));
koto_button_add_click_handler(self->header_expand_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_expander_toggle_content), self);
}
void koto_expander_set_secondary_button(KotoExpander *self, KotoButton *new_button) {
@ -215,6 +213,10 @@ void koto_expander_set_content(KotoExpander *self, GtkWidget *new_content) {
g_object_notify_by_pspec(G_OBJECT(self), expander_props[PROP_CONTENT]);
}
GtkWidget* koto_expander_get_content(KotoExpander *self) {
return self->content;
}
void koto_expander_toggle_content(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoExpander* self = data;

View file

@ -23,11 +23,12 @@
G_BEGIN_DECLS
#define KOTO_TYPE_EXPANDER (koto_expander_get_type())
G_DECLARE_FINAL_TYPE (KotoExpander, koto_expander, KOTO, EXPANDER, GtkBox)
#define KOTO_IS_EXPANDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_EXPANDER))
KotoExpander* koto_expander_new(gchar *primary_icon_name, gchar *primary_label_text);
KotoExpander* koto_expander_new_with_button(gchar *primary_icon_name, gchar *primary_label_text, KotoButton *secondary_button);
GtkWidget* koto_expander_get_content(KotoExpander *self);
void koto_expander_set_icon_name(KotoExpander *self, const gchar *in);
void koto_expander_set_label(KotoExpander *self, const gchar *label);
void koto_expander_set_secondary_button(KotoExpander *self, KotoButton *new_button);

View file

@ -16,12 +16,16 @@
*/
#include <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h"
#include "indexer/structs.h"
#include "playlist/playlist.h"
#include "koto-config.h"
#include "koto-button.h"
#include "koto-expander.h"
#include "koto-nav.h"
#include "koto-window.h"
extern KotoCartographer *koto_maps;
extern KotoWindow *main_window;
struct _KotoNav {
@ -46,6 +50,10 @@ struct _KotoNav {
KotoButton *music_local;
KotoButton *music_radio;
// Playlists
GHashTable *playlist_buttons;
// Podcasts
KotoButton *podcasts_local;
@ -63,6 +71,7 @@ static void koto_nav_class_init(KotoNavClass *c) {
}
static void koto_nav_init(KotoNav *self) {
self->playlist_buttons = g_hash_table_new(g_str_hash, g_str_equal);
self->win = gtk_scrolled_window_new();
gtk_widget_set_hexpand_set(self->win, TRUE); // using hexpand-set works, hexpand seems to break it by causing it to take up way too much space
gtk_widget_set_size_request(self->win, 300, -1);
@ -88,19 +97,7 @@ static void koto_nav_init(KotoNav *self) {
koto_nav_create_audiobooks_section(self);
koto_nav_create_music_section(self);
koto_nav_create_podcasts_section(self);
KotoButton *playlist_add_button = koto_button_new_with_icon("", "list-add-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL);
KotoExpander *pl_expander = koto_expander_new_with_button("playlist-symbolic", "Playlists", playlist_add_button);
if (pl_expander != NULL) {
self->playlists_expander = pl_expander;
gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->playlists_expander));
}
GtkGesture *playlist_add_gesture = gtk_gesture_click_new(); // Create a gesture for clicking on the playlist add
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(playlist_add_gesture), 1); // Only allow left click
g_signal_connect(playlist_add_gesture, "pressed", G_CALLBACK(koto_nav_handle_playlist_add_click), NULL);
gtk_widget_add_controller(GTK_WIDGET(playlist_add_button), GTK_EVENT_CONTROLLER(playlist_add_gesture));
koto_nav_create_playlist_section(self);
}
void koto_nav_create_audiobooks_section(KotoNav *self) {
@ -135,6 +132,24 @@ void koto_nav_create_music_section(KotoNav *self) {
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);
}
void koto_nav_create_playlist_section(KotoNav *self) {
KotoButton *playlist_add_button = koto_button_new_with_icon("", "list-add-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_SMALL);
KotoExpander *pl_expander = koto_expander_new_with_button("playlist-symbolic", "Playlists", playlist_add_button);
self->playlists_expander = pl_expander;
gtk_box_append(GTK_BOX(self->content), GTK_WIDGET(self->playlists_expander));
// TODO: Turn into ListBox to sort playlists
GtkWidget *playlist_list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
koto_expander_set_content(self->playlists_expander, playlist_list);
koto_button_add_click_handler(playlist_add_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_nav_handle_playlist_add_click), NULL);
g_signal_connect(koto_maps, "playlist-added", G_CALLBACK(koto_nav_handle_playlist_added), self);
g_signal_connect(koto_maps, "playlist-removed", G_CALLBACK(koto_nav_handle_playlist_removed), self);
}
void koto_nav_create_podcasts_section(KotoNav *self) {
@ -155,8 +170,76 @@ void koto_nav_create_podcasts_section(KotoNav *self) {
void koto_nav_handle_playlist_add_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;
g_message("plz");
koto_window_show_create_playlist_dialog(main_window);
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, gpointer user_data) {
(void) carto;
g_return_if_fail(KOTO_IS_PLAYLIST(playlist));
KotoNav *self = user_data;
g_return_if_fail(KOTO_IS_NAV(self));
gchar *playlist_uuid = koto_playlist_get_uuid(playlist); // Get the UUID for a playlist
if (g_hash_table_contains(self->playlist_buttons, playlist_uuid)) { // Already added button
g_free(playlist_uuid);
return;
}
gchar *playlist_name = koto_playlist_get_name(playlist);
gchar *playlist_art_path = koto_playlist_get_artwork(playlist); // Get any file path for it
KotoButton *playlist_button = NULL;
if ((playlist_art_path != NULL) && g_strcmp0(playlist_art_path, "") != 0) { // 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
// TODO: Make this a ListBox and sort the playlists alphabetically
GtkBox *playlist_expander_content = GTK_BOX(koto_expander_get_content(self->playlists_expander));
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_nav_handle_playlist_button_click), playlist_uuid);
koto_window_handle_playlist_added(koto_maps, playlist, main_window); // TODO: MOVE THIS
}
}
}
void koto_nav_handle_playlist_removed(KotoCartographer *carto, gchar *playlist_uuid, gpointer user_data) {
(void) carto;
KotoNav *self = user_data;
if (!g_hash_table_contains(self->playlist_buttons, playlist_uuid)) { // Does not contain this
return;
}
KotoButton *playlist_btn = g_hash_table_lookup(self->playlist_buttons, playlist_uuid); // Get the playlist button
if (!KOTO_IS_BUTTON(playlist_btn)) { // Not a playlist button
return;
}
GtkBox *playlist_expander_content = GTK_BOX(koto_expander_get_content(self->playlists_expander));
gtk_box_remove(playlist_expander_content, GTK_WIDGET(playlist_btn)); // Remove the button
g_hash_table_remove(self->playlist_buttons, playlist_uuid); // Remove from the playlist buttons hash table
}
GtkWidget* koto_nav_get_nav(KotoNav *self) {

View file

@ -17,6 +17,8 @@
#pragma once
#include <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h"
#include "indexer/structs.h"
G_BEGIN_DECLS
@ -27,8 +29,12 @@ G_DECLARE_FINAL_TYPE (KotoNav, koto_nav, KOTO, NAV, GObject)
KotoNav* koto_nav_new (void);
void koto_nav_create_audiobooks_section(KotoNav *self);
void koto_nav_create_music_section(KotoNav *self);
void koto_nav_create_playlist_section(KotoNav *self);
void koto_nav_create_podcasts_section(KotoNav *self);
void koto_nav_handle_playlist_add_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_nav_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data);
void koto_nav_handle_playlist_removed(KotoCartographer *carto, gchar *playlist_uuid, 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);

View file

@ -18,6 +18,7 @@
#include <gstreamer-1.0/gst/gst.h>
#include <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h"
#include "playlist/add-remove-track-popover.h"
#include "playlist/current.h"
#include "playlist/playlist.h"
#include "playback/engine.h"
@ -25,6 +26,7 @@
#include "koto-config.h"
#include "koto-playerbar.h"
extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup;
extern KotoCurrentPlaylist *current_playlist;
extern KotoCartographer *koto_maps;
extern KotoPlaybackEngine *playback_engine;
@ -82,7 +84,10 @@ static void koto_playerbar_class_init(KotoPlayerBarClass *c) {
static void koto_playerbar_constructed(GObject *obj) {
KotoPlayerBar *self = KOTO_PLAYERBAR(obj);
self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(self->main, "player-bar");
self->progress_bar = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 120, 1); // Default to 120 as random max
gtk_scale_set_draw_value(GTK_SCALE(self->progress_bar), FALSE);
gtk_scale_set_digits(GTK_SCALE(self->progress_bar), 0);
gtk_range_set_increments(GTK_RANGE(self->progress_bar), 1, 1);
@ -94,7 +99,6 @@ static void koto_playerbar_constructed(GObject *obj) {
g_signal_connect(press_controller, "begin", G_CALLBACK(koto_playerbar_handle_progressbar_gesture_begin), self);
g_signal_connect(press_controller, "end", G_CALLBACK(koto_playerbar_handle_progressbar_gesture_end), self);
g_signal_connect(press_controller, "pressed", G_CALLBACK(koto_playerbar_handle_progressbar_pressed), self);
//g_signal_connect(press_controller, "unpaired-release", G_CALLBACK(koto_playerbar_handle_progressbar_unpaired_release), self);
gtk_widget_add_controller(GTK_WIDGET(self->progress_bar), GTK_EVENT_CONTROLLER(press_controller));
@ -102,11 +106,15 @@ static void koto_playerbar_constructed(GObject *obj) {
self->controls = gtk_center_box_new();
gtk_center_box_set_baseline_position(GTK_CENTER_BOX(self->controls), GTK_BASELINE_POSITION_CENTER);
gtk_widget_add_css_class(self->main, "player-bar");
self->primary_controls_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(self->primary_controls_section, "playerbar-primary-controls");
self->playback_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
self->primary_controls_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(self->playback_section, "playerbar-info");
self->secondary_controls_section = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(self->secondary_controls_section, "playerbar-secondary-controls");
gtk_center_box_set_start_widget(GTK_CENTER_BOX(self->controls), GTK_WIDGET(self->primary_controls_section));
gtk_center_box_set_center_widget(GTK_CENTER_BOX(self->controls), GTK_WIDGET(self->playback_section));
@ -155,6 +163,7 @@ void koto_playerbar_create_playback_details(KotoPlayerBar* bar) {
gtk_image_set_from_paintable(GTK_IMAGE(bar->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);
}
@ -162,8 +171,13 @@ void koto_playerbar_create_playback_details(KotoPlayerBar* bar) {
}
bar->playback_title = gtk_label_new("Title");
gtk_label_set_xalign(GTK_LABEL(bar->playback_title), 0);
bar->playback_album = gtk_label_new("Album");
gtk_label_set_xalign(GTK_LABEL(bar->playback_album), 0);
bar->playback_artist = gtk_label_new("Artist");
gtk_label_set_xalign(GTK_LABEL(bar->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));
@ -179,26 +193,17 @@ void koto_playerbar_create_primary_controls(KotoPlayerBar* bar) {
if (KOTO_IS_BUTTON(bar->back_button)) {
gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->back_button));
GtkGesture *back_controller = gtk_gesture_click_new(); // Create a new GtkGestureClick
g_signal_connect(back_controller, "pressed", G_CALLBACK(koto_playerbar_go_backwards), NULL);
gtk_widget_add_controller(GTK_WIDGET(bar->back_button), GTK_EVENT_CONTROLLER(back_controller));
koto_button_add_click_handler(bar->back_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_backwards), bar);
}
if (KOTO_IS_BUTTON(bar->play_pause_button)) {
gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->play_pause_button));
GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick
g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_play_pause), NULL);
gtk_widget_add_controller(GTK_WIDGET(bar->play_pause_button), GTK_EVENT_CONTROLLER(controller));
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(bar->forward_button)) {
gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->forward_button));
GtkGesture *forwards_controller = gtk_gesture_click_new(); // Create a new GtkGestureClick
g_signal_connect(forwards_controller, "pressed", G_CALLBACK(koto_playerbar_go_forwards), NULL);
gtk_widget_add_controller(GTK_WIDGET(bar->forward_button), GTK_EVENT_CONTROLLER(forwards_controller));
koto_button_add_click_handler(bar->forward_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_forwards), bar);
}
}
@ -212,22 +217,17 @@ void koto_playerbar_create_secondary_controls(KotoPlayerBar* bar) {
if (KOTO_IS_BUTTON(bar->repeat_button)) {
gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->repeat_button));
GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick
g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_track_repeat), bar);
gtk_widget_add_controller(GTK_WIDGET(bar->repeat_button), GTK_EVENT_CONTROLLER(controller));
koto_button_add_click_handler(bar->repeat_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_track_repeat), bar);
}
if (KOTO_IS_BUTTON(bar->shuffle_button)) {
gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->shuffle_button));
GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick
g_signal_connect(controller, "pressed", G_CALLBACK(koto_playerbar_toggle_playlist_shuffle), bar);
gtk_widget_add_controller(GTK_WIDGET(bar->shuffle_button), GTK_EVENT_CONTROLLER(controller));
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(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(bar->eq_button)) {
@ -285,6 +285,18 @@ void koto_playerbar_handle_is_paused(KotoPlaybackEngine *engine, gpointer user_d
koto_button_show_image(bar->play_pause_button, FALSE); // Set to FALSE to show play as the next action
}
void koto_playerbar_handle_playlist_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoPlayerBar *self = data;
if (!KOTO_IS_PLAYERBAR(self)) { // Not a playerbar
return;
}
koto_add_remove_track_popover_set_pointing_to_widget(koto_add_remove_track_popup, GTK_WIDGET(self->playlist_button), GTK_POS_TOP); // Position above the playlist button
gtk_widget_show(GTK_WIDGET(koto_add_remove_track_popup));
}
void koto_playerbar_handle_progressbar_gesture_begin(GtkGesture *gesture, GdkEventSequence *seq, gpointer data) {
(void) gesture; (void) seq;
KotoPlayerBar *bar = data;
@ -421,7 +433,7 @@ void koto_playerbar_set_progressbar_duration(KotoPlayerBar* bar, gint64 duration
}
}
void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, gint64 progress) {
void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, double progress) {
gtk_range_set_value(GTK_RANGE(bar->progress_bar), progress);
}

View file

@ -35,6 +35,7 @@ void koto_playerbar_go_backwards(GtkGestureClick *gesture, int n_press, double x
void koto_playerbar_go_forwards(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);
void koto_playerbar_handle_is_playing(KotoPlaybackEngine *engine, gpointer user_data);
void koto_playerbar_handle_is_paused(KotoPlaybackEngine *engine, gpointer user_data);
void koto_playerbar_handle_playlist_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);
void koto_playerbar_handle_progressbar_scroll_begin(GtkEventControllerScroll *controller, gpointer data);
void koto_playerbar_handle_progressbar_gesture_begin(GtkGesture *gesture, GdkEventSequence *seq, gpointer data);
void koto_playerbar_handle_progressbar_gesture_end(GtkGesture *gesture, GdkEventSequence *seq, gpointer data);
@ -47,7 +48,7 @@ void koto_playerbar_handle_track_shuffle(KotoPlaybackEngine *engine, gpointer us
void koto_playerbar_handle_volume_button_change(GtkScaleButton *button, double value, gpointer user_data);
void koto_playerbar_reset_progressbar(KotoPlayerBar* bar);
void koto_playerbar_set_progressbar_duration(KotoPlayerBar* bar, gint64 duration);
void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, gint64 progress);
void koto_playerbar_set_progressbar_value(KotoPlayerBar* bar, gdouble progress);
void koto_playerbar_toggle_play_pause(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);
void koto_playerbar_toggle_playlist_shuffle(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);
void koto_playerbar_toggle_track_repeat(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data);

View file

@ -16,15 +16,18 @@
*/
#include <gtk-4.0/gtk/gtk.h>
#include "indexer/structs.h"
#include "playlist/add-remove-track-popover.h"
#include "koto-button.h"
#include "koto-track-item.h"
extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup;
struct _KotoTrackItem {
GtkBox parent_instance;
KotoIndexedTrack *track;
GtkWidget *track_label;
KotoButton *add_to_playlist_button;
};
struct _KotoTrackItemClass {
@ -91,14 +94,15 @@ static void koto_track_item_init(KotoTrackItem *self) {
self->track_label = gtk_label_new(NULL); // Create with no track name
gtk_label_set_xalign(GTK_LABEL(self->track_label), 0.0);
self->add_to_playlist_button = koto_button_new_with_icon(NULL, "playlist-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_TINY);
gtk_widget_add_css_class(GTK_WIDGET(self), "track-item");
gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE);
gtk_widget_set_hexpand(GTK_WIDGET(self->track_label), TRUE);
gtk_box_prepend(GTK_BOX(self), self->track_label);
gtk_box_append(GTK_BOX(self), GTK_WIDGET(self->add_to_playlist_button));
}
KotoIndexedTrack* koto_track_item_get_track(KotoTrackItem *self) {
return self->track;
}
void koto_track_item_set_track(KotoTrackItem *self, KotoIndexedTrack *track) {

View file

@ -28,6 +28,8 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE(KotoTrackItem, koto_track_item, KOTO, TRACK_ITEM, GtkBox)
KotoTrackItem* koto_track_item_new(KotoIndexedTrack *track);
void koto_track_item_handle_add_to_playlist_button_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
KotoIndexedTrack* koto_track_item_get_track(KotoTrackItem *self);
void koto_track_item_set_track(KotoTrackItem *self, KotoIndexedTrack *track);
G_END_DECLS

View file

@ -18,6 +18,25 @@
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
extern GtkWindow *main_window;
GtkFileChooserNative* koto_utils_create_image_file_chooser(gchar *file_chooser_label) {
GtkFileChooserNative* chooser = gtk_file_chooser_native_new(
file_chooser_label,
main_window,
GTK_FILE_CHOOSER_ACTION_OPEN,
"Choose",
"Cancel"
);
GtkFileFilter *image_filter = gtk_file_filter_new(); // Create our file filter
gtk_file_filter_add_mime_type(image_filter, "image/*"); // Only allow for images
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(chooser), image_filter); // Only allow picking images
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(chooser), FALSE);
return chooser;
}
GtkWidget* koto_utils_create_image_from_filepath(gchar *filepath, gchar *fallback_icon, guint width, guint height) {
GtkWidget* image = NULL;
@ -68,7 +87,11 @@ gchar* koto_utils_get_filename_without_extension(gchar *filename) {
return stripped_file_name;
}
gchar *koto_utils_replace_string_all(gchar *str, gchar *find, gchar *repl) {
void koto_utils_push_queue_element_to_store(gpointer data, gpointer user_data) {
g_list_store_append(G_LIST_STORE(user_data), data);
}
gchar* koto_utils_replace_string_all(gchar *str, gchar *find, gchar *repl) {
gchar *cleaned_string = "";
gchar **split = g_strsplit(str, find, -1); // Split on find

View file

@ -21,8 +21,10 @@
G_BEGIN_DECLS
GtkFileChooserNative* koto_utils_create_image_file_chooser(gchar *file_chooser_label);
GtkWidget* koto_utils_create_image_from_filepath(gchar *filepath, gchar *fallback_icon, guint width, guint height);
gchar* koto_utils_get_filename_without_extension(gchar *filename);
void koto_utils_push_queue_element_to_store(gpointer data, gpointer user_data);
gchar *koto_utils_replace_string_all(gchar *str, gchar *find, gchar *repl);
gchar* koto_utils_unquote_string(gchar *s);

View file

@ -16,17 +16,27 @@
*/
#include <gtk-4.0/gdk/x11/gdkx.h>
#include "components/koto-action-bar.h"
#include "db/cartographer.h"
#include "indexer/structs.h"
#include "pages/music/music-local.h"
#include "pages/playlist/list.h"
#include "playback/engine.h"
#include "playlist/add-remove-track-popover.h"
#include "playlist/current.h"
#include "playlist/create-dialog.h"
#include "playlist/create-modify-dialog.h"
#include "koto-config.h"
#include "koto-dialog-container.h"
#include "koto-nav.h"
#include "koto-playerbar.h"
#include "koto-window.h"
extern KotoActionBar *action_bar;
extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup;
extern KotoCartographer *koto_maps;
extern KotoCreateModifyPlaylistDialog *playlist_create_modify_dialog;
extern KotoCurrentPlaylist *current_playlist;
extern KotoPageMusicLocal *music_local_page;
extern KotoPlaybackEngine *playback_engine;
struct _KotoWindow {
@ -34,7 +44,7 @@ struct _KotoWindow {
KotoIndexedLibrary *library;
KotoCurrentPlaylist *current_playlist;
KotoCreatePlaylistDialog *playlist_create_dialog;
KotoDialogContainer *dialogs;
GtkWidget *overlay;
GtkWidget *header_bar;
@ -66,14 +76,18 @@ static void koto_window_init (KotoWindow *self) {
create_new_headerbar(self); // Create our headerbar
self->overlay = gtk_overlay_new(); // Create our overlay
self->playlist_create_dialog = koto_create_playlist_dialog_new(); // Create our Create Playlist dialog
self->dialogs = koto_dialog_container_new(); // Create our dialog container
self->primary_layout = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(self->primary_layout, "primary-layout");
gtk_widget_set_hexpand(self->primary_layout, TRUE);
gtk_widget_set_vexpand(self->primary_layout, TRUE);
playlist_create_modify_dialog = koto_create_modify_playlist_dialog_new(); // Create our Create Playlist dialog
koto_dialog_container_add_dialog(self->dialogs, "create-modify-playlist", GTK_WIDGET(playlist_create_modify_dialog));
gtk_overlay_set_child(GTK_OVERLAY(self->overlay), self->primary_layout); // Add our primary layout to the overlay
gtk_overlay_add_overlay(GTK_OVERLAY(self->overlay), GTK_WIDGET(self->dialogs)); // Add the stack as our overlay
self->content_layout = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(self->content_layout, "content-layout");
@ -97,9 +111,20 @@ static void koto_window_init (KotoWindow *self) {
gtk_box_prepend(GTK_BOX(self->primary_layout), self->content_layout);
koto_add_remove_track_popup = koto_add_remove_track_popover_new(); // Create our popover for adding and removing tracks
action_bar = koto_action_bar_new(); // Create our Koto Action Bar
if (KOTO_IS_ACTION_BAR(action_bar)) { // Is an action bar
GtkActionBar *bar = koto_action_bar_get_main(action_bar);
if (GTK_IS_ACTION_BAR(bar)) {
gtk_box_append(GTK_BOX(self->primary_layout), GTK_WIDGET(bar)); // Add the action
}
}
self->player_bar = koto_playerbar_new();
if (self->player_bar != NULL) {
if (KOTO_IS_PLAYERBAR(self->player_bar)) { // Is a playerbar
GtkWidget *playerbar_main = koto_playerbar_get_main(self->player_bar);
gtk_box_append(GTK_BOX(self->primary_layout), playerbar_main);
}
@ -116,6 +141,42 @@ static void koto_window_init (KotoWindow *self) {
g_thread_new("load-library", (void*) load_library, self);
}
void koto_window_add_page(KotoWindow *self, gchar *page_name, GtkWidget *page) {
gtk_stack_add_named(GTK_STACK(self->pages), page, page_name);
}
void koto_window_go_to_page(KotoWindow *self, gchar *page_name) {
gtk_stack_set_visible_child_name(GTK_STACK(self->pages), page_name);
}
void koto_window_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data) {
(void) carto;
if (!KOTO_IS_PLAYLIST(playlist)) {
return;
}
KotoWindow *self = user_data;
gchar *playlist_uuid = koto_playlist_get_uuid(playlist);
KotoPlaylistPage *playlist_page = koto_playlist_page_new(playlist_uuid); // Create our new Playlist Page
koto_window_add_page(self, playlist_uuid, koto_playlist_page_get_main(playlist_page)); // Get the GtkScrolledWindow "main" content of the playlist page and add that as a page to our stack by the playlist UUID
}
void koto_window_hide_dialogs(KotoWindow *self) {
koto_dialog_container_hide(self->dialogs); // Hide the dialog container
}
void koto_window_remove_page(KotoWindow *self, gchar *page_name) {
GtkWidget *page = gtk_stack_get_child_by_name(GTK_STACK(self->pages), page_name);
g_return_if_fail(page != NULL);
gtk_stack_remove(GTK_STACK(self->pages), page);
}
void koto_window_show_dialog(KotoWindow *self, gchar *dialog_name) {
koto_dialog_container_show_dialog(self->dialogs, dialog_name);
}
void create_new_headerbar(KotoWindow *self) {
self->header_bar = gtk_header_bar_new();
gtk_widget_add_css_class(self->header_bar, "hdr");
@ -136,26 +197,18 @@ void create_new_headerbar(KotoWindow *self) {
gtk_window_set_titlebar(GTK_WINDOW(self), self->header_bar);
}
void koto_window_hide_create_playlist_dialog(KotoWindow *self) {
gtk_overlay_remove_overlay(GTK_OVERLAY(self->overlay), koto_create_playlist_dialog_get_content(self->playlist_create_dialog));
}
void koto_window_show_create_playlist_dialog(KotoWindow *self) {
gtk_overlay_add_overlay(GTK_OVERLAY(self->overlay), koto_create_playlist_dialog_get_content(self->playlist_create_dialog));
}
void load_library(KotoWindow *self) {
KotoIndexedLibrary *lib = koto_indexed_library_new(g_get_user_special_dir(G_USER_DIRECTORY_MUSIC));
if (lib != NULL) {
self->library = lib;
KotoPageMusicLocal* l = koto_page_music_local_new();
music_local_page = koto_page_music_local_new();
// TODO: Remove and do some fancy state loading
gtk_stack_add_named(GTK_STACK(self->pages), GTK_WIDGET(l), "music.local");
gtk_stack_set_visible_child_name(GTK_STACK(self->pages), "music.local");
koto_window_add_page(self, "music.local", GTK_WIDGET(music_local_page));
koto_window_go_to_page(self, "music.local");
gtk_widget_show(self->pages); // Do not remove this. Will cause sporadic hiding of the local page content otherwise.
koto_page_music_local_set_library(l, self->library);
koto_page_music_local_set_library(music_local_page, self->library);
}
g_thread_exit(0);

View file

@ -18,6 +18,8 @@
#pragma once
#include <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h"
#include "playlist/playlist.h"
G_BEGIN_DECLS
@ -25,10 +27,15 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (KotoWindow, koto_window, KOTO, WINDOW, GtkApplicationWindow)
void koto_window_show_create_playlist_dialog(KotoWindow *self);
void koto_window_hide_create_playlist_dialog(KotoWindow *self);
void koto_window_add_page(KotoWindow *self, gchar *page_name, GtkWidget *page);
void koto_window_go_to_page(KotoWindow *self, gchar *page_name);
void koto_window_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data);
void koto_window_hide_dialogs(KotoWindow *self);
void koto_window_remove_page(KotoWindow *self, gchar *page_name);
void koto_window_show_dialog(KotoWindow *self, gchar *dialog_name);
void create_new_headerbar(KotoWindow *self);
void handle_album_added();
void load_library(KotoWindow *self);
void set_optimal_default_window_size(KotoWindow *self);

View file

@ -1,6 +1,8 @@
add_project_arguments('-Db_sanitize=address', language: 'c')
koto_sources = [
'components/koto-action-bar.c',
'components/koto-cover-art-button.c',
'db/cartographer.c',
'db/db.c',
'indexer/album.c',
@ -11,15 +13,18 @@ koto_sources = [
'pages/music/artist-view.c',
'pages/music/disc-view.c',
'pages/music/music-local.c',
'pages/playlist/list.c',
'playback/engine.c',
'playback/media-keys.c',
'playback/mimes.c',
'playback/mpris.c',
'playlist/create-dialog.c',
'playlist/add-remove-track-popover.c',
'playlist/create-modify-dialog.c',
'playlist/current.c',
'playlist/playlist.c',
'main.c',
'koto-button.c',
'koto-dialog-container.c',
'koto-expander.c',
'koto-nav.c',
'koto-playerbar.c',

View file

@ -82,9 +82,11 @@ static void koto_album_view_init(KotoAlbumView *self) {
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_show_separators(GTK_LIST_BOX(self->discs), FALSE);
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_focusable(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
@ -116,9 +118,7 @@ static void koto_album_view_init(KotoAlbumView *self) {
g_signal_connect(motion_controller, "leave", G_CALLBACK(koto_album_view_hide_overlay_controls), self);
gtk_widget_add_controller(self->album_overlay_container, motion_controller);
GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick
g_signal_connect(controller, "pressed", G_CALLBACK(koto_album_view_toggle_album_playback), self);
gtk_widget_add_controller(GTK_WIDGET(self->play_pause_button), GTK_EVENT_CONTROLLER(controller));
koto_button_add_click_handler(self->play_pause_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_album_view_toggle_album_playback), self);
}
GtkWidget* koto_album_view_get_main(KotoAlbumView *self) {

View file

@ -122,7 +122,7 @@ static void koto_artist_view_constructed(GObject *obj) {
gtk_widget_set_halign(self->favorites_list, GTK_ALIGN_START);
self->album_list = gtk_flow_box_new(); // Create our list of our albums
gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(self->album_list), FALSE);
//gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(self->album_list), FALSE);
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(self->album_list), GTK_SELECTION_NONE);
gtk_widget_add_css_class(self->album_list, "album-list");

View file

@ -16,11 +16,13 @@
*/
#include <gtk-4.0/gtk/gtk.h>
#include "../../components/koto-action-bar.h"
#include "../../db/cartographer.h"
#include "../../indexer/structs.h"
#include "../../koto-track-item.h"
#include "disc-view.h"
extern KotoActionBar *action_bar;
extern KotoCartographer *koto_maps;
struct _KotoDiscView {
@ -121,31 +123,17 @@ static void koto_disc_view_init(KotoDiscView *self) {
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_activate_on_single_click(GTK_LIST_BOX(self->list), FALSE);
gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->list), GTK_SELECTION_MULTIPLE);
gtk_widget_add_css_class(self->list, "track-list");
gtk_widget_set_can_focus(self->list, FALSE);
gtk_widget_set_focusable(self->list, FALSE);
gtk_widget_set_size_request(self->list, 600, -1);
gtk_box_append(GTK_BOX(self), self->list);
g_list_foreach(koto_indexed_album_get_tracks(self->album), koto_disc_view_list_tracks, self);
g_signal_connect(self->list, "selected-rows-changed", G_CALLBACK(koto_disc_view_handle_selected_rows_changed), self);
}
void koto_disc_view_list_tracks(gpointer data, gpointer selfptr) {
@ -163,6 +151,49 @@ void koto_disc_view_list_tracks(gpointer data, gpointer selfptr) {
gtk_list_box_append(GTK_LIST_BOX(self->list), GTK_WIDGET(track_item)); // Add to our tracks list box
}
void koto_disc_view_handle_selected_rows_changed(GtkListBox *box, gpointer user_data) {
KotoDiscView *self = user_data;
gchar *album_uuid = koto_indexed_album_get_album_uuid(self->album); // Get the UUID
if ((album_uuid == NULL) || g_strcmp0(album_uuid, "") == 0) { // Not set
return;
}
GList *selected_rows = gtk_list_box_get_selected_rows(box); // Get the selected rows
if (g_list_length(selected_rows) == 0) { // No rows selected
koto_action_bar_toggle_reveal(action_bar, FALSE); // Close the action bar
return;
}
GList *selected_tracks = NULL; // Create our list of KotoIndexedTracks
GList *cur_selected_rows;
for (cur_selected_rows = selected_rows; cur_selected_rows != NULL; cur_selected_rows = cur_selected_rows->next) { // Iterate over the rows
KotoTrackItem *track_item = (KotoTrackItem*) gtk_list_box_row_get_child(cur_selected_rows->data);
selected_tracks = g_list_append(selected_tracks, koto_track_item_get_track(track_item)); // Add the KotoIndexedTrack to our list
}
g_list_free(cur_selected_rows);
koto_action_bar_set_tracks_in_album_selection(action_bar, album_uuid, selected_tracks); // Set our album selection
koto_action_bar_toggle_reveal(action_bar, TRUE); // Show the action bar
}
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;
g_list_foreach(koto_indexed_album_get_tracks(self->album), koto_disc_view_list_tracks, self);
}
void koto_disc_view_set_disc_number(KotoDiscView *self, guint disc_number) {
if (disc_number == 0) {
return;

View file

@ -28,6 +28,7 @@ G_BEGIN_DECLS
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_handle_selected_rows_changed(GtkListBox *box, gpointer user_data);
void koto_disc_view_list_tracks(gpointer data, gpointer selfptr);
void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album);
void koto_disc_view_set_disc_label_visible(KotoDiscView *self, gboolean visible);

View file

@ -49,6 +49,8 @@ struct _KotoPageMusicLocalClass {
G_DEFINE_TYPE(KotoPageMusicLocal, koto_page_music_local, GTK_TYPE_BOX);
KotoPageMusicLocal *music_local_page;
static void koto_page_music_local_constructed(GObject *obj);
static void koto_page_music_local_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec);
static void koto_page_music_local_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec);
@ -145,6 +147,32 @@ void koto_page_music_local_add_artist(KotoPageMusicLocal *self, KotoIndexedArtis
gtk_stack_add_named(GTK_STACK(self->stack), koto_artist_view_get_main(artist_view), artist_name);
}
void koto_page_music_local_go_to_artist_by_name(KotoPageMusicLocal *self, gchar *artist_name) {
gtk_stack_set_visible_child_name(GTK_STACK(self->stack), artist_name);
}
void koto_page_music_local_go_to_artist_by_uuid(KotoPageMusicLocal *self, gchar *artist_uuid) {
KotoIndexedArtist *artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid); // Get the artist
if (!KOTO_IS_INDEXED_ARTIST(artist)) { // No artist for this UUID
return;
}
gchar *artist_name = NULL;
g_object_get(
artist,
"name",
&artist_name,
NULL
);
if (artist_name == NULL || (g_strcmp0(artist_name, "") == 0)) { // Failed to get the artist name
return;
}
koto_page_music_local_go_to_artist_by_name(self, artist_name);
}
void koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *row, gpointer data) {
(void) box;
KotoPageMusicLocal *self = (KotoPageMusicLocal*) data;
@ -152,7 +180,7 @@ void koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *r
gchar *artist_name;
g_object_get(btn, "button-text", &artist_name, NULL);
gtk_stack_set_visible_child_name(GTK_STACK(self->stack), artist_name);
koto_page_music_local_go_to_artist_by_name(self, artist_name);
}
void koto_page_music_local_set_library(KotoPageMusicLocal *self, KotoIndexedLibrary *lib) {

View file

@ -31,6 +31,8 @@ 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(GtkListBox *box, GtkListBoxRow *row, gpointer data);
void koto_page_music_local_go_to_artist_by_name(KotoPageMusicLocal *self, gchar *artist_name);
void koto_page_music_local_go_to_artist_by_uuid(KotoPageMusicLocal *self, gchar *artist_uuid);
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);

510
src/pages/playlist/list.c Normal file
View file

@ -0,0 +1,510 @@
/* list.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../../components/koto-action-bar.h"
#include "../../components/koto-cover-art-button.h"
#include "../../db/cartographer.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"
extern KotoActionBar *action_bar;
extern KotoCartographer *koto_maps;
extern KotoCreateModifyPlaylistDialog *playlist_create_modify_dialog;
extern KotoCurrentPlaylist *current_playlist;
extern KotoWindow *main_window;
enum {
PROP_0,
PROP_PLAYLIST_UUID,
N_PROPERTIES
};
static GParamSpec *props[N_PROPERTIES] = { NULL, };
struct _KotoPlaylistPage {
GObject parent_instance;
KotoPlaylist *playlist;
gchar *uuid;
GtkWidget *main; // Our Scrolled Window
GtkWidget *content; // Content inside scrolled window
GtkWidget *header;
KotoCoverArtButton *playlist_image;
GtkWidget *name_label;
GtkWidget *tracks_count_label;
GtkWidget *type_label;
KotoButton *favorite_button;
KotoButton *edit_button;
GtkListItemFactory *item_factory;
GListModel *model;
GtkSelectionModel *selection_model;
GtkWidget *track_list_content;
GtkWidget *track_list_header;
GtkWidget *track_list_view;
KotoButton *track_num_button;
KotoButton *track_title_button;
KotoButton *track_album_button;
KotoButton *track_artist_button;
GtkSizeGroup *track_pos_size_group;
GtkSizeGroup *track_name_size_group;
GtkSizeGroup *track_album_size_group;
GtkSizeGroup *track_artist_size_group;
};
struct _KotoPlaylistPageClass {
GObjectClass parent_class;
};
G_DEFINE_TYPE(KotoPlaylistPage, koto_playlist_page, G_TYPE_OBJECT);
static void koto_playlist_page_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec);
static void koto_playlist_page_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec);
static void koto_playlist_page_class_init(KotoPlaylistPageClass *c) {
GObjectClass *gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->get_property = koto_playlist_page_get_property;
gobject_class->set_property = koto_playlist_page_set_property;
props[PROP_PLAYLIST_UUID] = g_param_spec_string(
"uuid",
"UUID of associated Playlist",
"UUID of associated Playlist",
NULL,
G_PARAM_CONSTRUCT_ONLY|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
}
static void koto_playlist_page_init(KotoPlaylistPage *self) {
self->track_name_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
self->track_pos_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
self->track_album_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
self->track_artist_size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
self->main = gtk_scrolled_window_new(); // Create our scrolled window
self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(self->content, "playlist-page");
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(self->main), self->content);
self->header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create a horizontal box
gtk_widget_add_css_class(self->header, "playlist-page-header");
gtk_box_prepend(GTK_BOX(self->content), self->header);
self->playlist_image = koto_cover_art_button_new(220, 220, NULL); // Create our Cover Art Button with no art by default
KotoButton *cover_art_button = koto_cover_art_button_get_button(self->playlist_image); // Get the button for the cover art
koto_button_add_click_handler(cover_art_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_cover_art_clicked), self);
GtkWidget *info_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_size_request(info_box, -1, 220);
gtk_widget_add_css_class(info_box, "playlist-page-header-info");
gtk_widget_set_hexpand(info_box, TRUE);
self->type_label = gtk_label_new(NULL); // Create our type label
gtk_widget_set_halign(self->type_label, GTK_ALIGN_START);
self->name_label = gtk_label_new(NULL);
gtk_widget_set_halign(self->name_label, GTK_ALIGN_START);
self->tracks_count_label = gtk_label_new(NULL);
gtk_widget_set_halign(self->tracks_count_label, GTK_ALIGN_START);
gtk_widget_set_valign(self->tracks_count_label, GTK_ALIGN_END);
gtk_box_append(GTK_BOX(info_box), self->type_label);
gtk_box_append(GTK_BOX(info_box), self->name_label);
gtk_box_append(GTK_BOX(info_box), self->tracks_count_label);
self->favorite_button = koto_button_new_with_icon(NULL, "emblem-favorite-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL);
self->edit_button = koto_button_new_with_icon(NULL, "emblem-system-symbolic", NULL, KOTO_BUTTON_PIXBUF_SIZE_NORMAL);
koto_button_add_click_handler(self->edit_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_edit_button_clicked), self); // Set up our binding
gtk_box_append(GTK_BOX(self->header), koto_cover_art_button_get_main(self->playlist_image)); // Add the Cover Art Button main overlay
gtk_box_append(GTK_BOX(self->header), info_box); // Add our info box
gtk_box_append(GTK_BOX(self->header), GTK_WIDGET(self->favorite_button)); // Add the favorite button
gtk_box_append(GTK_BOX(self->header), GTK_WIDGET(self->edit_button)); // Add the edit button
self->item_factory = gtk_signal_list_item_factory_new(); // Create a new signal list item factory
g_signal_connect(self->item_factory, "setup", G_CALLBACK(koto_playlist_page_setup_track_item), self);
g_signal_connect(self->item_factory, "bind", G_CALLBACK(koto_playlist_page_bind_track_item), self);
self->track_list_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class((self->track_list_content), "track-list-content");
gtk_widget_set_hexpand(self->track_list_content, TRUE); // Expand horizontally
gtk_widget_set_vexpand(self->track_list_content, TRUE); // Expand vertically
self->track_list_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(self->track_list_header, "track-list-header");
koto_playlist_page_create_tracks_header(self); // Create our tracks header content
self->track_list_view = gtk_list_view_new(NULL, self->item_factory); // Create our list view with no model yet
gtk_widget_add_css_class(self->track_list_view, "track-list-columned");
gtk_widget_set_hexpand(self->track_list_view, TRUE); // Expand horizontally
gtk_widget_set_vexpand(self->track_list_view, TRUE); // Expand vertically
gtk_box_append(GTK_BOX(self->track_list_content), self->track_list_header);
gtk_box_append(GTK_BOX(self->track_list_content), self->track_list_view);
gtk_box_append(GTK_BOX(self->content), self->track_list_content);
g_signal_connect(action_bar, "closed", G_CALLBACK(koto_playlist_page_handle_action_bar_closed), self); // Handle closed action bar
}
static void koto_playlist_page_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) {
KotoPlaylistPage *self = KOTO_PLAYLIST_PAGE(obj);
switch (prop_id) {
case PROP_PLAYLIST_UUID:
g_value_set_string(val, self->uuid);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_playlist_page_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) {
KotoPlaylistPage *self = KOTO_PLAYLIST_PAGE(obj);
switch (prop_id) {
case PROP_PLAYLIST_UUID:
koto_playlist_page_set_playlist_uuid(self, g_strdup(g_value_get_string(val))); // Call to our playlist UUID set function
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
void koto_playlist_page_bind_track_item(GtkListItemFactory *factory, GtkListItem *item, KotoPlaylistPage *self) {
(void) factory;
GtkWidget *track_position_label = gtk_widget_get_first_child(gtk_list_item_get_child(item));
GtkWidget *track_name_label = gtk_widget_get_next_sibling(track_position_label);
GtkWidget *track_album_label = gtk_widget_get_next_sibling(track_name_label);
GtkWidget *track_artist_label = gtk_widget_get_next_sibling(track_album_label);
KotoIndexedTrack *track = gtk_list_item_get_item(item); // Get the track UUID from our model
g_return_if_fail(KOTO_IS_INDEXED_TRACK(track));
gchar *track_name = NULL;
gchar *album_uuid = NULL;
gchar *artist_uuid = NULL;
g_object_get(
track,
"parsed-name",
&track_name,
"album-uuid",
&album_uuid,
"artist-uuid",
&artist_uuid,
NULL
);
guint track_position = koto_playlist_get_position_of_track(self->playlist, track);
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
KotoIndexedAlbum *album = koto_cartographer_get_album_by_uuid(koto_maps, album_uuid);
if (KOTO_IS_INDEXED_ALBUM(album)) {
gtk_label_set_label(GTK_LABEL(track_album_label), koto_indexed_album_get_album_name(album)); // Get the name of the album and set it to the label
}
KotoIndexedArtist *artist = koto_cartographer_get_artist_by_uuid(koto_maps, artist_uuid);
if (KOTO_IS_INDEXED_ARTIST(artist)) {
gtk_label_set_label(GTK_LABEL(track_artist_label), koto_indexed_artist_get_name(artist)); // Get the name of the artist and set it to the label
}
}
void koto_playlist_page_create_tracks_header(KotoPlaylistPage *self) {
self->track_num_button = koto_button_new_with_icon("#", "pan-down-symbolic", "pan-up-symbolic", KOTO_BUTTON_PIXBUF_SIZE_SMALL);
koto_button_add_click_handler(self->track_num_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_track_num_clicked), self);
koto_button_set_image_position(self->track_num_button, KOTO_BUTTON_IMAGE_POS_RIGHT); // Move the image to the right
gtk_size_group_add_widget(self->track_pos_size_group, GTK_WIDGET(self->track_num_button));
self->track_title_button = koto_button_new_plain("Title");
koto_button_add_click_handler(self->track_title_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_track_name_clicked), self);
gtk_size_group_add_widget(self->track_name_size_group, GTK_WIDGET(self->track_title_button));
self->track_album_button = koto_button_new_plain("Album");
gtk_widget_set_margin_start(GTK_WIDGET(self->track_album_button), 50);
koto_button_add_click_handler(self->track_album_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_track_album_clicked), self);
gtk_size_group_add_widget(self->track_album_size_group, GTK_WIDGET(self->track_album_button));
self->track_artist_button = koto_button_new_plain("Artist");
gtk_widget_set_margin_start(GTK_WIDGET(self->track_artist_button), 50);
koto_button_add_click_handler(self->track_artist_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playlist_page_handle_track_artist_clicked), self);
gtk_size_group_add_widget(self->track_artist_size_group, GTK_WIDGET(self->track_artist_button));
gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_num_button));
gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_title_button));
gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_album_button));
gtk_box_append(GTK_BOX(self->track_list_header), GTK_WIDGET(self->track_artist_button));
}
GtkWidget* koto_playlist_page_get_main(KotoPlaylistPage *self) {
return self->main;
}
void koto_playlist_page_handle_action_bar_closed(KotoActionBar *bar, gpointer data) {
(void) bar;
KotoPlaylistPage *self = data;
if (!KOTO_IS_PLAYLIST(self->playlist)) { // No playlist set
return;
}
gtk_selection_model_unselect_all(self->selection_model);
gtk_widget_grab_focus(GTK_WIDGET(main_window)); // Focus on the window
}
void koto_playlist_page_handle_cover_art_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoPlaylistPage *self = user_data;
if (!KOTO_IS_PLAYLIST(self->playlist)) { // No playlist set
return;
}
koto_current_playlist_set_playlist(current_playlist, self->playlist);
}
void koto_playlist_page_handle_edit_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoPlaylistPage *self = user_data;
koto_create_modify_playlist_dialog_set_playlist_uuid(playlist_create_modify_dialog, koto_playlist_get_uuid(self->playlist));
koto_window_show_dialog(main_window, "create-modify-playlist");
}
void koto_playlist_page_handle_track_album_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoPlaylistPage *self = user_data;
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_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ALBUM);
}
void koto_playlist_page_handle_track_artist_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoPlaylistPage *self = user_data;
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_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST);
}
void koto_playlist_page_handle_track_name_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoPlaylistPage *self = user_data;
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_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME);
}
void koto_playlist_page_handle_track_num_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoPlaylistPage *self = user_data;
KotoPreferredModelType current_model = koto_playlist_get_current_model(self->playlist);
if (current_model == KOTO_PREFERRED_MODEL_TYPE_DEFAULT) { // Set to newest currently
koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST); // Sort reversed (oldest)
koto_button_show_image(self->track_num_button, TRUE); // Use inverted value (pan-up-symbolic)
} else {
koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // Sort newest
koto_button_show_image(self->track_num_button, FALSE); // Use pan down default
}
}
void koto_playlist_page_handle_tracks_selected(GtkSelectionModel *model, guint position, guint n_items, gpointer user_data) {
(void) position;
KotoPlaylistPage *self = user_data;
if (n_items == 0) { // No items selected
koto_action_bar_toggle_reveal(action_bar, FALSE); // Hide the action bar
return;
}
GtkBitset *selected_items_bitset = gtk_selection_model_get_selection(model); // Get the selected items as a GtkBitSet
GtkBitsetIter iter;
GList *selected_tracks = NULL;
GList *selected_tracks_pos = NULL;
guint first_track_pos;
if (!gtk_bitset_iter_init_first(&iter, selected_items_bitset, &first_track_pos)) { // Failed to get the first item
return;
}
selected_tracks_pos = g_list_append(selected_tracks_pos, GUINT_TO_POINTER(first_track_pos));
gboolean have_more_items = TRUE;
while (have_more_items) { // While we are able to get selected items
guint track_pos;
have_more_items = gtk_bitset_iter_next(&iter, &track_pos);
if (have_more_items) { // Got the next track
selected_tracks_pos = g_list_append(selected_tracks_pos, GUINT_TO_POINTER(track_pos));
}
}
GList *cur_pos_list;
for (cur_pos_list = selected_tracks_pos; cur_pos_list != NULL; cur_pos_list = cur_pos_list->next) { // Iterate over every position that we accumulated
KotoIndexedTrack *selected_track = g_list_model_get_item(self->model, GPOINTER_TO_UINT(cur_pos_list->data)); // Get the KotoIndexedTrack in the GListModel for this current position
selected_tracks = g_list_append(selected_tracks, selected_track); // Add to selected tracks
}
koto_action_bar_set_tracks_in_playlist_selection(action_bar, self->uuid, selected_tracks); // Set the tracks for the playlist selection
koto_action_bar_toggle_reveal(action_bar, TRUE); // Show the items
}
void koto_playlist_page_set_playlist_uuid(KotoPlaylistPage *self, gchar *playlist_uuid) {
g_return_if_fail(KOTO_IS_PLAYLIST_PAGE(self));
g_return_if_fail(g_strcmp0(playlist_uuid, "") != 0); // Return if empty string
g_return_if_fail(koto_cartographer_has_playlist_by_uuid(koto_maps, playlist_uuid)); // Don't have a playlist with this UUID
self->uuid = g_strdup(playlist_uuid); // Duplicate the playlist UUID
KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->uuid);
self->playlist = playlist;
koto_playlist_page_set_playlist_model(self, KOTO_PREFERRED_MODEL_TYPE_DEFAULT); // TODO: Enable this to be changed
koto_playlist_page_update_header(self); // Update our header
}
void koto_playlist_page_set_playlist_model(KotoPlaylistPage *self, KotoPreferredModelType model) {
g_return_if_fail(KOTO_IS_PLAYLIST_PAGE(self));
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
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
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
gtk_widget_remove_css_class(GTK_WIDGET(self->track_title_button), "active");
}
self->selection_model = GTK_SELECTION_MODEL(gtk_multi_selection_new(self->model));
g_signal_connect(self->selection_model, "selection-changed", G_CALLBACK(koto_playlist_page_handle_tracks_selected), self); // Bind to our selection changed
gtk_list_view_set_model(GTK_LIST_VIEW(self->track_list_view), self->selection_model); // Set our multi selection model to our provided model
}
void koto_playlist_page_setup_track_item(GtkListItemFactory *factory, GtkListItem *item, gpointer user_data) {
(void) factory;
KotoPlaylistPage *self = user_data;
GtkWidget *item_content = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Have a horizontal box for our content
gtk_widget_add_css_class(item_content, "track-list-columned-item");
GtkWidget *track_number = gtk_label_new(NULL); // Our track number
gtk_label_set_xalign(GTK_LABEL(track_number), 0);
gtk_widget_add_css_class(track_number, "track-column-number");
gtk_widget_set_halign(track_number, GTK_ALIGN_END);
gtk_widget_set_hexpand(track_number, FALSE);
gtk_widget_set_size_request(track_number, 150, -1);
gtk_box_append(GTK_BOX(item_content), track_number);
gtk_size_group_add_widget(self->track_pos_size_group, track_number);
GtkWidget *track_name = gtk_label_new(NULL); // Our track name
gtk_label_set_xalign(GTK_LABEL(track_name), 0);
gtk_widget_add_css_class(track_name, "track-column-name");
gtk_widget_set_halign(track_name, GTK_ALIGN_START);
gtk_widget_set_hexpand(track_name, FALSE);
gtk_widget_set_size_request(track_name, 350, -1);
gtk_box_append(GTK_BOX(item_content), track_name);
gtk_size_group_add_widget(self->track_name_size_group, track_name);
GtkWidget *track_album = gtk_label_new(NULL); // Our track album
gtk_label_set_xalign(GTK_LABEL(track_album), 0);
gtk_widget_add_css_class(track_album, "track-column-album");
gtk_widget_set_halign(track_album, GTK_ALIGN_START);
gtk_widget_set_hexpand(track_album, FALSE);
gtk_widget_set_margin_start(track_album, 50);
gtk_widget_set_size_request(track_album, 350, -1);
gtk_box_append(GTK_BOX(item_content), track_album);
gtk_size_group_add_widget(self->track_album_size_group, track_album);
GtkWidget *track_artist = gtk_label_new(NULL); // Our track artist
gtk_label_set_xalign(GTK_LABEL(track_artist), 0);
gtk_widget_add_css_class(track_artist, "track-column-artist");
gtk_widget_set_halign(track_artist, GTK_ALIGN_START);
gtk_widget_set_hexpand(track_artist, TRUE);
gtk_widget_set_margin_start(track_artist, 50);
gtk_widget_set_size_request(track_artist, 350, -1);
gtk_box_append(GTK_BOX(item_content), track_artist);
gtk_size_group_add_widget(self->track_artist_size_group, track_artist);
gtk_list_item_set_child(item, item_content);
}
void koto_playlist_page_update_header(KotoPlaylistPage *self) {
g_return_if_fail(KOTO_IS_PLAYLIST_PAGE(self));
g_return_if_fail(KOTO_IS_PLAYLIST(self->playlist)); // Not a valid playlist
gboolean ephemeral = TRUE;
g_object_get(
self->playlist,
"ephemeral",
&ephemeral,
NULL
);
gtk_label_set_text(GTK_LABEL(self->type_label), ephemeral ? "Generated playlist" : "Curated playlist");
gtk_label_set_text(GTK_LABEL(self->name_label), koto_playlist_get_name(self->playlist)); // Set the name label to our playlist name
guint track_count = koto_playlist_get_length(self->playlist); // Get the number of tracks
gtk_label_set_text(GTK_LABEL(self->tracks_count_label), g_strdup_printf(track_count != 1 ? "%u tracks" : "%u track", track_count)); // Set the text to "N tracks" where N is the number
gchar *artwork = koto_playlist_get_artwork(self->playlist);
if ((artwork != NULL) && g_strcmp0(artwork, "") != 0) { // Have artwork
koto_cover_art_button_set_art_path(self->playlist_image, artwork); // Update our artwork
}
KotoPreferredModelType current_model = koto_playlist_get_current_model(self->playlist); // Get the current model
if (current_model == KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST) {
koto_button_show_image(self->track_num_button, TRUE); // Immediately use pan-up-symbolic
}
}
KotoPlaylistPage* koto_playlist_page_new(gchar *playlist_uuid) {
return g_object_new(KOTO_TYPE_PLAYLIST_PAGE,
"uuid",
playlist_uuid,
NULL
);
}

55
src/pages/playlist/list.h Normal file
View file

@ -0,0 +1,55 @@
/* list.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 "../../components/koto-action-bar.h"
#include "../../playlist/playlist.h"
#include "../../koto-utils.h"
G_BEGIN_DECLS
#define KOTO_TYPE_PLAYLIST_PAGE koto_playlist_page_get_type()
G_DECLARE_FINAL_TYPE (KotoPlaylistPage, koto_playlist_page, KOTO, PLAYLIST_PAGE, GObject);
#define KOTO_IS_PLAYLIST_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYLIST_PAGE))
KotoPlaylistPage* koto_playlist_page_new(gchar *playlist_uuid);
void koto_playlist_page_add_track(KotoPlaylistPage* self, const gchar *track_uuid);
void koto_playlist_page_create_tracks_header(KotoPlaylistPage *self);
void koto_playlist_page_bind_track_item(GtkListItemFactory *factory, GtkListItem *item, KotoPlaylistPage *self);
void koto_playlist_page_remove_track(KotoPlaylistPage *self, const gchar *track_uuid);
GtkWidget* koto_playlist_page_get_main(KotoPlaylistPage *self);
void koto_playlist_page_handle_action_bar_closed(KotoActionBar *bar, gpointer data);
void koto_playlist_page_handle_cover_art_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_playlist_page_handle_edit_button_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_playlist_page_handle_track_album_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_playlist_page_handle_track_artist_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_playlist_page_handle_track_name_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_playlist_page_handle_track_num_clicked(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_playlist_page_handle_tracks_selected(GtkSelectionModel *model, guint position, guint n_items, gpointer user_data);
void koto_playlist_page_hide_popover(KotoPlaylistPage *self);
void koto_playlist_page_setup_track_item(GtkListItemFactory *factory, GtkListItem *item, gpointer user_data);
void koto_playlist_page_set_playlist_uuid(KotoPlaylistPage *self, gchar *playlist_uuid);
void koto_playlist_page_set_playlist_model(KotoPlaylistPage *self, KotoPreferredModelType model);
void koto_playlist_page_show_popover(KotoPlaylistPage *self);
void koto_playlist_page_update_header(KotoPlaylistPage *self);
void koto_playlist_page_handle_listitem_activate(GtkListView *list, guint position, gpointer user_data);
G_END_DECLS

View file

@ -36,7 +36,6 @@ enum {
N_SIGNALS
};
static glong NS = 1000000000;
static guint playback_engine_signals[N_SIGNALS] = { 0 };
extern KotoCartographer *koto_maps;
@ -51,12 +50,16 @@ struct _KotoPlaybackEngine {
GstElement *suppress_video;
GstBus *monitor;
GstQuery *duration_query;
GstQuery *position_query;
KotoIndexedTrack *current_track;
gboolean is_muted;
gboolean is_repeat_enabled;
gboolean is_playing;
gboolean is_playing_specific_track;
gboolean tick_duration_timer_running;
gboolean tick_track_timer_running;
@ -197,12 +200,17 @@ static void koto_playback_engine_init(KotoPlaybackEngine *self) {
gst_bin_add(GST_BIN(self->player), self->playbin);
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_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);
gst_element_set_bus(self->player, self->monitor); // Set our bus to monitor changes
}
self->is_muted = FALSE;
self->is_playing = FALSE;
self->is_playing_specific_track = FALSE;
self->is_repeat_enabled = FALSE;
self->tick_duration_timer_running = FALSE;
self->tick_track_timer_running = FALSE;
@ -219,6 +227,10 @@ void koto_playback_engine_backwards(KotoPlaybackEngine *self) {
return;
}
if (self->is_repeat_enabled || self->is_playing_specific_track) { // Repeat enabled or playing a specific track
return;
}
koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_previous(playlist));
}
@ -245,7 +257,7 @@ void koto_playback_engine_forwards(KotoPlaybackEngine *self) {
if (self->is_repeat_enabled) { // Is repeat enabled
koto_playback_engine_set_position(self, 0); // Set position back to 0 to repeat the track
} else { // Repeat not enabled
} else if (!self->is_repeat_enabled && !self->is_playing_specific_track) { // Repeat not enabled and we are not playing a specific track
koto_playback_engine_set_track_by_uuid(self, koto_playlist_go_to_next(playlist));
}
}
@ -256,27 +268,32 @@ KotoIndexedTrack* koto_playback_engine_get_current_track(KotoPlaybackEngine *sel
gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self) {
gint64 duration = 0;
if (gst_element_query_duration(self->player, GST_FORMAT_TIME, &duration)) {
duration = duration / NS; // Divide by NS to get seconds
if (gst_element_query(self->player, self->duration_query)) { // Able to query our duration
gst_query_parse_duration(self->duration_query, NULL, &duration); // Get the duration
duration = duration / GST_SECOND; // Divide by NS to get seconds
}
return duration;
}
gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self) {
gint64 progress = 0;
if (gst_element_query_position(self->player, GST_FORMAT_TIME, &progress)) {
progress = progress / NS; // Divide by NS to get seconds
gdouble koto_playback_engine_get_progress(KotoPlaybackEngine *self) {
gdouble progress = 0.0;
gint64 gstprog = 0;
if (gst_element_query(self->playbin, self->position_query)) { // Able to get our position
gst_query_parse_position(self->position_query, NULL, &gstprog); // Get the progress
if (gstprog < 1) { // Less than a second
return 0.0;
}
progress = gstprog / GST_SECOND; // Divide by GST_SECOND then again by 100.
}
return progress;
}
GstState koto_playback_engine_get_state(KotoPlaybackEngine *self) {
GstState current_state;
gst_element_get_state(self->player, &current_state, NULL, GST_SECOND); // Get the current state, allowing up to a second to get it
return current_state;
return GST_STATE(self->player);
}
gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine *self) {
@ -361,7 +378,7 @@ void koto_playback_engine_pause(KotoPlaybackEngine *self) {
}
void koto_playback_engine_set_position(KotoPlaybackEngine *self, int position) {
gst_element_seek_simple(self->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, position * NS);
gst_element_seek_simple(self->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, position * GST_SECOND);
}
void koto_playback_engine_set_track_repeat(KotoPlaybackEngine *self, gboolean enable_repeat) {

View file

@ -48,7 +48,7 @@ void koto_playback_engine_forwards(KotoPlaybackEngine *self);
KotoIndexedTrack* koto_playback_engine_get_current_track(KotoPlaybackEngine *self);
gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self);
GstState koto_playback_engine_get_state(KotoPlaybackEngine *self);
gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self);
gdouble koto_playback_engine_get_progress(KotoPlaybackEngine *self);
gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine *self);
gboolean koto_playback_engine_get_track_shuffle(KotoPlaybackEngine *self);
void koto_playback_engine_mute(KotoPlaybackEngine *self);

View file

@ -0,0 +1,249 @@
/* add-remove-track-popover.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../db/cartographer.h"
#include "../playback/engine.h"
#include "../playlist/playlist.h"
#include "add-remove-track-popover.h"
extern KotoCartographer *koto_maps;
struct _KotoAddRemoveTrackPopover {
GtkPopover parent_instance;
GtkWidget *list_box;
GHashTable *checkbox_to_playlist_uuid;
GHashTable *playlist_uuid_to_checkbox;
GList *tracks;
GHashTable *checkbox_to_signal_ids;
};
G_DEFINE_TYPE(KotoAddRemoveTrackPopover, koto_add_remove_track_popover, GTK_TYPE_POPOVER);
KotoAddRemoveTrackPopover *koto_add_remove_track_popup = NULL;
static void koto_add_remove_track_popover_class_init(KotoAddRemoveTrackPopoverClass *c) {
(void) c;
}
static void koto_add_remove_track_popover_init(KotoAddRemoveTrackPopover *self) {
self->list_box = gtk_list_box_new(); // Create our new GtkListBox
gtk_list_box_set_selection_mode(GTK_LIST_BOX(self->list_box), GTK_SELECTION_NONE);
self->checkbox_to_playlist_uuid = g_hash_table_new(g_str_hash, g_str_equal);
self->playlist_uuid_to_checkbox = g_hash_table_new(g_str_hash, g_str_equal);
self->checkbox_to_signal_ids = g_hash_table_new(g_str_hash, g_str_equal),
self->tracks = NULL; // Initialize our tracks
gtk_popover_set_autohide(GTK_POPOVER(self), TRUE); // Ensure autohide is enabled
gtk_popover_set_child(GTK_POPOVER(self), self->list_box);
g_signal_connect(koto_maps, "playlist-added", G_CALLBACK(koto_add_remove_track_popover_handle_playlist_added), self);
g_signal_connect(koto_maps, "playlist-removed", G_CALLBACK(koto_add_remove_track_popover_handle_playlist_removed), self);
}
void koto_add_remove_track_popover_add_playlist(KotoAddRemoveTrackPopover *self, KotoPlaylist *playlist) {
if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) {
return;
}
if (!KOTO_IS_PLAYLIST(playlist)) { // Not a playlist
return;
}
gchar *playlist_uuid = koto_playlist_get_uuid(playlist); // Get the UUID of the playlist
if (GTK_IS_CHECK_BUTTON(g_hash_table_lookup(self->playlist_uuid_to_checkbox, playlist_uuid))) { // Already have a check button for this
g_free(playlist_uuid);
return;
}
GtkWidget *playlist_button = gtk_check_button_new_with_label(koto_playlist_get_name(playlist)); // Create our GtkCheckButton
g_hash_table_insert(self->checkbox_to_playlist_uuid, playlist_button, playlist_uuid);
g_hash_table_insert(self->playlist_uuid_to_checkbox, playlist_uuid, playlist_button);
gulong playlist_sig_id = g_signal_connect(playlist_button, "toggled", G_CALLBACK(koto_add_remove_track_popover_handle_checkbutton_toggle), self);
g_hash_table_insert(self->checkbox_to_signal_ids, playlist_button, GUINT_TO_POINTER(playlist_sig_id)); // Add our GSignal handler ID
gtk_list_box_append(GTK_LIST_BOX(self->list_box), playlist_button); // Add the playlist to the list box
}
void koto_add_remove_track_popover_clear_tracks(KotoAddRemoveTrackPopover *self) {
if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) {
return;
}
if (self->tracks != NULL) { // Is a list
g_list_free(self->tracks);
self->tracks = NULL;
}
}
void koto_add_remove_track_popover_remove_playlist(KotoAddRemoveTrackPopover *self, gchar *playlist_uuid) {
if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) {
return;
}
if (!g_hash_table_contains(self->playlist_uuid_to_checkbox, playlist_uuid)) { // Playlist not added
return;
}
GtkCheckButton *btn = GTK_CHECK_BUTTON(g_hash_table_lookup(self->playlist_uuid_to_checkbox, playlist_uuid)); // Get the check button
if (GTK_IS_CHECK_BUTTON(btn)) { // Is a check button
g_hash_table_remove(self->checkbox_to_playlist_uuid, btn); // Remove uuid based on btn
gtk_list_box_remove(GTK_LIST_BOX(self->list_box), GTK_WIDGET(btn)); // Remove the button from the list box
}
g_hash_table_remove(self->playlist_uuid_to_checkbox, playlist_uuid);
}
void koto_add_remove_track_popover_handle_checkbutton_toggle(GtkCheckButton *btn, gpointer user_data) {
KotoAddRemoveTrackPopover *self = user_data;
if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) {
return;
}
gboolean should_add = gtk_check_button_get_active(btn); // Get the now active state
gchar *playlist_uuid = g_hash_table_lookup(self->checkbox_to_playlist_uuid, btn); // Get the playlist UUID for this button
KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid); // Get the playlist
if (!KOTO_IS_PLAYLIST(playlist)) { // Failed to get the playlist
return;
}
GList *pos;
for (pos = self->tracks; pos != NULL; pos = pos->next) { // Iterate over our KotoIndexedTracks
KotoIndexedTrack *track = pos->data;
if (!KOTO_INDEXED_TRACK(track)) { // Not a track
continue; // Skip this
}
gchar *track_uuid = koto_indexed_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
} else { // Should be removing
koto_playlist_remove_track_by_uuid(playlist, track_uuid); // Remove the track from the playlist
}
}
gtk_popover_popdown(GTK_POPOVER(self)); // Temporary to hopefully prevent a bork
}
void koto_add_remove_track_popover_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data) {
(void) carto;
KotoAddRemoveTrackPopover *self = user_data;
if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) {
return;
}
koto_add_remove_track_popover_add_playlist(self, playlist);
}
void koto_add_remove_track_popover_handle_playlist_removed(KotoCartographer *carto, gchar *playlist_uuid, gpointer user_data) {
(void) carto;
KotoAddRemoveTrackPopover *self = user_data;
if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) {
return;
}
koto_add_remove_track_popover_remove_playlist(self, playlist_uuid);
}
void koto_add_remove_track_popover_set_pointing_to_widget(KotoAddRemoveTrackPopover *self, GtkWidget *widget, GtkPositionType pos) {
if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) {
return;
}
if (!GTK_IS_WIDGET(widget)) { // Not a widget
return;
}
GtkWidget* existing_parent = gtk_widget_get_parent(GTK_WIDGET(self));
if (existing_parent != NULL) {
g_object_ref(GTK_WIDGET(self)); // Increment widget ref since unparent will do an unref
gtk_widget_unparent(GTK_WIDGET(self)); // Unparent the popup
}
gtk_widget_insert_after(GTK_WIDGET(self), widget, gtk_widget_get_last_child(widget));
gtk_popover_set_position(GTK_POPOVER(self), pos);
}
void koto_add_remove_track_popover_set_tracks(KotoAddRemoveTrackPopover *self, GList *tracks) {
if (!KOTO_JS_ADD_REMOVE_TRACK_POPOVER(self)) {
return;
}
gint tracks_len = g_list_length(tracks);
if (tracks_len == 0) { // No tracks
return;
}
self->tracks = g_list_copy(tracks);
GHashTable *playlists = koto_cartographer_get_playlists(koto_maps); // Get our playlists
GHashTableIter playlists_iter;
gpointer uuid, playlist_ptr;
g_hash_table_iter_init(&playlists_iter, playlists); // Init our HashTable iterator
while (g_hash_table_iter_next(&playlists_iter, &uuid, &playlist_ptr)) { // While we are iterating through our playlists
KotoPlaylist *playlist = playlist_ptr;
gboolean should_be_checked = FALSE;
if (tracks_len > 1) { // More than one track
GList *pos;
for (pos = self->tracks; pos != NULL; pos = pos->next) { // Iterate over our tracks
should_be_checked = (koto_playlist_get_position_of_track(playlist, pos->data) != -1);
if (!should_be_checked) { // Should not be checked
break;
}
}
} else {
KotoIndexedTrack *track = g_list_nth_data(self->tracks, 0); // Get the first track
if (KOTO_IS_INDEXED_TRACK(track)) {
gint pos = koto_playlist_get_position_of_track(playlist, track);
should_be_checked = (pos != -1);
}
}
GtkCheckButton *playlist_check = g_hash_table_lookup(self->playlist_uuid_to_checkbox, uuid); // Get the GtkCheckButton for this playlist
if (GTK_IS_CHECK_BUTTON(playlist_check)) { // Is a checkbox
gpointer sig_id_ptr = g_hash_table_lookup(self->checkbox_to_signal_ids, playlist_check);
gulong check_button_sig_id = GPOINTER_TO_UINT(sig_id_ptr);
g_signal_handler_block(playlist_check, check_button_sig_id); // Temporary ignore toggled signal, since set_active calls toggled
gtk_check_button_set_active(playlist_check, should_be_checked); // Set active to our should_be_checked bool
g_signal_handler_unblock(playlist_check, check_button_sig_id); // Unblock the signal
}
}
}
KotoAddRemoveTrackPopover* koto_add_remove_track_popover_new() {
return g_object_new(KOTO_TYPE_ADD_REMOVE_TRACK_POPOVER, NULL);
}

View file

@ -0,0 +1,48 @@
/* add-remove-track-popover.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 "../db/cartographer.h"
#include "playlist.h"
G_BEGIN_DECLS
/**
* Type Definition
**/
#define KOTO_TYPE_ADD_REMOVE_TRACK_POPOVER koto_add_remove_track_popover_get_type()
G_DECLARE_FINAL_TYPE(KotoAddRemoveTrackPopover, koto_add_remove_track_popover, KOTO, ADD_REMOVE_TRACK_POPOVER, GtkPopover);
#define KOTO_JS_ADD_REMOVE_TRACK_POPOVER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_ADD_REMOVE_TRACK_POPOVER))
/**
* Functions
**/
KotoAddRemoveTrackPopover* koto_add_remove_track_popover_new();
void koto_add_remove_track_popover_add_playlist(KotoAddRemoveTrackPopover *self, KotoPlaylist *playlist);
void koto_add_remove_track_popover_clear_tracks(KotoAddRemoveTrackPopover *self);
void koto_add_remove_track_popover_remove_playlist(KotoAddRemoveTrackPopover *self, gchar *playlist_uuid);
void koto_add_remove_track_popover_handle_checkbutton_toggle(GtkCheckButton *btn, gpointer user_data);
void koto_add_remove_track_popover_handle_playlist_added(KotoCartographer *carto, KotoPlaylist *playlist, gpointer user_data);
void koto_add_remove_track_popover_handle_playlist_removed(KotoCartographer *carto, gchar *playlist_uuid, gpointer user_data);
void koto_add_remove_track_popover_set_pointing_to_widget(KotoAddRemoveTrackPopover *self, GtkWidget *widget, GtkPositionType pos);
void koto_add_remove_track_popover_set_tracks(KotoAddRemoveTrackPopover *self, GList *tracks);
G_END_DECLS

View file

@ -1,99 +0,0 @@
/* create-dialog.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 <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
#include "../db/cartographer.h"
#include "../db/db.h"
#include "create-dialog.h"
extern GtkWindow *main_window;
struct _KotoCreatePlaylistDialog {
GObject parent_instance;
GtkWidget *content;
GtkWidget *playlist_image;
GtkFileChooserNative *playlist_file_chooser;
GtkWidget *name_entry;
};
G_DEFINE_TYPE(KotoCreatePlaylistDialog, koto_create_playlist_dialog, G_TYPE_OBJECT);
static void koto_create_playlist_dialog_class_init(KotoCreatePlaylistDialogClass *c) {
(void) c;
}
static void koto_create_playlist_dialog_init(KotoCreatePlaylistDialog *self) {
self->content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_halign(self->content, GTK_ALIGN_CENTER);
gtk_widget_set_valign(self->content, GTK_ALIGN_CENTER);
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(self->content));
GtkIconPaintable* audio_paintable = gtk_icon_theme_lookup_icon(default_icon_theme, "insert-image-symbolic", NULL, 96, scale_factor, GTK_TEXT_DIR_NONE, GTK_ICON_LOOKUP_PRELOAD);
if (GTK_IS_ICON_PAINTABLE(audio_paintable)) {
self->playlist_image = gtk_image_new_from_paintable(GDK_PAINTABLE(audio_paintable));
gtk_widget_set_size_request(self->playlist_image, 96, 96);
gtk_box_append(GTK_BOX(self->content), self->playlist_image); // Add our image to our content
GtkGesture *image_click_controller = gtk_gesture_click_new(); // Create a click gesture for the image clicking
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(image_click_controller), 1); // Only allow left click
g_signal_connect(GTK_EVENT_CONTROLLER(image_click_controller), "pressed", G_CALLBACK(koto_create_playlist_dialog_handle_image_click), self);
gtk_widget_add_controller(self->playlist_image, GTK_EVENT_CONTROLLER(image_click_controller));
}
}
self->playlist_file_chooser = gtk_file_chooser_native_new(
"Choose playlist image",
main_window,
GTK_FILE_CHOOSER_ACTION_OPEN,
"Choose",
"Cancel"
);
GtkFileFilter *image_filter = gtk_file_filter_new(); // Create our file filter
gtk_file_filter_add_pattern(image_filter, "image/*"); // Only allow for images
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(self->playlist_file_chooser), image_filter); // Only allow picking images
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(self->playlist_file_chooser), FALSE);
self->name_entry = gtk_entry_new(); // Create our text entry for the name of the playlist
gtk_entry_set_placeholder_text(GTK_ENTRY(self->name_entry), "Name of playlist");
gtk_entry_set_input_purpose(GTK_ENTRY(self->name_entry), GTK_INPUT_PURPOSE_NAME);
gtk_entry_set_input_hints(GTK_ENTRY(self->name_entry), GTK_INPUT_HINT_SPELLCHECK & GTK_INPUT_HINT_NO_EMOJI & GTK_INPUT_HINT_PRIVATE);
gtk_box_append(GTK_BOX(self->content), self->name_entry);
}
GtkWidget* koto_create_playlist_dialog_get_content(KotoCreatePlaylistDialog *self) {
return self->content;
}
void koto_create_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoCreatePlaylistDialog *self = user_data;
gtk_native_dialog_show(GTK_NATIVE_DIALOG(self->playlist_file_chooser)); // Show our file chooser
}
KotoCreatePlaylistDialog* koto_create_playlist_dialog_new() {
return g_object_new(KOTO_TYPE_CREATE_PLAYLIST_DIALOG, NULL);
}

View file

@ -0,0 +1,301 @@
/* create-modify-dialog.c
*
* Copyright 2021 Joshua Strobl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <glib-2.0/glib-object.h>
#include <gtk-4.0/gtk/gtk.h>
#include <magic.h>
#include "../db/cartographer.h"
#include "../playlist/playlist.h"
#include "../koto-utils.h"
#include "../koto-window.h"
#include "create-modify-dialog.h"
extern KotoCartographer *koto_maps;
extern KotoWindow *main_window;
enum {
PROP_DIALOG_0,
PROP_PLAYLIST_UUID,
N_PROPS
};
static GParamSpec *dialog_props[N_PROPS] = { NULL, };
struct _KotoCreateModifyPlaylistDialog {
GtkBox parent_instance;
GtkWidget *playlist_image;
GtkWidget *name_entry;
GtkWidget *create_button;
gchar *playlist_image_path;
gchar *playlist_uuid;
};
G_DEFINE_TYPE(KotoCreateModifyPlaylistDialog, koto_create_modify_playlist_dialog, GTK_TYPE_BOX);
KotoCreateModifyPlaylistDialog *playlist_create_modify_dialog;
static void koto_create_modify_playlist_dialog_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec);
static void koto_create_modify_playlist_dialog_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec);
static void koto_create_modify_playlist_dialog_class_init(KotoCreateModifyPlaylistDialogClass *c) {
GObjectClass *gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->set_property = koto_create_modify_playlist_dialog_set_property;
gobject_class->get_property = koto_create_modify_playlist_dialog_get_property;
dialog_props[PROP_PLAYLIST_UUID] = g_param_spec_string(
"playlist-uuid",
"Playlist UUID",
"Playlist UUID",
NULL,
G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY|G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_PROPS, dialog_props);
}
static void koto_create_modify_playlist_dialog_init(KotoCreateModifyPlaylistDialog *self) {
self->playlist_image_path = NULL;
gtk_widget_set_halign(GTK_WIDGET(self), GTK_ALIGN_CENTER);
gtk_widget_set_valign(GTK_WIDGET(self), GTK_ALIGN_CENTER);
self->playlist_image = gtk_image_new_from_icon_name("insert-image-symbolic");
gtk_image_set_pixel_size(GTK_IMAGE(self->playlist_image), 220);
gtk_widget_set_size_request(self->playlist_image, 220, 220);
gtk_box_append(GTK_BOX(self), self->playlist_image); // Add our image
GtkDropTarget *target = gtk_drop_target_new(G_TYPE_FILE, GDK_ACTION_COPY);
g_signal_connect(GTK_EVENT_CONTROLLER(target), "drop", G_CALLBACK(koto_create_modify_playlist_dialog_handle_drop), self);
gtk_widget_add_controller(self->playlist_image, GTK_EVENT_CONTROLLER(target));
GtkGesture *image_click_controller = gtk_gesture_click_new(); // Create a click gesture for the image clicking
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(image_click_controller), 1); // Only allow left click
g_signal_connect(GTK_EVENT_CONTROLLER(image_click_controller), "pressed", G_CALLBACK(koto_create_modify_playlist_dialog_handle_image_click), self);
gtk_widget_add_controller(self->playlist_image, GTK_EVENT_CONTROLLER(image_click_controller));
self->name_entry = gtk_entry_new(); // Create our text entry for the name of the playlist
gtk_entry_set_placeholder_text(GTK_ENTRY(self->name_entry), "Name of playlist");
gtk_entry_set_input_purpose(GTK_ENTRY(self->name_entry), GTK_INPUT_PURPOSE_NAME);
gtk_entry_set_input_hints(GTK_ENTRY(self->name_entry), GTK_INPUT_HINT_SPELLCHECK & GTK_INPUT_HINT_NO_EMOJI & GTK_INPUT_HINT_PRIVATE);
gtk_box_append(GTK_BOX(self), self->name_entry);
self->create_button = gtk_button_new_with_label("Create");
gtk_widget_add_css_class(self->create_button, "suggested-action");
g_signal_connect(self->create_button, "clicked", G_CALLBACK(koto_create_modify_playlist_dialog_handle_create_click), self);
gtk_box_append(GTK_BOX(self), self->create_button); // Add the create button
}
static void koto_create_modify_playlist_dialog_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec){
KotoCreateModifyPlaylistDialog *self = KOTO_CREATE_MODIFY_PLAYLIST_DIALOG(obj);
switch (prop_id) {
case PROP_PLAYLIST_UUID:
g_value_set_string(val, (self->playlist_uuid != NULL) ? g_strdup(self->playlist_uuid) : NULL);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_create_modify_playlist_dialog_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec) {
KotoCreateModifyPlaylistDialog *self = KOTO_CREATE_MODIFY_PLAYLIST_DIALOG(obj);
(void) self; (void) val;
switch (prop_id) {
case PROP_PLAYLIST_UUID:
// TODO: Implement
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
void koto_create_modify_playlist_dialog_handle_chooser_response(GtkNativeDialog *native, int response, gpointer user_data) {
if (response != GTK_RESPONSE_ACCEPT) { // Not accept
g_object_unref(native);
return;
}
KotoCreateModifyPlaylistDialog *self = user_data;
if (!KOTO_IS_CURRENT_MODIFY_PLAYLIST(self)) {
return;
}
GFile *file = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(native));
gchar *file_path = g_file_get_path(file); // Get the absolute path
if (file_path != NULL) {
self->playlist_image_path = g_strdup(file_path);
gtk_image_set_from_file(GTK_IMAGE(self->playlist_image), self->playlist_image_path); // Set the file path
g_free(file_path);
}
g_object_unref(file);
g_object_unref(native);
}
void koto_create_modify_playlist_dialog_handle_create_click(GtkButton *button, gpointer user_data) {
(void) button;
KotoCreateModifyPlaylistDialog *self = user_data;
if (!KOTO_IS_CURRENT_MODIFY_PLAYLIST(self)) {
return;
}
if (gtk_entry_get_text_length(GTK_ENTRY(self->name_entry)) == 0) { // No text
gtk_widget_grab_focus(GTK_WIDGET(self->name_entry)); // Select the name entry
return;
}
KotoPlaylist *playlist = NULL;
gboolean modify_existing_playlist = ((self->playlist_uuid != NULL) && (g_strcmp0(self->playlist_uuid, "") != 0));
if (modify_existing_playlist) { // Modifying an existing playlist
playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, self->playlist_uuid);
} else { // Creating a new playlist
playlist = koto_playlist_new(); // Create a new playlist with a new UUID
}
if (!KOTO_IS_PLAYLIST(playlist)) { // If this isn't a playlist
return;
}
koto_playlist_set_name(playlist, gtk_entry_buffer_get_text(gtk_entry_get_buffer(GTK_ENTRY(self->name_entry)))); // Set the name to the text we get from the entry buffer
koto_playlist_set_artwork(playlist, self->playlist_image_path); // Add the playlist path if any
koto_playlist_commit(playlist); // Save the playlist to the database
if (!modify_existing_playlist) { // Not a new playlist
koto_cartographer_add_playlist(koto_maps, playlist); // Add to cartographer
koto_playlist_mark_as_finalized(playlist); // Ensure our tracks loaded finalized signal is emitted for the new playlist
}
koto_create_modify_playlist_dialog_reset(self);
koto_window_hide_dialogs(main_window); // Hide the dialogs
}
gboolean koto_create_modify_playlist_dialog_handle_drop(GtkDropTarget *target, const GValue *val, double x, double y, gpointer user_data) {
(void) target; (void) x; (void) y;
if (!G_VALUE_HOLDS(val, G_TYPE_FILE)) { // Not a file
return FALSE;
}
KotoCreateModifyPlaylistDialog *self = user_data;
if (!KOTO_IS_CURRENT_MODIFY_PLAYLIST(self)) { // No dialog
return FALSE;
}
GFile *dropped_file = g_value_get_object(val); // Get the GValue
gchar *file_path = g_file_get_path(dropped_file); // Get the absolute path
g_object_unref(dropped_file); // Unref the file
if (file_path == NULL) {
return FALSE; // Failed to get the path so immediately return false
}
magic_t magic_cookie = magic_open(MAGIC_MIME);
if (magic_cookie == NULL) {
return FALSE;
}
if (magic_load(magic_cookie, NULL) != 0) {
goto cookie_closure;
}
const char *mime_type = magic_file(magic_cookie, file_path);
if ((mime_type != NULL) && g_str_has_prefix(mime_type, "image/")) { // Is an image
self->playlist_image_path = g_strdup(file_path);
gtk_image_set_from_file(GTK_IMAGE(self->playlist_image), self->playlist_image_path); // Set the file path
g_free(file_path);
return TRUE;
}
cookie_closure:
magic_close(magic_cookie);
return FALSE;
}
void koto_create_modify_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data) {
(void) gesture; (void) n_press; (void) x; (void) y;
KotoCreateModifyPlaylistDialog *self = user_data;
GtkFileChooserNative* chooser = koto_utils_create_image_file_chooser("Choose playlist image");
g_signal_connect(chooser, "response", G_CALLBACK(koto_create_modify_playlist_dialog_handle_chooser_response), self);
gtk_native_dialog_show(GTK_NATIVE_DIALOG(chooser)); // Show our file chooser
}
void koto_create_modify_playlist_dialog_reset(KotoCreateModifyPlaylistDialog *self) {
if (!KOTO_IS_CURRENT_MODIFY_PLAYLIST(self)) {
return;
}
gtk_entry_buffer_set_text(gtk_entry_get_buffer(GTK_ENTRY(self->name_entry)), "", -1); // Reset existing buffer to empty string
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");
}
void koto_create_modify_playlist_dialog_set_playlist_uuid(KotoCreateModifyPlaylistDialog *self, gchar *playlist_uuid) {
if ((playlist_uuid == NULL) || g_strcmp0(playlist_uuid, "") == 0) { // Is an empty string or not a string at all
return;
}
KotoPlaylist *playlist = koto_cartographer_get_playlist_by_uuid(koto_maps, playlist_uuid);
if (!KOTO_IS_PLAYLIST(playlist)) {
return;
}
self->playlist_uuid = g_strdup(playlist_uuid);
gtk_entry_buffer_set_text(gtk_entry_get_buffer(GTK_ENTRY(self->name_entry)), koto_playlist_get_name(playlist), -1); // Update the input buffer
gtk_entry_set_placeholder_text(GTK_ENTRY(self->name_entry), ""); // Clear placeholder
gchar *art = koto_playlist_get_artwork(playlist);
if ((art == NULL) || (g_strcmp0(art, "") == 0)) { // Is an empty string or not set
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);
g_free(art);
}
gtk_button_set_label(GTK_BUTTON(self->create_button), "Save");
}
KotoCreateModifyPlaylistDialog* koto_create_modify_playlist_dialog_new(char *playlist_uuid) {
(void) playlist_uuid;
return g_object_new(KOTO_TYPE_CREATE_MODIFY_PLAYLIST_DIALOG,
"orientation",
GTK_ORIENTATION_VERTICAL,
"spacing",
40,
NULL);
}

View file

@ -0,0 +1,44 @@
/* create-modify-dialog.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>
G_BEGIN_DECLS
/**
* Type Definition
**/
#define KOTO_TYPE_CREATE_MODIFY_PLAYLIST_DIALOG koto_create_modify_playlist_dialog_get_type()
G_DECLARE_FINAL_TYPE(KotoCreateModifyPlaylistDialog, koto_create_modify_playlist_dialog, KOTO, CREATE_MODIFY_PLAYLIST_DIALOG, GtkBox);
#define KOTO_IS_CURRENT_MODIFY_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_CREATE_MODIFY_PLAYLIST_DIALOG))
/**
* Functions
**/
KotoCreateModifyPlaylistDialog* koto_create_modify_playlist_dialog_new();
void koto_create_modify_playlist_dialog_handle_chooser_response(GtkNativeDialog *native, int response, gpointer user_data);
void koto_create_modify_playlist_dialog_handle_create_click(GtkButton *button, gpointer user_data);
gboolean koto_create_modify_playlist_dialog_handle_drop(GtkDropTarget *target, const GValue *val, double x, double y, gpointer user_data);
void koto_create_modify_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_create_modify_playlist_dialog_reset(KotoCreateModifyPlaylistDialog *self);
void koto_create_modify_playlist_dialog_set_playlist_uuid(KotoCreateModifyPlaylistDialog *self, gchar *playlist_uuid);
G_END_DECLS

View file

@ -112,6 +112,8 @@ void koto_current_playlist_set_playlist(KotoCurrentPlaylist *self, KotoPlaylist
}
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_object_notify_by_pspec(G_OBJECT(self), props[PROP_CURRENT_PLAYLIST]);
}

View file

@ -16,29 +16,16 @@
*/
#include <glib-2.0/glib.h>
#include <glib-2.0/gio/gio.h>
#include <magic.h>
#include <sqlite3.h>
#include "../db/cartographer.h"
#include "../koto-utils.h"
#include "playlist.h"
extern KotoCartographer *koto_maps;
extern sqlite3 *koto_db;
struct _KotoPlaylist {
GObject parent_instance;
gchar *uuid;
gchar *name;
gchar *art_path;
gint current_position;
gboolean ephemeral;
gboolean is_shuffle_enabled;
GQueue *tracks;
GQueue *played_tracks;
};
G_DEFINE_TYPE(KotoPlaylist, koto_playlist, G_TYPE_OBJECT);
enum {
PROP_0,
PROP_UUID,
@ -49,7 +36,47 @@ enum {
N_PROPERTIES,
};
enum {
SIGNAL_TRACK_ADDED,
SIGNAL_TRACK_LOAD_FINALIZED,
SIGNAL_TRACK_REMOVED,
N_SIGNALS
};
struct _KotoPlaylist {
GObject parent_instance;
gchar *uuid;
gchar *name;
gchar *art_path;
gint current_position;
gchar *current_uuid;
KotoPreferredModelType model;
gboolean ephemeral;
gboolean is_shuffle_enabled;
gboolean finalized;
GListStore *store;
GQueue *sorted_tracks;
GQueue *tracks; // This is effectively our vanilla value that should never change
GQueue *played_tracks;
};
struct _KotoPlaylistClass {
GObjectClass parent_class;
void (* track_added) (KotoPlaylist *playlist, gchar *track_uuid);
void (* track_load_finalized) (KotoPlaylist *playlist);
void (* track_removed) (KotoPlaylist *playlist, gchar *track_uuid);
};
G_DEFINE_TYPE(KotoPlaylist, koto_playlist, G_TYPE_OBJECT);
static GParamSpec *props[N_PROPERTIES] = { NULL };
static guint playlist_signals[N_SIGNALS] = { 0 };
static void koto_playlist_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec);;
static void koto_playlist_set_property(GObject *obj, guint prop_id, const GValue *val, GParamSpec *spec);
@ -100,6 +127,44 @@ static void koto_playlist_class_init(KotoPlaylistClass *c) {
);
g_object_class_install_properties(gobject_class, N_PROPERTIES, props);
playlist_signals[SIGNAL_TRACK_ADDED] = g_signal_new(
"track-added",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoPlaylistClass, track_added),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
playlist_signals[SIGNAL_TRACK_LOAD_FINALIZED] = g_signal_new(
"track-load-finalized",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoPlaylistClass, track_load_finalized),
NULL,
NULL,
NULL,
G_TYPE_NONE,
0
);
playlist_signals[SIGNAL_TRACK_REMOVED] = g_signal_new(
"track-removed",
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET(KotoPlaylistClass, track_removed),
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_CHAR
);
}
static void koto_playlist_get_property(GObject *obj, guint prop_id, GValue *val, GParamSpec *spec) {
@ -154,9 +219,16 @@ static void koto_playlist_set_property(GObject *obj, guint prop_id, const GValue
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->is_shuffle_enabled = FALSE;
self->played_tracks = g_queue_new(); // Set as an empty GQueue
self->ephemeral = FALSE;
self->finalized = FALSE;
self->tracks = g_queue_new(); // Set as an empty GQueue
self->played_tracks = g_queue_new(); // Set as an empty GQueue
self->sorted_tracks = g_queue_new(); // Set as an empty GQueue
self->store = g_list_store_new(KOTO_TYPE_INDEXED_TRACK);
}
void koto_playlist_add_to_played_tracks(KotoPlaylist *self, gchar *uuid) {
@ -167,17 +239,62 @@ void koto_playlist_add_to_played_tracks(KotoPlaylist *self, gchar *uuid) {
g_queue_push_tail(self->played_tracks, uuid); // Add to end
}
void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track) {
gchar *uuid = NULL;
g_object_get(track, "uuid", &uuid, NULL); // Get the UUID
koto_playlist_add_track_by_uuid(self, uuid); // Add by the file's UUID
void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track, gboolean current, gboolean commit_to_table) {
koto_playlist_add_track_by_uuid(self, koto_indexed_track_get_uuid(track), current, commit_to_table);
}
void koto_playlist_add_track_by_uuid(KotoPlaylist *self, const gchar *uuid) {
gchar *dup_uuid = g_strdup(uuid);
void koto_playlist_add_track_by_uuid(KotoPlaylist *self, gchar *uuid, gboolean current, gboolean commit_to_table) {
KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, uuid); // Get the track
g_queue_push_tail(self->tracks, dup_uuid); // Append the UUID to the tracks
// TODO: Add to table
if (!KOTO_IS_INDEXED_TRACK(track)) {
return;
}
GList *found_tracks_uuids = g_queue_find_custom(self->tracks, uuid, koto_playlist_compare_track_uuids);
if (found_tracks_uuids != NULL) { // Is somewhere in the tracks already
g_list_free(found_tracks_uuids);
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_list_store_append(self->store, track); // Add to the store
if (self->finalized) { // Is already finalized
koto_playlist_apply_model(self, self->model); // Re-apply our current model to ensure our "sorted tracks" is sorted, as is our GLIstStore used in the playlist page
}
if (commit_to_table) {
koto_indexed_track_save_to_playlist(track, self->uuid, (g_strcmp0(self->current_uuid, uuid) == 0) ? 1 : 0); // 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
}
g_signal_emit(
self,
playlist_signals[SIGNAL_TRACK_ADDED],
0,
uuid
);
}
void koto_playlist_apply_model(KotoPlaylist *self, KotoPreferredModelType preferred_model) {
GList *sort_user_data = NULL;
sort_user_data = g_list_prepend(sort_user_data, GUINT_TO_POINTER(preferred_model)); // Prepend our preferred model first
sort_user_data = g_list_prepend(sort_user_data, self); // Prepend ourself
g_queue_sort(self->sorted_tracks, koto_playlist_model_sort_by_uuid, sort_user_data); // Sort tracks, which is by UUID
g_list_store_sort(self->store, koto_playlist_model_sort_by_track, sort_user_data); // Sort tracks by indexed tracks
self->model = preferred_model; // Update our preferred model
/*if (self->current_position != -1) { // Have a position set
koto_playlist_set_track_as_current(self, self->current_uuid); // Update the position based on the new model just by setting it as current again
}*/
}
void koto_playlist_commit(KotoPlaylist *self) {
@ -186,8 +303,8 @@ void koto_playlist_commit(KotoPlaylist *self) {
}
gchar *commit_op = g_strdup_printf(
"INSERT INTO playlist_meta(id, name, art_path)"
"VALUES('%s', quote(\"%s\"), quote(\"%s\")"
"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;",
self->uuid,
self->name,
@ -215,22 +332,30 @@ void koto_playlist_commit_tracks(gpointer data, gpointer 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_indexed_track_save_to_playlist(track, playlist_uuid, g_queue_index(self->tracks, data), (data == current_track) ? 1 : 0); // Call to save the playlist to the track
//koto_indexed_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);
}
}
gint koto_playlist_compare_track_uuids(gconstpointer a, gconstpointer b) {
return g_strcmp0(a, b);
}
gchar* koto_playlist_get_artwork(KotoPlaylist *self) {
return g_strdup(self->art_path); // Return a duplicate of our art path
return (self->art_path == NULL) ? NULL : g_strdup(self->art_path); // Return a duplicate of our art path
}
KotoPreferredModelType koto_playlist_get_current_model(KotoPlaylist *self) {
return self->model;
}
guint koto_playlist_get_current_position(KotoPlaylist *self) {
return self->current_position;
}
gchar* koto_playlist_get_current_uuid(KotoPlaylist *self) {
return g_queue_peek_nth(self->tracks, self->current_position);
gboolean koto_playlist_get_is_finalized(KotoPlaylist *self) {
return self->finalized;
}
guint koto_playlist_get_length(KotoPlaylist *self) {
@ -241,12 +366,35 @@ gchar* koto_playlist_get_name(KotoPlaylist *self) {
return (self->name == NULL) ? NULL : g_strdup(self->name);
}
gint koto_playlist_get_position_of_track(KotoPlaylist *self, KotoIndexedTrack *track) {
if (!KOTO_IS_PLAYLIST(self)) {
return -1;
}
if (!G_IS_LIST_STORE(self->store)) {
return -1;
}
if (!KOTO_IS_INDEXED_TRACK(track)) {
return -1;
}
gint position = -1;
guint found_pos = 0;
if (g_list_store_find(self->store , track, &found_pos)) { // Found the item
position = (gint) found_pos; // Cast our found position from guint to gint
}
return position;
}
gchar* koto_playlist_get_random_track(KotoPlaylist *self) {
gchar *track_uuid = NULL;
guint tracks_len = g_queue_get_length(self->tracks);
guint tracks_len = g_queue_get_length(self->sorted_tracks);
if (tracks_len == g_queue_get_length(self->played_tracks)) { // Played all tracks
track_uuid = g_list_nth_data(self->tracks->head, 0); // Get the first
track_uuid = g_list_nth_data(self->sorted_tracks->head, 0); // Get the first
g_queue_clear(self->played_tracks); // Clear our played tracks
} else { // Have not played all tracks
GRand* rando_calrissian = g_rand_new(); // Create a new RNG
@ -255,7 +403,7 @@ gchar* koto_playlist_get_random_track(KotoPlaylist *self) {
while (track_uuid == NULL) { // Haven't selected a track yet
attempt++;
gint32 selected_item = g_rand_int_range(rando_calrissian, 0, (gint32) tracks_len);
gchar *selected_track = g_queue_peek_nth(self->tracks, (guint) selected_item); // Get the UUID of the selected item
gchar *selected_track = g_queue_peek_nth(self->sorted_tracks, (guint) selected_item); // Get the UUID of the selected item
if (g_queue_index(self->played_tracks, selected_track) == -1) { // Haven't played the track
self->current_position = (gint) selected_item;
@ -274,6 +422,10 @@ gchar* koto_playlist_get_random_track(KotoPlaylist *self) {
return track_uuid;
}
GListStore* koto_playlist_get_store(KotoPlaylist *self) {
return self->store;
}
GQueue* koto_playlist_get_tracks(KotoPlaylist *self) {
return self->tracks;
}
@ -283,22 +435,38 @@ gchar* koto_playlist_get_uuid(KotoPlaylist *self) {
}
gchar* koto_playlist_go_to_next(KotoPlaylist *self) {
if (!KOTO_IS_PLAYLIST(self)) {
return NULL;
}
if (self->is_shuffle_enabled) { // Shuffling enabled
gchar *random_track_uuid = koto_playlist_get_random_track(self); // Get a random track
koto_playlist_add_to_played_tracks(self, random_track_uuid);
return random_track_uuid;
}
gchar *current_uuid = koto_playlist_get_current_uuid(self); // Get the current UUID
if (self->current_uuid == NULL || (g_strcmp0(self->current_uuid, "") == 0)) {
self->current_position++;
} else { // Have a UUID currently
KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid);
if (current_uuid == self->tracks->tail->data) { // Current UUID matches the last item in the playlist
return NULL;
if (!KOTO_IS_INDEXED_TRACK(track)) {
return NULL;
}
gint pos_of_song = koto_playlist_get_position_of_track(self, track); // Get the position of the current track based on the current model
if ((guint) pos_of_song == (g_queue_get_length(self->sorted_tracks) - 1)) { // At end
return NULL;
}
self->current_position = pos_of_song+1; // Increment our position based on position of song
}
self->current_position++; // Increment our position
current_uuid = koto_playlist_get_current_uuid(self); // Return the new UUID
koto_playlist_add_to_played_tracks(self, current_uuid);
return current_uuid;
self->current_uuid = g_queue_peek_nth(self->sorted_tracks, self->current_position);
koto_playlist_add_to_played_tracks(self, self->current_uuid);
return self->current_uuid;
}
gchar* koto_playlist_go_to_previous(KotoPlaylist *self) {
@ -306,45 +474,217 @@ gchar* koto_playlist_go_to_previous(KotoPlaylist *self) {
return koto_playlist_get_random_track(self); // Get a random track
}
gchar *current_uuid = koto_playlist_get_current_uuid(self); // Get the current UUID
if (current_uuid == self->tracks->head->data) { // Current UUID matches the first item in the playlist
if (self->current_uuid == NULL || (g_strcmp0(self->current_uuid, "") == 0)) {
return NULL;
}
self->current_position--; // Decrement our position
return koto_playlist_get_current_uuid(self); // Return the new UUID
KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid);
if (!KOTO_IS_INDEXED_TRACK(track)) {
return NULL;
}
gint pos_of_song = koto_playlist_get_position_of_track(self, track); // Get the position of the current track based on the current model
if (pos_of_song == 0) {
return NULL;
}
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);
return self->current_uuid;
}
void koto_playlist_mark_as_finalized(KotoPlaylist *self) {
if (self->finalized) { // Already finalized
return;
}
self->finalized = TRUE;
koto_playlist_apply_model(self, self->model); // Re-apply our model to enforce mass sort
g_signal_emit(
self,
playlist_signals[SIGNAL_TRACK_LOAD_FINALIZED],
0
);
}
gint koto_playlist_model_sort_by_uuid(gconstpointer first_item, gconstpointer second_item, gpointer data_list) {
KotoIndexedTrack *first_track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) first_item);
KotoIndexedTrack *second_track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) second_item);
return koto_playlist_model_sort_by_track(first_track, second_track, data_list);
}
gint koto_playlist_model_sort_by_track(gconstpointer first_item, gconstpointer second_item, gpointer data_list) {
KotoIndexedTrack *first_track = (KotoIndexedTrack*) first_item;
KotoIndexedTrack *second_track = (KotoIndexedTrack*) second_item;
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
if (
(model == KOTO_PREFERRED_MODEL_TYPE_DEFAULT) || // Newest first model
(model == KOTO_PREFERRED_MODEL_TYPE_OLDEST_FIRST) // Oldest first
) {
gint first_track_pos = g_queue_index(self->tracks, koto_indexed_track_get_uuid(first_track));
gint second_track_pos = g_queue_index(self->tracks, koto_indexed_track_get_uuid(second_track));
if (first_track_pos == -1) { // First track isn't in tracks
return 1;
}
if (second_track_pos == -1) { // Second track isn't in tracks
return -1;
}
if (model == KOTO_PREFERRED_MODEL_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
gchar *first_album_uuid = NULL;
gchar *second_album_uuid = NULL;
g_object_get(
first_track,
"album-uuid",
&first_album_uuid,
NULL
);
g_object_get(
second_track,
"album-uuid",
&second_album_uuid,
NULL
);
if (g_strcmp0(first_album_uuid, second_album_uuid) == 0) { // Identical albums
g_free(first_album_uuid);
g_free(second_album_uuid);
return 0; // Don't get too granular, just consider them equal
}
KotoIndexedAlbum *first_album = koto_cartographer_get_album_by_uuid(koto_maps, first_album_uuid);
KotoIndexedAlbum *second_album = koto_cartographer_get_album_by_uuid(koto_maps, second_album_uuid);
g_free(first_album_uuid);
g_free(second_album_uuid);
if (!KOTO_IS_INDEXED_ALBUM(first_album) && !KOTO_IS_INDEXED_ALBUM(second_album)) { // Neither are valid albums
return 0; // Just consider them as equal
}
return g_utf8_collate(koto_indexed_album_get_album_name(first_album), koto_indexed_album_get_album_name(second_album));
}
if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_ARTIST) { // Sort by artist name
gchar *first_artist_uuid = NULL;
gchar *second_artist_uuid = NULL;
g_object_get(
first_track,
"artist-uuid",
&first_artist_uuid,
NULL
);
g_object_get(
second_track,
"artist-uuid",
&second_artist_uuid,
NULL
);
KotoIndexedArtist *first_artist = koto_cartographer_get_artist_by_uuid(koto_maps, first_artist_uuid);
KotoIndexedArtist *second_artist = koto_cartographer_get_artist_by_uuid(koto_maps, second_artist_uuid);
g_free(first_artist_uuid);
g_free(second_artist_uuid);
if (!KOTO_IS_INDEXED_ARTIST(first_artist) && !KOTO_IS_INDEXED_ARTIST(second_artist)) { // Neither are valid artists
return 0; // Just consider them as equal
}
return g_utf8_collate(koto_indexed_artist_get_name(first_artist), koto_indexed_artist_get_name(second_artist));
}
if (model == KOTO_PREFERRED_MODEL_TYPE_SORT_BY_TRACK_NAME) { // Track name
gchar *first_track_name = NULL;
gchar *second_track_name = NULL;
g_object_get(
first_track,
"parsed-name",
&first_track_name,
NULL
);
g_object_get(
second_track,
"parsed-name",
&second_track_name,
NULL
);
gint ret = g_utf8_collate(first_track_name, second_track_name);
g_free(first_track_name);
g_free(second_track_name);
return ret;
}
return 0;
}
void koto_playlist_remove_from_played_tracks(KotoPlaylist *self, gchar *uuid) {
g_queue_remove(self->played_tracks, uuid);
}
void koto_playlist_remove_track(KotoPlaylist *self, KotoIndexedTrack *track) {
gchar *track_uuid = NULL;
g_object_get(track, "uuid", &track_uuid, NULL);
if (track_uuid != NULL) {
koto_playlist_remove_track_by_uuid(self, track_uuid);
g_free(track_uuid);
}
return;
}
void koto_playlist_remove_track_by_uuid(KotoPlaylist *self, gchar *uuid) {
if (uuid == NULL) {
if (!KOTO_IS_PLAYLIST(self)) {
return;
}
gint file_index = g_queue_index(self->tracks, uuid); // Get the position of this uuid
if (file_index == -1) { // Does not exist in our tracks list
if (file_index != -1) { // Have in tracks
g_queue_pop_nth(self->tracks, file_index); // Remove nth where it is the file index
}
gint file_index_in_sorted = g_queue_index(self->sorted_tracks, uuid); // Get position in sorted tracks
if (file_index_in_sorted != -1) { // Have in sorted tracks
g_queue_pop_nth(self->sorted_tracks, file_index_in_sorted); // Remove nth where it is the index in sorted tracks
}
KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, uuid); // Get the track
if (!KOTO_IS_INDEXED_TRACK(track)) { // Is not a track
return;
}
g_queue_pop_nth(self->tracks, file_index); // Remove nth where it is the file index
return;
guint position = 0;
if (g_list_store_find(self->store, track, &position)) { // Got the position
g_list_store_remove(self->store, position); // Remove from the store
}
koto_indexed_track_remove_from_playlist(track, self->uuid);
g_signal_emit(
self,
playlist_signals[SIGNAL_TRACK_REMOVED],
0,
uuid
);
}
void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path) {
@ -376,11 +716,10 @@ void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path) {
free_cookie:
magic_close(cookie); // Close and free the cookie to the cookie monster
return;
}
void koto_playlist_set_name(KotoPlaylist *self, const gchar *name) {
if (name == NULL) { // No actual name
if (name == NULL) {
return;
}
@ -389,7 +728,16 @@ void koto_playlist_set_name(KotoPlaylist *self, const gchar *name) {
}
self->name = g_strdup(name);
return;
}
void koto_playlist_set_position(KotoPlaylist *self, gint position) {
self->current_position = position;
}
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
g_return_if_fail(position_of_track != -1);
self->current_position = position_of_track;
}
void koto_playlist_set_uuid(KotoPlaylist *self, const gchar *uuid) {
@ -402,7 +750,17 @@ void koto_playlist_set_uuid(KotoPlaylist *self, const gchar *uuid) {
}
self->uuid = g_strdup(uuid); // Set the new UUID
return;
}
void koto_playlist_tracks_queue_push_to_store(gpointer data, gpointer user_data) {
gchar *track_uuid = (gchar *) data;
KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, track_uuid);
if (!KOTO_IS_INDEXED_TRACK(track)) { // Not a track
return;
}
g_list_store_append(G_LIST_STORE(user_data), track);
}
void koto_playlist_unmap(KotoPlaylist *self) {

View file

@ -16,19 +16,34 @@
*/
#pragma once
#include <glib-2.0/glib-object.h>
#include <glib-2.0/glib.h>
#include <glib-2.0/gio/gio.h>
#include "../indexer/structs.h"
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;
/**
* Type Definition
**/
#define KOTO_TYPE_PLAYLIST koto_playlist_get_type()
G_DECLARE_FINAL_TYPE(KotoPlaylist, koto_playlist, KOTO, PLAYLIST, GObject);
#define KOTO_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), KOTO_TYPE_PLAYLIST, KotoPlaylist))
#define KOTO_IS_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), KOTO_TYPE_PLAYLIST))
typedef struct _KotoPlaylist KotoPlaylist;
typedef struct _KotoPlaylistClass KotoPlaylistClass;
GLIB_AVAILABLE_IN_ALL
GType koto_playlist_get_type(void) G_GNUC_CONST;
/**
* Playlist Functions
**/
@ -36,27 +51,36 @@ G_DECLARE_FINAL_TYPE(KotoPlaylist, koto_playlist, KOTO, PLAYLIST, GObject);
KotoPlaylist* koto_playlist_new();
KotoPlaylist* koto_playlist_new_with_uuid(const gchar *uuid);
void koto_playlist_add_to_played_tracks(KotoPlaylist *self, gchar *uuid);
void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track);
void koto_playlist_add_track_by_uuid(KotoPlaylist *self, const gchar *uuid);
void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track, gboolean current, 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);
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);
gchar* koto_playlist_get_artwork(KotoPlaylist *self);
KotoPreferredModelType koto_playlist_get_current_model(KotoPlaylist *self);
guint koto_playlist_get_current_position(KotoPlaylist *self);
gchar* koto_playlist_get_current_uuid(KotoPlaylist *self);
guint koto_playlist_get_length(KotoPlaylist *self);
gboolean koto_playlist_get_is_finalized(KotoPlaylist *self);
gchar* koto_playlist_get_name(KotoPlaylist *self);
gint koto_playlist_get_position_of_track(KotoPlaylist *self, KotoIndexedTrack *track);
GListStore* koto_playlist_get_store(KotoPlaylist *self);
GQueue* koto_playlist_get_tracks(KotoPlaylist *self);
gchar* koto_playlist_get_uuid(KotoPlaylist *self);
gchar* koto_playlist_go_to_next(KotoPlaylist *self);
gchar* koto_playlist_go_to_previous(KotoPlaylist *self);
void koto_playlist_mark_as_finalized(KotoPlaylist *self);
gint koto_playlist_model_sort_by_uuid(gconstpointer first_item, gconstpointer second_item, gpointer data_list);
gint koto_playlist_model_sort_by_track(gconstpointer first_item, gconstpointer second_item, gpointer model_ptr);
void koto_playlist_remove_from_played_tracks(KotoPlaylist *self, gchar *uuid);
void koto_playlist_remove_track(KotoPlaylist *self, KotoIndexedTrack *track);
void koto_playlist_remove_track_by_uuid(KotoPlaylist *self, gchar *uuid);
void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path);
void koto_playlist_save_state(KotoPlaylist *self);
void koto_playlist_set_name(KotoPlaylist *self, const gchar *name);
void koto_playlist_set_position(KotoPlaylist *self, guint pos);
void koto_playlist_set_position(KotoPlaylist *self, gint position);
void koto_playlist_set_track_as_current(KotoPlaylist *self, gchar *track_uuid);
void koto_playlist_set_uuid(KotoPlaylist *self, const gchar *uuid);
void koto_playlist_tracks_queue_push_to_store(gpointer data, gpointer user_data);
void koto_playlist_unmap(KotoPlaylist *self);
G_END_DECLS