*
* Copyright 2026 Alexander Vanhee
*
* 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 "config.h"
#include <glib/gi18n.h>
#include "bz-app-permissions.h"
#include "bz-context-row.h"
#include "bz-entry.h"
#include "bz-lozenge.h"
#include "bz-safety-dialog.h"
#include "bz-safety-row.h"
#include "safety-calculator.h"
#include "template-callbacks.h"
#define ANIMATION_DURATION 300
struct _BzSafetyDialog
{
AdwBin parent_instance;
BzEntry *entry;
gboolean has_sandbox_escape;
gboolean owns_dialog;
AdwAnimation *width_animation;
AdwAnimation *height_animation;
BzLozenge *lozenge;
GtkListBox *permissions_list;
AdwCarousel *carousel;
GtkBox *global_box;
GtkLabel *arbitrary_description;
};
G_DEFINE_FINAL_TYPE (BzSafetyDialog, bz_safety_dialog, ADW_TYPE_BIN)
enum
{
PROP_0,
PROP_ENTRY,
LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };
static void update_permissions_list (BzSafetyDialog *self);
static void animate_to_page (BzSafetyDialog *self, guint page_index);
static void
bz_safety_dialog_dispose (GObject *object)
{
BzSafetyDialog *self = BZ_SAFETY_DIALOG (object);
g_clear_object (&self->entry);
g_clear_object (&self->width_animation);
g_clear_object (&self->height_animation);
G_OBJECT_CLASS (bz_safety_dialog_parent_class)->dispose (object);
}
static void
bz_safety_dialog_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
BzSafetyDialog *self = BZ_SAFETY_DIALOG (object);
switch (prop_id)
{
case PROP_ENTRY:
g_value_set_object (value, self->entry);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_safety_dialog_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
BzSafetyDialog *self;
self = BZ_SAFETY_DIALOG (object);
switch (prop_id)
{
case PROP_ENTRY:
g_clear_object (&self->entry);
self->entry = g_value_dup_object (value);
update_permissions_list (self);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
get_target_size (BzSafetyDialog *self,
guint page_index,
int *out_width,
int *out_height)
{
if (page_index == 0)
{
int nat = 0;
*out_width = 450;
gtk_widget_measure (GTK_WIDGET (self->global_box),
GTK_ORIENTATION_VERTICAL,
*out_width,
NULL, &nat, NULL, NULL);
*out_height = CLAMP (nat + 48, 100, 600);
}
else
{
*out_width = 640;
*out_height = 576;
}
}
static void
animate_to_page (BzSafetyDialog *self,
guint page_index)
{
AdwDialog *dialog = NULL;
int target_width = 0;
int target_height = 0;
int cur_w = 0;
int cur_h = 0;
if (!self->owns_dialog)
return;
dialog = ADW_DIALOG (gtk_widget_get_ancestor (GTK_WIDGET (self), ADW_TYPE_DIALOG));
if (dialog == NULL)
return;
if (self->width_animation == NULL || self->height_animation == NULL)
return;
get_target_size (self, page_index, &target_width, &target_height);
cur_w = adw_dialog_get_content_width (dialog);
cur_h = adw_dialog_get_content_height (dialog);
adw_animation_skip (self->width_animation);
adw_animation_skip (self->height_animation);
adw_timed_animation_set_duration (ADW_TIMED_ANIMATION (self->width_animation), ANIMATION_DURATION);
adw_timed_animation_set_duration (ADW_TIMED_ANIMATION (self->height_animation), ANIMATION_DURATION);
adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->width_animation), cur_w);
adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->width_animation), target_width);
adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->height_animation), cur_h);
adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->height_animation), target_height);
adw_animation_play (self->width_animation);
adw_animation_play (self->height_animation);
}
static void
on_dialog_map (BzSafetyDialog *self)
{
guint page_index = self->has_sandbox_escape ? 0 : 1;
if (page_index == 1)
{
GtkWidget *page = NULL;
page = gtk_widget_get_last_child (GTK_WIDGET (self->carousel));
adw_carousel_scroll_to (self->carousel, page, FALSE);
}
if (self->owns_dialog)
{
AdwDialog *dialog = NULL;
int target_width = 0;
int target_height = 0;
dialog = ADW_DIALOG (gtk_widget_get_ancestor (GTK_WIDGET (self), ADW_TYPE_DIALOG));
if (dialog == NULL)
return;
get_target_size (self, page_index, &target_width, &target_height);
adw_dialog_set_content_width (dialog, target_width);
adw_dialog_set_content_height (dialog, target_height);
}
}
static void
on_height_animation_done (AdwAnimation *animation,
gpointer user_data)
{
BzSafetyDialog *self = BZ_SAFETY_DIALOG (user_data);
gtk_widget_child_focus (gtk_widget_get_last_child (GTK_WIDGET (self->carousel)),
GTK_DIR_TAB_FORWARD);
}
static void
next_page (BzSafetyDialog *self,
GtkButton *button)
{
GtkWidget *page = NULL;
page = gtk_widget_get_last_child (GTK_WIDGET (self->carousel));
gtk_accessible_announce (GTK_ACCESSIBLE (self),
bz_lozenge_get_title (self->lozenge),
GTK_ACCESSIBLE_ANNOUNCEMENT_PRIORITY_MEDIUM);
adw_carousel_scroll_to (self->carousel, page, TRUE);
animate_to_page (self, 1);
if (self->height_animation != NULL)
g_signal_connect_object (self->height_animation, "done",
G_CALLBACK (on_height_animation_done), self,
G_CONNECT_DEFAULT);
}
static gboolean
is_page (gpointer object,
gdouble position,
gint target)
{
return fabs (position - (gdouble) target) < 0.5 ||
fabs (position - round (position)) > 0.01;
}
static void
bz_safety_dialog_class_init (BzSafetyDialogClass *klass)
{
GObjectClass *object_class;
GtkWidgetClass *widget_class;
object_class = G_OBJECT_CLASS (klass);
widget_class = GTK_WIDGET_CLASS (klass);
object_class->dispose = bz_safety_dialog_dispose;
object_class->get_property = bz_safety_dialog_get_property;
object_class->set_property = bz_safety_dialog_set_property;
props[PROP_ENTRY] =
g_param_spec_object ("entry",
NULL, NULL,
BZ_TYPE_ENTRY,
G_PARAM_READWRITE);
g_object_class_install_properties (object_class, LAST_PROP, props);
g_type_ensure (BZ_TYPE_LOZENGE);
gtk_widget_class_set_template_from_resource (widget_class,
"/io/github/kolunmi/Bazaar/bz-safety-dialog.ui");
bz_widget_class_bind_all_util_callbacks (widget_class);
gtk_widget_class_bind_template_child (widget_class, BzSafetyDialog, lozenge);
gtk_widget_class_bind_template_child (widget_class, BzSafetyDialog, permissions_list);
gtk_widget_class_bind_template_child (widget_class, BzSafetyDialog, carousel);
gtk_widget_class_bind_template_child (widget_class, BzSafetyDialog, global_box);
gtk_widget_class_bind_template_child (widget_class, BzSafetyDialog, arbitrary_description);
gtk_widget_class_bind_template_callback (widget_class, next_page);
gtk_widget_class_bind_template_callback (widget_class, is_page);
}
static void
bz_safety_dialog_init (BzSafetyDialog *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
g_signal_connect_swapped (self, "map", G_CALLBACK (on_dialog_map), self);
}
AdwDialog *
bz_safety_dialog_new (BzEntry *entry)
{
BzSafetyDialog *widget = NULL;
AdwDialog *dialog = NULL;
AdwAnimationTarget *width_target = NULL;
AdwAnimationTarget *height_target = NULL;
g_autofree char *description = NULL;
widget = g_object_new (BZ_TYPE_SAFETY_DIALOG, NULL);
widget->owns_dialog = TRUE;
dialog = adw_dialog_new ();
adw_dialog_set_title (dialog, _ ("Safety"));
adw_dialog_set_content_height (dialog, 576);
adw_dialog_set_content_width (dialog, 640);
adw_dialog_set_child (dialog, GTK_WIDGET (widget));
width_target = adw_property_animation_target_new (G_OBJECT (dialog), "content-width");
widget->width_animation = adw_timed_animation_new (GTK_WIDGET (widget), 0, 0, ANIMATION_DURATION, width_target);
height_target = adw_property_animation_target_new (G_OBJECT (dialog), "content-height");
widget->height_animation = adw_timed_animation_new (GTK_WIDGET (widget), 0, 0, ANIMATION_DURATION, height_target);
g_object_set (widget, "entry", entry, NULL);
if (widget->has_sandbox_escape)
description = g_strdup_printf ("%s. %s",
_ ("Arbitrary Permissions"),
gtk_label_get_text (widget->arbitrary_description));
else
description = g_strdup (bz_lozenge_get_title (widget->lozenge));
gtk_accessible_update_property (GTK_ACCESSIBLE (dialog),
GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, description,
-1);
return dialog;
}
AdwNavigationPage *
bz_safety_dialog_page_new (BzEntry *entry)
{
BzSafetyDialog *widget = NULL;
AdwNavigationPage *page = NULL;
widget = g_object_new (BZ_TYPE_SAFETY_DIALOG, "entry", entry, NULL);
page = adw_navigation_page_new (GTK_WIDGET (widget), _ ("Safety"));
adw_navigation_page_set_tag (page, "safety");
return page;
}
static void
update_permissions_list (BzSafetyDialog *self)
{
const char *icon_names[2];
const char *app_name = NULL;
g_autofree char *title_text = NULL;
BzImportance importance = BZ_IMPORTANCE_UNIMPORTANT;
GtkWidget *child = NULL;
g_autoptr (GListModel) model = NULL;
guint n_items = 0;
g_autoptr (BzAppPermissions) permissions = NULL;
BzAppPermissionsFlags perm_flags = BZ_APP_PERMISSIONS_FLAGS_NONE;
while ((child = gtk_widget_get_first_child (GTK_WIDGET (self->permissions_list))) != NULL)
gtk_list_box_remove (self->permissions_list, child);
if (self->entry == NULL)
return;
g_object_get (self->entry, "permissions", &permissions, NULL);
if (permissions != NULL)
perm_flags = bz_app_permissions_get_flags (permissions);
self->has_sandbox_escape = (perm_flags & BZ_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX) != 0;
app_name = bz_entry_get_title (self->entry);
model = bz_safety_calculator_analyze_entry (self->entry);
importance = bz_safety_calculator_calculate_rating (self->entry);
n_items = g_list_model_get_n_items (model);
for (gint level = BZ_IMPORTANCE_IMPORTANT; level >= BZ_IMPORTANCE_UNIMPORTANT; level--)
{
for (gint j = 0; j < n_items; j++)
{
g_autoptr (BzSafetyRow) row_data = NULL;
AdwActionRow *row = NULL;
BzImportance row_importance = 0;
row_data = g_list_model_get_item (model, j);
row_importance = bz_safety_row_get_importance (row_data);
if (row_importance != level)
continue;
row = bz_context_row_new (bz_safety_row_get_icon_name (row_data),
bz_safety_row_get_importance (row_data),
bz_safety_row_get_title (row_data),
bz_safety_row_get_subtitle (row_data));
gtk_list_box_append (self->permissions_list, GTK_WIDGET (row));
}
}
switch (importance)
{
case BZ_IMPORTANCE_UNIMPORTANT:
icon_names[0] = "app-safety-ok-symbolic";
icon_names[1] = NULL;
title_text = g_strdup_printf (_ ("%s is Safe"), app_name);
break;
case BZ_IMPORTANCE_NEUTRAL:
icon_names[0] = "app-safety-ok-symbolic";
icon_names[1] = NULL;
title_text = g_strdup_printf (_ ("%s has no Unsafe Permissions"), app_name);
break;
case BZ_IMPORTANCE_INFORMATION:
icon_names[0] = "app-safety-ok-symbolic";
icon_names[1] = NULL;
title_text = g_strdup_printf (_ ("%s is Probably Safe"), app_name);
break;
case BZ_IMPORTANCE_WARNING:
icon_names[0] = "app-safety-unknown-symbolic";
icon_names[1] = NULL;
title_text = g_strdup_printf (_ ("%s is Possibly Unsafe"), app_name);
break;
case BZ_IMPORTANCE_IMPORTANT:
icon_names[0] = "app-safety-unsafe-symbolic";
icon_names[1] = NULL;
title_text = g_strdup_printf (_ ("%s is Unsafe"), app_name);
break;
default:
g_assert_not_reached ();
}
bz_lozenge_set_icon_names (self->lozenge, icon_names);
bz_lozenge_set_title (self->lozenge, title_text);
bz_lozenge_set_importance (self->lozenge, importance);
}