Compare commits

...

15 Commits
main ... main

15 changed files with 577 additions and 320 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: sergiotarxz

View File

@ -2,30 +2,78 @@
OpenMG is a GTK4 + Libadwaita manga reader written in C which uses `readmng` as its backend.
[![Please do not theme this app](https://stopthemingmy.app/badge.svg)](https://stopthemingmy.app)
## Demostration
![Demostration vídeo of the manga reader.](https://gitea.sergiotarxz.freemyip.com/sergiotarxz/mangareader/raw/branch/main/demostration.gif)
## Installing the app.
These are the installation methods supported currently.
### Flatpak
Download from https://gitea.sergiotarxz.freemyip.com/sergiotarxz/mangareader/releases the latest `openmg-x86_64-(version).flatpak` and run:
```shell
flatpak install openmg-x86_64-(version).flatpak
```
Beware that not being in Flathub yet you will have to come here again
to get updates.
### Gentoo
```shell
sudo eselect repository enable sergiotarxz
echo 'app-misc/openmg ~amd64' | sudo tee -a /etc/portage/package.accept_keywords/zz-autounmask
sudo emerge -a openmg --autounmask
```
If the installation ask you for a package masked for ~amd64 you can run
`sudo etc-update`, upgrade the `package.accept_keywords` config file
and try again the latest command of the installation instructions.
## Build from source
### Flatpak
First fine tune the options in `me.sergiotarxz.openmg.json` for
meson you want to have, for example preview images, complete list is
on `meson_options.txt`
```shell
flatpak --user remote-add --if-not-exists gnome-nightly https://nightly.gnome.org/gnome-nightly.flatpakrepo
flatpak install org.gnome.Sdk//master
flatpak install org.gnome.Platform//master
flatpak-builder --install --user build me.sergiotarxz.openmg.yml me.sergiotarxz.openmg
flatpak --user install org.gnome.Sdk//master
flatpak --user install org.gnome.Platform//master
flatpak-builder --install --user build me.sergiotarxz.openmg.json me.sergiotarxz.openmg
```
### Native
```shell
meson build
meson compile -C build
sudo meson install -C build
```
## Running the app
If using flatpak:
```shell
flatpak run me.sergiotarxz.openmg
```
If native installated:
```shell
openmg
```
## Donations welcome:
btc: `bc1q0apxdedrm5vjn3zr0hxswnruk2x2uecwqrusmj`

92
RELEASE.PL Normal file
View File

@ -0,0 +1,92 @@
use v5.30.0;
use strict;
use warnings;
use Const::Fast;
use Path::Tiny qw/path/;
use JSON;
use File::pushd;
use Mojo::URL;
use Mojo::UserAgent;
my $tag = $ARGV[0];
my $title = $ARGV[1] // "Automated release without title $tag";
my $description = $ARGV[2] // '';
const my $config_file => "$ENV{HOME}/.config/openmg_releaser.json";
if ( !-f $config_file ) {
die "No credentials in $config_file.";
}
my $config_file_contents = path($config_file)->slurp_utf8;
my $config = decode_json($config_file_contents);
my $username = $config->{username} // die "No user in config.";
my $token = $config->{token} // die "No token in config.";
my $host = $config->{host} // die "No host in config.";
my $commit = `git rev-parse HEAD`;
my $clone_path = Path::Tiny->tempdir;
system qw/git clone/,
'https://gitea.sergiotarxz.freemyip.com/sergiotarxz/mangareader/',
$clone_path;
my @subs = (compile('x86_64'), compile('aarch64'));
my $arch = shift;
my $release = request(
POST => '/repos/sergiotarxz/mangareader/releases' => json => {
body => $description,
name => $title,
tag_name => $tag,
target_commitish => 'main',
}
);
print Data::Dumper::Dumper $release;
my $release_id = $release->{id};
for my $sub (@subs) {
$sub->() if ref $sub eq 'CODE';
}
sub compile {
my $arch = shift;
my $flatpak_builder_file = 'me.sergiotarxz.openmg.json';
my $app_id = 'me.sergiotarxz.openmg';
my $app_output_name = path("openmg-$arch-$tag.flatpak")->absolute;
system 'cp', '/usr/bin/qemu-aarch64',
"$ENV{HOME}/.local/share/flatpak/runtime/org.gnome.Sdk/aarch64/master/active/files/bin/";
my $push = pushd $clone_path;
system 'flatpak-builder', '--force-clean', '--arch', $arch, '--install', '--user',
'build', $flatpak_builder_file, $app_id
and return 1;
system 'flatpak', 'build-bundle', '--arch', $arch,
path( $ENV{HOME} )->child('.local/share/flatpak/repo/'),
$app_output_name, $app_id
and return 1;
return sub {
print Data::Dumper::Dumper request( POST =>
"/repos/sergiotarxz/mangareader/releases/$release_id/assets?name=@{[$app_output_name->basename]}"
=> form => { attachment => { file => "" . $app_output_name } } );
}
}
sub request {
my $method = shift // die "No method passed.";
my $endpoint = shift // die "No endpoint passed.";
my $body_type = shift // die "No body type passed.";
my $body = shift // die "No body passed.";
my $ua = Mojo::UserAgent->new();
my $url = Mojo::URL->new("https://$host/api/v1/$endpoint");
$url->query( token => $token );
say $url;
my $tx = $ua->build_tx( $method => $url => {} => $body_type => $body );
$ua->start($tx);
my $response = $tx->result;
say $response->code;
say $response->message;
return decode_json( $response->body );
}

View File

@ -2,4 +2,6 @@
#include <glib-object.h>
void
g_object_set_property_int(GObject *object, char *property_key, int value);
g_object_set_property_int(GObject *object, char *property_key, gint value);
void
g_object_set_property_double(GObject *object, char *property_key, gdouble value);

View File

@ -8,10 +8,12 @@ G_DECLARE_FINAL_TYPE (MgUtilString, mg_util_string, MG, UTIL_STRING, GObject)
MgUtilString *mg_util_string_new ();
char *
mg_util_string_alloc_string(MgUtilString *self, size_t len);
mg_util_string_alloc_string (MgUtilString *self, size_t len);
void
mg_util_string_copy_substring(MgUtilString *self,
mg_util_string_copy_substring (MgUtilString *self,
const char *origin, char *dest, size_t dest_len, size_t start,
size_t len);
int
g_asprintf (char **strp, const char *format, ...);
G_END_DECLS

View File

@ -27,11 +27,15 @@ mg_util_xml_loop_search_class (MgUtilXML *self, const xmlNodePtr node, xmlNodePt
const char * class, size_t *len);
xmlXPathObjectPtr
mg_util_xml_get_nodes_xpath_expression (MgUtilXML *self,
const xmlDocPtr document, char *xpath);
const xmlDocPtr document, const xmlNodePtr node, char *xpath);
int
mg_util_xml_has_class (MgUtilXML *self,
const char *class_attribute, const char *class_to_check);
char *
mg_util_xml_get_title_text (MgUtilXML *self,
const char *const text);
void
mg_util_xml_print_debug_nodes (MgUtilXML *self,
const xmlDocPtr html_document, xmlNodePtr *nodes,
size_t nodes_len);
G_END_DECLS

View File

@ -4,7 +4,7 @@ GenericName=Manga Reader
Comment=A manga reader
Comment[es]=A manga reader
Exec=openmg
Icon=openmg
Icon=me.sergiotarxz.openmg
Type=Application
Terminal=false
StartupWMClass=openmg

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,4 +1,4 @@
project('openmg', 'c')
project('me.sergiotarxz.openmg', 'c')
inc = include_directories('include')
@ -56,12 +56,12 @@ icon_sizes = ['16', '24', '32', '48', '64', '128']
foreach i : icon_sizes
install_data(
'openmg.svg',
'me.sergiotarxz.openmg.svg',
install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i / 'apps',
rename: meson.project_name() + '.svg'
)
install_data(
'openmg.svg',
'me.sergiotarxz.openmg.svg',
install_dir: get_option('datadir') / 'icons' / 'hicolor' / i + 'x' + i + '@2' / 'apps',
rename: meson.project_name() + '.svg'
)

View File

@ -57,14 +57,6 @@ mg_backend_readmng_class_init (MgBackendReadmngClass *class) {
mg_backend_readmng_properties);
}
static xmlNodePtr
mg_backend_readmng_get_a_for_chapter (
MgBackendReadmng *self,
xmlNodePtr li);
static MgMangaChapter *
mg_backend_readmng_loop_li_chapter (
MgBackendReadmng *self,
xmlNodePtr li);
static char *
mg_backend_readmng_fetch_search (MgBackendReadmng *self,
const char *search_query, size_t *response_len);
@ -76,21 +68,10 @@ mg_backend_readmng_fetch_page_url (MgBackendReadmng *self,
MgMangaChapter *chapter);
static GListStore *
mg_backend_readmng_recover_chapter_list (MgBackendReadmng *self,
xmlDocPtr html_document_details);
xmlDocPtr html_document_details, MgManga *manga);
static xmlDocPtr
mg_backend_readmng_fetch_xml_details (MgBackendReadmng *self,
MgManga *manga);
static xmlNodePtr
mg_backend_readmng_retrieve_img_from_thumbnail (MgBackendReadmng *self, xmlNodePtr thumbnail);
static xmlNodePtr
mg_backend_readmng_retrieve_ul_slides (MgBackendReadmng *self, xmlNodePtr slides) ;
static void
mg_backend_readmng_extract_manga_info_from_current_li (MgBackendReadmng *self,
GListStore *mangas, xmlNodePtr current_li);
static xmlNodePtr *
mg_backend_readmng_retrieve_li_slides (MgBackendReadmng *self, const xmlNodePtr slides, size_t *li_len);
static xmlNodePtr
mg_backend_readmng_retrieve_slides (MgBackendReadmng *self, const xmlDocPtr html_document);
static const char *
mg_backend_readmng_get_main_page (MgBackendReadmng *self, size_t *len);
static GListStore *
@ -99,6 +80,21 @@ static xmlDocPtr
mg_backend_readmng_fetch_xml_main_page (MgBackendReadmng *self);
static char *
mg_backend_readmng_get_id_manga_link_from_string (MgBackendReadmng *self, const char *url);
static char *
mg_backend_readmng_get_manga_id_main_page (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr manga_slider_card);
static char *
mg_backend_readmng_get_manga_title_main_page (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr manga_slider_card);
static char *
mg_backend_readmng_get_manga_image_main_page (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr manga_slider_card);
static MgManga *
mg_backend_readmng_extract_from_manga_slider_card (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr node);
static MgMangaChapter *
mg_backend_readmng_get_data_from_check_box_card (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr check_box_card, MgManga *manga);
MgBackendReadmng *
mg_backend_readmng_new(void) {
@ -108,7 +104,7 @@ mg_backend_readmng_new(void) {
static void
mg_backend_readmng_init (MgBackendReadmng *self) {
if (!self->base_url) {
self->base_url = "https://www.readmng.com/";
self->base_url = "https://www.readmng.com";
}
self->xml_utils = mg_util_xml_new ();
}
@ -180,28 +176,216 @@ mg_backend_readmng_get_chapter_images (MgBackendReadmng *self, MgMangaChapter *c
static GListModel *
mg_backend_readmng_parse_page (MgBackendReadmng *self,
xmlDocPtr html_document) {
GListModel *images = G_LIST_MODEL
(gtk_string_list_new (NULL));
MgUtilXML *xml_utils = self->xml_utils;
xmlXPathObjectPtr xpath_result = NULL;
xmlNodeSetPtr node_set = NULL;
GListModel *images = G_LIST_MODEL (gtk_string_list_new (NULL));
MgUtilXML *xml_utils = self->xml_utils;
MgUtilRegex *regex_util = mg_util_regex_new ();
xmlNodeSetPtr node_set = NULL;
xmlXPathObjectPtr xpath_result = NULL;
xmlNodePtr script = NULL;
JsonParser *parser = json_parser_new ();
JsonNode *root = NULL;
JsonObject *root_object = NULL;
JsonArray *sources = NULL;
JsonArray *images_json_object = NULL;
JsonObject *source = NULL;
guint sources_len;
GError *error = NULL;
char *ts_reader_run = NULL;
char *ts_reader_run_json = NULL;
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils,
html_document, "//img[@class]");
html_document, NULL, "//script[contains(., 'ts_reader')]");
if (!xpath_result) {
fprintf(stderr, "No match for images.\n");
}
node_set = xpath_result->nodesetval;
if (!node_set) {
fprintf(stderr, "No match for images.\n");
goto cleanup_mg_backend_readmng_parse_page;
}
script = node_set->nodeTab[0];
ts_reader_run = (char *)xmlNodeGetContent (script);
ts_reader_run_json = mg_util_regex_match_1 (regex_util,
"^\\s+ts_reader\\.run\\(((?:.|\\r|\\n)+)\\);", ts_reader_run);
json_parser_load_from_data (parser, ts_reader_run_json, -1,
&error);
if (error) {
g_warning ("Unable to parse json: %s.", error->message);
g_clear_error (&error);
goto cleanup_mg_backend_readmng_parse_page;
}
root = json_parser_get_root (parser);
if (json_node_get_node_type (root) != JSON_NODE_OBJECT) {
fprintf(stderr, "Expected object as JSON root.\n");
goto cleanup_mg_backend_readmng_parse_page;
}
root_object = json_node_get_object (root);
sources = json_object_get_array_member (root_object, "sources");
if (!sources) {
fprintf(stderr, "No source in JSON.\n");
goto cleanup_mg_backend_readmng_parse_page;
}
sources_len = json_array_get_length (sources);
if (!sources_len) {
fprintf(stderr, "No source element in JSON.\n");
goto cleanup_mg_backend_readmng_parse_page;
}
source = json_array_get_object_element (sources, 0);
images_json_object = json_object_get_array_member (source, "images");
if (!images_json_object) {
fprintf(stderr, "No images in JSON.\n");
goto cleanup_mg_backend_readmng_parse_page;
}
for (int i = 0; i < json_array_get_length(images_json_object); i++) {
gtk_string_list_append (GTK_STRING_LIST (images),
json_array_get_string_element (images_json_object, i));
}
cleanup_mg_backend_readmng_parse_page:
if (ts_reader_run) {
g_free (ts_reader_run);
}
if (ts_reader_run_json) {
pcre2_substring_free ((PCRE2_UCHAR8 *)ts_reader_run_json);
}
if (xpath_result) {
xmlXPathFreeObject(xpath_result);
}
if (parser) {
g_clear_object (&parser);
}
return images;
}
static MgManga *
mg_backend_readmng_extract_from_manga_slider_card (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr node) {
MgManga *manga = NULL;
char *image = NULL;
char *title = NULL;
char *id = NULL;
image = mg_backend_readmng_get_manga_image_main_page (self, html_document,
node);
title = mg_backend_readmng_get_manga_title_main_page (self, html_document,
node);
id = mg_backend_readmng_get_manga_id_main_page (self, html_document,
node);
if (!image) {
fprintf (stderr, "Failed to find image\n");
goto cleanup_mg_backend_readmng_extract_from_manga_slider_card;
}
if (!title) {
fprintf (stderr, "Failed to find title\n");
goto cleanup_mg_backend_readmng_extract_from_manga_slider_card;
}
if (!id) {
fprintf (stderr, "Failed to find id\n");
goto cleanup_mg_backend_readmng_extract_from_manga_slider_card;
}
manga = mg_manga_new (image, title, id);
cleanup_mg_backend_readmng_extract_from_manga_slider_card:
if (image) {
g_free (image);
}
if (title) {
g_free (title);
}
if (id) {
pcre2_substring_free ((PCRE2_UCHAR8 *)id);
}
return manga;
}
static char *
mg_backend_readmng_get_manga_id_main_page (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr manga_slider_card) {
MgUtilXML *xml_utils = self->xml_utils;
MgUtilRegex *regex_util = mg_util_regex_new ();
xmlXPathObjectPtr xpath_result = NULL;
char *id = NULL;
char *new_id = NULL;
xmlNodeSetPtr node_set = NULL;
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils, html_document,
manga_slider_card, "./a");
if (!xpath_result) {
fprintf (stderr, "No matching id.\n");
goto cleanup_mg_backend_readmng_get_manga_id_main_page;
}
node_set = xpath_result->nodesetval;
if (!node_set) {
fprintf(stderr, "No match for images.\n");
goto cleanup_mg_backend_readmng_parse_page;
fprintf (stderr, "No matching id node set.\n");
goto cleanup_mg_backend_readmng_get_manga_id_main_page;
}
for (int i = 0; i < node_set->nodeNr; i++) {
xmlNodePtr node = node_set->nodeTab[i];
char *image_url = mg_util_xml_get_attr (xml_utils, node, "src");
gtk_string_list_append (GTK_STRING_LIST (images), image_url);
g_free (image_url);
xmlNodePtr a = node_set->nodeTab[0];
id = mg_util_xml_get_attr (xml_utils, a, "href");
if (id) {
new_id = mg_util_regex_match_1 (regex_util, "^/([^/]+)", id);
g_free (id);
id = new_id;
}
cleanup_mg_backend_readmng_parse_page:
xmlXPathFreeObject(xpath_result);
return images;
cleanup_mg_backend_readmng_get_manga_id_main_page:
if (xpath_result) {
xmlXPathFreeObject (xpath_result);
}
g_clear_object (&regex_util);
return id;
}
static char *
mg_backend_readmng_get_manga_title_main_page (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr manga_slider_card) {
MgUtilXML *xml_utils = self->xml_utils;
xmlXPathObjectPtr xpath_result = NULL;
char *title = NULL;
xmlNodeSetPtr node_set = NULL;
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils, html_document,
manga_slider_card, ".//div[@class='postDetail']//h2");
if (!xpath_result) {
fprintf (stderr, "No matching title.\n");
goto cleanup_mg_backend_readmng_get_manga_title_main_page;
}
node_set = xpath_result->nodesetval;
if (!node_set) {
fprintf (stderr, "No matching title node set.\n");
goto cleanup_mg_backend_readmng_get_manga_title_main_page;
}
xmlNodePtr h2 = node_set->nodeTab[0];
title = (char *)xmlNodeGetContent (h2);
cleanup_mg_backend_readmng_get_manga_title_main_page:
if (xpath_result) {
xmlXPathFreeObject (xpath_result);
}
return title;
}
static char *
mg_backend_readmng_get_manga_image_main_page (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr manga_slider_card) {
MgUtilXML *xml_utils = self->xml_utils;
xmlXPathObjectPtr xpath_result = NULL;
char *image = NULL;
xmlNodeSetPtr node_set = NULL;
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils, html_document,
manga_slider_card, ".//div[@class='sliderImg']//img");
if (!xpath_result) {
fprintf (stderr, "No matching image.\n");
goto cleanup_mg_backend_readmng_get_manga_image_main_page;
}
node_set = xpath_result->nodesetval;
if (!node_set) {
fprintf (stderr, "No matching image node set.\n");
goto cleanup_mg_backend_readmng_get_manga_image_main_page;
}
xmlNodePtr img = node_set->nodeTab[0];
image = mg_util_xml_get_attr (xml_utils, img, "src");
cleanup_mg_backend_readmng_get_manga_image_main_page:
if (xpath_result) {
xmlXPathFreeObject (xpath_result);
}
return image;
}
static xmlDocPtr
@ -239,12 +423,13 @@ mg_backend_readmng_search (MgBackendReadmng *self,
size_t response_len = 0;
char *response = mg_backend_readmng_fetch_search (self, search_query,
&response_len);
JsonParser *parser = json_parser_new ();
GListStore *mangas = g_list_store_new (MG_TYPE_MANGA);
GError *error = NULL;
JsonNode *root = NULL;
JsonParser *parser = json_parser_new ();
GListStore *mangas = g_list_store_new (MG_TYPE_MANGA);
GError *error = NULL;
JsonNode *root = NULL;
JsonArray *mangas_json_array = NULL;
guint mangas_json_array_len = 0;
JsonObject *root_object = NULL;
guint mangas_json_array_len = 0;
if (!response) {
g_warning ("Json search response is null.");
@ -257,13 +442,13 @@ mg_backend_readmng_search (MgBackendReadmng *self,
goto cleanup_mg_backend_readmng_search;
}
root = json_parser_get_root (parser);
if (json_node_get_node_type (root) != JSON_NODE_ARRAY) {
if (json_node_get_node_type (root) != JSON_NODE_OBJECT) {
goto cleanup_mg_backend_readmng_search;
}
mangas_json_array = json_node_get_array (root);
mangas_json_array_len = json_array_get_length (
mangas_json_array);
for (guint i = 0; i < mangas_json_array_len && i < 19; i++) {
root_object = json_node_get_object (root);
mangas_json_array = json_object_get_array_member (root_object, "manga");
mangas_json_array_len = json_array_get_length (mangas_json_array);
for (guint i = 0; i < mangas_json_array_len && i < 5; i++) {
JsonObject *manga_json_object =
json_array_get_object_element (mangas_json_array, i);
char *id_manga = NULL;
@ -294,13 +479,9 @@ mg_backend_readmng_fetch_search (MgBackendReadmng *self,
char *request_url;
size_t request_url_len;
util_soup = mg_util_soup_new ();
string_util = mg_util_string_new ();
request_url_len = snprintf ( NULL, 0, "%s/%s/", self->base_url, "service/search");
request_url = mg_util_string_alloc_string (string_util, request_url_len);
snprintf ( request_url, request_url_len+1, "%s/%s/", self->base_url, "service/search");
g_asprintf ( &request_url, "%s/%s/", self->base_url, "search/live");
SoupParam headers[] = {
{
@ -383,23 +564,17 @@ mg_backend_readmng_retrieve_manga_details (MgBackendReadmng *self,
html_document = mg_backend_readmng_fetch_xml_details (self,
manga);
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils,
html_document, "//li[@class]");
html_document, NULL, "//div[@class='infox']//div[@class='wd-full'][2]");
node_set = xpath_result->nodesetval;
if (!node_set) {
fprintf(stderr, "No match\n");
goto cleanup_mg_backend_readmng_retrieve_manga_details;
}
for (int i = 0; i < node_set->nodeNr; i++) {
xmlNodePtr node = node_set->nodeTab[i];
movie_detail = mg_util_xml_loop_search_class (xml_utils,
node, movie_detail, "movie-detail", &movie_detail_len);
}
if (movie_detail) {
char *description = (char *) xmlNodeGetContent (movie_detail[0]);
mg_manga_set_description (manga, description);
g_free (description);
}
manga_chapters = mg_backend_readmng_recover_chapter_list (self, html_document);
xmlNodePtr description_node = node_set->nodeTab[0];
char *description = (char *) xmlNodeGetContent (description_node);
mg_manga_set_description (manga, description);
g_free (description);
manga_chapters = mg_backend_readmng_recover_chapter_list (self, html_document, manga);
mg_manga_set_chapter_list (manga, manga_chapters);
mg_manga_details_recovered (manga);
cleanup_mg_backend_readmng_retrieve_manga_details:
@ -419,104 +594,74 @@ cleanup_mg_backend_readmng_retrieve_manga_details:
static GListStore *
mg_backend_readmng_recover_chapter_list (MgBackendReadmng *self,
xmlDocPtr html_document_details) {
MgUtilXML *xml_utils = self->xml_utils;
xmlDocPtr html_document_details, MgManga *manga) {
MgUtilXML *xml_utils = self->xml_utils;
xmlXPathObjectPtr xpath_result = NULL;
xmlNodeSetPtr node_set = NULL;
xmlNodePtr *uls = NULL;
xmlNodePtr ul;
GListStore *return_value = g_list_store_new (
xmlNodeSetPtr node_set = NULL;
GListStore *return_value = g_list_store_new (
MG_TYPE_MANGA_CHAPTER);
size_t ul_len = 0;
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils,
html_document_details, "//ul[@class]");
html_document_details, NULL, "//div[@class='checkBoxCard']");
node_set = xpath_result->nodesetval;
if (!node_set) {
fprintf(stderr, "No matching ul\n");
fprintf(stderr, "No matching chapter\n");
goto cleanup_mg_backend_readmng_recover_chapter_list;
}
for (int i = 0; i < node_set->nodeNr; i++) {
xmlNodePtr node = node_set->nodeTab[i];
uls = mg_util_xml_loop_search_class (xml_utils,
node, uls, "chp_lst", &ul_len);
}
if (!ul_len) {
fprintf(stderr, "No matching chp_lst\n");
goto cleanup_mg_backend_readmng_recover_chapter_list;
}
ul = uls[0];
for (xmlNodePtr li = ul->children; li; li = li->next) {
if (!strcmp ((char *) li->name, "li")) {
MgMangaChapter *chapter = mg_backend_readmng_loop_li_chapter (self, li);
if (chapter) {
g_list_store_append (return_value, chapter);
}
}
xmlNodePtr check_box_card = node_set->nodeTab[i];
MgMangaChapter *chapter = mg_backend_readmng_get_data_from_check_box_card
(self, html_document_details, check_box_card, manga);
if (chapter) {
g_list_store_append (return_value, chapter);
}
}
cleanup_mg_backend_readmng_recover_chapter_list:
if (xpath_result) {
xmlXPathFreeObject(xpath_result);
}
if (uls) {
for (size_t i = 0; i < ul_len; i++) {
xmlFreeNode(uls[i]);
}
g_free (uls);
}
return return_value;
}
static MgMangaChapter *
mg_backend_readmng_loop_li_chapter (
MgBackendReadmng *self,
xmlNodePtr li) {
MgUtilXML *xml_utils = self->xml_utils;
MgMangaChapter *chapter = NULL;
xmlNodePtr a = mg_backend_readmng_get_a_for_chapter (
self, li);
if (!a) return NULL;
char *url = mg_util_xml_get_attr (xml_utils, a, "href");
size_t val_len = 0;
size_t dte_len = 0;
xmlNodePtr *val = mg_util_xml_find_class (xml_utils, a, "val", &val_len, NULL, 1);
xmlNodePtr *dte = mg_util_xml_find_class (xml_utils, a, "dte", &dte_len, NULL, 1);
if (val_len && dte_len) {
char *val_str = (char *) xmlNodeGetContent (val[0]);
char *dte_str = (char *) xmlNodeGetContent (dte[0]);
chapter = mg_manga_chapter_new (val_str, dte_str, url);
g_free (val_str);
g_free (dte_str);
}
if (url) {
g_free (url);
}
if (val) {
g_free (val);
val = NULL;
}
if (dte) {
g_free (dte);
dte = NULL;
}
return chapter;
}
static xmlNodePtr
mg_backend_readmng_get_a_for_chapter (
MgBackendReadmng *self,
xmlNodePtr li) {
for (xmlNodePtr child = li->children; child; child = child->next) {
if (!strcmp((char *) child->name, "a")) {
return child;
}
}
return NULL;
mg_backend_readmng_get_data_from_check_box_card (MgBackendReadmng *self,
xmlDocPtr html_document, xmlNodePtr check_box_card, MgManga *manga) {
xmlXPathObjectPtr xpath_result = NULL;
xmlNodeSetPtr node_set = NULL;
MgMangaChapter *chapter = NULL;
MgUtilXML *xml_utils = self->xml_utils;
char *chapter_id = NULL;
char *title = NULL;
char *url = NULL;
char *manga_id = mg_manga_get_id (manga);
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils,
html_document, check_box_card, ".//label[@data-chapter-id]");
if (!xpath_result) {
fprintf(stderr, "Unable to parse chapter, xpath failed.\n");
goto cleanup_mg_backend_readmng_get_data_from_check_box_card;
}
node_set = xpath_result->nodesetval;
if (!node_set) {
fprintf(stderr, "Unable to parse chapter, no nodeset.\n");
goto cleanup_mg_backend_readmng_get_data_from_check_box_card;
}
xmlNodePtr chapter_node = node_set->nodeTab[0];
chapter_id = mg_util_xml_get_attr (xml_utils, chapter_node, "data-chapter-id");
g_asprintf (&title, "Chapter %s", chapter_id);
g_asprintf (&url, "%s/%s/%s", self->base_url, manga_id, chapter_id);
chapter = mg_manga_chapter_new (title, "", url);
cleanup_mg_backend_readmng_get_data_from_check_box_card:
if (xpath_result) {
xmlXPathFreeObject (xpath_result);
}
if (chapter_id) {
g_free (chapter_id);
}
return chapter;
}
static xmlDocPtr
@ -528,15 +673,12 @@ mg_backend_readmng_fetch_xml_details (MgBackendReadmng *self,
char *request_url;
char *manga_id;
size_t request_url_len;
size_t response_len = 0;
util_soup = mg_util_soup_new ();
string_util = mg_util_string_new ();
manga_id = mg_manga_get_id (manga);
request_url_len = snprintf ( NULL, 0, "%s/%s/", self->base_url, manga_id);
request_url = mg_util_string_alloc_string (string_util, request_url_len);
snprintf ( request_url, request_url_len+1, "%s/%s/", self->base_url, manga_id);
g_asprintf ( &request_url, "%s/%s", self->base_url, manga_id);
g_free (manga_id);
char *html_response = mg_util_soup_get_request (util_soup,
@ -583,176 +725,39 @@ mg_backend_readmng_get_main_page (MgBackendReadmng *self, size_t *len) {
static GListStore *
mg_backend_readmng_parse_main_page (MgBackendReadmng *self, const xmlDocPtr html_document) {
GListStore *mangas = g_list_store_new(MG_TYPE_MANGA);
xmlNodePtr *li;
GListStore *mangas = g_list_store_new (MG_TYPE_MANGA);
xmlNodePtr slides = mg_backend_readmng_retrieve_slides (self, html_document);
size_t li_len = 0;
li = mg_backend_readmng_retrieve_li_slides (self, slides, &li_len);
for (int i = 0; i<li_len; i++) {
xmlNodePtr current_li = li[i];
mg_backend_readmng_extract_manga_info_from_current_li (self,
mangas, current_li);
xmlFreeNode (current_li);
li[i] = NULL;
}
xmlFreeNode(slides);
g_free (li);
return mangas;
}
static xmlNodePtr *
mg_backend_readmng_retrieve_li_slides (MgBackendReadmng *self, const xmlNodePtr slides, size_t *li_len) {
xmlNodePtr ul_slides = mg_backend_readmng_retrieve_ul_slides (self, slides);
xmlNodePtr *li = NULL;
for (xmlNodePtr child = ul_slides->children; child; child=child->next) {
(*li_len)++;
li = g_realloc(li, sizeof *li * *li_len);
li[*li_len-1] = xmlCopyNode(child, XML_COPY_NODE_RECURSIVE);
}
return li;
}
static xmlNodePtr
mg_backend_readmng_retrieve_ul_slides(MgBackendReadmng *self, xmlNodePtr slides) {
for (xmlNodePtr child = slides->children; child; child = child->next) {
if (!strcmp((char *) child->name, "ul")) {
return child;
}
}
return NULL;
}
static xmlNodePtr
mg_backend_readmng_retrieve_slides (MgBackendReadmng *self, const xmlDocPtr html_document) {
xmlNodePtr *nodes = NULL;
xmlXPathObjectPtr xpath_result = NULL;
MgUtilXML *xml_utils = self->xml_utils;
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils,
html_document, "//div[@class]");
xmlNodePtr slides = NULL;
xmlXPathObjectPtr xpath_result = NULL;
xmlNodeSetPtr node_set = NULL;
size_t matching_classes_len = 0;
xpath_result = mg_util_xml_get_nodes_xpath_expression (xml_utils,
html_document, NULL, "//div[@class='mangaSliderCard']");
node_set = xpath_result->nodesetval;
if (!node_set) {
fprintf(stderr, "No match\n");
goto cleanup_mg_backend_readmng_retrieve_slides;
fprintf(stderr, "No match for mangas.\n");
goto cleanup_mg_backend_readmng_parse_main_page;
}
for (int i = 0; i < node_set->nodeNr; i++) {
xmlNodePtr node = node_set->nodeTab[i];
nodes = mg_util_xml_loop_search_class (xml_utils, node, nodes,
"slides", &matching_classes_len);
MgManga *manga = mg_backend_readmng_extract_from_manga_slider_card (self,
html_document, node);
if (!manga) {
continue;
}
g_list_store_append (mangas, manga);
}
if (nodes) {
slides = nodes[0];
}
cleanup_mg_backend_readmng_retrieve_slides:
cleanup_mg_backend_readmng_parse_main_page:
if (xpath_result) {
xmlXPathFreeObject(xpath_result);
xmlXPathFreeObject (xpath_result);
}
if (nodes) {
for (size_t i = 1; i < matching_classes_len; i++)
{
xmlFreeNode(nodes[i]);
}
g_free (nodes);
}
return slides;
}
static xmlNodePtr
mg_backend_readmng_retrieve_thumbnail_from_li (MgBackendReadmng *self, xmlNodePtr current_li) {
size_t thumbnail_len = 0;
MgUtilXML *xml_utils = self->xml_utils;
xmlNodePtr return_value = NULL;
xmlNodePtr *thumbnail = mg_util_xml_find_class (xml_utils, current_li, "thumbnail",
&thumbnail_len, NULL, 1);
if (!thumbnail_len) goto cleanup_mg_backend_retrieve_thumbnail_from_li;
return_value = thumbnail[0];
cleanup_mg_backend_retrieve_thumbnail_from_li:
if (thumbnail) {
g_free (thumbnail);
}
return return_value;
}
static xmlNodePtr
mg_backend_readmng_retrieve_title_from_li (MgBackendReadmng *self, xmlNodePtr li) {
size_t title_len = 0;
MgUtilXML *xml_utils = self->xml_utils;
xmlNodePtr return_value = NULL;
xmlNodePtr *title = mg_util_xml_find_class (xml_utils, li, "title", &title_len, NULL, 1);
if (title_len) {
return_value = title[0];
}
if (title) {
g_free (title);
}
return return_value;
}
static xmlNodePtr
mg_backend_readmng_find_a_link_chapter (MgBackendReadmng *self,
xmlNodePtr current_li) {
for (xmlNodePtr child = current_li->children; child; child = child->next) {
if (!strcmp((char *)child->name, "a")) {
return child;
}
}
return NULL;
}
static char *
mg_backend_readmng_get_id_manga_link (MgBackendReadmng *self, xmlNodePtr a) {
MgUtilXML *xml_utils = self->xml_utils;
char *href = mg_util_xml_get_attr (xml_utils, a, "href");
char *result = mg_backend_readmng_get_id_manga_link_from_string (self, href);
g_free (href);
return result;
return mangas;
}
static char *
mg_backend_readmng_get_id_manga_link_from_string (MgBackendReadmng *self, const char *url) {
MgUtilRegex *regex_util = mg_util_regex_new ();
char *re_str = "readmng\\.com/([^/]+)";
char *re_str = "/([^/]+)";
char *result = mg_util_regex_match_1 (regex_util, re_str, url);
g_clear_object (&regex_util);
return result;
}
static void
mg_backend_readmng_extract_manga_info_from_current_li (MgBackendReadmng *self,
GListStore *mangas, xmlNodePtr current_li) {
xmlNodePtr thumbnail = mg_backend_readmng_retrieve_thumbnail_from_li (self, current_li);
xmlNodePtr title = mg_backend_readmng_retrieve_title_from_li (self, current_li);
xmlNodePtr a = mg_backend_readmng_find_a_link_chapter (self, current_li);
xmlNodePtr img;
MgUtilXML *xml_utils = self->xml_utils;
char *id_manga = NULL;
if (thumbnail && title && (img = mg_backend_readmng_retrieve_img_from_thumbnail (self, thumbnail))
&& a && (id_manga = mg_backend_readmng_get_id_manga_link (self, a))) {
char *src = mg_util_xml_get_attr (xml_utils, img, "src");
char *title_string = (char *)xmlNodeGetContent (title);
g_list_store_append (mangas, mg_manga_new (src, title_string, id_manga));
g_free (src);
g_free (title_string);
pcre2_substring_free ((PCRE2_UCHAR8 *) id_manga);
}
}
static xmlNodePtr
mg_backend_readmng_retrieve_img_from_thumbnail (MgBackendReadmng *self, xmlNodePtr thumbnail) {
for (xmlNodePtr child = thumbnail->children; child; child=child->next) {
if (!strcmp((char *)child->name, "img")) {
return child;
}
}
return NULL;
}

View File

@ -1,7 +1,7 @@
#include <glib-object.h>
void
g_object_set_property_int(GObject *object, char *property_key, int value) {
g_object_set_property_int(GObject *object, char *property_key, gint value) {
GValue property = G_VALUE_INIT;
g_value_init (&property, G_TYPE_INT);
g_value_set_int (&property, value);
@ -9,3 +9,10 @@ g_object_set_property_int(GObject *object, char *property_key, int value) {
}
void
g_object_set_property_double(GObject *object, char *property_key, gdouble value) {
GValue property = G_VALUE_INIT;
g_value_init (&property, G_TYPE_DOUBLE);
g_value_set_double (&property, value);
g_object_set_property (object, property_key, &property);
}

View File

@ -1,5 +1,6 @@
#include <stdio.h>
#include <glib/gprintf.h>
#include <glib-object.h>
#include <openmg/util/string.h>
@ -46,3 +47,11 @@ mg_util_string_new () {
self = MG_UTIL_STRING ((g_object_new (MG_TYPE_UTIL_STRING, NULL)));
return self;
}
int
g_asprintf (char **strp, const char *format, ...) {
va_list ap;
va_start (ap, format);
int retval = g_vasprintf (strp, format, ap);
va_end (ap);
return retval;
}

View File

@ -143,11 +143,14 @@ mg_util_xml_loop_search_class (MgUtilXML *self, const xmlNodePtr node, xmlNodePt
xmlXPathObjectPtr
mg_util_xml_get_nodes_xpath_expression (MgUtilXML *self,
const xmlDocPtr document, char *xpath) {
const xmlDocPtr document, const xmlNodePtr node, char *xpath) {
xmlXPathContextPtr context;
xmlXPathObjectPtr result;
context = xmlXPathNewContext (document);
if (node) {
context->node = node;
}
result = xmlXPathEvalExpression ((const xmlChar *)xpath, context);
xmlXPathFreeContext (context);

View File

@ -10,6 +10,11 @@
#include <openmg/util/string.h>
#include <openmg/backend/readmng.h>
#define KEY_LEFT 65361
#define KEY_RIGHT 65363
#define KEY_UP 65362
#define KEY_DOWN 65364
static void
fire_zoom (GtkGestureZoom *zoom,
gdouble scale,
@ -53,7 +58,10 @@ static void
picture_ready_manga_page (GObject *source_object,
GAsyncResult *res,
gpointer user_data);
static void
zoomable_container_keybinding_handle (GtkEventControllerKey *self,
guint keyval, guint keycode, GdkModifierType state, gpointer user_data);
static void
image_page_show (GtkWidget *picture, gpointer user_data) {
ChapterVisorData *chapter_visor_data = (ChapterVisorData *) user_data;
@ -91,7 +99,7 @@ set_image_dimensions (GtkWidget *picture,
gdk_paintable_get_intrinsic_height (paintable),
&final_width,
&final_height
);
);
g_object_set_property_int (G_OBJECT (picture),
"width-request", (int) final_width);
g_object_set_property_int (G_OBJECT (picture),
@ -110,13 +118,15 @@ setup_chapter_view (MgMangaChapter *chapter, AdwLeaflet *views_leaflet) {
GtkOverlay *overlay = GTK_OVERLAY (gtk_overlay_new ());
GListModel *pages = mg_backend_readmng_get_chapter_images (readmng, chapter);
ChapterVisorData *chapter_visor_data = g_malloc (sizeof *chapter_visor_data);
chapter_visor_data->current_page = 0;
chapter_visor_data->pages = pages;
chapter_visor_data->views_leaflet = views_leaflet;
chapter_visor_data->zoom = 1;
chapter_visor_data->zoomable_picture_container = zoomable_picture_container;
set_zoomable_picture_container_properties (zoomable_picture_container,
chapter_visor_data);
chapter_visor_data);
set_image_zoomable_picture_container (chapter_visor_data);
gtk_overlay_set_child (overlay, GTK_WIDGET (zoomable_picture_container));
@ -133,6 +143,8 @@ add_controls_overlay (GtkOverlay *overlay, ChapterVisorData *chapter_visor_data)
("go-next-symbolic"));
GtkButton *previous_button = GTK_BUTTON (gtk_button_new_from_icon_name
("go-previous-symbolic"));
gtk_widget_set_focusable (GTK_WIDGET (next_button), false);
gtk_widget_set_focusable (GTK_WIDGET (previous_button), false);
g_signal_connect (G_OBJECT (next_button), "clicked", G_CALLBACK (go_next), chapter_visor_data);
g_signal_connect (G_OBJECT (previous_button), "clicked", G_CALLBACK (go_prev), chapter_visor_data);
gtk_widget_set_valign (GTK_WIDGET (next_button), GTK_ALIGN_CENTER);
@ -142,6 +154,19 @@ add_controls_overlay (GtkOverlay *overlay, ChapterVisorData *chapter_visor_data)
gtk_overlay_add_overlay (overlay, GTK_WIDGET (next_button));
gtk_overlay_add_overlay (overlay, GTK_WIDGET (previous_button));
}
static void
configure_zoomable_for_new_page (ChapterVisorData *chapter_visor_data) {
GtkScrolledWindow *zoomable_picture_container =
chapter_visor_data->zoomable_picture_container;
GtkAdjustment *hadjustment = gtk_scrolled_window_get_hadjustment (zoomable_picture_container);
GtkAdjustment *vadjustment = gtk_scrolled_window_get_vadjustment (zoomable_picture_container);
gtk_widget_grab_focus (GTK_WIDGET (chapter_visor_data->zoomable_picture_container));
set_image_zoomable_picture_container (chapter_visor_data);
g_object_set_property_double (G_OBJECT (vadjustment), "value", 0);
g_object_set_property_double (G_OBJECT (hadjustment), "value", 999);
}
static void
go_next (GtkButton *next,
gpointer user_data) {
@ -149,7 +174,7 @@ go_next (GtkButton *next,
GListModel *pages = chapter_visor_data->pages;
if (chapter_visor_data->current_page < g_list_model_get_n_items (pages) -1) {
chapter_visor_data->current_page = chapter_visor_data->current_page + 1;
set_image_zoomable_picture_container (chapter_visor_data);
configure_zoomable_for_new_page (chapter_visor_data);
}
}
static void
@ -158,7 +183,7 @@ go_prev (GtkButton *prev,
ChapterVisorData *chapter_visor_data = (ChapterVisorData *) user_data;
if (chapter_visor_data->current_page > 0) {
chapter_visor_data->current_page = chapter_visor_data->current_page - 1;
set_image_zoomable_picture_container (chapter_visor_data);
configure_zoomable_for_new_page (chapter_visor_data);
}
}
@ -210,6 +235,13 @@ set_zoomable_picture_container_properties (
GtkScrolledWindow *zoomable_picture_container,
ChapterVisorData *chapter_visor_data) {
GtkGesture *zoom_controller = gtk_gesture_zoom_new ();
GtkEventController *key_controller = gtk_event_controller_key_new ();
g_signal_connect (G_OBJECT (key_controller), "key-pressed", G_CALLBACK (zoomable_container_keybinding_handle),
chapter_visor_data);
gtk_widget_add_controller (GTK_WIDGET (zoomable_picture_container),
key_controller);
g_object_set_property_int (G_OBJECT (zoomable_picture_container), "hexpand", 1);
g_object_set_property_int (G_OBJECT (zoomable_picture_container), "vexpand", 1);
gtk_widget_add_controller (GTK_WIDGET (zoomable_picture_container),
@ -218,6 +250,56 @@ set_zoomable_picture_container_properties (
g_signal_connect (G_OBJECT (zoom_controller), "end", G_CALLBACK (zoom_end), chapter_visor_data);
}
static void
zoomable_container_keybinding_handle (GtkEventControllerKey *self,
guint keyval, guint keycode, GdkModifierType state, gpointer user_data) {
ChapterVisorData *chapter_visor_data = (ChapterVisorData *) user_data;
GtkScrolledWindow *zoomable_picture_container = chapter_visor_data->zoomable_picture_container;
GtkAdjustment *vadjustment = gtk_scrolled_window_get_vadjustment (zoomable_picture_container);
GtkAdjustment *hadjustment = gtk_scrolled_window_get_hadjustment (zoomable_picture_container);
GValue adjustment = G_VALUE_INIT;
gdouble current_adjustment;
gdouble change_rate_key_movement = 50;
if (state & GDK_CONTROL_MASK ) {
if ( keyval == '+' ) {
set_image_dimensions (GTK_WIDGET (chapter_visor_data->current_picture),
chapter_visor_data, 2);
}
if ( keyval == '-' ) {
set_image_dimensions (GTK_WIDGET (chapter_visor_data->current_picture),
chapter_visor_data, 0.5);
}
}
if (state & GDK_SHIFT_MASK ) {
g_object_get_property (G_OBJECT (hadjustment), "value", &adjustment);
current_adjustment = g_value_get_double (&adjustment);
if (keyval == KEY_LEFT) {
g_object_set_property_double (G_OBJECT (hadjustment), "value", current_adjustment - change_rate_key_movement);
}
if (keyval == KEY_RIGHT) {
g_object_set_property_double (G_OBJECT (hadjustment), "value", current_adjustment + change_rate_key_movement);
}
g_object_get_property (G_OBJECT (vadjustment), "value", &adjustment);
current_adjustment = g_value_get_double (&adjustment);
if (keyval == KEY_UP) {
// UP
g_object_set_property_double (G_OBJECT (vadjustment), "value", current_adjustment - change_rate_key_movement);
}
if (keyval == KEY_DOWN) {
// UP
g_object_set_property_double (G_OBJECT (vadjustment), "value", current_adjustment + change_rate_key_movement);
}
} else {
if (keyval == KEY_LEFT) {
go_prev (NULL, chapter_visor_data);
}
if (keyval == KEY_RIGHT) {
go_next (NULL, chapter_visor_data);
}
}
}
static void
append_chapter_view_leaflet (AdwLeaflet *views_leaflet,

View File

@ -38,6 +38,8 @@ activate (AdwApplication *app,
AdwLeaflet *views_leaflet_explore;
AdwLeaflet *views_leaflet_search;
AdwHeaderBar *header_bar;
gtk_window_set_default_size (GTK_WINDOW (window), 3000, 3000);
#ifndef _WIN32
swipe_back_t swipe_back = (swipe_back_t) dlsym