From de0bc9a6554945f7ba72eb8c2c560b7bb512eb00 Mon Sep 17 00:00:00 2001 From: Sergiotarxz Date: Wed, 16 Oct 2024 19:57:50 +0200 Subject: [PATCH] Finishing image storage in .exd and adding save and open capability. --- lib/Exd/FileFormat.pm | 80 ++++++++- lib/Exd/FileFormat/DB.pm | 95 +++++++++++ lib/Exd/FileFormat/DB/Migrations.pm | 23 +++ lib/Exd/Gui.pm | 249 +++++++++++++++++++++++++++- 4 files changed, 432 insertions(+), 15 deletions(-) create mode 100644 lib/Exd/FileFormat/DB.pm create mode 100644 lib/Exd/FileFormat/DB/Migrations.pm diff --git a/lib/Exd/FileFormat.pm b/lib/Exd/FileFormat.pm index a255103..53a58f1 100644 --- a/lib/Exd/FileFormat.pm +++ b/lib/Exd/FileFormat.pm @@ -10,8 +10,12 @@ use Moo; use Path::Tiny; use Archive::Zip; +use Exd::Utils; + use Digest::SHA qw/sha256_hex/; +use Exd::FileFormat::DB; + has dir => ( is => 'rw' ); sub new_tmp($class) { @@ -22,21 +26,85 @@ sub new_tmp($class) { has _images_dir => ( is => 'lazy' ); sub _build__images_dir($self) { - my $dir = $self->dir; + my $dir = $self->dir; my $images = $dir->child('images'); $images->mkpath; return $images; } -sub add_png_image( $self, $file_contents ) { +sub add_png_image( $self, $file_contents, $label = '' ) { my $sha_image = sha256_hex($file_contents); - $self->_images_dir->child("$sha_image.png")->spew_raw($file_contents); + my $file = $self->_images_dir->child("$sha_image.png"); + $file->spew_raw($file_contents); + if ( defined $label && '' . $label ) { + $self->_register_label( $label, $sha_image ); + } +} + +sub _image_hashes($self) { + my $glob = ( '' . $self->_images_dir ) . '/*.png'; + my @images = glob( $glob ); + @images = map { + my $image = path($_); + $image = $image->basename; + $image =~ s/\.png$//r; + } @images; + return \@images; +} + +sub image_hashes_to_label($self) { + my %images; + for my $image_hash ( $self->_image_hashes->@* ) { + my $dbh = $self->_dbh; + my $hashes = $dbh->selectall_arrayref( + 'SELECT label FROM label_to_image_hash WHERE hash = ?', + {Slice => {}}, $image_hash ); + $images{$image_hash} = [ sort { $a cmp $b } map { $_->{label} } @$hashes ]; + } + return \%images; +} + +sub _dbh($self) { + return Exd::FileFormat::DB->connect( '' . $self->dir->child('db.sqlite3') ); +} + +sub _register_label( $self, $label, $sha_image ) { + my $dbh = $self->_dbh; + eval { + $dbh->do( + 'INSERT INTO label_to_image_hash (label, hash) VALUES (?, ?);', + {}, $label, $sha_image ); + }; + if ($@) { + warn $@; + } +} + +sub get_image_gd_from_label ( $self, $label ) { + return $self->get_image_gd( $self->_get_hash_from_label($label) ); +} + +sub _get_hash_from_label($self, $label) { + my $dbh = $self->_dbh; + my $row = $dbh->selectrow_hashref( + 'SELECT hash FROM label_to_image_hash WHERE label = ?', + {}, $label ); + die 'No such image in file' if !defined $row || !defined $row->{hash}; + return $row->{hash}; +} + +sub get_image_from_label ( $self, $label ) { + return $self->get_image( $self->_get_hash_from_label($label) ); } sub get_image( $self, $sha ) { return $self->_images_dir->child("$sha.png"); } +sub get_image_gd ( $self, $sha ) { + return Exd::Utils::get_gd_image( $self->get_image($sha) ); +} + sub get_script($self) { return $self->dir->child('script.pl')->slurp_raw; } @@ -51,7 +119,7 @@ sub to_zip($self) { sub( $path, $state ) { return if $path->is_dir; my $path_relative = $path->relative( $self->dir ); - $zip->addFile( $path, $path_relative ); + $zip->addFile( ''.$path, ''.$path_relative ); }, { recurse => 1 } ); @@ -61,7 +129,7 @@ sub to_zip($self) { sub from_zip_file( $class, $zip_file ) { my $tempdir = Path::Tiny->tempdir(); my $zip = Archive::Zip->new($zip_file); - $zip->extractTree( '.', $tempdir ); + $zip->extractTree( '', $tempdir ); return $class->new( dir => $tempdir ); } @@ -74,7 +142,7 @@ sub execute( $self, $printer ) { if ($@) { die $@; } - $sub->($self, $printer); + $sub->( $self, $printer ); } sub save( $self, $output_file ) { diff --git a/lib/Exd/FileFormat/DB.pm b/lib/Exd/FileFormat/DB.pm new file mode 100644 index 0000000..60776ce --- /dev/null +++ b/lib/Exd/FileFormat/DB.pm @@ -0,0 +1,95 @@ +package Exd::FileFormat::DB; + +use v5.40.0; + +use strict; +use warnings; +use utf8; + +use DBI; +use Path::Tiny; + +use Exd::FileFormat::DB::Migrations; + +my $dbh; + +sub connect { + if ( defined $dbh ) { + return $dbh; + } + my $class = shift or die 'Missing arg class'; + my $database = shift or die 'Missing arg database file'; + $dbh = DBI->connect( + "dbi:SQLite:dbname=$database", + , + undef, undef, + { + RaiseError => 1, + }, + ); + $class->_migrate($dbh); + return $dbh; +} + +sub _migrate { + my $class = shift; + my $dbh = shift; + local $dbh->{RaiseError} = 0; + local $dbh->{PrintError} = 0; + my @migrations = Exd::FileFormat::DB::Migrations::MIGRATIONS(); + if ( $class->get_current_migration($dbh) > @migrations ) { + warn "Something happened there, wrong migration number."; + } + if ( $class->get_current_migration($dbh) >= @migrations ) { + say STDERR "Migrations already applied."; + return; + } + $class->_apply_migrations( $dbh, \@migrations ); +} + +sub _apply_migrations { + my $class = shift; + my $dbh = shift; + my $migrations = shift; + for ( + my $i = $class->get_current_migration($dbh) ; + $i < @$migrations ; + $i++ + ) + { + local $dbh->{RaiseError} = 1; + my $current_migration = $migrations->[$i]; + my $migration_number = $i + 1; + $class->_apply_migration( $dbh, $current_migration, $migration_number ); + } +} + +sub _apply_migration { + my $class = shift; + my $dbh = shift; + my $current_migration = shift; + my $migration_number = shift; + { + if (ref $current_migration eq 'CODE') { + $current_migration->($dbh); + next; + } + $dbh->do($current_migration); + } + $dbh->do( <<'EOF', undef, 'current_migration', $migration_number ); +INSERT INTO options +VALUES ($1, $2) +ON CONFLICT (name) DO +UPDATE SET value = $2; +EOF +} + +sub get_current_migration { + my $class = shift; + my $dbh = shift; + my $result = $dbh->selectrow_hashref( <<'EOF', undef, 'current_migration' ); +select value from options where name = ?; +EOF + return int( $result->{value} // 0 ); +} +1; diff --git a/lib/Exd/FileFormat/DB/Migrations.pm b/lib/Exd/FileFormat/DB/Migrations.pm new file mode 100644 index 0000000..5bf4016 --- /dev/null +++ b/lib/Exd/FileFormat/DB/Migrations.pm @@ -0,0 +1,23 @@ +package Exd::FileFormat::DB::Migrations; + +use v5.40.0; + +use strict; +use warnings; +use utf8; + +use feature 'signatures'; + +sub MIGRATIONS { + return ( + 'CREATE TABLE options ( + name TEXT PRIMARY KEY, + value TEXT + );', + 'CREATE TABLE label_to_image_hash ( + label TEXT NOT NULL PRIMARY KEY, + hash TEXT NOT NULL + );', + ); +} +1; diff --git a/lib/Exd/Gui.pm b/lib/Exd/Gui.pm index 8e4c75e..d0fcaef 100644 --- a/lib/Exd/Gui.pm +++ b/lib/Exd/Gui.pm @@ -38,6 +38,12 @@ Glib::Object::Introspection->setup( package => 'Gdk', ); +Glib::Object::Introspection->setup( + basename => 'GdkWayland', + version => '4.0', + package => 'GdkWayland', +); + Glib::Object::Introspection->setup( basename => 'GtkSource', version => '5', @@ -66,7 +72,6 @@ has _file_format => ( is => 'rw', default => sub { my $tmp = Exd::FileFormat->new_tmp; - say $tmp->dir; $tmp->set_script(<<'EOF'); sub ($exd, $printer) { $printer->print_text( @@ -106,6 +111,11 @@ sub start($self) { $self->_daemon_script_runner; my $app = Gtk4::Application->new( 'me.sergiotarxz.Exd', 'default-flags' ); $self->_app($app); + $app->signal_connect( + startup => sub { + $self->_startup; + } + ); $app->signal_connect( activate => sub { $self->_activate; @@ -114,6 +124,54 @@ sub start($self) { $app->run; } +sub _save_action($self) { + my $file_format = $self->_file_format; + my $dialog = Gtk4::FileDialog->new; + $dialog->set_initial_name('project.exd'); + $dialog->save($self->window, undef, sub ($source, $res, $data) { + eval { + my $file = $dialog->save_finish($res); + $file = $file->get_path; + my $zip = $file_format->to_zip; + $zip->writeToFileNamed(''.$file); + }; + }); +} + +sub _open_action($self) { + my $file_format = $self->_file_format; + my $dialog = Gtk4::FileDialog->new; + $dialog->set_initial_name('project.exd'); + $dialog->open($self->window, undef, sub ($source, $res, $data) { + eval { + my $file = $dialog->open_finish($res); + $file = $file->get_path; + $self->_file_format(Exd::FileFormat->from_zip_file($file)); + $self->_update_editor_buffer; + }; + }); +} + +sub _startup($self) { + my $app = $self->_app; + my $menu_model = Glib::IO::Menu->new; + my $file_menu = Glib::IO::Menu->new; + my $open_action = Glib::IO::SimpleAction->new('open', undef); + my $save_action = Glib::IO::SimpleAction->new('save', undef); + $open_action->signal_connect('activate', sub { + $self->_open_action; + }); + $save_action->signal_connect('activate', sub { + $self->_save_action; + }); + $app->add_action($open_action); + $app->add_action($save_action); + $file_menu->append('Open', 'app.open'); + $file_menu->append('Save', 'app.save'); + $menu_model->append_submenu('File', $file_menu); + $app->set_menubar($menu_model); +} + sub _generate_preview_file( $self, $verbose = 1 ) { $self->_preview_file(1) if -f $self->_preview_file; my $device = @@ -137,7 +195,7 @@ sub _run_script( $self, $device = undef, $verbose = 1 ) { my $fh = $self->_write_to_script; print $fh JSON::to_json( { - exd_dir => ''.$self->_file_format->dir, + exd_dir => '' . $self->_file_format->dir, device => $device->serialize, verbose => $verbose, } @@ -179,8 +237,8 @@ sub _daemon_script_runner($self) { my $new_pid = fork; if ( !$new_pid ) { eval { - $self->_on_run_script( path($exd_dir), $device, $write_to_parent, - $verbose ); + $self->_on_run_script( path($exd_dir), $device, + $write_to_parent, $verbose ); }; if ($@) { warn $@; @@ -219,12 +277,10 @@ sub _on_run_script( $self, $exd_dir, $device, $write_to_parent, $verbose = 1 ) { my $printer = Exd::Printer->new( device => $device ); local $| = 1; - my $exd = Exd::FileFormat->new(dir => $exd_dir); + my $exd = Exd::FileFormat->new( dir => $exd_dir ); my ( $stdout, $stderr, $exit ) = capture { - eval { - $exd->execute($printer); - }; + eval { $exd->execute($printer); }; if ($@) { if ($verbose) { print $write_to_parent $@ . "\n"; @@ -279,6 +335,12 @@ sub _on_preview($self) { } } +sub _update_editor_buffer($self) { + my $editor = $self->_editor; + my $buffer = $editor->get_buffer(); + $buffer->set_text( $self->_file_format->get_script, -1 ); +} + sub _populate_editor( $self, $box_editor_preview ) { my $editor = Gtk4::Source::View->new; $self->_editor($editor); @@ -291,7 +353,7 @@ sub _populate_editor( $self, $box_editor_preview ) { $editor->set_smart_backspace(1); $editor->set_show_line_numbers(1); my $buffer = $editor->get_buffer(); - $buffer->set_text( $self->_file_format->get_script, -1 ); + $self->_update_editor_buffer; $buffer->set_language( Gtk4::Source::LanguageManager::get_default()->get_language('perl') ); $buffer->set_highlight_syntax(1); @@ -337,6 +399,166 @@ sub _populate_preview( $self, $box_editor_preview ) { $self->_generate_preview_file; } +sub _open_gallery($self) { + my $window = Gtk4::Window->new; + $window->set_transient_for( $self->window ); + $window->set_default_size( 600, 600 ); + $window->set_title('Image gallery'); + my $box = Gtk4::Box->new( 'vertical', 1 ); + my $box_upper_menu = Gtk4::Box->new( 'horizontal', 10 ); + my $add_image = Gtk4::Button->new_with_label('Add PNG image'); + $add_image->set_halign('start'); + my $label_entry_label = + Gtk4::Label->new('(Recommended) Put a label to the new image:'); + my $entry_label = Gtk4::Entry->new; + $box_upper_menu->append($label_entry_label); + $box_upper_menu->append($entry_label); + my $images_scroll = Gtk4::ScrolledWindow->new; + $add_image->signal_connect( + 'clicked', + sub { + my $dialog = Gtk4::FileDialog->new; + $dialog->open( + $window, undef, + sub( $source, $res, $data ) { + eval { + my $file = $dialog->open_finish($res); + my $buffer = $entry_label->get_buffer; + my $label = $buffer->get_text; + $file = $file->get_path; + if ( $file !~ /\.png$/ ) { + $self->write_to_parent->say("$file is not png."); + $self->write_to_parent->flush; + return; + } + eval { + $self->_file_format->add_png_image( + path($file)->slurp_raw, $label ); + $buffer->set_text( '', -1 ); + $self->_update_gallery_images( $window, + $images_scroll ); + }; + if ($@) { + warn $@; + $self->write_to_parent->print($@); + $self->write_to_parent->flush; + } + }; + if ($@) { + warn $@; + return; + } + } + ); + } + ); + $self->_update_gallery_images( $window, $images_scroll ); + $box->append($add_image); + $box->append($box_upper_menu); + $box->append($images_scroll); + $images_scroll->set_vexpand(1); + $window->set_child($box); + $window->present; +} + +sub _update_gallery_images( $self, $window, $images_scroll ) { + my %image_hashes_to_label = $self->_file_format->image_hashes_to_label->%*; + my $gallery_box = Gtk4::Box->new( 'vertical', 10 ); + my $i = 0; + my $row_image; + for my $hash ( sort { $a cmp $b } keys %image_hashes_to_label ) { + my @labels = $image_hashes_to_label{$hash}->@*; + if ( scalar @labels == 0 ) { + $row_image = $self->_get_row_image( $i, $gallery_box, $row_image ); + my $picture = $self->_create_gallery_image($hash); + my $box_image = Gtk4::Box->new( 'vertical', 1 ); + $box_image->set_property( 'width-request', 180 ); + my $copy = Gtk4::Button->new_with_label('Copy to clipboard'); + my $copy_gd = Gtk4::Button->new_with_label('Copy GD to clipboard'); + $copy->signal_connect( + 'clicked', + sub { + $self->_set_clipboard("\$exd->get_image('$hash')"); + $window->close; + } + ); + $copy_gd->signal_connect( + 'clicked', + sub { + $self->_set_clipboard("\$exd->get_image_gd('$hash')"); + $window->close; + } + ); + $box_image->append($picture); + my $scroll = Gtk4::ScrolledWindow->new; + $scroll->set_child( Gtk4::Label->new($hash) ); + $box_image->append($scroll); + $box_image->append($copy); + $box_image->append($copy_gd); + $row_image->append($box_image); + $i++; + next; + } + for my $label (@labels) { + $row_image = $self->_get_row_image( $i, $gallery_box, $row_image ); + my $picture = $self->_create_gallery_image($hash); + my $copy = Gtk4::Button->new_with_label('Copy to clipboard'); + my $copy_gd = Gtk4::Button->new_with_label('Copy GD to clipboard'); + my $box_image = Gtk4::Box->new( 'vertical', 1 ); + $box_image->set_property( 'width-request', 180 ); + $copy->signal_connect( + 'clicked', + sub { + $self->_set_clipboard("\$exd->get_image_from_label('$label')"); + $window->close; + } + ); + $copy_gd->signal_connect( + 'clicked', + sub { + $self->_set_clipboard("\$exd->get_image_gd_from_label('$label')"); + $window->close; + } + ); + $box_image->append($picture); + my $scroll = Gtk4::ScrolledWindow->new; + $scroll->set_child( Gtk4::Label->new($label) ); + $box_image->append($scroll); + $box_image->append($copy); + $box_image->append($copy_gd); + $row_image->append($box_image); + $i++; + } + } + $images_scroll->set_child($gallery_box); +} + +sub _set_clipboard($self, $text) { + my $display = Gdk::Display::get_default(); + my $clipboard = $display->get_clipboard; + my $wrapper = + Glib::Object::Introspection::GValueWrapper->new( 'Glib::String', $text ); + $clipboard->set($wrapper); +} + +sub _get_row_image( $self, $i, $gallery_box, $row_image ) { + if ( $i % 3 == 0 ) { + $row_image = Gtk4::Box->new( 'horizontal', 10 ); + $gallery_box->append($row_image); + } + return $row_image; +} + +sub _create_gallery_image( $self, $hash ) { + my $picture = Gtk4::Picture->new; + my $image_file = $self->_file_format->get_image($hash); + $picture->set_property( 'width-request', 180 ); + $picture->set_property( 'height-request', 180 ); + $picture->set_content_fit('fill'); + $picture->set_filename($image_file); + return $picture; +} + sub _activate($self) { Glib::Timeout->add( 1000, @@ -347,12 +569,20 @@ sub _activate($self) { ); my $app = $self->_app; my $win = Gtk4::ApplicationWindow->new($app); + $win->set_show_menubar(1); $self->window($win); my $box_vertical = Gtk4::Box->new( 'vertical', 10 ); my $execute_log = Gtk4::TextView->new; my $run_button = Gtk4::Button->new_with_label('Run'); my $preview_button = Gtk4::Button->new_with_label('Preview'); my $select_printer = Gtk4::Button->new_with_label('Select Printer'); + my $open_gallery = Gtk4::Button->new_with_label('Open image gallery'); + $open_gallery->signal_connect( + 'clicked', + sub { + $self->_open_gallery; + } + ); $select_printer->signal_connect( 'clicked', sub { @@ -388,6 +618,7 @@ sub _activate($self) { $box_buttons->append($run_button); $box_buttons->append($preview_button); $box_buttons->append($select_printer); + $box_buttons->append($open_gallery); $self->_populate_editor_and_preview($box_vertical); $box_vertical->append($box_buttons); my $execute_log_window = Gtk4::ScrolledWindow->new;