package JapaChar; use v5.38.2; use strict; use warnings; use feature 'signatures'; use Moo; use Path::Tiny; use Glib::Object::Introspection; use YAML::PP; use JapaChar::DB; use JapaChar::Characters; use Pango; use JapaChar::Random; use Glib::IO; use constant PANGO_SCALE => 1024; Glib::Object::Introspection->setup( basename => 'Gtk', version => '4.0', package => 'Gtk', ); Glib::Object::Introspection->setup( basename => 'Gdk', version => '4.0', package => 'Gtk::Gdk', ); Glib::Object::Introspection->setup( basename => 'Gsk', version => '4.0', package => 'Gtk::Gsk', ); Glib::Object::Introspection->setup( basename => 'Adw', version => '1', package => 'Adw', ); has _counter => ( is => 'rw', ); has _headerbar => ( is => 'rw', ); has _on_resize_lesson => ( is => 'rw', ); has _gresources_path => ( is => 'lazy', ); has _first_press_continue => ( is => 'rw'); has _final_answer => (is => 'rw'); sub _build__gresources_path($self) { my $root = path(__FILE__)->parent->parent; my $gresources = $root->child('resources.gresource'); 0 == system( 'which', 'glib-compile-resources' ) && system( 'glib-compile-resources', $root->child('resources.xml') ); if ( !-e $gresources ) { { die 'No gresources'; } } return $gresources; } sub config($class) { my $ypp = YAML::PP->new; $ypp->load_file( '' . path(__FILE__)->parent->parent->child('config.yml') ); } sub _start_lesson( $self, $window, $type = undef ) { $self->_counter(11); $self->_new_challenge( $window, $type ); } sub _new_challenge( $self, $window, $type = undef ) { $self->_counter( $self->_counter - 1 ); if ( $self->_counter < 1 ) { $self->_create_main_menu($window); return; } my $rng = JapaChar::Random->new->get( 1, 100 ); if ( $rng > 50 ) { $self->_new_challenge_romanji( $window, $type ); return; } $self->_new_challenge_kana( $window, $type ); } sub _new_challenge_romanji( $self, $window, $type = undef ) { my $show = 'romanji'; my $guess = 'kana'; $self->_new_challenge_generic_code( $window, $type, $show, $guess ); } sub _new_typing_romanji_challenge($self, $window, $char, $type) { my $grid = $self->_create_grid_challenge; my $kana_label = $self->_get_label_featured_character( $char->get('kana') ); $kana_label->set_halign('center'); $kana_label->set_valign('center'); $grid->attach( $kana_label, 0, 0, 12, 1 ); $self->_window_set_child($window, $grid); my $back_button = $self->_create_exit_lesson_back_button($window); $self->_headerbar->pack_start($back_button); my $romanji_entry = Gtk::Entry->new; my $attr_list = Pango::AttrList->new; my $size_number = 60 * $window->get_property('default-width'); my $size_pango_number = PANGO_SCALE * 60; my $size = Pango::AttrSize->new( $size_number ); if ($size_pango_number < $size_number) { $size = Pango::AttrSize->new( $size_pango_number ); } $attr_list->insert($size); $romanji_entry->set_attributes($attr_list); my $buffer = $romanji_entry->get_buffer; $self->_first_press_continue(1); my $continue_button = $self->_create_continue_lesson_button($window, $grid, $char, $type, 'romanji'); my $on_change_buffer = sub { my $text = $buffer->get_text; if (!$text) { $continue_button->set_sensitive(0); return; } $self->_final_answer(lc($text)); $continue_button->set_sensitive(1); }; $buffer->signal_connect('inserted-text', sub { $on_change_buffer->(); }); $buffer->signal_connect('deleted-text', sub { $on_change_buffer->(); }); $romanji_entry->set_valign('center'); $romanji_entry->set_halign('center'); $grid->attach($romanji_entry, 2, 1, 8, 1); $grid->attach( $continue_button, 6, 2, 5, 1 ); } sub _create_exit_lesson_back_button($self, $window) { my $back_button = Gtk::Button->new_from_icon_name('go-previous-symbolic'); $back_button->signal_connect( 'clicked', sub { my $dialog = Adw::AlertDialog->new( 'Exit the lessson', 'On exit you will lose your progress' ); $dialog->add_response( 'close', 'Continue' ); my $exit_the_lesson_id = 'exit-the-lesson'; $dialog->add_response( $exit_the_lesson_id, 'Exit' ); $dialog->set_response_appearance( $exit_the_lesson_id, 'destructive' ); $dialog->present($window); $dialog->signal_connect( 'response', sub( $obj, $response ) { if ( $response eq $exit_the_lesson_id ) { $self->_create_main_menu($window); return; } } ); } ); return $back_button; } sub _new_challenge_generic_code( $self, $window, $type, $show, $guess, $can_be_typed = 0 ) { my $grid = $self->_create_grid_challenge; my $char = JapaChar::Characters->new->next_char($type); my $kana_label = $self->_get_label_featured_character( $char->get($show) ); my $rng = JapaChar::Random->new->get( 1, 100 ); if ($char->score > 60 && $can_be_typed && $rng > 30 ) { $self->_new_typing_romanji_challenge($window, $char, $type); return; } $kana_label->set_halign('center'); $kana_label->set_valign('center'); $grid->attach( $kana_label, 0, 0, 12, 1 ); $self->_window_set_child( $window, $grid ); my $back_button = $self->_create_exit_lesson_back_button($window); $self->_headerbar->pack_start($back_button); my $incorrect_chars = JapaChar::Characters->new->get_4_incorrect_answers($char); my @buttons; my $continue_button = $self->_create_continue_lesson_button($window, $grid, $char, $type, $guess); my $on_answer = sub { $continue_button->set_sensitive(1); }; my $correct_answer_button = Gtk::ToggleButton->new_with_label( $char->get($guess) ); $correct_answer_button->signal_connect( 'clicked', sub { $self->_final_answer($char->get($guess)); $on_answer->(); } ); push @buttons, $correct_answer_button; for my $char (@$incorrect_chars) { my $incorrect_button = Gtk::ToggleButton->new_with_label( $char->get($guess) ); $incorrect_button->set_group($correct_answer_button); $incorrect_button->signal_connect( 'clicked', sub { $self->_final_answer($char->get($guess)); $on_answer->(); } ); push @buttons, $incorrect_button; } @buttons = sort { rand() <=> rand() } @buttons; my $box = Gtk::Box->new( 'horizontal', 10 ); $box->set_valign('center'); $box->set_halign('center'); my $resize_buttons = sub { my $window_size = $window->get_property('default-width'); # $box->set_spacing(20 * $window_size / 420); for my $button (@buttons) { my $attr_list = Pango::AttrList->new; my $size_number = 60 * $window_size; my $size_pango_number = PANGO_SCALE * 60; my $size = Pango::AttrSize->new( $size_number ); if ($size_pango_number < $size_number) { $size = Pango::AttrSize->new( $size_pango_number ); } $attr_list->insert($size); $button->get_child->set_attributes($attr_list); } }; $resize_buttons->(); for my $button (@buttons) { $box->append($button); } $self->_on_resize_lesson($resize_buttons); $self->_first_press_continue(1); $grid->attach( $box, 0, 1, 12, 1 ); $grid->attach( $continue_button, 6, 2, 5, 1 ); } sub _create_continue_lesson_button($self, $window, $grid, $char, $type, $guess) { my $continue_button = Gtk::Button->new_with_label('Continue'); $continue_button->set_valign('center'); $continue_button->set_halign('end'); $continue_button->set_sensitive(0); $continue_button->add_css_class('accent'); my $attr_list = Pango::AttrList->new; my $size = Pango::AttrSize->new( 25 * PANGO_SCALE ); $attr_list->insert($size); $continue_button->get_child->set_attributes($attr_list); $continue_button->signal_connect( 'clicked', sub { if ( !$self->_first_press_continue ) { $self->_new_challenge( $window, $type ); return; } $self->_first_press_continue(0); my $label_feedback; { if ( $self->_final_answer eq $char->get($guess) ) { $label_feedback = Gtk::Label->new('You are doing it great.'); $label_feedback->add_css_class('success'); $char->success; next; } $label_feedback = Gtk::Label->new( 'Meck!! The correct answer is ' . $char->get($guess) ); $label_feedback->add_css_class('error'); $char->fail; } my $attr_list = Pango::AttrList->new; my $size = Pango::AttrSize->new( 23 * $window->get_property('default-width') ); $attr_list->insert($size); $label_feedback->set_halign('center'); $label_feedback->set_attributes($attr_list); $grid->attach( $label_feedback, 0, 2, 7, 1 ); } ); return $continue_button; } sub _new_challenge_kana( $self, $window, $type = undef ) { my $show = 'kana'; my $guess = 'romanji'; $self->_new_challenge_generic_code( $window, $type, $show, $guess, 1 ); } sub _create_grid_challenge($self) { my $grid = Gtk::Grid->new; $grid->set_column_homogeneous(1); $grid->set_row_homogeneous(1); return $grid; } sub _get_label_featured_character( $self, $text ) { my $label = Gtk::Label->new($text); my $attr_list = Pango::AttrList->new; my $size = Pango::AttrSize->new( 72 * PANGO_SCALE ); $attr_list->insert($size); $label->set_attributes($attr_list); $label->set_halign('center'); return $label; } sub _create_main_menu( $self, $window ) { my $grid = Gtk::Grid->new; my $button_start_basic_lesson = Gtk::Button->new_with_label('Basic Characters'); $self->_on_resize_lesson(undef); $button_start_basic_lesson->signal_connect( 'clicked', sub { $self->_start_lesson($window); } ); $grid->set_column_homogeneous(1); $grid->set_row_homogeneous(1); my $button_start_hiragana_lesson = Gtk::Button->new_with_label('Hiragana'); $button_start_hiragana_lesson->signal_connect( 'clicked', sub { $self->_start_lesson( $window, 'hiragana' ); } ); my $button_start_katakana_lesson = Gtk::Button->new_with_label('Katakana'); $button_start_katakana_lesson->signal_connect( 'clicked', sub { $self->_start_lesson( $window, 'katakana' ); } ); for my $button ( $button_start_basic_lesson, $button_start_hiragana_lesson, $button_start_katakana_lesson ) { my $attr_list = Pango::AttrList->new; my $size = Pango::AttrSize->new( 25 * PANGO_SCALE ); $attr_list->insert($size); $button->get_child->set_attributes($attr_list); } my $box = Gtk::Box->new( 'horizontal', 10 ); $grid->attach( $button_start_basic_lesson, 0, 0, 5, 1 ); $button_start_basic_lesson->set_valign('end'); $button_start_basic_lesson->set_halign('center'); $box->set_margin_top(40); $box->append($button_start_hiragana_lesson); $box->append($button_start_katakana_lesson); $box->set_valign('start'); $box->set_halign('center'); $grid->attach( $box, 0, 1, 5, 1 ); $self->_window_set_child( $window, $grid ); } sub _window_set_child( $self, $window, $child ) { my $box = Gtk::Box->new( 'vertical', 0 ); my $headerbar = Adw::HeaderBar->new; $headerbar->set_title_widget( Gtk::Label->new('Japachar') ); $box->append($headerbar); $box->append($child); $child->set_vexpand(1); $window->set_content($box); $self->_headerbar($headerbar); } sub _application_start( $self, $app ) { my $main_window = Adw::ApplicationWindow->new($app); $main_window->set_default_size( 600, 600 ); $main_window->signal_connect(notify => sub($object, $param) { if ($param->{name} eq 'default-width') { if (defined $self->_on_resize_lesson) { $self->_on_resize_lesson->(); } } }); my $display = $main_window->get_property('display'); $self->_create_main_menu($main_window); $main_window->present; } sub start($self) { Glib::IO::resources_register(Glib::IO::Resource::load($self->_gresources_path)); my $app = Adw::Application->new( 'me.sergiotarxz.JapaChar', 'default-flags' ); $app->signal_connect( 'activate' => sub { $self->_application_start($app); } ); $app->run; } 1;