...
 
Commits (21)
......@@ -2,29 +2,29 @@
# Moody #
This software is in alpha: it still has some rough edges.
For example there is still no support for folders in master.
For example the username setup require restarting the app; I ignore why.
See [CONTRIBUTING.md](/CONTRIBUTING.md) for informations about how it works.
## Quick Install ##
For Arch Linux, there is the [https://aur.archlinux.org/packages/epfl-moody-git/](epfl-moody-git) AUR package.
For Arch Linux, there is the [epfl-moody-git](https://aur.archlinux.org/packages/epfl-moody-git/) AUR package.
Fedora and Debian packages are planned.
### Flatpak ###
(for it to work, you may need to [install flatpak](https://flatpak.org/setup/))
You can also install using flatpak, which should work for every Linux distribution
```sh
flatpak-builder --install-deps-from=flatpak --install --user --force-clean _flatbuild ch.gnugen.Moody.json
```
For some reason, you may need to install elfutils `sudo apt install elfutils`
Flatpak will build libhandy and myhtml from their repositry, and use libsoup and libsecret
Flatpak will build libhandy and myhtml from their repository, and use libsoup and libsecret
from the Gnome runtime.
You can also install it system-wide by removing the `--user` flags.
you may need to [https://flatpak.org/setup/](install flatpak) for it to work
## Build it Yourself ##
### Gnome Builder ###
......@@ -36,7 +36,7 @@ and ask it to clone the repositry. Have a look at the code while it gets the dep
Alternatively, you can install it system-wide outside of the flatpak sandbox;
You first need to install [https://github.com/lexborisov/myhtml](myhtml)
You first need to install [myhtml](https://github.com/lexborisov/myhtml)
or the modest rendering engine; libhandy, libsecret and libsoup.
You may find these in your repos.
......
......@@ -3,12 +3,18 @@
// enables non-standard api connection (the only one supported at the moment)
#define EPFL
#define USERNAME "afontain"
#define CACHE_ROOT @CACHE_ROOT@
// how verbose the http requests are: 0–none, 1–headers only, 2–headers and beginning of text 3+–dump all
#mesondefine CACHE_ROOT
// how long we consider the cache «up to date»: for index files,
// the default is one hour; but for regular file, it is 10 years.
#define CACHE_TIME_HTML 3600
#define CACHE_TIME_FILES (10*365*24*3600)
// how verbose the http requests are:
// 0 – none
// 1 – headers only
// 2 – headers and beginning of response text
// 3+ – dump all
#define LOGGING_REQUESTS 0
#define APP_NAME "ch.gnugen.Moody"
......@@ -17,3 +23,5 @@
#include "epfl.h"
#endif // EPFL
......@@ -18,20 +18,23 @@
#pragma once
#define EPFL_MAIN_PAGE "https://moodle.epfl.ch/my/"
#define EPFL_TEQUILA_LOGIN "https://moodle.epfl.ch/auth/tequila/index.php"
//#define EPFL_MOODLE_ROOT "https://moodlearchive.epfl.ch/2018-2019/" // doesn't quite work: cannot get course list
#define EPFL_MOODLE_ROOT "https://moodle.epfl.ch"
#define EPFL_MAIN_PAGE EPFL_MOODLE_ROOT"/my/"
#define EPFL_TEQUILA_LOGIN EPFL_MOODLE_ROOT"/auth/tequila/index.php"
#define EPFL_TEQUILA_LOGIN_ACTION "https://tequila.epfl.ch/cgi-bin/tequila/login"
#define EPFL_ENROL_LINK "https://moodle.epfl.ch/enrol/"
#define EPFL_ENROL_LINK EPFL_MOODLE_ROOT"/enrol/"
#define EPFL_COURSE_LINK "https://moodle.epfl.ch/course/view.php?id="
#define EPFL_COURSE_LINK EPFL_MOODLE_ROOT"/course/view.php?id="
#define EPFL_ASSIGNMENT_LINK "https://moodle.epfl.ch/mod/assign/view.php?id="
#define EPFL_FILE_LINK "https://moodle.epfl.ch/mod/resource/view.php?id="
#define EPFL_FOLDERFILE_LINK "https://moodle.epfl.ch/pluginfile.php/%u/mod_folder/content/0/"
#define EPFL_FOLDER_LINK "https://moodle.epfl.ch/mod/folder/view.php?id="
#define EPFL_FORUM_LINK "https://moodle.epfl.ch/mod/forum/view.php?id="
#define EPFL_PAGE_LINK "https://moodle.epfl.ch/mod/page/view.php?id="
#define EPFL_QUIZ_LINK "https://moodle.epfl.ch/mod/quiz/view.php?id="
#define EPFL_URL_LINK "https://moodle.epfl.ch/mod/url/view.php?id="
#define EPFL_ASSIGNMENT_LINK EPFL_MOODLE_ROOT"/mod/assign/view.php?id="
#define EPFL_FILE_LINK EPFL_MOODLE_ROOT"/mod/resource/view.php?id="
#define EPFL_FOLDERFILE_LINK EPFL_MOODLE_ROOT"/pluginfile.php/%u/mod_folder/content/0/"
#define EPFL_FOLDER_LINK EPFL_MOODLE_ROOT"/mod/folder/view.php?id="
#define EPFL_FORUM_LINK EPFL_MOODLE_ROOT"/mod/forum/view.php?id="
#define EPFL_PAGE_LINK EPFL_MOODLE_ROOT"/mod/page/view.php?id="
#define EPFL_QUIZ_LINK EPFL_MOODLE_ROOT"/mod/quiz/view.php?id="
#define EPFL_URL_LINK EPFL_MOODLE_ROOT"/mod/url/view.php?id="
......@@ -9,7 +9,7 @@ src_inc = include_directories('src')
config_h = configuration_data()
config_h.set_quoted('GETTEXT_PACKAGE', 'moody')
config_h.set_quoted('CACHE_ROOT', meson.source_root())
#config_h.set_quoted('CACHE_ROOT', meson.source_root())
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir')))
configure_file(
......@@ -135,7 +135,13 @@ moody_sources = [
libhandy_dep = dependency('libhandy-0.0', version: '>= 0.0.8')
libsoup_dep = dependency('libsoup-2.4')
libsecret_dep = dependency('libsecret-1')
libmyhtml_dep = dependency('myhtml', method: 'pkg-config')
libmyhtml_dep = dependency('myhtml', required: false)
if not libmyhtml_dep.found()
message('MyHTML isn\'t found, using Modest instead')
libmyhtml_dep = dependency('modest')
endif
moody = executable('moody', moody_sources,
dependencies: [libhandy_dep, libsoup_dep, libsecret_dep, libmyhtml_dep],
include_directories: [root_inc, src_inc],
......
......@@ -32,8 +32,8 @@ enum resource_type {
};
struct Courses {
struct Course *courses;
size_t course_count;
struct Course *courses;
};
struct Course {
......
......@@ -84,6 +84,15 @@ update_course_content (GtkWidget *self_widget, gpointer unused)
}
}
void
download_course_content (GtkWidget *self_widget, gpointer bool_download_selected_elements)
{
GuiCourse *self = HDY_GUI_COURSE (self_widget);
gtk_container_foreach (GTK_CONTAINER (self->box_week),
&download_week_content,
bool_download_selected_elements);
}
static void
course_content_obtained_cb (gpointer ptr, gpointer data)
{
......
......@@ -34,5 +34,8 @@ GuiCourse* gui_course_new (struct Course course_info);
void update_course_content (GtkWidget *self_widget, gpointer unused);
void
download_course_content (GtkWidget *self_widget,
gpointer bool_download_selected_elements);
G_END_DECLS
......@@ -25,6 +25,17 @@
#define HANDY_USE_UNSTABLE_API
#include <handy.h>
struct FolderFileInfo {
guint folder_id;
gchar *filename;
};
union ResourceInfo {
struct FolderFileInfo folderfileinfo;
const gchar *url;
guint id;
};
struct _GuiResource
{
HdyActionRow parent_instance;
......@@ -150,6 +161,39 @@ activated_cb (GtkListBox* unused, GuiResource *self)
gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (self), TRUE);
}
static void
download_folder_content (gpointer self_ptr, gpointer folder_content_ptr) //REWRITEME
{
struct FolderContent *content = folder_content_ptr;
for (guint i=0; i<content->resource_count; i++) {
GuiResource *resource = gui_resource_new (content->resources[i]);
resource_download_content (GTK_WIDGET (resource), NULL);
}
open_folder_cb (self_ptr, folder_content_ptr); // since we know the folder content, display it
}
void
resource_download_content (GtkWidget *self_widget, gpointer unused)
{
GuiResource *self = HDY_GUI_RESOURCE (self_widget);
switch (self->type) {
case RESOURCE_FILE:
moodle_provider_download_file_async (self->id, NULL, self);
break;
case RESOURCE_FOLDERFILE:
if (self->filename && self->id)
moodle_provider_download_file_by_filename_async (self->id, self->filename, NULL, self);
else
g_warning ("That's odd: missing filename and/or id :-/\n");
break;
case RESOURCE_FOLDER:
moodle_provider_get_folder_content_async (download_folder_content, self->id, self);
break;
default:
break;
}
}
static void
gui_resource_set_property (GObject *object,
guint property_id,
......
......@@ -34,4 +34,6 @@ GuiResource *gui_resource_new (struct Resource file_info);
void activated_cb (GtkListBox* unused, GuiResource *self);
void resource_download_content (GtkWidget *self_widget, gpointer unused);
G_END_DECLS
......@@ -63,6 +63,22 @@ update_files (GuiWeek* self)
}
}
void
download_week_content (GtkWidget *self_widget, gpointer bool_download_selected_elements)
{
GuiWeek *self = HDY_GUI_WEEK (self_widget);
if (GPOINTER_TO_INT (bool_download_selected_elements)) {
GList *resources = gtk_list_box_get_selected_rows (self->week_content);
g_list_foreach (resources,
(GFunc)&resource_download_content,
NULL);
} else {
gtk_container_foreach (GTK_CONTAINER (self->week_content),
&resource_download_content,
NULL);
}
}
static void
gui_week_set_property (GObject *object,
guint property_id,
......
......@@ -31,4 +31,8 @@ G_DECLARE_FINAL_TYPE (GuiWeek, gui_week, HDY, GUI_WEEK, GtkBox)
GuiWeek* gui_week_new (struct Week week_info);
void
download_week_content (GtkWidget *self_widget,
gpointer bool_download_selected_elements);
G_END_DECLS
......@@ -37,7 +37,8 @@ struct _GuiWindow
HdyLeaflet *header_box;
HdyLeaflet *content_box;
GtkButton *back;
GtkMenuButton *menu;
GtkMenuButton *primary_menu;
GtkMenuButton *secondary_menu;
//GtkToggleButton *search_button;
GtkStackSidebar *sidebar;
GtkStack *stack;
......@@ -91,6 +92,17 @@ activate_ask_pass (GSimpleAction *simple,
gtk_widget_show (GTK_WIDGET (self->dialog));
}
static void
activate_download_current_course (GSimpleAction *simple,
GVariant *parameter,
gpointer ptr)
{
// get current course
GuiWindow *self = ptr;
download_course_content (gtk_stack_get_visible_child (self->stack),
GINT_TO_POINTER (FALSE));
}
/*
static void
activate_print_string (GSimpleAction *simple,
......@@ -117,39 +129,66 @@ gui_window_key_pressed_cb (GtkWidget *sender,
case GDK_CONTROL_MASK:
switch (keyval) {
// quit
case GDK_KEY_q:
gtk_widget_destroy (GTK_WIDGET (self));
break;
// go to first course
case GDK_KEY_Begin:
case GDK_KEY_KP_Begin:
case GDK_KEY_Home:
case GDK_KEY_KP_Home:
if (self->course_count > 0) {
const gchar *name = self->courses[0].name;
gtk_stack_set_visible_child_name (self->stack, name);
}
break;
// go to last course
case GDK_KEY_End:
case GDK_KEY_KP_End:
if (self->course_count > 0) {
const gchar *name = self->courses[self->course_count-1].name;
gtk_stack_set_visible_child_name (self->stack, name);
}
break;
// go to previous course
case GDK_KEY_Page_Up:
case GDK_KEY_KP_Page_Up:
{ // that's a little hackish, but works well enough
const gchar *name = gtk_stack_get_visible_child_name (self->stack);
for (guint i=0; i<self->course_count; i++) {
if (!g_strcmp0 (name, self->courses[i].name)) {
if (i!=0) {
const gchar *target_name = self->courses[i-1].name;
gtk_stack_set_visible_child_name (self->stack, target_name);
if (self->course_count > 1) { // that's a little hackish, but works well enough
const gchar *name = gtk_stack_get_visible_child_name (self->stack);
for (guint i=0; i<self->course_count; i++) {
if (!g_strcmp0 (name, self->courses[i].name)) {
if (i > 0) { // we don't go before the first element
const gchar *target_name = self->courses[i-1].name;
gtk_stack_set_visible_child_name (self->stack, target_name);
}
break;
}
break;
}
}
}
break;
// go to next course
case GDK_KEY_Page_Down:
case GDK_KEY_KP_Page_Down:
{
const gchar *name = gtk_stack_get_visible_child_name (self->stack);
for (guint i=0; i<self->course_count; i++) {
if (!g_strcmp0 (name, self->courses[i].name)) {
if (i!=self->course_count-1) {
const gchar *target_name = self->courses[i+1].name;
gtk_stack_set_visible_child_name (self->stack, target_name);
if (self->course_count > 1) { // that's a little hackish, but works well enough
const gchar *name = gtk_stack_get_visible_child_name (self->stack);
for (guint i=0; i<self->course_count; i++) {
if (!g_strcmp0 (name, self->courses[i].name)) {
if (i < self->course_count-1) { // we don't go past the last
const gchar *target_name = self->courses[i+1].name;
gtk_stack_set_visible_child_name (self->stack, target_name);
}
break;
}
break;
}
}
}
break;
default:
return FALSE;
}
......@@ -201,6 +240,9 @@ update_courses (GuiWindow *self)
self->courses[i].name);
}
// make the secondary menu visible
gtk_widget_show (GTK_WIDGET (self->secondary_menu));
// ellipsize the labels
GtkWidget *scrolled_win = gtk_bin_get_child (GTK_BIN (self->sidebar));
GtkWidget *viewport = gtk_bin_get_child (GTK_BIN (scrolled_win));
......@@ -282,7 +324,8 @@ gui_window_class_init (GuiWindowClass *class)
gtk_widget_class_bind_template_child (widget_class, GuiWindow, header_box);
gtk_widget_class_bind_template_child (widget_class, GuiWindow, content_box);
gtk_widget_class_bind_template_child (widget_class, GuiWindow, back);
gtk_widget_class_bind_template_child (widget_class, GuiWindow, menu);
gtk_widget_class_bind_template_child (widget_class, GuiWindow, primary_menu);
gtk_widget_class_bind_template_child (widget_class, GuiWindow, secondary_menu);
//gtk_widget_class_bind_template_child (widget_class, GuiWindow, search_button);
gtk_widget_class_bind_template_child (widget_class, GuiWindow, sidebar);
gtk_widget_class_bind_template_child (widget_class, GuiWindow, stack);
......@@ -328,7 +371,8 @@ gui_window_init (GuiWindow *self)
initialize_moodle_provider (self);
const GActionEntry entries[] = {
{ "ask-pass", activate_ask_pass },
{ "ask-pass", activate_ask_pass },
{ "download-current-course", activate_download_current_course },
//{ "print-string", activate_print_string, "s" }
};
g_action_map_add_action_entries (G_ACTION_MAP (self),
......
......@@ -18,7 +18,6 @@
#include <myhtml/myhtml.h>
#include <assert.h>
#include <stdlib.h>
#include "moodle-parser.h"
#include "data-struct.h"
#include "config.h"
......@@ -128,7 +127,10 @@ moodle_parse_folder_content (const gchar *html, gsize html_size, const gchar *mo
}
if (href) {
type = read_link (href, &id, &filename);
if ((type = read_link (href, &id, &filename) == -1)) {
g_warning ("Some unknown link\n");
continue;
}
if (node->child && node->child->next && node->child->next->child) {
name = g_strdup (myhtml_node_text(node->child->next->child, NULL));
}
......@@ -192,6 +194,7 @@ moodle_parse_course_list (const gchar *html, gsize html_size, const gchar *filen
if (strcmp (myhtml_attribute_key (attr, NULL), "href") == 0) {
// we found the href attribute
if (g_str_has_prefix (myhtml_attribute_value (attr, NULL), EPFL_COURSE_LINK))
if (g_strcmp0 (myhtml_attribute_value (attr, NULL), "https://moodle.epfl.ch/course/view.php?id=14247")) // FIXME
string = myhtml_attribute_value (attr, &size); // yay!
break;
}
......@@ -219,8 +222,7 @@ moodle_parse_course_list (const gchar *html, gsize html_size, const gchar *filen
myhtml_collection_destroy (hrefs);
myhtml_tree_destroy (tree);
struct Courses courses_struct = {courses, count};
return courses_struct;
return (struct Courses) {count, courses};
}
......@@ -279,8 +281,7 @@ moodle_extract_week (gchar *week_name, myhtml_tree_t *tree, myhtml_tree_node_t *
}
}
struct Week week = {week_name, resource_count, resources};
return week;
return (struct Week) {week_name, resource_count, resources};
}
......
......@@ -16,11 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <libsoup/soup.h>
#include "moodle-provider.h"
#include "moodle-parser.h"
#include "password.h"
#include "utils.h"
#include "config.h"
......@@ -120,6 +118,27 @@ get_index_filename (void)
}
static gboolean
cache_is_usable_for (const gchar *filename)
{
g_autoptr (GFile) file = g_file_new_for_uri (filename);
g_autoptr (GFileInfo) fileinfo = g_file_query_info (file,
G_FILE_ATTRIBUTE_TIME_MODIFIED,
G_FILE_QUERY_INFO_NONE,
NULL,
NULL);
if (fileinfo) {
guint64 mtime = g_file_info_get_attribute_uint64 (fileinfo, G_FILE_ATTRIBUTE_TIME_MODIFIED);
guint64 now = g_get_real_time () / 1000000; // that one is in µs
if (g_str_has_suffix (filename, ".html")) {
return CACHE_TIME_HTML > (now - mtime); // if we updated recently enough, that's good
}
return CACHE_TIME_FILES > (now - mtime); // non-html files are less likely to be updated
}
return FALSE; // no file info: this file doesn't exists; we better not rely on cache
}
static void
request_url_cb (GObject *unused,
GAsyncResult *res,
......@@ -160,7 +179,8 @@ request_url (const gchar *url,
provider_cb final_cb,
gpointer user_data)
{
if (self.online_mode == MOODLE_MODE_ONLINE) {
if (!cache_is_usable_for (filename) && self.online_mode == MOODLE_MODE_ONLINE) {
SoupMessage *msg;
msg = soup_message_new ("GET", url);
......@@ -172,7 +192,7 @@ request_url (const gchar *url,
soup_session_send_async (self.session, msg, NULL, request_url_cb, (gpointer)cb_info);
} else { // offline_mode
g_print ("using cache for %s\n", filename);
// this is not quite async, but works «fast enough»
g_autoptr (GFile) file = g_file_new_for_uri (filename);
GInputStream *is;
......@@ -221,7 +241,8 @@ moodle_downloaded_file_cb (GInputStream *is,
write_g_input_stream_to_file (is, real_filename ?: filename_looked_for);
}
final_cb (user_data, g_strdup (filename_looked_for));
if (final_cb)
final_cb (user_data, g_strdup (filename_looked_for));
}
void
......@@ -324,7 +345,8 @@ got_course_content_cb (GInputStream *is,
struct CourseContent content = moodle_parse_course_content (html, html_size, filename);
if (should_save_to_disk)
write_file_async (filename, g_steal_pointer (&html), html_size);
final_cb (user_data, &content);
if (final_cb)
final_cb (user_data, &content);
}
......@@ -342,7 +364,8 @@ got_folder_content_cb (GInputStream *is,
struct FolderContent content = moodle_parse_folder_content (html, html_size, filename);
if (should_save_to_disk)
write_file_async (filename, g_steal_pointer (&html), html_size);
final_cb (user_data, &content);
if (final_cb)
final_cb (user_data, &content);
}
void
......
......@@ -25,6 +25,7 @@ typedef void (*courses_cb) (gpointer user_data, struct Courses);
typedef void (*provider_cb) (gpointer user_data, gpointer retrieved_content);
// retrieved_content will be a struct Courses *, struct FolderContent *
// according to the called function name.
// when giving to a function a provider_cb, you can pass NULL instead.
enum moodle_error {
MOODLE_ERROR_OK,
......
......@@ -74,7 +74,7 @@
<child>
<object class="GtkLinkButton" id="url">
<style><class name="url"/></style>
<property name="visible">True</property>
<property name="visible">False</property>
<property name="opacity">0.4</property>
<property name="label" translatable="no">https://…</property>
<!--<property name="margin_top">3</property>-->
......@@ -134,3 +134,4 @@
</interface>
......@@ -6,9 +6,10 @@
<requires lib="libhandy" version="0.0"/>
<template class="GuiResource" parent="HdyActionRow">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can_focus">True</property>
<property name="icon_name">dialog-question</property>
<property name="subtitle">filename.ext</property>
<property name="title">File Title</property>
</template>
</interface>
......@@ -30,7 +30,7 @@
<child>
<object class="GtkListBox" id="week_content">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can_focus">True</property>
<property name="selection_mode">none</property>
<signal name="row-activated" handler="file_selected_cb"/>
<child type="placeholder">
......@@ -74,3 +74,4 @@
</interface>
......@@ -3,7 +3,7 @@
<interface>
<requires lib="gtk+" version="3.16"/>
<requires lib="libhandy" version="0.0"/>
<object class="GtkPopoverMenu" id="menu_popover">
<object class="GtkPopoverMenu" id="primary_menu_popover">
<child>
<object class="GtkBox">
<property name="visible">True</property>
......@@ -44,9 +44,32 @@
</object>
</child>
</object>
<object class="GtkPopoverMenu" id="secondary_menu_popover">
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="margin">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkModelButton" id="download">
<property name="text" translatable="yes">Download course files</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action-name">win.download-current-course</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
<template class="GuiWindow" parent="GtkApplicationWindow">
<property name="can_focus">False</property>
<property name="title">Handy Moodle</property>
<property name="title">Moody</property>
<property name="default_width">360</property>
<property name="default_height">576</property>
<signal name="key-press-event" handler="key_pressed_cb" after="yes" swapped="no"/>
......@@ -65,8 +88,27 @@
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title">Handy Moodle</property>
<property name="title">Moody</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkMenuButton" id="primary_menu">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="popover">primary_menu_popover</property>
<child>
<object class="GtkImage" id="open_primary_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">open-menu-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
</packing>
</child>
</object>
<packing>
<property name="name">sidebar</property>
......@@ -112,16 +154,16 @@
</packing>
</child>
<child>
<object class="GtkMenuButton" id="menu">
<property name="visible">True</property>
<object class="GtkMenuButton" id="secondary_menu">
<property name="visible">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="popover">menu_popover</property>
<property name="popover">secondary_menu_popover</property>
<child>
<object class="GtkImage" id="open_menu">
<object class="GtkImage" id="open_secondary_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">open-menu-symbolic</property>
<property name="icon_name">view-more-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
......@@ -217,7 +259,7 @@
<property name="opacity">0.7</property>
<property name="halign">center</property>
<property name="margin_bottom">12</property>
<property name="label" translatable="yes">Welcome to Handy Moodle</property>
<property name="label" translatable="yes">Welcome!</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<attributes>
......@@ -236,7 +278,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="opacity">0.7</property>
<property name="label" translatable="yes">This is a very αλφα version.</property>
<property name="label" translatable="yes">You should set you username to use Moody :-)</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="use_markup">True</property>
......@@ -335,3 +377,5 @@