viewer2.pl (6859B)
1 #!/usr/bin/env perl 2 3 use strict; 4 use warnings; 5 use utf8; 6 use open qw< :encoding(UTF-8) >; 7 binmode(STDOUT, ":utf8"); 8 use Gtk2 '-init'; 9 use Glib qw/TRUE FALSE/; 10 use JSON qw(decode_json encode_json); 11 use List::Util qw(min); 12 13 sub load_cards { 14 my %cards; 15 my %cards_backreference; 16 opendir my $dir, "latex" or die "Unable to open flashcards directory.\n"; 17 while (my $filename = readdir($dir)) { 18 next if ($filename =~ /\A\.\.?\z/); 19 open my $fh, "<", "latex/$filename" || die "Unable to open file \"latex/$filename\"\n"; 20 my $line = <$fh>; 21 $line =~ /\|(.*)$/; 22 $cards{$filename} = $1; 23 $cards_backreference{$1} = $filename if $1; 24 if ($1 && !-f "latex/$1") { 25 die "Flashcard \"$1\" mentioned in \"$filename\" does not exist.\n"; 26 } 27 close $fh; 28 } 29 closedir $dir; 30 return \%cards, \%cards_backreference; 31 } 32 33 sub load_config { 34 my $path = shift; 35 my $config; 36 if (-e $path) { 37 open my $fh, "<", $path or die "Unable to open config \"$config\".\n"; 38 my $json_raw = do {local $/; <$fh>}; 39 $config = decode_json $json_raw; 40 close $fh; 41 } else { 42 $config = {}; 43 } 44 return $config; 45 } 46 47 sub remove_cruft { 48 my ($config, $cards) = @_; 49 my $clean_config; 50 my $max_view_count = 0; 51 foreach my $card (keys %$cards) { 52 if (exists $config->{cards}->{$card}) { 53 $clean_config->{cards}->{$card} = $config->{cards}->{$card}; 54 } else { 55 $clean_config->{cards}->{$card} = 0; 56 } 57 } 58 return $clean_config; 59 } 60 61 sub sort_cards { 62 my ($config, $cards) = @_; 63 my $sorted_cards; 64 foreach my $card (keys %$cards) { 65 my $view_count = $config->{cards}->{$card}; 66 $sorted_cards->{$view_count}->{$card} = $cards->{$card}; 67 } 68 return $sorted_cards; 69 } 70 71 sub get_rand_card { 72 my $cards = shift; 73 my $min_view_count = min keys %$cards; 74 my @card_ids = keys %{$cards->{$min_view_count}}; 75 my $card = $card_ids[rand @card_ids]; 76 return ($min_view_count, $card); 77 } 78 79 sub find_card_view_count { 80 my ($cards, $card_id) = @_; 81 foreach my $view_count (keys %$cards) { 82 return $view_count if exists $cards->{$view_count}->{$card_id}; 83 } 84 } 85 86 sub load_images_front { 87 my ($image_front1, $image_front2, $window_w, $card_id) = @_; 88 my ($fmt, $w, $h) = Gtk2::Gdk::Pixbuf->get_file_info("cache/${card_id}_front1.png"); 89 $w = $w < $window_w ? $w : $window_w; 90 my $pixbuf1 = Gtk2::Gdk::Pixbuf->new_from_file_at_size("cache/${card_id}_front1.png", $w, -1); 91 ($fmt, $w, $h) = Gtk2::Gdk::Pixbuf->get_file_info("cache/${card_id}_front2.png"); 92 $w = $w < $window_w ? $w : $window_w; 93 my $pixbuf2 = Gtk2::Gdk::Pixbuf->new_from_file_at_size("cache/${card_id}_front2.png", $w, -1); 94 $image_front1->set_from_pixbuf($pixbuf1); 95 $image_front2->set_from_pixbuf($pixbuf2); 96 } 97 98 sub load_image_back { 99 my ($image_back, $window_w, $image_back_h, $card_id) = @_; 100 my ($fmt, $w, $h) = Gtk2::Gdk::Pixbuf->get_file_info("cache/${card_id}_back.png"); 101 my $w_final = $w < $window_w ? $w : $window_w; 102 my $h_final = $h < $image_back_h ? $h : $image_back_h; 103 my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file_at_size("cache/${card_id}_back.png", $w_final, $h_final); 104 $image_back->set_from_pixbuf($pixbuf); 105 } 106 107 sub next_card { 108 my ($config, $cards, $cards_backreference, $image_front1, $image_front2, $window_w, $window, $cur_card_id) = @_; 109 my ($view_count, $card_id); 110 if ($cur_card_id && ($card_id = $cards_backreference->{$cur_card_id})) { 111 $view_count = find_card_view_count $cards, $card_id; 112 } else { 113 ($view_count, $card_id) = get_rand_card $cards; 114 while (my $req_card = $cards->{$view_count}->{$card_id}) { 115 $card_id = $req_card; 116 # efficiency is for sissies 117 $view_count = find_card_view_count $cards, $card_id; 118 } 119 } 120 $window->set_title($card_id); 121 load_images_front $image_front1, $image_front2, $window_w, $card_id; 122 return ($view_count, $card_id); 123 } 124 125 sub gui { 126 my $config = load_config("config.json"); 127 my ($cards, $cards_backreference) = load_cards; 128 $config = remove_cruft $config, $cards; 129 $cards = sort_cards $config, $cards; 130 131 my $window = Gtk2::Window->new('toplevel'); 132 $window->signal_connect(delete_event => sub {return FALSE}); 133 $window->signal_connect(destroy => sub { Gtk2->main_quit; }); 134 my $window_w = 0; 135 my $image_back_h = 0; 136 $window->signal_connect(configure_event => sub { 137 my ($window, $event) = @_; 138 # subtract 20 to account for window border width 139 $window_w = $event->width - 20; 140 return FALSE; 141 }); 142 $window->set_border_width(10); 143 144 my $view_count; 145 my $card_id; 146 147 my $vbox = Gtk2::VBox->new(FALSE, 5); 148 my $hbox = Gtk2::HBox->new(FALSE, 5); 149 150 my $image_front1 = Gtk2::Image->new(); 151 my $image_front2 = Gtk2::Image->new(); 152 my $image_back = Gtk2::Image->new(); 153 $image_back->signal_connect(size_allocate => sub { 154 my ($image, $event) = @_; 155 $image_back_h = $event->height; 156 return FALSE; 157 }, $window); 158 159 my $revealed = 0; 160 my $label = Gtk2::Label->new(""); 161 my $button = Gtk2::Button->new_with_mnemonic("_Next card"); 162 $button->signal_connect(clicked => sub { 163 if ($revealed) { 164 $config->{cards}->{$card_id}++; 165 $cards->{$view_count+1}->{$card_id} = $cards->{$view_count}->{$card_id}; 166 delete $cards->{$view_count}->{$card_id}; 167 if (!%{$cards->{$view_count}}) { 168 delete $cards->{$view_count}; 169 } 170 } 171 $revealed = 0; 172 ($view_count, $card_id) = next_card $config, $cards, $cards_backreference, $image_front1, $image_front2, $window_w, $window, $card_id; 173 $label->set_text("View count: " . $view_count); 174 $image_back->clear(); 175 }, $window); 176 $hbox->pack_start($button, FALSE, FALSE, 0); 177 $button = Gtk2::Button->new_with_mnemonic("Next (no _view increase)"); 178 $button->signal_connect(clicked => sub { 179 $revealed = 0; 180 ($view_count, $card_id) = next_card $config, $cards, $cards_backreference, $image_front1, $image_front2, $window_w, $window, $card_id; 181 $label->set_text("View count: " . $view_count); 182 $image_back->clear(); 183 }, $window); 184 $hbox->pack_start($button, FALSE, FALSE, 0); 185 $button = Gtk2::Button->new_with_mnemonic("Reveal _back"); 186 $button->signal_connect(clicked => sub { 187 if (!$revealed) { 188 load_image_back $image_back, $window_w, $image_back_h, $card_id; 189 $revealed = 1; 190 } 191 }, $window); 192 $hbox->pack_start($button, FALSE, FALSE, 0); 193 $button = Gtk2::Button->new_with_mnemonic("_Reload images"); 194 $button->signal_connect(clicked => sub { 195 load_images_front $image_front1, $image_front2, $window_w, $card_id; 196 if ($revealed) { 197 load_image_back $image_back, $window_w, $image_back_h, $card_id; 198 } 199 }, $window); 200 $hbox->pack_start($button, FALSE, FALSE, 0); 201 $hbox->pack_start($label, FALSE, FALSE, 0); 202 $vbox->pack_start($hbox, FALSE, FALSE, 0); 203 $vbox->pack_start($image_front1, FALSE, FALSE, 0); 204 $vbox->pack_start($image_front2, FALSE, FALSE, 0); 205 $vbox->pack_start($image_back, TRUE, TRUE, 0); 206 $window->add($vbox); 207 $window->show_all; 208 Gtk2->main; 209 open my $fh, ">", "config.json" or die "Unable to save config.\n"; 210 my $json_encoded = encode_json $config; 211 print $fh "$json_encoded\n"; 212 close $fh; 213 } 214 215 gui;