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

4
.vscode/launch.json vendored
View file

@ -14,7 +14,7 @@
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"environment": [ "environment": [
{"name" : "G_MESSAGES_DEBUG", "value": "all" }, {"name" : "G_MESSAGES_DEBUG", "value": "all" },
{ "name": "GTK_THEME", "value": "Default:dark" } { "name": "GTK_THEME", "value": "Adwaita:dark" }
], ],
"externalConsole": false, "externalConsole": false,
"MIMode": "gdb", "MIMode": "gdb",
@ -37,7 +37,7 @@
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"environment": [ "environment": [
{"name" : "G_MESSAGES_DEBUG", "value": "all" }, {"name" : "G_MESSAGES_DEBUG", "value": "all" },
{ "name": "GTK_THEME", "value": "Default:dark" } { "name": "GTK_THEME", "value": "Adwaita:dark" }
], ],
"externalConsole": false, "externalConsole": false,
"linux": { "linux": {

View file

@ -1,5 +1,10 @@
{ {
"files.associations": { "files.associations": {
"glib.h": "c" "glib.h": "c",
"ios": "c",
"__node_handle": "c",
"gtk.h": "c",
"gtktreeview.h": "c",
"cartographer.h": "c"
} }
} }

View file

@ -4,6 +4,8 @@ Koto is an in-development audiobook, music, and podcast manager that is designed
## Blog ## Blog
- [Dev Diary 6: Koto April Progress Report (A-side)](https://joshuastrobl.com/2021/04/26/dev-diary-6-koto-april-progress-report-a-side/)
- [Dev Diary 5: Koto March Progress Report (B-side)](https://joshuastrobl.com/2021/04/08/dev-diary-5-koto-march-progress-report-b-side/)
- [Dev Diary 4: Koto March Progress Report (A-side)](https://joshuastrobl.com/2021/03/26/dev-diary-4-koto-march-progress-report-a-side/) - [Dev Diary 4: Koto March Progress Report (A-side)](https://joshuastrobl.com/2021/03/26/dev-diary-4-koto-march-progress-report-a-side/)
- [Dev Diary 3: Koto February Progress Report (B-side)](https://joshuastrobl.com/2021/03/05/dev-diary-3-koto-february-progress-report-b-side/) - [Dev Diary 3: Koto February Progress Report (B-side)](https://joshuastrobl.com/2021/03/05/dev-diary-3-koto-february-progress-report-b-side/)
- [Dev Diary 2: Koto February Progress Report (A-side)](https://joshuastrobl.com/2021/02/17/dev-diary-2-koto-february-progress-report-a-side/) - [Dev Diary 2: Koto February Progress Report (A-side)](https://joshuastrobl.com/2021/02/17/dev-diary-2-koto-february-progress-report-a-side/)

2631
jsc.cfg Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@ project('koto', 'c',
meson_version: '>= 0.57.0', meson_version: '>= 0.57.0',
default_options: [ 'warning_level=2', default_options: [ 'warning_level=2',
'c_std=gnu11', 'c_std=gnu11',
'werror=true',
], ],
) )

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 <glib-2.0/glib.h>
#include "cartographer.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 { struct _KotoCartographer {
GObject parent_instance; GObject parent_instance;
@ -27,12 +41,130 @@ struct _KotoCartographer {
GHashTable *tracks; 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); G_DEFINE_TYPE(KotoCartographer, koto_cartographer, G_TYPE_OBJECT);
KotoCartographer *koto_maps = NULL; KotoCartographer *koto_maps = NULL;
static void koto_cartographer_class_init(KotoCartographerClass *c) { 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) { static void koto_cartographer_init(KotoCartographer *self) {
@ -46,36 +178,80 @@ void koto_cartographer_add_album(KotoCartographer *self, KotoIndexedAlbum *album
gchar *album_uuid = NULL; gchar *album_uuid = NULL;
g_object_get(album, "uuid", &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 if ((album_uuid == NULL) || koto_cartographer_has_album_by_uuid(self, album_uuid)) { // Have the album or invalid UUID
g_hash_table_replace(self->albums, album_uuid, album); 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) { void koto_cartographer_add_artist(KotoCartographer *self, KotoIndexedArtist *artist) {
gchar *artist_uuid = NULL; gchar *artist_uuid = NULL;
g_object_get(artist, "uuid", &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 if ((artist_uuid == NULL) || koto_cartographer_has_artist_by_uuid(self, artist_uuid)) { // Have the artist or invalid UUID
g_hash_table_replace(self->artists, artist_uuid, artist); 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) { void koto_cartographer_add_playlist(KotoCartographer *self, KotoPlaylist *playlist) {
gchar *playlist_uuid = NULL; gchar *playlist_uuid = NULL;
g_object_get(playlist, "uuid", &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 if ((playlist_uuid == NULL) || koto_cartographer_has_playlist_by_uuid(self, playlist_uuid)) { // Have the playlist or invalid UUID
g_hash_table_replace(self->playlists, playlist_uuid, playlist); 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) { void koto_cartographer_add_track(KotoCartographer *self, KotoIndexedTrack *track) {
gchar *track_uuid = NULL; gchar *track_uuid = NULL;
g_object_get(track, "uuid", &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 if ((track_uuid == NULL) || koto_cartographer_has_track_by_uuid(self, track_uuid)) { // Have the track or invalid UUID
g_hash_table_replace(self->tracks, track_uuid, track); 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) { 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); 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) { KotoPlaylist* koto_cartographer_get_playlist_by_uuid(KotoCartographer *self, gchar* playlist_uuid) {
return g_hash_table_lookup(self->playlists, 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) { void koto_cartographer_remove_album(KotoCartographer *self, KotoIndexedAlbum *album) {
gchar *album_uuid = NULL; gchar *album_uuid = NULL;
g_object_get(album, "uuid", &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) { void koto_cartographer_remove_album_by_uuid(KotoCartographer *self, gchar* album_uuid) {
if (album_uuid != NULL) { if (album_uuid != NULL) {
g_hash_table_remove(self->albums, album_uuid); 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) { void koto_cartographer_remove_artist(KotoCartographer *self, KotoIndexedArtist *artist) {
gchar *artist_uuid = NULL; gchar *artist_uuid = NULL;
g_object_get(artist, "uuid", &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) { void koto_cartographer_remove_artist_by_uuid(KotoCartographer *self, gchar* artist_uuid) {
if (artist_uuid == NULL) { if (artist_uuid == NULL) {
g_hash_table_remove(self->artists, artist_uuid); 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) { void koto_cartographer_remove_playlist(KotoCartographer *self, KotoPlaylist *playlist) {
gchar *playlist_uuid = NULL; gchar *playlist_uuid = NULL;
g_object_get(playlist, "uuid", &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) { void koto_cartographer_remove_playlist_by_uuid(KotoCartographer *self, gchar* playlist_uuid) {
if (playlist_uuid != NULL) { if (playlist_uuid != NULL) {
g_hash_table_remove(self->playlists, playlist_uuid); 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) { void koto_cartographer_remove_track(KotoCartographer *self, KotoIndexedTrack *track) {
gchar *track_uuid = NULL; gchar *track_uuid = NULL;
g_object_get(track, "uuid", &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) { void koto_cartographer_remove_track_by_uuid(KotoCartographer *self, gchar* track_uuid) {
if (track_uuid != NULL) { if (track_uuid != NULL) {
g_hash_table_remove(self->tracks, track_uuid); 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() { KotoCartographer* koto_cartographer_new() {

View file

@ -27,7 +27,12 @@ G_BEGIN_DECLS
**/ **/
#define KOTO_TYPE_CARTOGRAPHER koto_cartographer_get_type() #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 * 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_playlist(KotoCartographer *self, KotoPlaylist *playlist);
void koto_cartographer_add_track(KotoCartographer *self, KotoIndexedTrack *track); 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); KotoIndexedAlbum* koto_cartographer_get_album_by_uuid(KotoCartographer *self, gchar* album_uuid);
KotoIndexedArtist* koto_cartographer_get_artist_by_uuid(KotoCartographer *self, gchar* artist_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); 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); KotoIndexedTrack* koto_cartographer_get_track_by_uuid(KotoCartographer *self, gchar* track_uuid);
gboolean koto_cartographer_has_album(KotoCartographer *self, KotoIndexedAlbum *album); 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);" 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 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 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_meta(id string UNIQUE PRIMARY KEY, name string, art_path string, preferred_model int);"
"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_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; gchar *create_tables_errmsg = NULL;
int rc = sqlite3_exec(koto_db, tables_creation_queries, 0,0, &create_tables_errmsg); 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) { 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) { GList* koto_indexed_album_get_tracks(KotoIndexedAlbum *self) {
if (!KOTO_IS_INDEXED_ALBUM(self)) { // Not an album
return NULL;
}
return self->tracks; // Return return self->tracks; // Return
} }
void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album_art) { 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 if (album_art == NULL) { // Not valid album art
return; 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) { 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 if (track == NULL) { // Not a file
return; 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) { 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 if (album_name == NULL) { // Not valid album name
return; 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) { 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) { if (artist_uuid == NULL) {
return; 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) { 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 if (self->tracks == NULL) { // No files to add to the playlist
return; 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 KotoPlaylist *new_album_playlist = koto_playlist_new(); // Create a new playlist
g_object_set(new_album_playlist, "ephemeral", TRUE, NULL); // Set as ephemeral / temporary 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; GList *t;
for (t = self->tracks; t != NULL; t = t->next) { // For each of the tracks for (t = reversed_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 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 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) { 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; 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); g_free(self->path);
} }
self->path = g_strdup(new_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 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 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) { 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; return;
} }
@ -163,11 +167,27 @@ void koto_indexed_artist_add_album(KotoIndexedArtist *self, gchar *album_uuid) {
} }
GList* koto_indexed_artist_get_albums(KotoIndexedArtist *self) { 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); 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) { 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; 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) { 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; 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 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) { 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; 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); g_free(self->artist_name);
} }

View file

@ -22,6 +22,7 @@
#include <taglib/tag_c.h> #include <taglib/tag_c.h>
#include "../db/cartographer.h" #include "../db/cartographer.h"
#include "../db/db.h" #include "../db/db.h"
#include "../playlist/playlist.h"
#include "../koto-utils.h" #include "../koto-utils.h"
#include "structs.h" #include "structs.h"
@ -242,6 +243,57 @@ int process_albums(void *data, int num_columns, char **fields, char **column_nam
return 0; 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) { int process_tracks(void *data, int num_columns, char **fields, char **column_names) {
(void) num_columns; (void) column_names; // Don't need these (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); 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) { 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 KotoIndexedArtist *artist = koto_indexed_library_get_artist(self, artist_name); // Get the artist
if (artist == NULL) { if (artist == NULL) {
g_message("Failed to get artist by name of: %s", artist_name);
continue; 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 output_track(gpointer data, gpointer user_data) {
(void) 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); KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, (gchar*) data);
if (track == NULL) { if (track == NULL) {

View file

@ -27,12 +27,15 @@ G_BEGIN_DECLS
#define KOTO_TYPE_INDEXED_LIBRARY koto_indexed_library_get_type() #define KOTO_TYPE_INDEXED_LIBRARY koto_indexed_library_get_type()
G_DECLARE_FINAL_TYPE(KotoIndexedLibrary, koto_indexed_library, KOTO, INDEXED_LIBRARY, GObject); 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() #define KOTO_TYPE_INDEXED_ARTIST koto_indexed_artist_get_type()
G_DECLARE_FINAL_TYPE (KotoIndexedArtist, koto_indexed_artist, KOTO, INDEXED_ARTIST, GObject); 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() #define KOTO_TYPE_INDEXED_ALBUM koto_indexed_album_get_type()
G_DECLARE_FINAL_TYPE (KotoIndexedAlbum, koto_indexed_album, KOTO, INDEXED_ALBUM, GObject); 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() #define KOTO_TYPE_INDEXED_TRACK koto_indexed_track_get_type()
G_DECLARE_FINAL_TYPE(KotoIndexedTrack, koto_indexed_track, KOTO, INDEXED_TRACK, GObject); 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); 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_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_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); int process_tracks(void *data, int num_columns, char **fields, char **column_names);
void read_from_db(KotoIndexedLibrary *self); void read_from_db(KotoIndexedLibrary *self);
void start_indexing(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); void koto_indexed_artist_commit(KotoIndexedArtist *self);
guint koto_indexed_artist_find_album_with_name(gconstpointer *album_data, gconstpointer *album_name_data); guint koto_indexed_artist_find_album_with_name(gconstpointer *album_data, gconstpointer *album_name_data);
GList* koto_indexed_artist_get_albums(KotoIndexedArtist *self); 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(KotoIndexedArtist *self, KotoIndexedAlbum *album);
void koto_indexed_artist_remove_album_by_name(KotoIndexedArtist *self, gchar *album_name); 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); 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_album_art(KotoIndexedAlbum *self);
void koto_indexed_album_find_tracks(KotoIndexedAlbum *self, magic_t magic_cookie, const gchar *path); 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_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); GList* koto_indexed_album_get_tracks(KotoIndexedAlbum *self);
void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *track); void koto_indexed_album_remove_file(KotoIndexedAlbum *self, KotoIndexedTrack *track);
void koto_indexed_album_set_album_art(KotoIndexedAlbum *self, const gchar *album_art); 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); KotoIndexedTrack* koto_indexed_track_new_with_uuid(const gchar *uuid);
void koto_indexed_track_commit(KotoIndexedTrack *self); 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_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_file_name(KotoIndexedTrack *self, gchar *new_file_name);
void koto_indexed_track_set_cd(KotoIndexedTrack *self, guint cd); void koto_indexed_track_set_cd(KotoIndexedTrack *self, guint cd);
void koto_indexed_track_set_parsed_name(KotoIndexedTrack *self, gchar *new_parsed_name); 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); 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) { void koto_indexed_track_parse_name(KotoIndexedTrack *self) {
gchar *copied_file_name = g_strdelimit(g_strdup(self->file_name), "_", ' '); // Replace _ with whitespace for starters 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); 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( gchar *commit_op = g_strdup_printf(
"INSERT INTO playlist_tracks(playlist_id, track_id, position, current)" "DELETE FROM playlist_tracks WHERE track_id='%s' AND playlist_id='%s'",
"VALUES('%s', '%s', quote(\"%d\"), quote(\"%d\")" self->uuid,
"ON CONFLICT(playlist_id, track_id) DO UPDATE SET position=excluded.position, current=excluded.current;", 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, playlist_uuid,
self->uuid, self->uuid,
position,
current current
); );

View file

@ -51,10 +51,11 @@ guint koto_get_pixbuf_size(KotoButtonPixbufSize s) {
enum { enum {
PROP_BTN_0, PROP_BTN_0,
PROP_USE_FROM_FILE,
PROP_PIX_SIZE, PROP_PIX_SIZE,
PROP_TEXT, PROP_TEXT,
PROP_BADGE_TEXT, PROP_BADGE_TEXT,
PROP_USE_FROM_FILE,
PROP_IMAGE_FILE_PATH,
PROP_ICON_NAME, PROP_ICON_NAME,
PROP_ALT_ICON_NAME, PROP_ALT_ICON_NAME,
N_BTN_PROPERTIES N_BTN_PROPERTIES
@ -70,11 +71,16 @@ struct _KotoButton {
GtkWidget *badge_label; GtkWidget *badge_label;
GtkWidget *button_label; GtkWidget *button_label;
GtkGesture *left_click_gesture;
GtkGesture *right_click_gesture;
gchar *image_file_path;
gchar *badge_text; gchar *badge_text;
gchar *icon_name; gchar *icon_name;
gchar *alt_icon_name; gchar *alt_icon_name;
gchar *text; gchar *text;
KotoButtonImagePosition image_position;
gboolean use_from_file; gboolean use_from_file;
gboolean currently_showing_alt; 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->set_property = koto_button_set_property;
gobject_class->get_property = koto_button_get_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( btn_props[PROP_PIX_SIZE] = g_param_spec_uint(
"pixbuf-size", "pixbuf-size",
"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 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( btn_props[PROP_ICON_NAME] = g_param_spec_string(
"icon-name", "icon-name",
"Icon Name", "Icon Name",
@ -151,6 +165,16 @@ static void koto_button_class_init(KotoButtonClass *c) {
static void koto_button_init(KotoButton *self) { static void koto_button_init(KotoButton *self) {
self->currently_showing_alt = FALSE; 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) { 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); KotoButton *self = KOTO_BUTTON(obj);
switch (prop_id) { switch (prop_id) {
case PROP_IMAGE_FILE_PATH:
g_value_set_string(val, self->image_file_path);
break;
case PROP_USE_FROM_FILE: case PROP_USE_FROM_FILE:
g_value_set_boolean(val, self->use_from_file); g_value_set_boolean(val, self->use_from_file);
break; break;
@ -193,23 +220,24 @@ static void koto_button_set_property(GObject *obj, guint prop_id, const GValue *
KotoButton *self = KOTO_BUTTON(obj); KotoButton *self = KOTO_BUTTON(obj);
switch (prop_id) { switch (prop_id) {
case PROP_USE_FROM_FILE:
self->use_from_file = g_value_get_boolean(val);
break;
case PROP_PIX_SIZE: case PROP_PIX_SIZE:
koto_button_set_pixbuf_size(self, g_value_get_uint(val)); koto_button_set_pixbuf_size(self, g_value_get_uint(val));
break; break;
case PROP_TEXT: case PROP_TEXT:
if (val == NULL) { if (val != NULL) {
koto_button_set_text(self, NULL); koto_button_set_text(self, (gchar*) g_value_get_string(val));
} else {
koto_button_set_text(self, g_strdup(g_value_get_string(val)));
} }
break; break;
case PROP_BADGE_TEXT: case PROP_BADGE_TEXT:
koto_button_set_badge_text(self, g_strdup(g_value_get_string(val))); koto_button_set_badge_text(self, g_strdup(g_value_get_string(val)));
break; 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: case PROP_ICON_NAME:
koto_button_set_icon_name(self, g_strdup(g_value_get_string(val)), FALSE); koto_button_set_icon_name(self, g_strdup(g_value_get_string(val)), FALSE);
if (!self->currently_showing_alt) { // Not showing alt 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) { void koto_button_flip(KotoButton *self) {
if (!KOTO_IS_BUTTON(self)) {
return;
}
koto_button_show_image(self, !self->currently_showing_alt); 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) { void koto_button_set_badge_text(KotoButton *self, gchar *text) {
if ((text == NULL) || (strcmp(text, "") == 0)) { // If the text is empty if ((text == NULL) || (strcmp(text, "") == 0)) { // If the text is empty
self->badge_text = g_strdup(""); 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]); 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) { void koto_button_set_icon_name(KotoButton *self, gchar *icon_name, gboolean for_alt) {
gchar *copied_icon_name = g_strdup(icon_name); 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]); 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) { 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 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; return;
} }
gchar *copied_text = g_strdup(text); // Copy our text if (self->text != NULL) { // Text defined
if (strcmp(copied_text, "") == 0) { // Clearing our text
g_free(self->text); // Free existing text 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 (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_label_set_text(GTK_LABEL(self->button_label), self->text);
gtk_widget_show(self->button_label); // Show the label gtk_widget_show(self->button_label); // Show the label
} else { // Have a label but no longer text } 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); g_free(self->button_label);
} }
} else { // If we do not have a 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 self->button_label = gtk_label_new(self->text); // Create our label
gtk_label_set_xalign(GTK_LABEL(self->button_label), 0); 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; return;
} }
if (self->use_from_file) { // Use from a file instead of icon name
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 if (use_alt && ((self->alt_icon_name == NULL) || (strcmp(self->alt_icon_name, "") == 0))) { // Don't have an alt icon set
return; return;
} else if (!use_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Don't have icon set } else if (!use_alt && ((self->icon_name == NULL) || (strcmp(self->icon_name, "") == 0))) { // Don't have icon set
return; return;
} }
if (self->use_from_file) { // Use from a file instead of icon name
// TODO: Add
} else { // From icon name
self->currently_showing_alt = use_alt; self->currently_showing_alt = use_alt;
gchar *name = use_alt ? self->alt_icon_name : self->icon_name; 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 gtk_image_set_from_icon_name(GTK_IMAGE(self->button_pic), name); // Just update the existing iamge
} else { // Not an image } else { // Not an image
self->button_pic = gtk_image_new_from_icon_name(name); // Get our new 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_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) { 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 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 G_BEGIN_DECLS
typedef enum {
KOTO_BUTTON_CLICK_TYPE_PRIMARY = 1,
KOTO_BUTTON_CLICK_TYPE_SECONDARY = 3
} KotoButtonClickType;
typedef enum { typedef enum {
KOTO_BUTTON_PIXBUF_SIZE_INVALID, KOTO_BUTTON_PIXBUF_SIZE_INVALID,
KOTO_BUTTON_PIXBUF_SIZE_TINY, KOTO_BUTTON_PIXBUF_SIZE_TINY,
@ -33,6 +38,11 @@ typedef enum {
KOTO_BUTTON_PIXBUF_SIZE_GODLIKE KOTO_BUTTON_PIXBUF_SIZE_GODLIKE
} KotoButtonPixbufSize; } KotoButtonPixbufSize;
typedef enum {
KOTO_BUTTON_IMAGE_POS_LEFT,
KOTO_BUTTON_IMAGE_POS_RIGHT
} KotoButtonImagePosition;
#define NUM_BUILTIN_SIZES 7 #define NUM_BUILTIN_SIZES 7
#define KOTO_TYPE_BUTTON (koto_button_get_type()) #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_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_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_flip(KotoButton *self);
void koto_button_hide_image(KotoButton *self);
void koto_button_set_badge_text(KotoButton *self, gchar *text); 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_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(KotoButton *self, GdkPixbuf *pix);
void koto_button_set_pixbuf_size(KotoButton *self, guint size); void koto_button_set_pixbuf_size(KotoButton *self, guint size);
void koto_button_set_text(KotoButton *self, gchar *text); void koto_button_set_text(KotoButton *self, gchar *text);
void koto_button_show_image(KotoButton *self, gboolean use_alt); void koto_button_show_image(KotoButton *self, gboolean use_alt);
void koto_button_unflatten(KotoButton *self);
G_END_DECLS 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 * Copyright 2021 Joshua Strobl
* *
@ -25,17 +25,18 @@ G_BEGIN_DECLS
* Type Definition * Type Definition
**/ **/
#define KOTO_TYPE_CREATE_PLAYLIST_DIALOG koto_create_playlist_dialog_get_type() #define KOTO_TYPE_DIALOG_CONTAINER koto_dialog_container_get_type()
G_DECLARE_FINAL_TYPE(KotoCreatePlaylistDialog, koto_create_playlist_dialog, KOTO, CREATE_PLAYLIST_DIALOG, GObject); 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(); KotoDialogContainer* koto_dialog_container_new();
GtkWidget* koto_create_playlist_dialog_get_content(KotoCreatePlaylistDialog *self); void koto_dialog_container_add_dialog(KotoDialogContainer *self, gchar *dialog_name, GtkWidget *dialog);
void koto_create_playlist_dialog_handle_close(KotoCreatePlaylistDialog *self); void koto_dialog_container_handle_close_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data);
void koto_create_playlist_dialog_handle_create(KotoCreatePlaylistDialog *self); void koto_dialog_container_hide(KotoDialogContainer *self);
void koto_create_playlist_dialog_handle_image_click(GtkGestureClick *gesture, int n_press, double x, double y, gpointer user_data); void koto_dialog_container_show_dialog(KotoDialogContainer *self, gchar *dialog_name);
G_END_DECLS 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); KotoExpander *self = KOTO_EXPANDER(obj);
if (!GTK_IS_WIDGET(self->header_button)) { // Header Button is not a widget 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 if (GTK_IS_WIDGET(new_button)) { // Created our widget successfully
self->header_button = new_button; self->header_button = new_button;
@ -175,9 +175,7 @@ static void koto_expander_init(KotoExpander *self) {
self->constructed = TRUE; self->constructed = TRUE;
GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick koto_button_add_click_handler(self->header_expand_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_expander_toggle_content), self);
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));
} }
void koto_expander_set_secondary_button(KotoExpander *self, KotoButton *new_button) { 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]); 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 koto_expander_toggle_content(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data) {
(void) gesture; (void) n_press; (void) x; (void) y; (void) gesture; (void) n_press; (void) x; (void) y;
KotoExpander* self = data; KotoExpander* self = data;

View file

@ -23,11 +23,12 @@
G_BEGIN_DECLS G_BEGIN_DECLS
#define KOTO_TYPE_EXPANDER (koto_expander_get_type()) #define KOTO_TYPE_EXPANDER (koto_expander_get_type())
G_DECLARE_FINAL_TYPE (KotoExpander, koto_expander, KOTO, EXPANDER, GtkBox) 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(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); 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_icon_name(KotoExpander *self, const gchar *in);
void koto_expander_set_label(KotoExpander *self, const gchar *label); void koto_expander_set_label(KotoExpander *self, const gchar *label);
void koto_expander_set_secondary_button(KotoExpander *self, KotoButton *new_button); 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 <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h"
#include "indexer/structs.h"
#include "playlist/playlist.h"
#include "koto-config.h" #include "koto-config.h"
#include "koto-button.h" #include "koto-button.h"
#include "koto-expander.h" #include "koto-expander.h"
#include "koto-nav.h" #include "koto-nav.h"
#include "koto-window.h" #include "koto-window.h"
extern KotoCartographer *koto_maps;
extern KotoWindow *main_window; extern KotoWindow *main_window;
struct _KotoNav { struct _KotoNav {
@ -46,6 +50,10 @@ struct _KotoNav {
KotoButton *music_local; KotoButton *music_local;
KotoButton *music_radio; KotoButton *music_radio;
// Playlists
GHashTable *playlist_buttons;
// Podcasts // Podcasts
KotoButton *podcasts_local; KotoButton *podcasts_local;
@ -63,6 +71,7 @@ static void koto_nav_class_init(KotoNavClass *c) {
} }
static void koto_nav_init(KotoNav *self) { 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(); 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_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); 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_audiobooks_section(self);
koto_nav_create_music_section(self); koto_nav_create_music_section(self);
koto_nav_create_podcasts_section(self); koto_nav_create_podcasts_section(self);
koto_nav_create_playlist_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));
} }
void koto_nav_create_audiobooks_section(KotoNav *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)); gtk_box_append(GTK_BOX(new_content), GTK_WIDGET(self->music_radio));
koto_expander_set_content(m_expander, new_content); 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) { 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 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; (void) gesture; (void) n_press; (void) x; (void) y; (void) user_data;
g_message("plz"); koto_window_show_dialog(main_window, "create-modify-playlist");
koto_window_show_create_playlist_dialog(main_window); }
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) { GtkWidget* koto_nav_get_nav(KotoNav *self) {

View file

@ -17,6 +17,8 @@
#pragma once #pragma once
#include <gtk-4.0/gtk/gtk.h> #include <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h"
#include "indexer/structs.h"
G_BEGIN_DECLS G_BEGIN_DECLS
@ -27,8 +29,12 @@ G_DECLARE_FINAL_TYPE (KotoNav, koto_nav, KOTO, NAV, GObject)
KotoNav* koto_nav_new (void); KotoNav* koto_nav_new (void);
void koto_nav_create_audiobooks_section(KotoNav *self); void koto_nav_create_audiobooks_section(KotoNav *self);
void koto_nav_create_music_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_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_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); GtkWidget* koto_nav_get_nav(KotoNav *self);

View file

@ -18,6 +18,7 @@
#include <gstreamer-1.0/gst/gst.h> #include <gstreamer-1.0/gst/gst.h>
#include <gtk-4.0/gtk/gtk.h> #include <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h" #include "db/cartographer.h"
#include "playlist/add-remove-track-popover.h"
#include "playlist/current.h" #include "playlist/current.h"
#include "playlist/playlist.h" #include "playlist/playlist.h"
#include "playback/engine.h" #include "playback/engine.h"
@ -25,6 +26,7 @@
#include "koto-config.h" #include "koto-config.h"
#include "koto-playerbar.h" #include "koto-playerbar.h"
extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup;
extern KotoCurrentPlaylist *current_playlist; extern KotoCurrentPlaylist *current_playlist;
extern KotoCartographer *koto_maps; extern KotoCartographer *koto_maps;
extern KotoPlaybackEngine *playback_engine; extern KotoPlaybackEngine *playback_engine;
@ -82,7 +84,10 @@ static void koto_playerbar_class_init(KotoPlayerBarClass *c) {
static void koto_playerbar_constructed(GObject *obj) { static void koto_playerbar_constructed(GObject *obj) {
KotoPlayerBar *self = KOTO_PLAYERBAR(obj); KotoPlayerBar *self = KOTO_PLAYERBAR(obj);
self->main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 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 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_draw_value(GTK_SCALE(self->progress_bar), FALSE);
gtk_scale_set_digits(GTK_SCALE(self->progress_bar), 0); gtk_scale_set_digits(GTK_SCALE(self->progress_bar), 0);
gtk_range_set_increments(GTK_RANGE(self->progress_bar), 1, 1); 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, "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, "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, "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)); 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(); self->controls = gtk_center_box_new();
gtk_center_box_set_baseline_position(GTK_CENTER_BOX(self->controls), GTK_BASELINE_POSITION_CENTER); 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->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); 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_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)); 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)); gtk_image_set_from_paintable(GTK_IMAGE(bar->artwork), GDK_PAINTABLE(audio_paintable));
} else { // Not an image } else { // Not an image
bar->artwork = gtk_image_new_from_paintable(GDK_PAINTABLE(audio_paintable)); 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_widget_set_size_request(bar->artwork, 96, 96);
gtk_box_append(GTK_BOX(bar->playback_section), bar->artwork); 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"); bar->playback_title = gtk_label_new("Title");
gtk_label_set_xalign(GTK_LABEL(bar->playback_title), 0);
bar->playback_album = gtk_label_new("Album"); bar->playback_album = gtk_label_new("Album");
gtk_label_set_xalign(GTK_LABEL(bar->playback_album), 0);
bar->playback_artist = gtk_label_new("Artist"); 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_title));
gtk_box_append(GTK_BOX(bar->playback_details_section), GTK_WIDGET(bar->playback_album)); gtk_box_append(GTK_BOX(bar->playback_details_section), GTK_WIDGET(bar->playback_album));
@ -179,26 +193,17 @@ void koto_playerbar_create_primary_controls(KotoPlayerBar* bar) {
if (KOTO_IS_BUTTON(bar->back_button)) { if (KOTO_IS_BUTTON(bar->back_button)) {
gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->back_button)); gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->back_button));
koto_button_add_click_handler(bar->back_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_backwards), bar);
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));
} }
if (KOTO_IS_BUTTON(bar->play_pause_button)) { if (KOTO_IS_BUTTON(bar->play_pause_button)) {
gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->play_pause_button)); gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->play_pause_button));
koto_button_add_click_handler(bar->play_pause_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_play_pause), bar);
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));
} }
if (KOTO_IS_BUTTON(bar->forward_button)) { if (KOTO_IS_BUTTON(bar->forward_button)) {
gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->forward_button)); gtk_box_append(GTK_BOX(bar->primary_controls_section), GTK_WIDGET(bar->forward_button));
koto_button_add_click_handler(bar->forward_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_go_forwards), bar);
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));
} }
} }
@ -212,22 +217,17 @@ void koto_playerbar_create_secondary_controls(KotoPlayerBar* bar) {
if (KOTO_IS_BUTTON(bar->repeat_button)) { if (KOTO_IS_BUTTON(bar->repeat_button)) {
gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->repeat_button)); gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->repeat_button));
koto_button_add_click_handler(bar->repeat_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_track_repeat), bar);
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));
} }
if (KOTO_IS_BUTTON(bar->shuffle_button)) { if (KOTO_IS_BUTTON(bar->shuffle_button)) {
gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->shuffle_button)); gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(bar->shuffle_button));
koto_button_add_click_handler(bar->shuffle_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_playerbar_toggle_playlist_shuffle), bar);
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));
} }
if (KOTO_IS_BUTTON(bar->playlist_button)) { if (KOTO_IS_BUTTON(bar->playlist_button)) {
gtk_box_append(GTK_BOX(bar->secondary_controls_section), GTK_WIDGET(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)) { 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 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 koto_playerbar_handle_progressbar_gesture_begin(GtkGesture *gesture, GdkEventSequence *seq, gpointer data) {
(void) gesture; (void) seq; (void) gesture; (void) seq;
KotoPlayerBar *bar = data; 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); 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_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_playing(KotoPlaybackEngine *engine, gpointer user_data);
void koto_playerbar_handle_is_paused(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_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_begin(GtkGesture *gesture, GdkEventSequence *seq, gpointer data);
void koto_playerbar_handle_progressbar_gesture_end(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_handle_volume_button_change(GtkScaleButton *button, double value, gpointer user_data);
void koto_playerbar_reset_progressbar(KotoPlayerBar* bar); void koto_playerbar_reset_progressbar(KotoPlayerBar* bar);
void koto_playerbar_set_progressbar_duration(KotoPlayerBar* bar, gint64 duration); 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_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_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); 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 <gtk-4.0/gtk/gtk.h>
#include "indexer/structs.h"
#include "playlist/add-remove-track-popover.h"
#include "koto-button.h" #include "koto-button.h"
#include "koto-track-item.h" #include "koto-track-item.h"
extern KotoAddRemoveTrackPopover *koto_add_remove_track_popup;
struct _KotoTrackItem { struct _KotoTrackItem {
GtkBox parent_instance; GtkBox parent_instance;
KotoIndexedTrack *track; KotoIndexedTrack *track;
GtkWidget *track_label; GtkWidget *track_label;
KotoButton *add_to_playlist_button;
}; };
struct _KotoTrackItemClass { 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 self->track_label = gtk_label_new(NULL); // Create with no track name
gtk_label_set_xalign(GTK_LABEL(self->track_label), 0.0); 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_add_css_class(GTK_WIDGET(self), "track-item");
gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE); gtk_widget_set_hexpand(GTK_WIDGET(self), TRUE);
gtk_widget_set_hexpand(GTK_WIDGET(self->track_label), TRUE); gtk_widget_set_hexpand(GTK_WIDGET(self->track_label), TRUE);
gtk_box_prepend(GTK_BOX(self), self->track_label); 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) { 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) G_DECLARE_FINAL_TYPE(KotoTrackItem, koto_track_item, KOTO, TRACK_ITEM, GtkBox)
KotoTrackItem* koto_track_item_new(KotoIndexedTrack *track); 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); void koto_track_item_set_track(KotoTrackItem *self, KotoIndexedTrack *track);
G_END_DECLS G_END_DECLS

View file

@ -18,6 +18,25 @@
#include <glib-2.0/glib.h> #include <glib-2.0/glib.h>
#include <gtk-4.0/gtk/gtk.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* koto_utils_create_image_from_filepath(gchar *filepath, gchar *fallback_icon, guint width, guint height) {
GtkWidget* image = NULL; GtkWidget* image = NULL;
@ -68,7 +87,11 @@ gchar* koto_utils_get_filename_without_extension(gchar *filename) {
return stripped_file_name; 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 *cleaned_string = "";
gchar **split = g_strsplit(str, find, -1); // Split on find gchar **split = g_strsplit(str, find, -1); // Split on find

View file

@ -21,8 +21,10 @@
G_BEGIN_DECLS 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); 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); 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_replace_string_all(gchar *str, gchar *find, gchar *repl);
gchar* koto_utils_unquote_string(gchar *s); gchar* koto_utils_unquote_string(gchar *s);

View file

@ -16,17 +16,27 @@
*/ */
#include <gtk-4.0/gdk/x11/gdkx.h> #include <gtk-4.0/gdk/x11/gdkx.h>
#include "components/koto-action-bar.h"
#include "db/cartographer.h"
#include "indexer/structs.h" #include "indexer/structs.h"
#include "pages/music/music-local.h" #include "pages/music/music-local.h"
#include "pages/playlist/list.h"
#include "playback/engine.h" #include "playback/engine.h"
#include "playlist/add-remove-track-popover.h"
#include "playlist/current.h" #include "playlist/current.h"
#include "playlist/create-dialog.h" #include "playlist/create-modify-dialog.h"
#include "koto-config.h" #include "koto-config.h"
#include "koto-dialog-container.h"
#include "koto-nav.h" #include "koto-nav.h"
#include "koto-playerbar.h" #include "koto-playerbar.h"
#include "koto-window.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 KotoCurrentPlaylist *current_playlist;
extern KotoPageMusicLocal *music_local_page;
extern KotoPlaybackEngine *playback_engine; extern KotoPlaybackEngine *playback_engine;
struct _KotoWindow { struct _KotoWindow {
@ -34,7 +44,7 @@ struct _KotoWindow {
KotoIndexedLibrary *library; KotoIndexedLibrary *library;
KotoCurrentPlaylist *current_playlist; KotoCurrentPlaylist *current_playlist;
KotoCreatePlaylistDialog *playlist_create_dialog; KotoDialogContainer *dialogs;
GtkWidget *overlay; GtkWidget *overlay;
GtkWidget *header_bar; GtkWidget *header_bar;
@ -66,14 +76,18 @@ static void koto_window_init (KotoWindow *self) {
create_new_headerbar(self); // Create our headerbar create_new_headerbar(self); // Create our headerbar
self->overlay = gtk_overlay_new(); // Create our overlay 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); self->primary_layout = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(self->primary_layout, "primary-layout"); gtk_widget_add_css_class(self->primary_layout, "primary-layout");
gtk_widget_set_hexpand(self->primary_layout, TRUE); gtk_widget_set_hexpand(self->primary_layout, TRUE);
gtk_widget_set_vexpand(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_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); self->content_layout = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(self->content_layout, "content-layout"); 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); 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(); 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); GtkWidget *playerbar_main = koto_playerbar_get_main(self->player_bar);
gtk_box_append(GTK_BOX(self->primary_layout), playerbar_main); 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); 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) { void create_new_headerbar(KotoWindow *self) {
self->header_bar = gtk_header_bar_new(); self->header_bar = gtk_header_bar_new();
gtk_widget_add_css_class(self->header_bar, "hdr"); 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); 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) { void load_library(KotoWindow *self) {
KotoIndexedLibrary *lib = koto_indexed_library_new(g_get_user_special_dir(G_USER_DIRECTORY_MUSIC)); KotoIndexedLibrary *lib = koto_indexed_library_new(g_get_user_special_dir(G_USER_DIRECTORY_MUSIC));
if (lib != NULL) { if (lib != NULL) {
self->library = lib; 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 // TODO: Remove and do some fancy state loading
gtk_stack_add_named(GTK_STACK(self->pages), GTK_WIDGET(l), "music.local"); koto_window_add_page(self, "music.local", GTK_WIDGET(music_local_page));
gtk_stack_set_visible_child_name(GTK_STACK(self->pages), "music.local"); 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. 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); g_thread_exit(0);

View file

@ -18,6 +18,8 @@
#pragma once #pragma once
#include <gtk-4.0/gtk/gtk.h> #include <gtk-4.0/gtk/gtk.h>
#include "db/cartographer.h"
#include "playlist/playlist.h"
G_BEGIN_DECLS G_BEGIN_DECLS
@ -25,10 +27,15 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (KotoWindow, koto_window, KOTO, WINDOW, GtkApplicationWindow) G_DECLARE_FINAL_TYPE (KotoWindow, koto_window, KOTO, WINDOW, GtkApplicationWindow)
void koto_window_show_create_playlist_dialog(KotoWindow *self); void koto_window_add_page(KotoWindow *self, gchar *page_name, GtkWidget *page);
void koto_window_hide_create_playlist_dialog(KotoWindow *self); 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 create_new_headerbar(KotoWindow *self);
void handle_album_added();
void load_library(KotoWindow *self); void load_library(KotoWindow *self);
void set_optimal_default_window_size(KotoWindow *self); void set_optimal_default_window_size(KotoWindow *self);

View file

@ -1,6 +1,8 @@
add_project_arguments('-Db_sanitize=address', language: 'c') add_project_arguments('-Db_sanitize=address', language: 'c')
koto_sources = [ koto_sources = [
'components/koto-action-bar.c',
'components/koto-cover-art-button.c',
'db/cartographer.c', 'db/cartographer.c',
'db/db.c', 'db/db.c',
'indexer/album.c', 'indexer/album.c',
@ -11,15 +13,18 @@ koto_sources = [
'pages/music/artist-view.c', 'pages/music/artist-view.c',
'pages/music/disc-view.c', 'pages/music/disc-view.c',
'pages/music/music-local.c', 'pages/music/music-local.c',
'pages/playlist/list.c',
'playback/engine.c', 'playback/engine.c',
'playback/media-keys.c', 'playback/media-keys.c',
'playback/mimes.c', 'playback/mimes.c',
'playback/mpris.c', 'playback/mpris.c',
'playlist/create-dialog.c', 'playlist/add-remove-track-popover.c',
'playlist/create-modify-dialog.c',
'playlist/current.c', 'playlist/current.c',
'playlist/playlist.c', 'playlist/playlist.c',
'main.c', 'main.c',
'koto-button.c', 'koto-button.c',
'koto-dialog-container.c',
'koto-expander.c', 'koto-expander.c',
'koto-nav.c', 'koto-nav.c',
'koto-playerbar.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 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_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_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_add_css_class(self->discs, "discs-list");
gtk_widget_set_can_focus(self->discs, FALSE); 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_widget_set_size_request(self->discs, 600, -1);
gtk_box_append(GTK_BOX(self->main), self->album_tracks_box); // Add the tracks box to the art info combo box gtk_box_append(GTK_BOX(self->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); 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); gtk_widget_add_controller(self->album_overlay_container, motion_controller);
GtkGesture *controller = gtk_gesture_click_new(); // Create a new GtkGestureClick koto_button_add_click_handler(self->play_pause_button, KOTO_BUTTON_CLICK_TYPE_PRIMARY, G_CALLBACK(koto_album_view_toggle_album_playback), self);
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));
} }
GtkWidget* koto_album_view_get_main(KotoAlbumView *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); gtk_widget_set_halign(self->favorites_list, GTK_ALIGN_START);
self->album_list = gtk_flow_box_new(); // Create our list of our albums 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_flow_box_set_selection_mode(GTK_FLOW_BOX(self->album_list), GTK_SELECTION_NONE);
gtk_widget_add_css_class(self->album_list, "album-list"); gtk_widget_add_css_class(self->album_list, "album-list");

View file

@ -16,11 +16,13 @@
*/ */
#include <gtk-4.0/gtk/gtk.h> #include <gtk-4.0/gtk/gtk.h>
#include "../../components/koto-action-bar.h"
#include "../../db/cartographer.h" #include "../../db/cartographer.h"
#include "../../indexer/structs.h" #include "../../indexer/structs.h"
#include "../../koto-track-item.h" #include "../../koto-track-item.h"
#include "disc-view.h" #include "disc-view.h"
extern KotoActionBar *action_bar;
extern KotoCartographer *koto_maps; extern KotoCartographer *koto_maps;
struct _KotoDiscView { 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->header), self->label);
gtk_box_append(GTK_BOX(self), self->header); 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 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_list_box_set_selection_mode(GTK_LIST_BOX(self->list), GTK_SELECTION_MULTIPLE);
gtk_widget_add_css_class(self->list, "track-list"); 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_widget_set_size_request(self->list, 600, -1);
gtk_box_append(GTK_BOX(self), self->list); 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) { 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 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) { void koto_disc_view_set_disc_number(KotoDiscView *self, guint disc_number) {
if (disc_number == 0) { if (disc_number == 0) {
return; return;

View file

@ -28,6 +28,7 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE(KotoDiscView, koto_disc_view, KOTO, DISC_VIEW, GtkBox) G_DECLARE_FINAL_TYPE(KotoDiscView, koto_disc_view, KOTO, DISC_VIEW, GtkBox)
KotoDiscView* koto_disc_view_new(KotoIndexedAlbum *album, guint *disc); 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_list_tracks(gpointer data, gpointer selfptr);
void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album); void koto_disc_view_set_album(KotoDiscView *self, KotoIndexedAlbum *album);
void koto_disc_view_set_disc_label_visible(KotoDiscView *self, gboolean visible); void koto_disc_view_set_disc_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); 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_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_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); 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); 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 koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *row, gpointer data) {
(void) box; (void) box;
KotoPageMusicLocal *self = (KotoPageMusicLocal*) data; KotoPageMusicLocal *self = (KotoPageMusicLocal*) data;
@ -152,7 +180,7 @@ void koto_page_music_local_handle_artist_click(GtkListBox *box, GtkListBoxRow *r
gchar *artist_name; gchar *artist_name;
g_object_get(btn, "button-text", &artist_name, NULL); 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) { 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(); KotoPageMusicLocal* koto_page_music_local_new();
void koto_page_music_local_add_artist(KotoPageMusicLocal *self, KotoIndexedArtist *artist); 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_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); 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); 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 N_SIGNALS
}; };
static glong NS = 1000000000;
static guint playback_engine_signals[N_SIGNALS] = { 0 }; static guint playback_engine_signals[N_SIGNALS] = { 0 };
extern KotoCartographer *koto_maps; extern KotoCartographer *koto_maps;
@ -51,12 +50,16 @@ struct _KotoPlaybackEngine {
GstElement *suppress_video; GstElement *suppress_video;
GstBus *monitor; GstBus *monitor;
GstQuery *duration_query;
GstQuery *position_query;
KotoIndexedTrack *current_track; KotoIndexedTrack *current_track;
gboolean is_muted; gboolean is_muted;
gboolean is_repeat_enabled; gboolean is_repeat_enabled;
gboolean is_playing; gboolean is_playing;
gboolean is_playing_specific_track;
gboolean tick_duration_timer_running; gboolean tick_duration_timer_running;
gboolean tick_track_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); gst_bin_add(GST_BIN(self->player), self->playbin);
self->monitor = gst_bus_new(); // Get the bus for the 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)) { if (GST_IS_BUS(self->monitor)) {
gst_bus_add_watch(self->monitor, koto_playback_engine_monitor_changed, self); 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 gst_element_set_bus(self->player, self->monitor); // Set our bus to monitor changes
} }
self->is_muted = FALSE; self->is_muted = FALSE;
self->is_playing = FALSE;
self->is_playing_specific_track = FALSE;
self->is_repeat_enabled = FALSE; self->is_repeat_enabled = FALSE;
self->tick_duration_timer_running = FALSE; self->tick_duration_timer_running = FALSE;
self->tick_track_timer_running = FALSE; self->tick_track_timer_running = FALSE;
@ -219,6 +227,10 @@ void koto_playback_engine_backwards(KotoPlaybackEngine *self) {
return; 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)); 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 if (self->is_repeat_enabled) { // Is repeat enabled
koto_playback_engine_set_position(self, 0); // Set position back to 0 to repeat the track 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)); 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 koto_playback_engine_get_duration(KotoPlaybackEngine *self) {
gint64 duration = 0; gint64 duration = 0;
if (gst_element_query_duration(self->player, GST_FORMAT_TIME, &duration)) { if (gst_element_query(self->player, self->duration_query)) { // Able to query our duration
duration = duration / NS; // Divide by NS to get seconds gst_query_parse_duration(self->duration_query, NULL, &duration); // Get the duration
duration = duration / GST_SECOND; // Divide by NS to get seconds
} }
return duration; return duration;
} }
gint64 koto_playback_engine_get_progress(KotoPlaybackEngine *self) { gdouble koto_playback_engine_get_progress(KotoPlaybackEngine *self) {
gint64 progress = 0; gdouble progress = 0.0;
if (gst_element_query_position(self->player, GST_FORMAT_TIME, &progress)) { gint64 gstprog = 0;
progress = progress / NS; // Divide by NS to get seconds 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; return progress;
} }
GstState koto_playback_engine_get_state(KotoPlaybackEngine *self) { GstState koto_playback_engine_get_state(KotoPlaybackEngine *self) {
GstState current_state; return GST_STATE(self->player);
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;
} }
gboolean koto_playback_engine_get_track_repeat(KotoPlaybackEngine *self) { 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) { 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) { 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); KotoIndexedTrack* koto_playback_engine_get_current_track(KotoPlaybackEngine *self);
gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self); gint64 koto_playback_engine_get_duration(KotoPlaybackEngine *self);
GstState koto_playback_engine_get_state(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_repeat(KotoPlaybackEngine *self);
gboolean koto_playback_engine_get_track_shuffle(KotoPlaybackEngine *self); gboolean koto_playback_engine_get_track_shuffle(KotoPlaybackEngine *self);
void koto_playback_engine_mute(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; 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_ref(playlist); // Increment the reference
g_object_notify_by_pspec(G_OBJECT(self), props[PROP_CURRENT_PLAYLIST]); 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/glib.h>
#include <glib-2.0/gio/gio.h>
#include <magic.h> #include <magic.h>
#include <sqlite3.h> #include <sqlite3.h>
#include "../db/cartographer.h" #include "../db/cartographer.h"
#include "../koto-utils.h"
#include "playlist.h" #include "playlist.h"
extern KotoCartographer *koto_maps; extern KotoCartographer *koto_maps;
extern sqlite3 *koto_db; 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 { enum {
PROP_0, PROP_0,
PROP_UUID, PROP_UUID,
@ -49,7 +36,47 @@ enum {
N_PROPERTIES, 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 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_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); 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); 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) { 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) { static void koto_playlist_init(KotoPlaylist *self) {
self->current_position = -1; // Default to -1 so first time incrementing puts it at 0 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->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->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) { 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 g_queue_push_tail(self->played_tracks, uuid); // Add to end
} }
void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track) { void koto_playlist_add_track(KotoPlaylist *self, KotoIndexedTrack *track, gboolean current, gboolean commit_to_table) {
gchar *uuid = NULL; koto_playlist_add_track_by_uuid(self, koto_indexed_track_get_uuid(track), current, commit_to_table);
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_by_uuid(KotoPlaylist *self, const gchar *uuid) { void koto_playlist_add_track_by_uuid(KotoPlaylist *self, gchar *uuid, gboolean current, gboolean commit_to_table) {
gchar *dup_uuid = g_strdup(uuid); 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 if (!KOTO_IS_INDEXED_TRACK(track)) {
// TODO: Add to table 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) { void koto_playlist_commit(KotoPlaylist *self) {
@ -186,8 +303,8 @@ void koto_playlist_commit(KotoPlaylist *self) {
} }
gchar *commit_op = g_strdup_printf( gchar *commit_op = g_strdup_printf(
"INSERT INTO playlist_meta(id, name, art_path)" "INSERT INTO playlist_meta(id, name, art_path, preferred_model)"
"VALUES('%s', quote(\"%s\"), quote(\"%s\")" "VALUES('%s', quote(\"%s\"), quote(\"%s\"), 0)"
"ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;", "ON CONFLICT(id) DO UPDATE SET name=excluded.name, art_path=excluded.art_path;",
self->uuid, self->uuid,
self->name, 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 *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 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(playlist_uuid);
g_free(current_track); 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) { 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) { guint koto_playlist_get_current_position(KotoPlaylist *self) {
return self->current_position; return self->current_position;
} }
gchar* koto_playlist_get_current_uuid(KotoPlaylist *self) { gboolean koto_playlist_get_is_finalized(KotoPlaylist *self) {
return g_queue_peek_nth(self->tracks, self->current_position); return self->finalized;
} }
guint koto_playlist_get_length(KotoPlaylist *self) { 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); 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* koto_playlist_get_random_track(KotoPlaylist *self) {
gchar *track_uuid = NULL; 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 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 g_queue_clear(self->played_tracks); // Clear our played tracks
} else { // Have not played all tracks } else { // Have not played all tracks
GRand* rando_calrissian = g_rand_new(); // Create a new RNG 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 while (track_uuid == NULL) { // Haven't selected a track yet
attempt++; attempt++;
gint32 selected_item = g_rand_int_range(rando_calrissian, 0, (gint32) tracks_len); 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 if (g_queue_index(self->played_tracks, selected_track) == -1) { // Haven't played the track
self->current_position = (gint) selected_item; self->current_position = (gint) selected_item;
@ -274,6 +422,10 @@ gchar* koto_playlist_get_random_track(KotoPlaylist *self) {
return track_uuid; return track_uuid;
} }
GListStore* koto_playlist_get_store(KotoPlaylist *self) {
return self->store;
}
GQueue* koto_playlist_get_tracks(KotoPlaylist *self) { GQueue* koto_playlist_get_tracks(KotoPlaylist *self) {
return self->tracks; return self->tracks;
} }
@ -283,22 +435,38 @@ gchar* koto_playlist_get_uuid(KotoPlaylist *self) {
} }
gchar* koto_playlist_go_to_next(KotoPlaylist *self) { gchar* koto_playlist_go_to_next(KotoPlaylist *self) {
if (!KOTO_IS_PLAYLIST(self)) {
return NULL;
}
if (self->is_shuffle_enabled) { // Shuffling enabled if (self->is_shuffle_enabled) { // Shuffling enabled
gchar *random_track_uuid = koto_playlist_get_random_track(self); // Get a random track gchar *random_track_uuid = koto_playlist_get_random_track(self); // Get a random track
koto_playlist_add_to_played_tracks(self, random_track_uuid); koto_playlist_add_to_played_tracks(self, random_track_uuid);
return 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 if (!KOTO_IS_INDEXED_TRACK(track)) {
return NULL; return NULL;
} }
self->current_position++; // Increment our position gint pos_of_song = koto_playlist_get_position_of_track(self, track); // Get the position of the current track based on the current model
current_uuid = koto_playlist_get_current_uuid(self); // Return the new UUID
koto_playlist_add_to_played_tracks(self, current_uuid); if ((guint) pos_of_song == (g_queue_get_length(self->sorted_tracks) - 1)) { // At end
return current_uuid; return NULL;
}
self->current_position = pos_of_song+1; // Increment our position based on position of song
}
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) { 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 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 (self->current_uuid == NULL || (g_strcmp0(self->current_uuid, "") == 0)) {
if (current_uuid == self->tracks->head->data) { // Current UUID matches the first item in the playlist
return NULL; return NULL;
} }
self->current_position--; // Decrement our position KotoIndexedTrack *track = koto_cartographer_get_track_by_uuid(koto_maps, self->current_uuid);
return koto_playlist_get_current_uuid(self); // Return the new 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) { void koto_playlist_remove_from_played_tracks(KotoPlaylist *self, gchar *uuid) {
g_queue_remove(self->played_tracks, 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) { void koto_playlist_remove_track_by_uuid(KotoPlaylist *self, gchar *uuid) {
if (uuid == NULL) { if (!KOTO_IS_PLAYLIST(self)) {
return; return;
} }
gint file_index = g_queue_index(self->tracks, uuid); // Get the position of this uuid 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; return;
} }
g_queue_pop_nth(self->tracks, file_index); // Remove nth where it is the file index guint position = 0;
return;
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) { 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: free_cookie:
magic_close(cookie); // Close and free the cookie to the cookie monster magic_close(cookie); // Close and free the cookie to the cookie monster
return;
} }
void koto_playlist_set_name(KotoPlaylist *self, const gchar *name) { void koto_playlist_set_name(KotoPlaylist *self, const gchar *name) {
if (name == NULL) { // No actual name if (name == NULL) {
return; return;
} }
@ -389,7 +728,16 @@ void koto_playlist_set_name(KotoPlaylist *self, const gchar *name) {
} }
self->name = g_strdup(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) { 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 self->uuid = g_strdup(uuid); // Set the new UUID
}
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; return;
}
g_list_store_append(G_LIST_STORE(user_data), track);
} }
void koto_playlist_unmap(KotoPlaylist *self) { void koto_playlist_unmap(KotoPlaylist *self) {

View file

@ -16,19 +16,34 @@
*/ */
#pragma once #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" #include "../indexer/structs.h"
G_BEGIN_DECLS 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 * Type Definition
**/ **/
#define KOTO_TYPE_PLAYLIST koto_playlist_get_type() #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)) #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 * Playlist Functions
**/ **/
@ -36,27 +51,36 @@ G_DECLARE_FINAL_TYPE(KotoPlaylist, koto_playlist, KOTO, PLAYLIST, GObject);
KotoPlaylist* koto_playlist_new(); KotoPlaylist* koto_playlist_new();
KotoPlaylist* koto_playlist_new_with_uuid(const gchar *uuid); KotoPlaylist* koto_playlist_new_with_uuid(const gchar *uuid);
void koto_playlist_add_to_played_tracks(KotoPlaylist *self, 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(KotoPlaylist *self, KotoIndexedTrack *track, gboolean current, gboolean commit_to_table);
void koto_playlist_add_track_by_uuid(KotoPlaylist *self, const gchar *uuid); 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(KotoPlaylist *self);
void koto_playlist_commit_tracks(gpointer data, gpointer user_data); 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); gchar* koto_playlist_get_artwork(KotoPlaylist *self);
KotoPreferredModelType koto_playlist_get_current_model(KotoPlaylist *self);
guint koto_playlist_get_current_position(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); guint koto_playlist_get_length(KotoPlaylist *self);
gboolean koto_playlist_get_is_finalized(KotoPlaylist *self);
gchar* koto_playlist_get_name(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); GQueue* koto_playlist_get_tracks(KotoPlaylist *self);
gchar* koto_playlist_get_uuid(KotoPlaylist *self); gchar* koto_playlist_get_uuid(KotoPlaylist *self);
gchar* koto_playlist_go_to_next(KotoPlaylist *self); gchar* koto_playlist_go_to_next(KotoPlaylist *self);
gchar* koto_playlist_go_to_previous(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_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_remove_track_by_uuid(KotoPlaylist *self, gchar *uuid);
void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path); void koto_playlist_set_artwork(KotoPlaylist *self, const gchar *path);
void koto_playlist_save_state(KotoPlaylist *self); void koto_playlist_save_state(KotoPlaylist *self);
void koto_playlist_set_name(KotoPlaylist *self, const gchar *name); 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_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); void koto_playlist_unmap(KotoPlaylist *self);
G_END_DECLS G_END_DECLS

View file

@ -1,3 +1,5 @@
@import "vars";
.koto-button { .koto-button {
& > image { & > image {
margin-right: 10px; margin-right: 10px;

View file

@ -8,6 +8,7 @@
& .track-list { & .track-list {
& > row { & > row {
&:not(:active):not(:selected) { // Neither active nor selected, see gtk overrides
&:nth-child(odd):not(:hover) { &:nth-child(odd):not(:hover) {
background-color: $midnight; background-color: $midnight;
} }
@ -17,4 +18,5 @@
} }
} }
} }
}
} }

View file

@ -14,4 +14,32 @@
color: white; color: white;
} }
} }
.playerbar-info { // Central info section
& > box { // Info labels
margin-left: 2ex;
& > label {
margin-top: 6px;
margin-bottom: 6px;
&:nth-child(1) { // Title
font-size: x-large;
font-weight: bold;
}
&:not(:nth-child(1)) { // Album and Artist
font-size: large;
}
&:nth-child(2) { // Album
}
&:nth-child(3) { // Artist
}
}
}
}
} }

View file

@ -2,6 +2,8 @@ $grey: #2e2e2e;
$midnight: #1d1d1d; $midnight: #1d1d1d;
$darkgrey: #666666; $darkgrey: #666666;
$green: #60E078; $green: #60E078;
$palewhite: #cccccc;
$red : #FF4652;
$itempadding: 40px; $itempadding: 40px;
$halvedpadding: $itempadding / 2; $halvedpadding: $itempadding / 2;

View file

@ -0,0 +1,5 @@
.cover-art-button {
& > revealer > box { // Inner controls
background-color: rgba(0,0,0,0.75);
}
}

View file

@ -0,0 +1,63 @@
@import '../vars';
@mixin selected-row-styling {
color: $midnight;
background-color: $green;
border: 0; // Don't have a border
border-image: none; // GTK uses an image which is weird
border-image-width: 0;
outline: none;
outline-offset: 0;
outline-style: none;
}
button {
&.destructive-action {
background-color: $red;
background-image: none;
}
&.suggested-action { // Adwaita makes it blue but we want it green
color: $midnight;
background-color: $green;
background-image: none;
}
}
listview {
background-color: transparent;
}
list:not(.discs-list), listview {
&:not(.track-list) > row { // Rows which are now in the track list
&:active, &:selected { // Active or selected
@include selected-row-styling;
}
}
&.track-list > row {
&:selected { // Only selected rows
@include selected-row-styling;
}
}
}
range {
&.dragging { // Dragging a range
& > trough {
& > slider {
background-color: $green;
}
}
}
}
scale { // Progress bar
highlight {
background-color: $green;
}
slider { // Slider
outline-color: $green;
}
}

View file

@ -1,4 +1,7 @@
@import 'pages/music-local.scss'; @import 'components/cover-art-button';
@import 'components/gtk-overrides';
@import 'pages/music-local';
@import 'pages/playlist-page';
@import 'button'; @import 'button';
@import 'disc-view'; @import 'disc-view';
@ -15,4 +18,15 @@ window {
background-color: $midnight; background-color: $midnight;
background-image: none; background-image: none;
} }
.koto-dialog-container {
background-color: transparentize($midnight, 0.5);
padding: 20px;
}
// All the classes we want consistent padding applied to for its primary content
.artist-view-content, // Has the albums
.playlist-page { // Individual playlists
padding: $itempadding;
}
} }

View file

@ -9,7 +9,10 @@ theme = custom_target('Theme generation',
'@INPUT@', '@OUTPUT@', '@INPUT@', '@OUTPUT@',
], ],
depend_files: files([ depend_files: files([
'components/_cover-art-button.scss',
'components/_gtk-overrides.scss',
'pages/_music-local.scss', 'pages/_music-local.scss',
'pages/_playlist-page.scss',
'_button.scss', '_button.scss',
'_disc-view.scss', '_disc-view.scss',
'_expander.scss', '_expander.scss',

View file

@ -16,8 +16,6 @@
& > stack { & > stack {
& > .artist-view { & > .artist-view {
& > viewport > .artist-view-content { & > viewport > .artist-view-content {
padding: $itempadding;
& > .album-strip { & > .album-strip {
margin-bottom: $itempadding; margin-bottom: $itempadding;
& > flowboxchild { & > flowboxchild {

View file

@ -0,0 +1,84 @@
@import '../vars';
.playlist-page {
.playlist-page-header { // Our header
& > .playlist-page-header-info { // Our info centerbox
margin-left: 40px;
& > label { // All labels
color: $palewhite;
}
& > label:nth-child(1) { // First item (type of playlist)
font-size: 3ex;
font-weight: 700;
margin-top: 30px;
margin-bottom: 10px;
}
& > label:nth-child(2),
& > label:nth-child(3) {
font-weight: 800;
}
& > label:nth-child(2) { // Second item (playlist name)
font-size: 10ex;
}
& > label:nth-child(3) { // Third item (number of tracks)
font-size: 4ex;
margin-top: 40px;
}
}
& > .koto-button {
}
}
.track-list-content { // Our Track List
& > .track-list-header,
.track-list-columned-item {
font-size: x-large;
padding: 3ex 2ex;
}
& > .track-list-header { // Headers
font-weight: bold;
.koto-button { // All Koto buttons in our header
&.active { // Is active
color: $green;
}
}
}
& > .track-list-columned { // Column content
& > row {
& > .track-list-columned-item { // Track rows
font-size: x-large;
}
&:nth-child(odd):not(:selected) {
background-color: $midnight;
}
}
}
.track-column-number { // Column section within header and track items
}
.track-column-name { // Name section within header and track items
}
.track-column-album { // Album section within headers and track items
}
.track-column-artist { // Artist section within headers and track items
}
}
}