/* PureAdmin
* Copyright (C) 2003 Isak Savo
*
* 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 2 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
/*
* Preferences management - the GUI-part.
*
* Copyright (C) 2003 - Isak Savo
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <gtk/gtk.h>
#include "cfg.h"
#include "prefwin.h"
#include "helper.h"
#include "gui_helper.h"
#include "logfile.h"
#include "globals.h"
#include "system_accounts.h"
#include "dirbrowser.h"
static void prefwin_menu_gid_changed (GtkOptionMenu *optionmenu, gpointer user_data);
static void prefwin_menu_uid_changed (GtkOptionMenu *optionmenu, gpointer user_data);
static void prefwin_rdo_toggled (GtkToggleButton *togglebutton, gpointer user_data);
static void prefwin_entry_changed (GtkEditable *editable, gpointer user_data);
static void prefwin_btn_search_clicked (GtkButton *button, gpointer user_data);
static void prefwin_chkbtn_toggled (GtkToggleButton *togglebutton, gpointer user_data);
static void prefwin_chkencoding_toggled (GtkToggleButton *togglebutton, gpointer user_data);
static void prefwin_btn_browse_clicked (GtkButton *button, GtkWidget *entry);
static void prefwin_btn_browse_home_clicked (GtkButton *button, gpointer user_data);
static void prefwin_btn_encoding_sel_clicked (GtkButton *button, gpointer user_data);
GladeXML *pref_xml = NULL;
prefwin_widgets *prefwin_get_widgets (void)
{
static prefwin_widgets *w = NULL;
if (G_UNLIKELY (!w))
{
w = g_new0 (prefwin_widgets, 1);
w->menu_uid = PW("opt_menu_uid");
w->menu_gid = PW("opt_menu_gid");
w->home = PW("entry_home");
w->cmd_purepw = PW("entry_purepw_cmd");
w->pwfile = PW("entry_passwd_file");
w->pdbfile = PW("entry_pdb_file");
w->encoding = PW("entry_encoding");
w->btn_search = PW("btn_search_set_defaults");
w->rdo_syslog = PW("rdo_syslog");
w->rdo_custom = PW("rdo_custom");
w->logfile = PW("entry_logfile");
w->chk_resolve = PW("chk_resolve_hostnames");
w->btn_home_browse = PW("btn_home_dir_browse");
w->btn_encoding_sel = PW("btn_encoding_sel");
w->chk_save_window_geo = PW("chk_save_window_geo");
w->chk_use_system_encoding = PW("chk_use_system_encoding");
w->chk_use_tray_icon = PW("chk_use_tray_icon");
w->chk_always_notify = PW("chk_always_notify");
w->btn_browse_purepw = PW("btn_browse_purepw");
w->btn_browse_pw = PW("btn_browse_pw");
w->btn_browse_pdb = PW("btn_browse_pdb");
}
return w;
}
void prefwin_close (GtkWidget *w, gpointer data)
{
cfg_write_settings ();
}
static GtkWidget *create_prefwin (void)
{
gchar *our_path;
our_path = g_build_filename (datadir, PACKAGE, "prefwin.glade", NULL);
pref_xml = glade_xml_new (our_path, NULL, NULL);
g_free (our_path);
/* FIXME: autoconnect? */
return PW("preferences");
}
GtkWidget *pref_init_prefwindow (GtkWidget *parent)
{
prefwin_widgets *w;
GList *id;
GtkWidget *menu;
static GtkWidget *prefwin = NULL;
gint i;
if (!prefwin)
prefwin = create_prefwin ();
else
return prefwin;
w = prefwin_get_widgets ();
/* GID/UID Option menus */
id = sys_get_user_ids ();
menu = gui_create_system_account_menu(id);
gtk_option_menu_remove_menu (GTK_OPTION_MENU (w->menu_uid));
gtk_option_menu_set_menu (GTK_OPTION_MENU (w->menu_uid), menu);
/* Keep the list attached to the menu so that it's available in the callbacks.. */
g_object_set_data (G_OBJECT (w->menu_uid), "items", id);
i = 0;
while (id)
{
if (cfg.default_uid == ((SystemAccount*) id->data)->id)
break;
i++;
id = g_list_next (id);
}
gtk_option_menu_set_history (GTK_OPTION_MENU (w->menu_uid), i);
id = sys_get_group_ids ();
menu = gui_create_system_account_menu(id);
gtk_option_menu_remove_menu (GTK_OPTION_MENU (w->menu_gid));
gtk_option_menu_set_menu (GTK_OPTION_MENU (w->menu_gid), menu);
g_object_set_data_full (G_OBJECT (w->menu_gid), "items", id, (GDestroyNotify) system_account_list_free);
i = 0;
while (id)
{
if (cfg.default_gid == ((SystemAccount*) id->data)->id)
break;
i++;
id = g_list_next (id);
}
gtk_option_menu_set_history (GTK_OPTION_MENU (w->menu_gid), i);
if (cfg.logmethod == LOG_SYSLOG)
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w->rdo_syslog), TRUE);
else
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w->rdo_custom), TRUE);
gtk_entry_set_text (GTK_ENTRY (w->home), cfg.default_home);
gtk_entry_set_text (GTK_ENTRY (w->encoding), cfg.uname_encoding);
gtk_entry_set_text (GTK_ENTRY (w->cmd_purepw), cfg.cmd_purepw);
gtk_entry_set_text (GTK_ENTRY (w->pwfile), cfg.pwfile);
gtk_entry_set_text (GTK_ENTRY (w->pdbfile), cfg.pdbfile);
gtk_entry_set_text (GTK_ENTRY (w->logfile), cfg.logfile);
/* Use these paths as a fallback for the fileselector */
g_object_set_data (G_OBJECT(w->cmd_purepw), "default_path", "/usr/bin/");
g_object_set_data (G_OBJECT(w->pwfile), "default_path", "/etc/");
g_object_set_data (G_OBJECT(w->pdbfile), "default_path", "/etc/");
/* Used to hint the file selector on the type of file we're selecting */
g_object_set_data (G_OBJECT(w->cmd_purepw), "whatfile", _("pure-pw executable"));
g_object_set_data (G_OBJECT(w->pwfile), "whatfile", _("password file"));
g_object_set_data (G_OBJECT(w->pdbfile), "whatfile", _("database file"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w->chk_resolve), cfg.resolve_hostnames);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w->chk_save_window_geo), cfg.save_window_geometry);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w->chk_use_tray_icon), cfg.use_tray_icon);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w->chk_always_notify), cfg.always_notify_activities);
g_signal_connect ((gpointer) w->chk_use_system_encoding, "toggled",
G_CALLBACK (prefwin_chkencoding_toggled),
NULL);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w->chk_use_system_encoding), cfg.use_system_encoding);
/* Hookup signals for the widgets to allow 'instant-apply' */
g_signal_connect ((gpointer) w->menu_uid, "changed",
G_CALLBACK (prefwin_menu_uid_changed),
NULL);
g_signal_connect ((gpointer) w->menu_gid, "changed",
G_CALLBACK (prefwin_menu_gid_changed),
NULL);
g_signal_connect ((gpointer) w->home, "changed",
G_CALLBACK (prefwin_entry_changed),
"home");
g_signal_connect ((gpointer) w->encoding, "changed",
G_CALLBACK (prefwin_entry_changed),
"encoding");
g_signal_connect ((gpointer) w->cmd_purepw, "changed",
G_CALLBACK (prefwin_entry_changed),
"cmd_purepw");
g_signal_connect ((gpointer) w->pwfile, "changed",
G_CALLBACK (prefwin_entry_changed),
"pwfile");
g_signal_connect ((gpointer) w->pdbfile, "changed",
G_CALLBACK (prefwin_entry_changed),
"pdbfile");
g_signal_connect ((gpointer) w->rdo_syslog, "toggled",
G_CALLBACK (prefwin_rdo_toggled),
GINT_TO_POINTER (LOG_SYSLOG));
g_signal_connect ((gpointer) w->rdo_custom, "toggled",
G_CALLBACK (prefwin_rdo_toggled),
GINT_TO_POINTER (LOG_CUSTOM));
g_signal_connect ((gpointer) w->logfile, "changed",
G_CALLBACK (prefwin_entry_changed),
"logfile");
g_signal_connect ((gpointer) w->chk_resolve, "toggled",
G_CALLBACK (prefwin_chkbtn_toggled),
"resolve");
g_signal_connect ((gpointer) w->chk_use_tray_icon, "toggled",
G_CALLBACK (prefwin_chkbtn_toggled),
"use_tray_icon");
g_signal_connect ((gpointer) w->chk_always_notify, "toggled",
G_CALLBACK (prefwin_chkbtn_toggled),
"always_notify_activities");
g_signal_connect ((gpointer) w->chk_save_window_geo, "toggled",
G_CALLBACK (prefwin_chkbtn_toggled),
"save_window");
/* Button Callbacks */
g_signal_connect ((gpointer) w->btn_search, "clicked",
G_CALLBACK (prefwin_btn_search_clicked),
NULL);
g_signal_connect ((gpointer) w->btn_encoding_sel, "clicked",
G_CALLBACK (prefwin_btn_encoding_sel_clicked),
NULL);
g_signal_connect ((gpointer) w->btn_home_browse, "clicked",
G_CALLBACK (prefwin_btn_browse_home_clicked),
NULL);
g_signal_connect ((gpointer) w->btn_browse_purepw, "clicked",
G_CALLBACK (prefwin_btn_browse_clicked),
(gpointer) w->cmd_purepw);
g_signal_connect ((gpointer) w->btn_browse_pw, "clicked",
G_CALLBACK (prefwin_btn_browse_clicked),
(gpointer) w->pwfile);
g_signal_connect ((gpointer) w->btn_browse_pdb, "clicked",
G_CALLBACK (prefwin_btn_browse_clicked),
(gpointer) w->pdbfile);
/* Signals for the prefwindow actual prefwindow */
g_signal_connect_swapped (G_OBJECT (PW("btn_prefwin_close")),
"clicked",
G_CALLBACK (gtk_widget_hide),
(gpointer) prefwin);
g_signal_connect (G_OBJECT (prefwin),
"delete-event",
G_CALLBACK (gtk_widget_hide),
NULL);
g_signal_connect (G_OBJECT (prefwin),
"hide",
G_CALLBACK (prefwin_close),
NULL);
gtk_window_set_transient_for (GTK_WINDOW (prefwin), GTK_WINDOW (parent));
return prefwin;
}
static void prefwin_menu_gid_changed (GtkOptionMenu *optionmenu, gpointer user_data)
{
GList *id;
gint node_no;
id = (GList *) g_object_get_data (G_OBJECT (optionmenu), "items");
node_no = gtk_option_menu_get_history (optionmenu);
id = g_list_nth (id, node_no);
if (!id)
g_print ("Gid change: Wrong history: %d\n", node_no);
}
static void prefwin_menu_uid_changed (GtkOptionMenu *optionmenu, gpointer user_data)
{
GList *id;
gint node_no;
id = (GList *) g_object_get_data (G_OBJECT (optionmenu), "items");
node_no = gtk_option_menu_get_history (optionmenu);
id = g_list_nth (id, node_no);
if (!id)
g_print ("Uid change: Wrong history: %d\n", node_no);
}
static void prefwin_rdo_toggled (GtkToggleButton *togglebutton, gpointer user_data)
{
GtkWidget *entry;
if (!gtk_toggle_button_get_active (togglebutton))
return;
entry = PW("entry_logfile");
cfg.logmethod = GPOINTER_TO_INT (user_data);
switch (GPOINTER_TO_INT (user_data))
{
case LOG_SYSLOG: /* Syslog selected */
gtk_entry_set_text (GTK_ENTRY (entry), "/var/log/messages");
break;
case LOG_CUSTOM: /* Custom selected */
gtk_entry_set_text (GTK_ENTRY (entry), "/var/log/pureftpd.log");
break;
}
}
static void prefwin_chkbtn_toggled (GtkToggleButton *togglebutton, gpointer user_data)
{
gchar *arg = (gchar *) user_data;
gboolean toggled = gtk_toggle_button_get_active (togglebutton);
if (g_str_equal (arg, "resolve"))
cfg.resolve_hostnames = toggled;
else if (g_str_equal (arg, "save_window"))
cfg.save_window_geometry = toggled;
else if (g_str_equal (arg, "use_tray_icon")) {
if (!toggled)
/* Remove it if it's shown */
hide_status_icon ();
cfg.use_tray_icon = toggled;
}
else if (g_str_equal (arg, "always_notify_activities"))
cfg.always_notify_activities = toggled;
}
static void prefwin_chkencoding_toggled (GtkToggleButton *togglebutton, gpointer user_data)
{
prefwin_widgets *w;
gboolean active;
w = prefwin_get_widgets ();
active = gtk_toggle_button_get_active (togglebutton);
cfg.use_system_encoding = active;
gtk_widget_set_sensitive (w->encoding, !active);
gtk_widget_set_sensitive (w->btn_encoding_sel, !active);
}
static void prefwin_entry_changed (GtkEditable *editable, gpointer user_data)
{
gchar *tmp;
gchar *ename;
ename = (gchar *) user_data;
tmp = gtk_editable_get_chars (GTK_EDITABLE (editable), 0, -1);
/* g_print ("Entry [%s] changed into: %s\n", ename, tmp); */
if (g_str_equal (ename, "home"))
{
g_free (cfg.default_home);
cfg.default_home = tmp;
}
else if (g_str_equal (ename, "cmd_purepw"))
{
g_free (cfg.cmd_purepw);
cfg.cmd_purepw = tmp;
}
else if (g_str_equal (ename, "pwfile"))
{
g_free (cfg.pwfile);
cfg.pwfile = tmp;
}
else if (g_str_equal (ename, "pdbfile"))
{
g_free (cfg.pdbfile);
cfg.pdbfile = tmp;
}
else if (g_str_equal(ename, "logfile"))
{
if (g_path_is_absolute (tmp) && g_file_test (tmp, G_FILE_TEST_IS_REGULAR))
{
close_logfile ();
g_free (cfg.logfile);
cfg.logfile = tmp;
init_logfile ();
}
else
g_free (tmp);
}
else if (g_str_equal (ename, "encoding"))
{
g_free (cfg.uname_encoding);
cfg.uname_encoding = tmp;
}
else
g_free (tmp);
}
static void prefwin_btn_search_clicked (GtkButton *button, gpointer user_data)
{
gchar *tmp;
prefwin_widgets *w;
w = prefwin_get_widgets ();
tmp = g_find_program_in_path ("pure-pw");
gtk_entry_set_text (GTK_ENTRY (w->cmd_purepw), tmp ? tmp : _("Not Found"));
g_free (tmp);
tmp = cfg_find_pwfile ();
gtk_entry_set_text (GTK_ENTRY (w->pwfile), tmp ? tmp : _("Not Found"));
g_free (tmp);
tmp = cfg_find_pdbfile ();
gtk_entry_set_text (GTK_ENTRY (w->pdbfile), tmp ? tmp : _("Not Found"));
g_free (tmp);
}
static void store_filename (GtkWidget *widget, GtkWidget *fs)
{
GtkWidget *entry;
const gchar *selected_filename;
entry = (GtkWidget *) g_object_get_data (G_OBJECT (fs), "affected_entry");
selected_filename = gtk_file_selection_get_filename (GTK_FILE_SELECTION (fs));
gtk_entry_set_text (GTK_ENTRY (entry), selected_filename);
}
static GtkWidget *create_fileselector (const gchar *title, const gchar *preselected)
{
GtkWidget *file_selector;
file_selector = gtk_file_selection_new (title);
g_signal_connect (GTK_FILE_SELECTION (file_selector)->ok_button,
"clicked",
G_CALLBACK (store_filename),
file_selector);
/* Ensure that the dialog box is destroyed when the user clicks a button. */
g_signal_connect_swapped (GTK_FILE_SELECTION (file_selector)->ok_button,
"clicked",
G_CALLBACK (gtk_widget_destroy),
file_selector);
g_signal_connect_swapped (GTK_FILE_SELECTION (file_selector)->cancel_button,
"clicked",
G_CALLBACK (gtk_widget_destroy),
file_selector);
gtk_file_selection_set_filename (GTK_FILE_SELECTION (file_selector),
preselected);
gtk_window_set_transient_for (GTK_WINDOW (file_selector),
GTK_WINDOW (PW("preferences")));
return file_selector;
}
static void prefwin_btn_browse_clicked (GtkButton *button, GtkWidget *entry)
{
prefwin_widgets *w;
GtkWidget *fs;
gchar *title;
w = prefwin_get_widgets ();
/* Translators: %s will expand to the type of file. For instance, 'password file' or 'database file'.
* This is used as title for the file selector. */
title = g_strdup_printf (_("Please select %s"), (gchar *) g_object_get_data (G_OBJECT (entry), "whatfile"));
if (strlen (gtk_entry_get_text (GTK_ENTRY (entry))) > 0)
fs = create_fileselector (title, gtk_entry_get_text (GTK_ENTRY (entry)));
else
fs = create_fileselector (title, g_object_get_data (G_OBJECT(entry), "default_path"));
g_object_set_data (G_OBJECT (fs), "affected_entry", (gpointer) entry);
gtk_widget_show (fs);
}
static void prefwin_btn_browse_home_clicked (GtkButton *button, gpointer user_data)
{
prefwin_widgets *w;
GtkWidget *dbr;
const gchar *dir;
gint response;
w = prefwin_get_widgets ();
dbr = dirbrowser_new (_("Select Default Home Directory"));
if (g_file_test (cfg.default_home, G_FILE_TEST_IS_DIR))
dirbrowser_set_directory (DIRBROWSER (dbr), cfg.default_home);
response = gtk_dialog_run (GTK_DIALOG (dbr));
if (response == GTK_RESPONSE_OK)
{
dir = dirbrowser_get_directory (DIRBROWSER (dbr));
gtk_entry_set_text (GTK_ENTRY (w->home), dir);
}
gtk_widget_destroy (dbr);
}
static void
encoding_changed_cb (GtkTreeView *tree, gpointer user_data)
{
GtkEntry *entry = GTK_ENTRY (user_data);
GtkTreeModel *model;
GtkTreeSelection *sel;
GtkTreeIter iter;
gchar *encoding;
g_print ("encoding changed\n");
sel = gtk_tree_view_get_selection (tree);
model = gtk_tree_view_get_model (tree);
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_tree_model_get (model, &iter,
0, &encoding,
-1);
gtk_entry_set_text (entry, encoding);
}
return;
}
static GList *
get_available_encodings (void)
{
GList *retval = NULL;
#define BUFSIZE 256
gchar buf[BUFSIZE], *tmp;
gchar *prgm, *full_prgm;
FILE *p;
if (!(prgm = g_find_program_in_path ("iconv"))) {
pur_log_wrn ("Couldn't find the program \"iconv\". Unable to fetch list of encodings");
return NULL;
}
full_prgm = g_strdup_printf ("%s --list", prgm);
g_free (prgm);
if ((p = popen (full_prgm, "r")) == NULL)
{
pur_log_wrn ("popen %s: %s", full_prgm, strerror(errno));
return NULL;
}
g_free (full_prgm);
while (fgets (buf, BUFSIZE, p))
{
if (!strstr (buf, "//"))
continue;
tmp = buf;
while (tmp){
if (*tmp == '/' && *(tmp +1) == '/') {
*tmp = '\0';
break;
}
tmp++;
}
g_strstrip (buf);
retval = g_list_append (retval, g_strdup (buf));
}
pclose (p);
return retval;
}
static void
free_encoding_list (GtkWidget *w, gpointer list)
{
g_list_free ((GList *) list);
}
static void
filter_encoding_list (GtkWidget *entry, gpointer data)
{
GtkTreeView *tree = GTK_TREE_VIEW (data);
GtkTreeIter iter, iter_sel;
GtkListStore *model;
GList *list;
gint handler_id;
const gchar *filter_str, *encoding;
gboolean sel = FALSE;
filter_str = gtk_entry_get_text (GTK_ENTRY(entry));
handler_id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tree), "callback_handler_id"));
list = g_object_get_data (G_OBJECT (tree), "encoding_list");
model = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (tree)));
g_signal_handler_block (G_OBJECT (tree), handler_id);
gtk_list_store_clear (GTK_LIST_STORE (model));
while (list)
{
encoding = (const gchar *) list->data;
if (misc_str_isearch (encoding, filter_str))
{
gtk_list_store_append (GTK_LIST_STORE (model), &iter);
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
0, encoding,
-1);
if (g_str_equal (encoding, cfg.uname_encoding)) {
memcpy (&iter_sel, &iter, sizeof (GtkTreeIter));
sel = TRUE;
}
}
list = g_list_next (list);
}
if (sel)
gui_select_treeview_row (&iter_sel, tree);
g_signal_handler_unblock (G_OBJECT (tree), handler_id);
return;
}
static void
prefwin_btn_encoding_sel_clicked (GtkButton *button, gpointer user_data)
{
prefwin_widgets *w;
static GtkWidget *dlg = NULL;
GList *list, *head;
GtkListStore *model;
GtkCellRenderer *renderer;
GtkTreeViewColumn *column;
GtkTreeIter iter, iter_sel;
GtkTreeView *tree;
GtkWidget *entry;
guint handler_id;
gboolean sel = FALSE;
if (dlg) {
gtk_window_present (GTK_WINDOW (dlg));
return;
}
w = prefwin_get_widgets ();
dlg = PW("dlg_encoding");
tree = GTK_TREE_VIEW (PW("tree_encoding"));
model = gtk_list_store_new (1, G_TYPE_STRING);
gtk_tree_view_set_model (GTK_TREE_VIEW (tree), GTK_TREE_MODEL (model));
gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (tree)),
GTK_SELECTION_SINGLE);
renderer = gtk_cell_renderer_text_new();
column = gtk_tree_view_column_new_with_attributes (NULL, renderer,
"text", 0,
NULL);
gtk_tree_view_append_column (GTK_TREE_VIEW(tree), column);
head = list = get_available_encodings ();
while (list)
{
gtk_list_store_append (GTK_LIST_STORE (model), &iter);
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
0, (gchar *) list->data,
-1);
if (g_str_equal ((gchar*) list->data, cfg.uname_encoding)) {
memcpy (&iter_sel, &iter, sizeof (GtkTreeIter));
sel = TRUE;
}
list = g_list_next (list);
}
if (sel)
gui_select_treeview_row (&iter_sel, tree);
entry = PW("entry_filter");
g_signal_connect (G_OBJECT (entry), "changed",
G_CALLBACK (filter_encoding_list), (gpointer) tree);
handler_id = g_signal_connect (G_OBJECT (tree), "cursor-changed",
G_CALLBACK (encoding_changed_cb), w->encoding);
g_object_set_data (G_OBJECT (tree), "callback_handler_id", GINT_TO_POINTER (handler_id));
g_object_set_data (G_OBJECT (tree), "encoding_list", head);
g_signal_connect (G_OBJECT (dlg), "destroy",
G_CALLBACK (free_encoding_list), (gpointer) head);
gtk_dialog_run (GTK_DIALOG (dlg));
gtk_widget_destroy (dlg);
dlg = NULL;
}
syntax highlighted by Code2HTML, v. 0.9.1