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,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

@ -1,41 +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.
*/
#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_PLAYLIST_DIALOG koto_create_playlist_dialog_get_type()
G_DECLARE_FINAL_TYPE(KotoCreatePlaylistDialog, koto_create_playlist_dialog, KOTO, CREATE_PLAYLIST_DIALOG, GObject);
/**
* Create Dialog 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);
G_END_DECLS

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