🏡 index : quine-global-labs/bazaar.git

/* bz-preferences-dialog.c
 *
 * 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;

  /* Template widgets */
  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;

  /* Bind all boolean settings to their respective switches */
  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,
      // Translators: Please treat "Bazaar" as a brand name and avoid translating.
      _ ("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);
}