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:
parent
9f0e8dfbc8
commit
fa19fd23b1
45 changed files with 1004 additions and 282 deletions
505
src/config/config.c
Normal file
505
src/config/config.c
Normal 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, §ion_name, §ion_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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue