*
* Copyright 2025 Adam Masciola
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "bz-preferences-dialog.h"
#include <gio/gio.h>
#include <glib/gi18n.h>
#include <libdex.h>
#include "env.h"
#include "template-callbacks.h"
#include "util.h"
#define AUTOSTART_DESKTOP_FILE_NAME "bazaar.desktop"
#define AUTOSTART_DESKTOP_RESOURCE_PATH "/io/github/kolunmi/Bazaar/bazaar.desktop"
typedef struct
{
const char *id;
const char *style_class;
const char *tooltip;
} BarTheme;
static const BarTheme bar_themes[] = {
{ "accent-color", "accent-color-theme", N_ ("Accent Color") },
{ "pride-rainbow-flag", "pride-rainbow-flag-theme", N_ ("Pride Colors") },
{ "lesbian-pride-flag", "lesbian-pride-flag-theme", N_ ("Lesbian Pride Colors") },
{ "gay-pride-flag", "gay-pride-flag-theme", N_ ("Male Homosexual Pride Colors") },
{ "transgender-flag", "transgender-flag-theme", N_ ("Transgender Pride Colors") },
{ "nonbinary-flag", "nonbinary-flag-theme", N_ ("Nonbinary Pride Colors") },
{ "bisexual-flag", "bisexual-flag-theme", N_ ("Bisexual Pride Colors") },
{ "asexual-flag", "asexual-flag-theme", N_ ("Asexual Pride Colors") },
{ "pansexual-flag", "pansexual-flag-theme", N_ ("Pansexual Pride Colors") },
{ "aromantic-flag", "aromantic-flag-theme", N_ ("Aromantic Pride Colors") },
{ "genderfluid-flag", "genderfluid-flag-theme", N_ ("Genderfluid Pride Colors") },
{ "polysexual-flag", "polysexual-flag-theme", N_ ("Polysexual Pride Colors") },
{ "omnisexual-flag", "omnisexual-flag-theme", N_ ("Omnisexual Pride Colors") },
{ "aroace-flag", "aroace-flag-theme", N_ ("Aroace Pride Colors") },
{ "agender-flag", "agender-flag-theme", N_ ("Agender Pride Colors") },
{ "genderqueer-flag", "genderqueer-flag-theme", N_ ("Genderqueer Pride Colors") },
{ "intersex-flag", "intersex-flag-theme", N_ ("Intersex Pride Colors") },
{ "demigender-flag", "demigender-flag-theme", N_ ("Demigender Pride Colors") },
{ "biromantic-flag", "biromantic-flag-theme", N_ ("Biromantic Pride Colors") },
{ "disability-flag", "disability-flag-theme", N_ ("Disability Pride Colors") },
{ "femboy-flag", "femboy-flag-theme", N_ ("Femboy Pride Colors") },
{ "neutrois-flag", "neutrois-flag-theme", N_ ("Neutrois Pride Colors") },
};
struct _BzPreferencesDialog
{
AdwPreferencesDialog parent_instance;
BzStateInfo *state;
GSettings *settings;
GtkCheckButton *automatic_updates_check;
AdwSwitchRow *auto_notif_switch;
AdwSwitchRow *only_foss_switch;
AdwSwitchRow *only_flathub_switch;
AdwSwitchRow *only_verified_switch;
GtkFlowBox *flag_buttons_box;
AdwSwitchRow *hide_eol_switch;
GtkToggleButton *flag_buttons[G_N_ELEMENTS (bar_themes)];
};
G_DEFINE_FINAL_TYPE (BzPreferencesDialog, bz_preferences_dialog, ADW_TYPE_PREFERENCES_DIALOG)
enum
{
PROP_0,
PROP_STATE,
LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };
static void bind_settings (BzPreferencesDialog *self);
static void create_flag_buttons (BzPreferencesDialog *self);
static void request_autostart (gboolean enable);
static DexFuture *
request_autostart_fiber (gpointer user_data);
static void
bz_preferences_dialog_dispose (GObject *object)
{
BzPreferencesDialog *self = BZ_PREFERENCES_DIALOG (object);
g_clear_object (&self->state);
g_clear_object (&self->settings);
G_OBJECT_CLASS (bz_preferences_dialog_parent_class)->dispose (object);
}
static void
flag_button_toggled (GtkToggleButton *button,
BzPreferencesDialog *self)
{
const char *theme_id = NULL;
if (!gtk_toggle_button_get_active (button))
return;
theme_id = g_object_get_data (G_OBJECT (button), "theme-id");
if (theme_id != NULL)
{
g_settings_set_string (self->settings, "global-progress-bar-theme", theme_id);
}
}
static void
global_progress_theme_settings_changed (BzPreferencesDialog *self,
const char *key,
GSettings *settings)
{
const char *theme = NULL;
theme = g_settings_get_string (self->settings, "global-progress-bar-theme");
for (guint i = 0; i < G_N_ELEMENTS (bar_themes); i++)
{
if (g_strcmp0 (theme, bar_themes[i].id) == 0)
{
gtk_toggle_button_set_active (self->flag_buttons[i], TRUE);
break;
}
}
}
static void
create_flag_buttons (BzPreferencesDialog *self)
{
GtkToggleButton *first_button = NULL;
for (guint i = 0; i < G_N_ELEMENTS (bar_themes); i++)
{
GtkToggleButton *button = NULL;
button = GTK_TOGGLE_BUTTON (gtk_toggle_button_new ());
gtk_widget_set_tooltip_text (GTK_WIDGET (button), Q_ (bar_themes[i].tooltip));
gtk_widget_add_css_class (GTK_WIDGET (button), "accent-button");
gtk_widget_add_css_class (GTK_WIDGET (button), bar_themes[i].style_class);
g_object_set_data_full (G_OBJECT (button),
"theme-id",
g_strdup (bar_themes[i].id),
g_free);
if (i == 0)
{
first_button = button;
}
else
{
gtk_toggle_button_set_group (button, first_button);
}
g_signal_connect (button, "toggled",
G_CALLBACK (flag_button_toggled), self);
self->flag_buttons[i] = button;
gtk_flow_box_append (self->flag_buttons_box, GTK_WIDGET (button));
}
}
static void
bind_settings (BzPreferencesDialog *self)
{
if (self->settings == NULL)
return;
g_settings_bind (self->settings, "show-only-foss",
self->only_foss_switch, "active",
G_SETTINGS_BIND_DEFAULT);
g_settings_bind (self->settings, "show-only-flathub",
self->only_flathub_switch, "active",
G_SETTINGS_BIND_DEFAULT);
g_settings_bind (self->settings, "show-only-verified",
self->only_verified_switch, "active",
G_SETTINGS_BIND_DEFAULT);
g_settings_bind (self->settings, "hide-eol",
self->hide_eol_switch, "active",
G_SETTINGS_BIND_DEFAULT);
g_settings_bind (self->settings, "auto-update",
self->automatic_updates_check, "active",
G_SETTINGS_BIND_DEFAULT);
g_settings_bind (self->settings, "auto-update-notifications",
self->auto_notif_switch, "active",
G_SETTINGS_BIND_DEFAULT);
g_signal_connect_object (
self->settings,
"changed::global-progress-bar-theme",
G_CALLBACK (global_progress_theme_settings_changed),
self, G_CONNECT_SWAPPED);
global_progress_theme_settings_changed (self, "global-progress-bar-theme", self->settings);
}
static void
bz_preferences_dialog_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
BzPreferencesDialog *self = BZ_PREFERENCES_DIALOG (object);
switch (prop_id)
{
case PROP_STATE:
g_value_set_object (value, self->state);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_preferences_dialog_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
switch (prop_id)
{
case PROP_STATE:
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static DexFuture *
request_autostart_fiber (gpointer user_data)
{
gboolean enable = GPOINTER_TO_INT (user_data);
#ifdef SANDBOXED_LIBFLATPAK
g_autoptr (GDBusConnection) bus = NULL;
g_autoptr (GError) local_error = NULL;
g_autoptr (GVariant) reply = NULL;
g_autofree char *token = NULL;
GVariant *options = NULL;
static guint request_count = 0;
bus = dex_await_object (dex_bus_get (G_BUS_TYPE_SESSION), &local_error);
if (bus == NULL)
{
g_warning ("Could not connect to session bus: %s", local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
token = g_strdup_printf ("bazaar_autostart_%u", request_count++);
options = g_variant_new_parsed (
"{'handle_token': <%s>, 'reason': <%s>, "
"'autostart': <%b>, 'dbus-activatable': <false>, "
"'commandline': <['bazaar-daemon', '--no-window']>}",
token,
_ ("Bazaar needs to run in the background to check for app updates"),
enable);
reply = dex_await_variant (
dex_dbus_connection_call (
bus, "org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop", "org.freedesktop.portal.Background",
"RequestBackground", g_variant_new ("(s@a{sv})", "", options), NULL,
G_DBUS_CALL_FLAGS_NONE, -1),
&local_error);
if (reply == NULL)
{
g_warning ("Failed to call RequestBackground: %s", local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
return dex_future_new_true ();
#else
g_autoptr (GError) local_error = NULL;
g_autofree char *autostart_dir = NULL;
g_autofree char *dest_path = NULL;
g_autoptr (GFile) dir = NULL;
g_autoptr (GFile) dst = NULL;
autostart_dir = g_build_filename (g_get_user_config_dir (), "autostart", NULL);
dest_path = g_build_filename (autostart_dir, AUTOSTART_DESKTOP_FILE_NAME, NULL);
dir = g_file_new_for_path (autostart_dir);
dst = g_file_new_for_path (dest_path);
if (!dex_await (dex_file_make_directory_with_parents (dir), &local_error) &&
!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS))
{
g_warning ("Could not create autostart dir: %s", local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
g_clear_error (&local_error);
if (enable)
{
g_autoptr (GFile) src = NULL;
src = g_file_new_for_uri ("resource://" AUTOSTART_DESKTOP_RESOURCE_PATH);
if (!dex_await (dex_file_copy (src, dst, G_FILE_COPY_OVERWRITE, G_PRIORITY_DEFAULT), &local_error))
{
g_warning ("Failed to install autostart file: %s", local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
}
else
{
if (!dex_await (dex_file_delete (dst, G_PRIORITY_DEFAULT), &local_error) &&
!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
{
g_warning ("Failed to remove autostart file: %s", local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
}
return dex_future_new_true ();
#endif
}
static void
request_autostart (gboolean enable)
{
dex_future_disown (
dex_scheduler_spawn (
dex_scheduler_get_default (),
bz_get_dex_stack_size (),
request_autostart_fiber,
GINT_TO_POINTER (enable),
NULL));
}
static void
auto_updates_toggled_cb (GtkCheckButton *button,
BzPreferencesDialog *self)
{
request_autostart (gtk_check_button_get_active (self->automatic_updates_check));
}
static void
bz_preferences_dialog_class_init (BzPreferencesDialogClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->set_property = bz_preferences_dialog_set_property;
object_class->get_property = bz_preferences_dialog_get_property;
object_class->dispose = bz_preferences_dialog_dispose;
props[PROP_STATE] =
g_param_spec_object (
"state",
NULL, NULL,
BZ_TYPE_STATE_INFO,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, LAST_PROP, props);
gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-preferences-dialog.ui");
bz_widget_class_bind_all_util_callbacks (widget_class);
gtk_widget_class_bind_template_child (widget_class, BzPreferencesDialog, only_foss_switch);
gtk_widget_class_bind_template_child (widget_class, BzPreferencesDialog, only_flathub_switch);
gtk_widget_class_bind_template_child (widget_class, BzPreferencesDialog, only_verified_switch);
gtk_widget_class_bind_template_child (widget_class, BzPreferencesDialog, flag_buttons_box);
gtk_widget_class_bind_template_child (widget_class, BzPreferencesDialog, hide_eol_switch);
gtk_widget_class_bind_template_child (widget_class, BzPreferencesDialog, automatic_updates_check);
gtk_widget_class_bind_template_child (widget_class, BzPreferencesDialog, auto_notif_switch);
gtk_widget_class_bind_template_callback (widget_class, auto_updates_toggled_cb);
}
static void
bz_preferences_dialog_init (BzPreferencesDialog *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
create_flag_buttons (self);
}
AdwDialog *
bz_preferences_dialog_new (BzStateInfo *state)
{
BzPreferencesDialog *dialog = NULL;
g_return_val_if_fail (BZ_IS_STATE_INFO (state), NULL);
dialog = g_object_new (BZ_TYPE_PREFERENCES_DIALOG, NULL);
dialog->state = g_object_ref (state);
g_object_get (state, "settings", &dialog->settings, NULL);
bind_settings (dialog);
g_object_notify_by_pspec (G_OBJECT (dialog), props[PROP_STATE]);
return ADW_DIALOG (dialog);
}