Implement TOML-based configuration support leveraging tomlc99 and custom file writer.

Fix code related to changing from temporary playlists to permanent ones.

Implement new centralized Koto Paths for defining our variables and revdns name for consistency.

Updated Album View to leverage KotoCoverArtButton component.

Started refactoring our theming to support multiple variants. It isn't where I want it yet but we'll get there.
This commit is contained in:
Joshua Strobl 2021-05-27 12:56:36 +03:00
parent 9f0e8dfbc8
commit fa19fd23b1
45 changed files with 1004 additions and 282 deletions

505
src/config/config.c Normal file
View file

@ -0,0 +1,505 @@
/* config.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 <glib-2.0/gio/gio.h>
#include <errno.h>
#include <toml.h>
#include "../playback/engine.h"
#include "../koto-paths.h"
#include "../koto-utils.h"
#include "config.h"
extern int errno;
extern const gchar * koto_config_template;
extern KotoPlaybackEngine * playback_engine;
enum {
PROP_0,
PROP_PLAYBACK_CONTINUE_ON_PLAYLIST,
PROP_PLAYBACK_LAST_USED_VOLUME,
PROP_PLAYBACK_MAINTAIN_SHUFFLE,
PROP_UI_THEME_DESIRED,
PROP_UI_THEME_OVERRIDE,
N_PROPS,
};
static GParamSpec * config_props[N_PROPS] = {
0
};
struct _KotoConfig {
GObject parent_instance;
GFile * config_file;
GFileMonitor * config_file_monitor;
gchar * path;
gboolean finalized;
/* Playback Settings */
gboolean playback_continue_on_playlist;
gdouble playback_last_used_volume;
gboolean playback_maintain_shuffle;
/* UI Settings */
gchar * ui_theme_desired;
gboolean ui_theme_override;
};
struct _KotoConfigClass {
GObjectClass parent_class;
};
G_DEFINE_TYPE(KotoConfig, koto_config, G_TYPE_OBJECT);
KotoConfig * config;
static void koto_config_constructed(GObject *obj);
static void koto_config_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
);
static void koto_config_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
);
static void koto_config_class_init(KotoConfigClass *c) {
GObjectClass * gobject_class;
gobject_class = G_OBJECT_CLASS(c);
gobject_class->constructed = koto_config_constructed;
gobject_class->get_property = koto_config_get_property;
gobject_class->set_property = koto_config_set_property;
config_props[PROP_PLAYBACK_CONTINUE_ON_PLAYLIST] = g_param_spec_boolean(
"playback-continue-on-playlist",
"Continue Playback of Playlist",
"Continue playback of a Playlist after playing a specific track in the playlist",
FALSE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_PLAYBACK_LAST_USED_VOLUME] = g_param_spec_double(
"playback-last-used-volume",
"Last Used Volume",
"Last Used Volume",
0,
1,
0.5, // 50%
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_PLAYBACK_MAINTAIN_SHUFFLE] = g_param_spec_boolean(
"playback-maintain-shuffle",
"Maintain Shuffle on Playlist Change",
"Maintain shuffle setting when changing playlists",
TRUE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_UI_THEME_DESIRED] = g_param_spec_string(
"ui-theme-desired",
"Desired Theme",
"Desired Theme",
"dark", // Like my soul
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
config_props[PROP_UI_THEME_OVERRIDE] = g_param_spec_boolean(
"ui-theme-override",
"Override built-in theming",
"Override built-in theming",
FALSE,
G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE
);
g_object_class_install_properties(gobject_class, N_PROPS, config_props);
}
static void koto_config_init(KotoConfig * self) {
self->finalized = FALSE;
}
static void koto_config_constructed(GObject *obj) {
KotoConfig * self = KOTO_CONFIG(obj);
self->finalized = TRUE;
}
static void koto_config_get_property(
GObject * obj,
guint prop_id,
GValue * val,
GParamSpec * spec
) {
KotoConfig * self = KOTO_CONFIG(obj);
switch (prop_id) {
case PROP_PLAYBACK_CONTINUE_ON_PLAYLIST:
g_value_set_boolean(val, self->playback_continue_on_playlist);
break;
case PROP_PLAYBACK_LAST_USED_VOLUME:
g_value_set_double(val, self->playback_last_used_volume);
break;
case PROP_PLAYBACK_MAINTAIN_SHUFFLE:
g_value_set_boolean(val, self->playback_maintain_shuffle);
break;
case PROP_UI_THEME_DESIRED:
g_value_set_string(val, g_strdup(self->ui_theme_desired));
break;
case PROP_UI_THEME_OVERRIDE:
g_value_set_boolean(val, self->ui_theme_override);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
}
static void koto_config_set_property(
GObject * obj,
guint prop_id,
const GValue * val,
GParamSpec * spec
) {
KotoConfig * self = KOTO_CONFIG(obj);
switch (prop_id) {
case PROP_PLAYBACK_CONTINUE_ON_PLAYLIST:
self->playback_continue_on_playlist = g_value_get_boolean(val);
break;
case PROP_PLAYBACK_LAST_USED_VOLUME:
self->playback_last_used_volume = g_value_get_double(val);
break;
case PROP_PLAYBACK_MAINTAIN_SHUFFLE:
self->playback_maintain_shuffle = g_value_get_boolean(val);
break;
case PROP_UI_THEME_DESIRED:
self->ui_theme_desired = g_strdup(g_value_get_string(val));
break;
case PROP_UI_THEME_OVERRIDE:
self->ui_theme_override = g_value_get_boolean(val);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, spec);
break;
}
if (self->finalized) { // Loaded the config
g_object_notify_by_pspec(obj, config_props[prop_id]); // Notify that a change happened
}
}
/**
* Load our TOML file from the specified path into our KotoConfig
**/
void koto_config_load(KotoConfig * self, gchar *path) {
if (!koto_utils_is_string_valid(path)) { // Path is not valid
return;
}
self->path = g_strdup(path);
self->config_file = g_file_new_for_path(path);
gboolean config_file_exists = g_file_query_exists(self->config_file, NULL);
if (!config_file_exists) { // File does not exist
GError * create_err;
GFileOutputStream * stream = g_file_create(
self->config_file,
G_FILE_CREATE_PRIVATE,
NULL,
&create_err
);
if (create_err != NULL) {
if (create_err->code != G_IO_ERROR_EXISTS) { // Not an error indicating the file already exists
g_message("Failed to create or open file: %s", create_err->message);
return;
}
}
g_object_unref(stream);
}
GError * file_info_query_err;
GFileInfo * file_info = g_file_query_info( // Get the size of our TOML file
self->config_file,
G_FILE_ATTRIBUTE_STANDARD_SIZE,
G_FILE_QUERY_INFO_NONE,
NULL,
&file_info_query_err
);
if (file_info != NULL) { // Got info
goffset size = g_file_info_get_size(file_info); // Get the size from info
g_object_unref(file_info); // Unref immediately
if (size == 0) { // If we don't have any file contents (new file), skip parsing
goto monitor;
}
} else { // Failed to get the info
g_warning("Failed to get size info of %s: %s", self->path, file_info_query_err->message);
}
FILE * file;
file = fopen(self->path, "r"); // Open the file as read only
if (file == NULL) { // Failed to get the file
/** Handle error checking here*/
return;
}
char errbuf[200];
toml_table_t * conf = toml_parse_file(file, errbuf, sizeof(errbuf));
fclose(file); // Close the file
if (!conf) {
g_error("Failed to read our config file. %s", errbuf);
return;
}
/** Supplemental Libraries (Excludes Built-in) */
toml_table_t * libraries_section = toml_table_in(conf, "libraries");
if (libraries_section) { // Have supplemental libraries
toml_array_t * library_uuids = toml_array_in(libraries_section, "uuids");
if (library_uuids && (toml_array_nelem(library_uuids) != 0)) { // Have UUIDs
for (int i = 0; i < toml_array_nelem(library_uuids); i++) { // Iterate over each UUID
toml_datum_t uuid = toml_string_at(library_uuids, i); // Get the UUID
if (!uuid.ok) { // Not a UUID string
continue; // Skip this entry in the array
}
g_message("UUID: %s", uuid.u.s);
// TODO: Implement Koto library creation
free(uuid.u.s);
toml_free(conf);
}
}
}
/** Playback Section */
toml_table_t * playback_section = toml_table_in(conf, "playback");
if (playback_section) { // Have playback section
toml_datum_t continue_on_playlist = toml_bool_in(playback_section, "continue-on-playlist");
toml_datum_t last_used_volume = toml_double_in(playback_section, "last-used-volume");
toml_datum_t maintain_shuffle = toml_bool_in(playback_section, "maintain-shuffle");
if (continue_on_playlist.ok && (self->playback_continue_on_playlist != continue_on_playlist.u.b)) { // If we have a continue-on-playlist set and they are different
g_object_set(self, "playback-continue-on-playlist", continue_on_playlist.u.b, NULL);
}
if (last_used_volume.ok && (self->playback_last_used_volume != last_used_volume.u.d)) { // If we have last-used-volume set and they are different
g_object_set(self, "playback-last-used-volume", last_used_volume.u.d, NULL);
}
if (maintain_shuffle.ok && (self->playback_maintain_shuffle != maintain_shuffle.u.b)) { // If we have a "maintain shuffle set" and they are different
g_object_set(self, "playback-maintain-shuffle", maintain_shuffle.u.b, NULL);
}
}
/* UI Section */
toml_table_t * ui_section = toml_table_in(conf, "ui");
if (ui_section) { // Have UI section
toml_datum_t name = toml_string_in(ui_section, "theme-desired");
if (name.ok && (g_strcmp0(name.u.s, self->ui_theme_desired) != 0)) { // Have a name specified and they are different
g_object_set(self, "ui-theme-desired", g_strdup(name.u.s), NULL);
free(name.u.s);
}
toml_datum_t override_app = toml_bool_in(ui_section, "theme-override");
if (override_app.ok && (override_app.u.b != self->ui_theme_override)) { // Changed if we are overriding theme
g_object_set(self, "ui-theme-override", override_app.u.b, NULL);
}
}
monitor:
if (self->config_file_monitor != NULL) { // If we already have a file monitor for the file
return;
}
self->config_file_monitor = g_file_monitor_file(
self->config_file,
G_FILE_MONITOR_NONE,
NULL,
NULL
);
g_signal_connect(self->config_file_monitor, "changed", G_CALLBACK(koto_config_monitor_handle_changed), self); // Monitor changes to our config file
if (!config_file_exists) { // File did not originally exist
koto_config_save(self); // Save immediately
}
}
void koto_config_monitor_handle_changed(GFileMonitor * monitor, GFile * file, GFile *other_file, GFileMonitorEvent ev, gpointer user_data) {
(void) monitor;
(void) file;
(void) other_file;
KotoConfig * config = user_data;
if (
(ev == G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED) || // Attributes changed
(ev == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) // Changes done
) {
koto_config_refresh(config); // Refresh the config
}
}
/**
* Refresh will handle any FS notify change on our Koto config file and call load
**/
void koto_config_refresh(KotoConfig * self) {
koto_config_load(self, self->path);
}
/**
* Save will write our config back out
**/
void koto_config_save(KotoConfig *self) {
GStrvBuilder * root_builder = g_strv_builder_new(); // Create a new strv builder
GParamSpec ** props_list = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), NULL); // Get the propreties associated with our settings
GHashTable * sections_to_prop_keys = g_hash_table_new(g_str_hash, g_str_equal); // Create our section to hold our various sections based on props
/* Section Hashes*/
gchar * playback_hash = g_strdup("playback");
gchar * ui_hash = g_strdup("ui");
gdouble current_playback_volume = koto_playback_engine_get_volume(playback_engine); // Get the last used volume in the playback engine
self->playback_last_used_volume = current_playback_volume; // Update our value so we have it during save
int i;
for (i = 0; i < N_PROPS; i++) { // For each property
GParamSpec *spec = props_list[i]; // Get the prop
if (!G_IS_PARAM_SPEC(spec)) { // Not a spec
continue; // Skip
}
const gchar * prop_name = g_param_spec_get_name(spec);
gpointer respective_prop = NULL;
if (g_str_has_prefix(prop_name, "playback")) { // Is playback
respective_prop = playback_hash;
} else if (g_str_has_prefix(prop_name, "ui")) { // Is UI
respective_prop = ui_hash;
}
if (respective_prop == NULL) { // No property
continue;
}
GList * keys;
if (g_hash_table_contains(sections_to_prop_keys, respective_prop)) { // Already has list
keys = g_hash_table_lookup(sections_to_prop_keys, respective_prop); // Get the list
} else { // Don't have list
keys = NULL;
}
keys = g_list_append(keys, g_strdup(prop_name)); // Add the name in full
g_hash_table_insert(sections_to_prop_keys, respective_prop, keys); // Replace list (or add it)
}
GHashTableIter iter;
gpointer section_name, section_props;
g_hash_table_iter_init(&iter, sections_to_prop_keys);
while (g_hash_table_iter_next(&iter, &section_name, &section_props)) {
GStrvBuilder * section_builder = g_strv_builder_new(); // Make our string builder
g_strv_builder_add(section_builder, g_strdup_printf("[%s]", (gchar *) section_name)); // Add section as [section]
GList * current_section_keyname;
for (current_section_keyname = section_props; current_section_keyname != NULL; current_section_keyname = current_section_keyname->next) { // Iterate over property names
GValue prop_val_raw = G_VALUE_INIT; // Initialize our GValue
g_object_get_property(G_OBJECT(self), current_section_keyname->data, &prop_val_raw);
gchar * prop_val = g_strdup_value_contents(&prop_val_raw);
if ((g_strcmp0(prop_val, "TRUE") == 0) || (g_strcmp0(prop_val, "FALSE") == 0)) { // TRUE or FALSE from a boolean type
prop_val = g_utf8_strdown(prop_val, -1); // Change it to be lowercased
}
gchar * key_name = g_strdup(current_section_keyname->data);
gchar * key_name_replaced = koto_utils_replace_string_all(key_name, g_strdup_printf("%s-", (gchar *) section_name), ""); // Remove SECTIONNAME-
const gchar * line = g_strdup_printf("\t%s = %s", key_name_replaced, prop_val);
g_strv_builder_add(section_builder, line); // Add the line
g_free(key_name_replaced);
g_free(key_name);
}
GStrv lines = g_strv_builder_end(section_builder); // Get all the lines as a GStrv which is a gchar **
gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline
g_strfreev(lines); // Free our lines
g_strv_builder_add(root_builder, content); // Add section content to root builder
g_strv_builder_unref(section_builder); // Unref our builder
}
g_hash_table_unref(sections_to_prop_keys); // Free our hash table
GStrv lines = g_strv_builder_end(root_builder); // Get all the lines as a GStrv which is a gchar **
gchar * content = g_strjoinv("\n", lines); // Separate all lines with newline
g_strfreev(lines); // Free our lines
g_strv_builder_unref(root_builder); // Unref our root builder
ulong file_content_length = g_utf8_strlen(content, -1);
g_file_replace_contents(
self->config_file,
content,
file_content_length,
NULL,
FALSE,
G_FILE_CREATE_PRIVATE,
NULL,
NULL,
NULL
);
}
KotoConfig * koto_config_new() {
return g_object_new (KOTO_TYPE_CONFIG, NULL);
}