viewer2.pl - flashcards - Stupid LaTeX flashcard viewer
HTML git clone git://lumidify.org/flashcards.git (fast, but not encrypted)
HTML git clone https://lumidify.org/git/flashcards.git (encrypted, but very slow)
HTML git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/flashcards.git (over tor)
DIR Log
DIR Files
DIR Refs
DIR README
---
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;