2013-09-08 11:39:44 +00:00
|
|
|
#include <glib/gi18n.h>
|
2013-09-17 16:35:43 +00:00
|
|
|
#include <libxml/parser.h>
|
|
|
|
#include <libxml/xpath.h>
|
|
|
|
#include <errno.h>
|
2013-09-08 11:39:44 +00:00
|
|
|
|
|
|
|
#include "ag-app.h"
|
2013-09-08 21:02:05 +00:00
|
|
|
#include "ag-window.h"
|
2013-09-08 11:39:44 +00:00
|
|
|
#include "config.h"
|
2013-09-17 12:46:09 +00:00
|
|
|
#include "astrognome.h"
|
2013-09-08 11:39:44 +00:00
|
|
|
|
2013-09-17 16:35:43 +00:00
|
|
|
typedef enum {
|
|
|
|
XML_CONVERT_STRING,
|
|
|
|
XML_CONVERT_DOUBLE,
|
|
|
|
XML_CONVERT_INT
|
|
|
|
} XmlConvertType;
|
|
|
|
|
2013-09-08 11:39:44 +00:00
|
|
|
struct _AgAppPrivate {
|
|
|
|
};
|
|
|
|
|
|
|
|
G_DEFINE_TYPE(AgApp, ag_app, GTK_TYPE_APPLICATION);
|
|
|
|
|
|
|
|
GtkWindow *
|
2013-09-17 09:41:34 +00:00
|
|
|
ag_app_peek_first_window(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
|
|
|
GList *l;
|
|
|
|
|
2013-09-17 09:41:34 +00:00
|
|
|
for (l = gtk_application_get_windows(GTK_APPLICATION(app)); l; l = g_list_next(l)) {
|
2013-09-08 11:39:44 +00:00
|
|
|
if (GTK_IS_WINDOW(l->data)) {
|
|
|
|
return (GTK_WINDOW(l->data));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-09-17 09:41:34 +00:00
|
|
|
ag_app_new_window(app);
|
2013-09-08 11:39:44 +00:00
|
|
|
|
2013-09-17 09:41:34 +00:00
|
|
|
return ag_app_peek_first_window(app);
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2013-09-17 09:41:34 +00:00
|
|
|
ag_app_new_window(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
2013-09-17 09:41:34 +00:00
|
|
|
g_action_group_activate_action(G_ACTION_GROUP(app), "new-window", NULL);
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2013-09-17 09:41:34 +00:00
|
|
|
ag_app_quit(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
2013-09-17 09:41:34 +00:00
|
|
|
g_action_group_activate_action(G_ACTION_GROUP(app), "quit", NULL);
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2013-09-17 09:41:34 +00:00
|
|
|
ag_app_raise(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
2013-09-17 09:41:34 +00:00
|
|
|
g_action_group_activate_action(G_ACTION_GROUP(app), "raise", NULL);
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|
2013-09-17 11:26:16 +00:00
|
|
|
static GtkWidget *
|
|
|
|
ag_app_create_window(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
|
|
|
GtkWidget *window;
|
|
|
|
|
2013-09-17 09:41:34 +00:00
|
|
|
window = ag_window_new(app);
|
|
|
|
gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(window));
|
2013-09-08 11:39:44 +00:00
|
|
|
gtk_widget_show_all(window);
|
2013-09-17 11:26:16 +00:00
|
|
|
|
|
|
|
return window;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
new_window_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data)
|
|
|
|
{
|
|
|
|
ag_app_create_window(AG_APP(user_data));
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
preferences_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data)
|
|
|
|
{
|
|
|
|
//ag_preferences_show_dialog();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
about_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data)
|
|
|
|
{
|
|
|
|
const gchar *authors[] = {
|
|
|
|
"Gergely Polonkai <gergely@polonkai.eu>",
|
|
|
|
"Jean-André Santoni <jean.andre.santoni@gmail.com>",
|
|
|
|
NULL
|
|
|
|
};
|
|
|
|
|
|
|
|
const gchar **documentors = NULL;
|
|
|
|
const gchar *translator_credits = _("translator_credits");
|
|
|
|
|
|
|
|
/* i18n: Please don't translate "Astrognome" (it's marked as translatable for transliteration only */
|
|
|
|
gtk_show_about_dialog(NULL,
|
|
|
|
"name", _("Astrognome"),
|
|
|
|
"version", PACKAGE_VERSION,
|
|
|
|
"comments", _("Astrologers' software for GNOME"),
|
|
|
|
"authors", authors,
|
|
|
|
"documentors", documentors,
|
|
|
|
"translator_credits", ((strcmp(translator_credits, "translator_credits") != 0) ? translator_credits : NULL),
|
|
|
|
"website", PACKAGE_URL,
|
|
|
|
"website-label", _("Astrognome Website"),
|
|
|
|
"logo-icon-name", PACKAGE_TARNAME,
|
|
|
|
NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
quit_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data)
|
|
|
|
{
|
|
|
|
GList *l;
|
|
|
|
|
2013-09-17 09:41:34 +00:00
|
|
|
while ((l = gtk_application_get_windows(GTK_APPLICATION(user_data)))) {
|
|
|
|
gtk_application_remove_window(GTK_APPLICATION(user_data), GTK_WINDOW(l->data));
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-09-17 16:35:43 +00:00
|
|
|
GVariant *
|
|
|
|
get_by_xpath(xmlXPathContextPtr ctx, const gchar *xpath, XmlConvertType type)
|
|
|
|
{
|
|
|
|
xmlXPathObjectPtr xpathObj;
|
|
|
|
const gchar *text;
|
|
|
|
char *endptr;
|
|
|
|
GVariant *ret = NULL;
|
|
|
|
gdouble d;
|
|
|
|
gint i;
|
|
|
|
|
|
|
|
if ((xpathObj = xmlXPathEvalExpression((const xmlChar *)xpath, ctx)) == NULL) {
|
|
|
|
// TODO: Warn with a popup or similar way
|
|
|
|
g_warning("Could not initialize XPath");
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2013-09-17 19:50:05 +00:00
|
|
|
if (xpathObj->nodesetval == NULL) {
|
|
|
|
// TODO: Warn with a popup or similar way
|
|
|
|
g_warning("Required element not found. This is not a valid save file!");
|
|
|
|
xmlXPathFreeObject(xpathObj);
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2013-09-17 16:35:43 +00:00
|
|
|
if (xpathObj->nodesetval->nodeNr > 1) {
|
|
|
|
// TODO: Warn with a popup or similar way
|
|
|
|
g_warning("Too many elements. This is not a valid save file!");
|
|
|
|
xmlXPathFreeObject(xpathObj);
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
text = (const gchar *)xpathObj->nodesetval->nodeTab[0]->content;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case XML_CONVERT_STRING:
|
|
|
|
ret = g_variant_new_string(text);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case XML_CONVERT_DOUBLE:
|
|
|
|
d = g_ascii_strtod(text, &endptr);
|
|
|
|
|
|
|
|
if ((*endptr != 0) || (errno != 0)) {
|
|
|
|
ret = NULL;
|
|
|
|
} else {
|
|
|
|
ret = g_variant_new_double(d);
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case XML_CONVERT_INT:
|
|
|
|
i = strtol(text, &endptr, 10);
|
|
|
|
|
|
|
|
if ((*endptr != 0) || (errno != 0)) {
|
|
|
|
ret = NULL;
|
|
|
|
} else {
|
|
|
|
ret = g_variant_new_int32(i);
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
xmlXPathFreeObject(xpathObj);
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2013-09-17 12:52:12 +00:00
|
|
|
static void
|
|
|
|
ag_app_open_chart(AgApp *app, GFile *file)
|
|
|
|
{
|
2013-09-17 16:35:43 +00:00
|
|
|
GError *err = NULL;
|
2013-09-17 19:50:58 +00:00
|
|
|
gchar *uri,
|
|
|
|
*xml;
|
2013-09-17 16:35:43 +00:00
|
|
|
guint length;
|
|
|
|
xmlDocPtr doc;
|
|
|
|
xmlXPathContextPtr xpathCtx;
|
|
|
|
GVariant *chart_name,
|
|
|
|
*country,
|
|
|
|
*city,
|
|
|
|
*longitude,
|
|
|
|
*latitude,
|
|
|
|
*altitude,
|
|
|
|
*year,
|
|
|
|
*month,
|
|
|
|
*day,
|
|
|
|
*hour,
|
|
|
|
*minute,
|
|
|
|
*second;
|
|
|
|
|
2013-09-17 19:50:58 +00:00
|
|
|
uri = g_file_get_uri(file);
|
|
|
|
|
2013-09-17 16:35:43 +00:00
|
|
|
if (!g_file_load_contents(file, NULL, &xml, &length, NULL, &err)) {
|
|
|
|
// TODO: Warn with a popup or similar way
|
|
|
|
g_warning("Could not open file '%s': %s", uri, err->message);
|
|
|
|
g_clear_error(&err);
|
2013-09-17 19:50:58 +00:00
|
|
|
g_free(uri);
|
2013-09-17 16:35:43 +00:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((doc = xmlReadMemory(xml, length, "chart.xml", NULL, 0)) == NULL) {
|
|
|
|
// TODO: Warn with a popup or similar way
|
|
|
|
g_warning("Saved chart is corrupt (or not a saved chart at all)");
|
2013-09-17 19:51:43 +00:00
|
|
|
g_free(xml);
|
2013-09-17 19:50:58 +00:00
|
|
|
g_free(uri);
|
2013-09-17 16:35:43 +00:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((xpathCtx = xmlXPathNewContext(doc)) == NULL) {
|
|
|
|
// TODO: Warn with a popup or similar way
|
|
|
|
g_warning("Could not initialize XPath");
|
|
|
|
xmlFreeDoc(doc);
|
2013-09-17 19:51:43 +00:00
|
|
|
g_free(xml);
|
2013-09-17 19:50:58 +00:00
|
|
|
g_free(uri);
|
2013-09-17 16:35:43 +00:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
chart_name = get_by_xpath(xpathCtx, "/chartinfo/data/name/text()", XML_CONVERT_STRING);
|
|
|
|
country = get_by_xpath(xpathCtx, "/chartinfo/data/place/country/text()", XML_CONVERT_STRING);
|
|
|
|
city = get_by_xpath(xpathCtx, "/chartinfo/data/place/city/text()", XML_CONVERT_STRING);
|
|
|
|
longitude = get_by_xpath(xpathCtx, "/chartinfo/data/place/longitude/text()", XML_CONVERT_DOUBLE);
|
|
|
|
latitude = get_by_xpath(xpathCtx, "/chartinfo/data/place/latitude/text()", XML_CONVERT_DOUBLE);
|
|
|
|
altitude = get_by_xpath(xpathCtx, "/chartinfo/data/place/altitude/text()", XML_CONVERT_DOUBLE);
|
|
|
|
year = get_by_xpath(xpathCtx, "/chartinfo/data/time/year/text()", XML_CONVERT_INT);
|
|
|
|
month = get_by_xpath(xpathCtx, "/chartinfo/data/time/month/text()", XML_CONVERT_INT);
|
|
|
|
day = get_by_xpath(xpathCtx, "/chartinfo/data/time/day/text()", XML_CONVERT_INT);
|
|
|
|
hour = get_by_xpath(xpathCtx, "/chartinfo/data/time/hour/text()", XML_CONVERT_INT);
|
|
|
|
minute = get_by_xpath(xpathCtx, "/chartinfo/data/time/minute/text()", XML_CONVERT_INT);
|
|
|
|
second = get_by_xpath(xpathCtx, "/chartinfo/data/time/second/text()", XML_CONVERT_INT);
|
|
|
|
|
|
|
|
g_variant_unref(chart_name);
|
|
|
|
g_variant_unref(country);
|
|
|
|
g_variant_unref(city);
|
|
|
|
g_variant_unref(longitude);
|
|
|
|
g_variant_unref(latitude);
|
|
|
|
g_variant_unref(altitude);
|
|
|
|
g_variant_unref(year);
|
|
|
|
g_variant_unref(month);
|
|
|
|
g_variant_unref(day);
|
|
|
|
g_variant_unref(hour);
|
|
|
|
g_variant_unref(minute);
|
|
|
|
g_variant_unref(second);
|
|
|
|
|
2013-09-17 19:51:43 +00:00
|
|
|
g_free(xml);
|
2013-09-17 19:50:58 +00:00
|
|
|
g_free(uri);
|
2013-09-17 16:35:43 +00:00
|
|
|
xmlXPathFreeContext(xpathCtx);
|
|
|
|
xmlFreeDoc(doc);
|
2013-09-17 12:52:12 +00:00
|
|
|
}
|
|
|
|
|
2013-09-17 11:58:49 +00:00
|
|
|
static void
|
|
|
|
open_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data)
|
|
|
|
{
|
2013-09-17 12:46:09 +00:00
|
|
|
gint response;
|
|
|
|
GtkWidget *fs;
|
|
|
|
GSList *filenames = NULL;
|
|
|
|
|
|
|
|
fs = gtk_file_chooser_dialog_new(_("Select charts"),
|
|
|
|
NULL,
|
|
|
|
GTK_FILE_CHOOSER_ACTION_OPEN,
|
|
|
|
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
|
|
|
|
GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
|
|
|
|
NULL);
|
|
|
|
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(fs), filter_all);
|
|
|
|
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(fs), filter_chart);
|
|
|
|
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(fs), filter_chart);
|
|
|
|
gtk_dialog_set_default_response(GTK_DIALOG(fs), GTK_RESPONSE_ACCEPT);
|
|
|
|
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fs), TRUE);
|
|
|
|
gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(fs), FALSE);
|
|
|
|
|
|
|
|
response = gtk_dialog_run(GTK_DIALOG(fs));
|
|
|
|
|
|
|
|
if (response == GTK_RESPONSE_ACCEPT) {
|
|
|
|
filenames = gtk_file_chooser_get_uris(GTK_FILE_CHOOSER(fs));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filenames != NULL) {
|
|
|
|
GSList *l;
|
|
|
|
|
|
|
|
for (l = filenames; l; l = g_slist_next(l)) {
|
|
|
|
GFile *file;
|
2013-09-17 12:52:12 +00:00
|
|
|
char *data = l->data;
|
2013-09-17 12:46:09 +00:00
|
|
|
|
|
|
|
if (data == NULL) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
file = g_file_new_for_commandline_arg(data);
|
2013-09-17 12:52:12 +00:00
|
|
|
ag_app_open_chart(AG_APP(user_data), file);
|
2013-09-17 12:46:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
gtk_widget_destroy(fs);
|
2013-09-17 11:58:49 +00:00
|
|
|
}
|
|
|
|
|
2013-09-08 11:39:44 +00:00
|
|
|
static void
|
|
|
|
raise_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data)
|
|
|
|
{
|
2013-09-17 09:41:34 +00:00
|
|
|
AgApp *app = AG_APP(user_data);
|
2013-09-08 11:39:44 +00:00
|
|
|
GtkWindow *window;
|
|
|
|
|
2013-09-17 09:41:34 +00:00
|
|
|
window = ag_app_peek_first_window(app);
|
2013-09-08 11:39:44 +00:00
|
|
|
gtk_window_present(window);
|
|
|
|
}
|
|
|
|
|
|
|
|
static GActionEntry app_entries[] = {
|
|
|
|
{ "new-window", new_window_cb, NULL, NULL, NULL },
|
|
|
|
{ "preferences", preferences_cb, NULL, NULL, NULL },
|
|
|
|
{ "about", about_cb, NULL, NULL, NULL },
|
|
|
|
{ "quit", quit_cb, NULL, NULL, NULL },
|
|
|
|
{ "raise", raise_cb, NULL, NULL, NULL },
|
2013-09-17 11:58:49 +00:00
|
|
|
{ "open", open_cb, NULL, NULL, NULL },
|
2013-09-08 11:39:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
2013-09-17 09:41:34 +00:00
|
|
|
setup_actions(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
2013-09-17 09:41:34 +00:00
|
|
|
g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app);
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2013-09-17 09:41:34 +00:00
|
|
|
setup_accelerators(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
2013-09-17 09:41:34 +00:00
|
|
|
gtk_application_add_accelerator(GTK_APPLICATION(app), "<Primary>w", "win.close", NULL);
|
2013-09-17 11:58:49 +00:00
|
|
|
gtk_application_add_accelerator(GTK_APPLICATION(app), "<Primary>s", "win.save", NULL);
|
2013-09-17 09:41:34 +00:00
|
|
|
gtk_application_add_accelerator(GTK_APPLICATION(app), "F10", "win.gear-menu", NULL);
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2013-09-17 09:41:34 +00:00
|
|
|
setup_menu(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
|
|
|
GtkBuilder *builder;
|
|
|
|
GMenuModel *model;
|
|
|
|
GError *err = NULL;
|
|
|
|
|
|
|
|
builder = gtk_builder_new();
|
|
|
|
|
|
|
|
if (!gtk_builder_add_from_resource(builder, "/eu/polonkai/gergely/astrognome/astrognome.ui", &err)) {
|
|
|
|
g_error("%s", (err) ? err->message : "unknown error");
|
|
|
|
}
|
|
|
|
|
|
|
|
model = G_MENU_MODEL(gtk_builder_get_object(builder, "app-menu"));
|
2013-09-17 09:41:34 +00:00
|
|
|
gtk_application_set_app_menu(GTK_APPLICATION(app), model);
|
2013-09-08 11:39:44 +00:00
|
|
|
|
|
|
|
g_object_unref(builder);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2013-09-17 09:41:34 +00:00
|
|
|
startup(GApplication *gapp)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
2013-09-17 09:41:34 +00:00
|
|
|
AgApp *app = AG_APP(gapp);
|
2013-09-08 11:39:44 +00:00
|
|
|
|
2013-09-17 09:41:34 +00:00
|
|
|
G_APPLICATION_CLASS(ag_app_parent_class)->startup(gapp);
|
2013-09-08 11:39:44 +00:00
|
|
|
|
2013-09-17 09:41:34 +00:00
|
|
|
setup_actions(app);
|
|
|
|
setup_menu(app);
|
|
|
|
setup_accelerators(app);
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|
2013-09-17 11:28:07 +00:00
|
|
|
static void
|
|
|
|
ag_app_open(GApplication *gapp, GFile **files, gint n_files, const gchar *hint)
|
|
|
|
{
|
|
|
|
gint i;
|
|
|
|
|
|
|
|
for (i = 0; i < n_files; i++) {
|
2013-09-17 12:52:12 +00:00
|
|
|
ag_app_open_chart(AG_APP(gapp), files[i]);
|
2013-09-17 11:28:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-09-08 11:39:44 +00:00
|
|
|
AgApp *
|
|
|
|
ag_app_new(void)
|
|
|
|
{
|
|
|
|
AgApp *app;
|
|
|
|
|
|
|
|
/* i18n: Please don't translate "Astrognome" (it's marked as translatable for transliteration only */
|
|
|
|
g_set_application_name(_("Astrognome"));
|
|
|
|
|
|
|
|
app = g_object_new(AG_TYPE_APP,
|
|
|
|
"application-id", "eu.polonkai.gergely.Astrognome",
|
2013-09-17 11:28:07 +00:00
|
|
|
"flags", G_APPLICATION_HANDLES_OPEN,
|
2013-09-08 11:39:44 +00:00
|
|
|
"register-session", TRUE,
|
|
|
|
NULL);
|
|
|
|
|
|
|
|
return app;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2013-09-17 09:41:34 +00:00
|
|
|
ag_app_init(AgApp *app)
|
2013-09-08 11:39:44 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
ag_app_class_init(AgAppClass *klass)
|
|
|
|
{
|
|
|
|
GApplicationClass *application_class = G_APPLICATION_CLASS(klass);
|
|
|
|
|
|
|
|
application_class->startup = startup;
|
2013-09-17 11:28:07 +00:00
|
|
|
application_class->open = ag_app_open;
|
2013-09-08 11:39:44 +00:00
|
|
|
}
|
|
|
|
|