feat: Implement tiling engine for image rendering in manga reader, more features, more optimizations

This commit is contained in:
Moustapha Kodjo Amadou 2026-07-13 14:39:38 +01:00
parent c130260876
commit 45ec2d1c8a
79 changed files with 22140 additions and 11788 deletions

27
hook/build.dart Normal file
View file

@ -0,0 +1,27 @@
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:code_assets/code_assets.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
final builder = CBuilder.library(
name: 'subsampling_scale_image_view',
assetName: 'subsampling_scale_image_view.dart',
sources: ['lib/ffi/image_decoder.cpp'],
includes: ['lib/ffi/'],
flags: [
if (input.config.code.targetOS == OS.iOS ||
input.config.code.targetOS == OS.macOS) ...[
'-framework',
'CoreFoundation',
'-framework',
'CoreGraphics',
'-framework',
'ImageIO',
],
],
);
await builder.run(input: input, output: output);
});
}

View file

@ -10,7 +10,10 @@ ExtensionService getExtensionService(Source source, String androidProxyServer) {
return switch (source.sourceCodeLanguage) {
SourceCodeLanguage.dart => DartExtensionService(source),
SourceCodeLanguage.javascript => JsExtensionService(source),
SourceCodeLanguage.mihon => MihonExtensionService(source, androidProxyServer),
SourceCodeLanguage.mihon => MihonExtensionService(
source,
androidProxyServer,
),
SourceCodeLanguage.lnreader => LNReaderExtensionService(source),
};
}

1155
lib/ffi/image_decoder.cpp Normal file

File diff suppressed because it is too large Load diff

28
lib/ffi/image_decoder.h Normal file
View file

@ -0,0 +1,28 @@
#ifndef IMAGE_DECODER_H_
#define IMAGE_DECODER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#ifdef _WIN32
#define DECODER_EXPORT __declspec(dllexport)
#else
#define DECODER_EXPORT __attribute__((visibility("default")))
#endif
typedef struct ImageDecoderContext ImageDecoderContext;
DECODER_EXPORT ImageDecoderContext* init_decoder(const char* file_path, bool crop_borders, int* out_width, int* out_height);
DECODER_EXPORT bool decode_region(ImageDecoderContext* ctx, int left, int top, int right, int bottom, int sample_size, unsigned char* out_rgba_buffer);
DECODER_EXPORT void free_decoder(ImageDecoderContext* ctx);
#ifdef __cplusplus
}
#endif
#endif // IMAGE_DECODER_H_

File diff suppressed because it is too large Load diff

View file

@ -787,82 +787,97 @@
"average_chapters_per_title": "প্ৰতিটো শিৰোনামৰ গড় অধ্যায়",
"read_percentage": "পঢ়াৰ শতাংশ",
"entries": "প্ৰৱেশসমূহ",
"extension_server_intro_with_jre": "Mihon এক্সটেনচন ব্যৱহাৰ কৰাৰ আগতে প্ৰক্সী চাৰ্ভাৰ বাণ্ডিল ডাউনলোড কৰক। বাণ্ডিলটোত JRE আৰু এক্সটেনচন চাৰ্ভাৰ JAR অন্তৰ্ভুক্ত কৰা হৈছে।",
"extension_server_intro_ios": "Mihon এক্সটেনচন ব্যৱহাৰ কৰাৰ আগতে প্ৰক্সী চাৰ্ভাৰ JAR ডাউনলোড কৰক। iOS ৰ বাবে কেৱল এক্সটেনচন চাৰ্ভাৰ JAR প্ৰয়োজন।",
"checking_files": "ফাইলসমূহ পৰীক্ষা কৰি থকা হৈছে",
"files_installed": "ফাইলসমূহ ইনষ্টল কৰা হৈছে",
"files_missing": "ফাইলসমূহ নাই",
"update_files": "ফাইলসমূহ আপডেট কৰক",
"up_to_date": "শেহতীয়া",
"choose_location": "স্থান বাছনি কৰক",
"import_existing_jar": "বৰ্তমান থকা JAR আমদানি কৰক",
"detect_files_in_selected_folder": "নিৰ্বাচিত ফোল্ডাৰত ফাইলসমূহ চিনাক্ত কৰক",
"preparing_download": "ডাউনলোডৰ বাবে প্ৰস্তুতি চলাই থকা হৈছে...",
"app_install_location": "এপ ইনষ্টল কৰাৰ স্থান",
"install_location": "ইনষ্টল কৰাৰ স্থান",
"jre_executable": "JRE এগজিকিউটেবল",
"extension_server_jar": "এক্সটেনচন চাৰ্ভাৰ JAR",
"installed_version": "ইনষ্টল থকা সংস্কৰণ",
"latest_version": "শেহতীয়া সংস্কৰণ",
"apkbridge_description": "যেতিয়া আপোনাক এটা পৃথক এণ্ড্ৰইড ডিভাইচ প্ৰক্সীৰ প্ৰয়োজন হয় তেতিয়া ApkBridge ব্যৱহাৰ কৰক। ইয়াত প্ৰক্সী এড্ৰেছটো ছেট কৰক আৰু GitHub ৰ পৰা APK টো ডাউনলোড কৰক।",
"set_proxy_address": "প্ৰক্সী এড্ৰেছ ছেট কৰক",
"no_extension_server_bundle_available_for_this_platform": "এই প্লেটফৰ্মৰ বাবে কোনো এক্সটেনচন চাৰ্ভাৰ বাণ্ডিল উপলব্ধ নহয়।",
"failed_to_download_bundle": "বাণ্ডিল ডাউনলোড ব্যৰ্থ হ’ল ({statusCode})।",
"extension_server_intro_with_jre": "Download the proxy server bundle before using Mihon extensions. The bundle includes the JRE and extension server JAR.",
"extension_server_intro_ios": "Download the proxy server JAR before using Mihon extensions. iOS only needs the extension server JAR.",
"checking_files": "Checking files",
"files_installed": "Files installed",
"files_missing": "Files missing",
"update_files": "Update files",
"up_to_date": "Up to date",
"choose_location": "Choose location",
"import_existing_jar": "Import existing JAR",
"detect_files_in_selected_folder": "Detect files in selected folder",
"preparing_download": "Preparing download...",
"app_install_location": "App install location",
"install_location": "Install location",
"jre_executable": "JRE executable",
"extension_server_jar": "Extension server JAR",
"installed_version": "Installed version",
"latest_version": "Latest version",
"apkbridge_description": "Use ApkBridge when you need a separate Android device proxy. Set the proxy address here and download the APK from GitHub.",
"set_proxy_address": "Set proxy address",
"no_extension_server_bundle_available_for_this_platform": "No extension server bundle is available for this platform.",
"failed_to_download_bundle": "Failed to download bundle ({statusCode}).",
"@failed_to_download_bundle": {
"placeholders": {
"statusCode": {}
}
},
"downloaded_bundle_missing_expected_files": "ডাউনলোড কৰা বাণ্ডিলটোত প্ৰত্যাশিত ফাইলসমূহ নাই।",
"extension_server_files_ready": "এক্সটেনচন চাৰ্ভাৰ ফাইলসমূহ সাজু হৈছে।",
"ios_extension_server_import_hint": "iOS ত চাৰ্ভাৰটো এপ ছেণ্ডবক্সৰ ভিতৰত ইনষ্টল কৰা হয়। ডাউনলোড কৰা ফাইল এটা আনিবলৈ \"বৰ্তমান থকা JAR আমদানি কৰক\" ব্যৱহাৰ কৰক।",
"select_extension_server_folder": "এক্সটেনচন চাৰ্ভাৰ ফোল্ডাৰ বাছনি কৰক",
"selected_folder_does_not_exist": "নিৰ্বাচিত ফোল্ডাৰটো নাই।",
"no_extension_server_files_found_in_selected_folder": "নিৰ্বাচিত ফোল্ডাৰত কোনো এক্সটেনচন চাৰ্ভাৰ ফাইল পোৱা নগ’ল।",
"extension_server_files_linked": "এক্সটেনচন চাৰ্ভাৰ ফাইলসমূহ সংযোগ কৰা হৈছে।",
"selected_file_could_not_be_accessed": "নিৰ্বাচিত ফাইলটো লাভ কৰিব পৰা নগ’ল।",
"extension_server_jar_imported": "এক্সটেনচন চাৰ্ভাৰ JAR আমদানি কৰা হৈছে।",
"could_not_launch_apk_bridge_page": "ApkBridge পৃষ্ঠা মুকলি কৰিব পৰা নগ’ল।",
"not_configured": "কনফিগাৰ কৰা হোৱা নাই",
"webview": "ৱেবভিউ",
"no_extension_server_bundle_available_for_this_platform": "No extension server bundle is available for this platform.",
"extension_server_intro_with_jre": "Download the proxy server bundle before using Mihon extensions. The bundle includes the JRE and extension server JAR.",
"failed_to_download_bundle": "Failed to download bundle ({statusCode}).",
"selected_folder_does_not_exist": "The selected folder does not exist.",
"jre_executable": "JRE executable",
"installed_version": "Installed version",
"choose_location": "Choose location",
"extension_server_intro_ios": "Download the proxy server JAR before using Mihon extensions. iOS only needs the extension server JAR.",
"import_existing_jar": "Import existing JAR",
"android_proxy_server_mihon_description": "Download and configure the proxy server required to use Mihon extensions.",
"files_missing": "Files missing",
"select_extension_server_folder": "Select extension server folder",
"up_to_date": "Up to date",
"preparing_download": "Preparing download...",
"could_not_launch_apk_bridge_page": "Could not launch the ApkBridge page.",
"could_not_check_proxy_server_updates": "Could not check for proxy server updates.",
"apkbridge_description": "Use ApkBridge when you need a separate Android device proxy. Set the proxy address here and download the APK from GitHub.",
"install_location": "Install location",
"not_configured": "Not configured",
"detect_files_in_selected_folder": "Detect files in selected folder",
"ios_extension_server_import_hint": "On iOS the server is installed inside the app sandbox. Use \"Import existing JAR\" to bring in a downloaded file.",
"app_install_location": "App install location",
"files_installed": "Files installed",
"no_extension_server_files_found_in_selected_folder": "No extension server files were found in the selected folder.",
"extension_server_jar_imported": "Extension server JAR was imported.",
"proxy_server_ip_hint": "Server IP (e.g., 10.0.0.5 or https://example.com)",
"selected_file_could_not_be_accessed": "The selected file could not be accessed.",
"no_newer_proxy_server_release_available": "No newer proxy server release is available.",
"latest_version": "Latest version",
"extension_server_jar": "Extension server JAR",
"set_proxy_address": "Set proxy address",
"mihon_proxy_server": "Mihon proxy server",
"extension_server_files_linked": "Extension server files were linked.",
"extension_server_files_ready": "Extension server files are ready.",
"webview": "Webview",
"update_files": "Update files",
"checking_files": "Checking files",
"downloaded_bundle_missing_expected_files": "The downloaded bundle does not contain the expected files.",
"extension_server_files_ready": "Extension server files are ready.",
"ios_extension_server_import_hint": "On iOS the server is installed inside the app sandbox. Use \"Import existing JAR\" to bring in a downloaded file.",
"select_extension_server_folder": "Select extension server folder",
"selected_folder_does_not_exist": "The selected folder does not exist.",
"no_extension_server_files_found_in_selected_folder": "No extension server files were found in the selected folder.",
"extension_server_files_linked": "Extension server files were linked.",
"selected_file_could_not_be_accessed": "The selected file could not be accessed.",
"extension_server_jar_imported": "Extension server JAR was imported.",
"could_not_launch_apk_bridge_page": "Could not launch the ApkBridge page.",
"not_configured": "Not configured",
"webview": "Webview",
"android_proxy_server_mihon_description": "Download and configure the proxy server required to use Mihon extensions.",
"could_not_check_proxy_server_updates": "Could not check for proxy server updates.",
"proxy_server_ip_hint": "Server IP (e.g., 10.0.0.5 or https://example.com)",
"no_newer_proxy_server_release_available": "No newer proxy server release is available.",
"mihon_proxy_server": "Mihon proxy server",
"android_proxy_server_mihon": "Android Proxy Server (Mihon)",
"select_extension_server_jar": "Select extension server JAR"
"select_extension_server_jar": "Select extension server JAR",
"webtoon_disable_zoom_out": "Webtoon disable zoom out",
"webtoon_double_tap_zoom_enabled": "Webtoon double tap zoom",
"navigate_to_pan": "Navigate to pan",
"navigate_to_pan_subtitle": "Move zoomed image before turning page",
"split_wide_pages": "Split wide pages",
"dual_page_invert": "Invert cut half pages",
"dual_page_rotate_to_fit": "Rotate to fit",
"dual_page_rotate_to_fit_invert": "Invert rotation direction",
"landscape_zoom": "Automatic landscape zoom",
"zoom_start_position": "Zoom start position",
"zoom_start_left": "Left",
"zoom_start_right": "Right",
"zoom_start_center": "Center",
"automatic_background": "Automatic background",
"tapping_inversion": "Tapping Inversion",
"tapping_inversion_none": "None",
"tapping_inversion_horizontal": "Horizontal",
"tapping_inversion_vertical": "Vertical",
"tapping_inversion_both": "Both",
"flash_on_page_change": "Flash on page change",
"flash_on_page_change_subtitle": "AMOLED anti-retention helper",
"flash_color": "Flash Color",
"flash_color_black": "Black",
"flash_color_white": "White",
"flash_color_white_black": "White & Black",
"flash_interval": "Flash Interval: {n} pages",
"@flash_interval": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"flash_duration": "Flash Duration: {n} ms",
"@flash_duration": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"show_navigation_overlay_on_start": "Show Navigation Overlay on startup",
"reader_hide_threshold": "Reader Hide Threshold",
"reader_hide_threshold_highest": "Highest (5 px)",
"reader_hide_threshold_high": "High (13 px)",
"reader_hide_threshold_low": "Low (31 px)",
"reader_hide_threshold_lowest": "Lowest (47 px)",
"error_no_pages_available": "Error: no pages available"
}

File diff suppressed because it is too large Load diff

View file

@ -853,5 +853,53 @@
}
},
"tts_settings": "TTS Settings",
"tts_default": "Default"
"tts_default": "Default",
"webtoon_disable_zoom_out": "Webtoon disable zoom out",
"webtoon_double_tap_zoom_enabled": "Webtoon double tap zoom",
"navigate_to_pan": "Navigate to pan",
"navigate_to_pan_subtitle": "Move zoomed image before turning page",
"split_wide_pages": "Split wide pages",
"dual_page_invert": "Invert cut half pages",
"dual_page_rotate_to_fit": "Rotate to fit",
"dual_page_rotate_to_fit_invert": "Invert rotation direction",
"landscape_zoom": "Automatic landscape zoom",
"zoom_start_position": "Zoom start position",
"zoom_start_left": "Left",
"zoom_start_right": "Right",
"zoom_start_center": "Center",
"automatic_background": "Automatic background",
"tapping_inversion": "Tapping Inversion",
"tapping_inversion_none": "None",
"tapping_inversion_horizontal": "Horizontal",
"tapping_inversion_vertical": "Vertical",
"tapping_inversion_both": "Both",
"flash_on_page_change": "Flash on page change",
"flash_on_page_change_subtitle": "AMOLED anti-retention helper",
"flash_color": "Flash Color",
"flash_color_black": "Black",
"flash_color_white": "White",
"flash_color_white_black": "White & Black",
"flash_interval": "Flash Interval: {n} pages",
"@flash_interval": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"flash_duration": "Flash Duration: {n} ms",
"@flash_duration": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"show_navigation_overlay_on_start": "Show Navigation Overlay on startup",
"reader_hide_threshold": "Reader Hide Threshold",
"reader_hide_threshold_highest": "Highest (5 px)",
"reader_hide_threshold_high": "High (13 px)",
"reader_hide_threshold_low": "Low (31 px)",
"reader_hide_threshold_lowest": "Lowest (47 px)",
"error_no_pages_available": "Error: no pages available"
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -746,5 +746,53 @@
"tts_next": "Paragraphe suivant",
"tts_paragraph_progress": "Paragraphe {current} sur {total}",
"tts_settings": "Paramètres TTS",
"tts_default": "Par défaut"
"tts_default": "Par défaut",
"webtoon_disable_zoom_out": "Désactiver le zoom arrière Webtoon",
"webtoon_double_tap_zoom_enabled": "Double-tap pour zoomer Webtoon",
"navigate_to_pan": "Naviguer vers le panoramique",
"navigate_to_pan_subtitle": "Déplace d'abord l'image zoomée avant de tourner la page",
"split_wide_pages": "Découper les pages doubles",
"dual_page_invert": "Inverser les demi-pages découpées",
"dual_page_rotate_to_fit": "Faire pivoter pour adapter",
"dual_page_rotate_to_fit_invert": "Inverser le sens de rotation",
"landscape_zoom": "Zoom paysage automatique",
"zoom_start_position": "Position de départ du zoom",
"zoom_start_left": "Gauche",
"zoom_start_right": "Droite",
"zoom_start_center": "Centre",
"automatic_background": "Arrière-plan automatique",
"tapping_inversion": "Inversion du tap",
"tapping_inversion_none": "Aucune",
"tapping_inversion_horizontal": "Horizontale",
"tapping_inversion_vertical": "Verticale",
"tapping_inversion_both": "Les deux",
"flash_on_page_change": "Flash au changement de page",
"flash_on_page_change_subtitle": "Assistant anti-rétention AMOLED",
"flash_color": "Couleur du flash",
"flash_color_black": "Noir",
"flash_color_white": "Blanc",
"flash_color_white_black": "Blanc & Noir",
"flash_interval": "Intervalle de flash : {n} pages",
"@flash_interval": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"flash_duration": "Durée de flash : {n} ms",
"@flash_duration": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"show_navigation_overlay_on_start": "Afficher les zones de navigation au démarrage",
"reader_hide_threshold": "Seuil de masquage de l'interface",
"reader_hide_threshold_highest": "Le plus sensible (5 px)",
"reader_hide_threshold_high": "Sensible (13 px)",
"reader_hide_threshold_low": "Faible (31 px)",
"reader_hide_threshold_lowest": "Le moins sensible (47 px)",
"error_no_pages_available": "Erreur : aucune page disponible"
}

View file

@ -592,11 +592,11 @@
"nav_layout_right_and_left": "दाएं और बाएं",
"nav_layout_disabled": "अक्षम",
"color_enhancements": "रंग बढ़ाना",
"split_epub_chapters": "अध्यायों में विभाजित करें",
"split_epub_chapters_description": "प्रत्येक EPUB अध्याय को अलग प्रविष्टि के रूप में आयात करें",
"compression_level": "संपीड़न स्तर (Compression level)",
"split_epub_chapters": "अध्याय में विभाजित करें",
"split_epub_chapters_description": "प्रत्येक EPUB अध्याय को एक अलग प्रविष्टि के रूप में आयात करें",
"compression_level": "संपीड़न स्तर",
"compression_info": "संपीड़न जितना अधिक होगा, बैकअप फ़ाइल उतनी ही कम जगह लेगी, लेकिन यह अधिक CPU का उपयोग करेगी। डिफ़ॉल्ट: {level}",
"mass_migration_title": "सामूहिक प्रवासन (Mass migration)",
"mass_migration_title": "मास माइग्रेशन",
"mass_migration_preview_items": "आइटम पूर्वावलोकन",
"mass_migration_destination_source": "गंतव्य स्रोत",
"mass_migration_no_library_items": "सामूहिक प्रवासन के लिए कोई लाइब्रेरी आइटम उपलब्ध नहीं हैं।",
@ -789,23 +789,23 @@
"entries": "प्रविष्टियाँ",
"extension_server_intro_with_jre": "मिहोन एक्सटेंशन (Mihon extensions) का उपयोग करने से पहले प्रॉक्सी सर्वर बंडल डाउनलोड करें। बंडल में JRE और एक्सटेंशन सर्वर JAR शामिल है।",
"extension_server_intro_ios": "मिहोन एक्सटेंशन का उपयोग करने से पहले प्रॉक्सी सर्वर JAR डाउनलोड करें। iOS को केवल एक्सटेंशन सर्वर JAR की आवश्यकता है।",
"checking_files": "फ़ाइलों की जाँच की जा रही है",
"files_installed": "फ़ाइलें स्थापित हैं",
"checking_files": "Checking files",
"files_installed": "Files installed",
"files_missing": "फ़ाइलें गायब हैं",
"update_files": "फ़ाइलें अपडेट करें",
"update_files": "Update files",
"up_to_date": "अप-टू-डेट",
"choose_location": "स्थान चुनें",
"import_existing_jar": "मौजूदा JAR आयात करें",
"detect_files_in_selected_folder": "चयनित फ़ोल्डर में फ़ाइलों का पता लगाएं",
"detect_files_in_selected_folder": "Detect files in selected folder",
"preparing_download": "डाउनलोड की तैयारी की जा रही है...",
"app_install_location": "ऐप स्थापना स्थान",
"install_location": "स्थापना स्थान",
"app_install_location": "App install location",
"install_location": "Install location",
"jre_executable": "JRE निष्पादन योग्य (Executable)",
"extension_server_jar": "एक्सटेंशन सर्वर JAR",
"extension_server_jar": "Extension server JAR",
"installed_version": "स्थापित संस्करण",
"latest_version": "नवीनतम संस्करण",
"apkbridge_description": "जब आपको एक अलग एंड्रॉइड डिवाइस प्रॉक्सी की आवश्यकता हो तो ApkBridge का उपयोग करें। यहां प्रॉक्सी पता सेट करें और GitHub से APK डाउनलोड करें।",
"set_proxy_address": "प्रॉक्सी पता सेट करें",
"latest_version": "Latest version",
"apkbridge_description": "Use ApkBridge when you need a separate Android device proxy. Set the proxy address here and download the APK from GitHub.",
"set_proxy_address": "Set proxy address",
"no_extension_server_bundle_available_for_this_platform": "इस प्लेटफ़ॉर्म के लिए कोई एक्सटेंशन सर्वर बंडल उपलब्ध नहीं है।",
"failed_to_download_bundle": "बंडल डाउनलोड करने में विफल रहा ({statusCode})।",
"@failed_to_download_bundle": {
@ -813,45 +813,24 @@
"statusCode": {}
}
},
"downloaded_bundle_missing_expected_files": "डाउनलोड किए गए बंडल में अपेक्षित फ़ाइलें नहीं हैं।",
"extension_server_files_ready": "एक्सटेंशन सर्वर फ़ाइलें तैयार हैं।",
"ios_extension_server_import_hint": "iOS पर सर्वर ऐप सैंडबॉक्स के अंदर स्थापित होता है। डाउनलोड की गई फ़ाइल लाने के लिए \"मौजूदा JAR आयात करें\" का उपयोग करें।",
"downloaded_bundle_missing_expected_files": "The downloaded bundle does not contain the expected files.",
"extension_server_files_ready": "Extension server files are ready.",
"ios_extension_server_import_hint": "On iOS the server is installed inside the app sandbox. Use \"Import existing JAR\" to bring in a downloaded file.",
"select_extension_server_folder": "एक्सटेंशन सर्वर फ़ोल्डर चुनें",
"selected_folder_does_not_exist": "चयनित फ़ोल्डर मौजूद नहीं है।",
"no_extension_server_files_found_in_selected_folder": "चयनित फ़ोल्डर में कोई एक्सटेंशन सर्वर फ़ाइलें नहीं मिलीं।",
"extension_server_files_linked": "एक्सटेंशन सर्वर फ़ाइलें लिंक की गईं।",
"selected_file_could_not_be_accessed": "चयनित फ़ाइल तक नहीं पहुँचा जा सका।",
"extension_server_jar_imported": "एक्सटेंशन सर्वर JAR आयात किया गया।",
"could_not_launch_apk_bridge_page": "ApkBridge पृष्ठ लॉन्च नहीं किया जा सका।",
"not_configured": "कॉन्फ़िगर नहीं किया गया",
"webview": "वेबव्यू (Webview)",
"could_not_launch_apk_bridge_page": "Could not launch the ApkBridge page.",
"could_not_check_proxy_server_updates": "Could not check for proxy server updates.",
"apkbridge_description": "Use ApkBridge when you need a separate Android device proxy. Set the proxy address here and download the APK from GitHub.",
"install_location": "Install location",
"not_configured": "Not configured",
"detect_files_in_selected_folder": "Detect files in selected folder",
"ios_extension_server_import_hint": "On iOS the server is installed inside the app sandbox. Use \"Import existing JAR\" to bring in a downloaded file.",
"app_install_location": "App install location",
"files_installed": "Files installed",
"no_extension_server_files_found_in_selected_folder": "No extension server files were found in the selected folder.",
"extension_server_jar_imported": "Extension server JAR was imported.",
"proxy_server_ip_hint": "Server IP (e.g., 10.0.0.5 or https://example.com)",
"selected_file_could_not_be_accessed": "The selected file could not be accessed.",
"no_newer_proxy_server_release_available": "No newer proxy server release is available.",
"latest_version": "Latest version",
"extension_server_jar": "Extension server JAR",
"set_proxy_address": "Set proxy address",
"mihon_proxy_server": "Mihon proxy server",
"extension_server_files_linked": "Extension server files were linked.",
"extension_server_files_ready": "Extension server files are ready.",
"selected_file_could_not_be_accessed": "The selected file could not be accessed.",
"extension_server_jar_imported": "Extension server JAR was imported.",
"could_not_launch_apk_bridge_page": "Could not launch the ApkBridge page.",
"not_configured": "Not configured",
"webview": "Webview",
"update_files": "Update files",
"checking_files": "Checking files",
"downloaded_bundle_missing_expected_files": "The downloaded bundle does not contain the expected files.",
"could_not_check_proxy_server_updates": "प्रॉक्सी सर्वर अपडेट की जांच नहीं की जा सकी।",
"proxy_server_ip_hint": "सर्वर IP (उदा: 10.0.0.5 या https://example.com)",
"no_newer_proxy_server_release_available": "कोई नया प्रॉक्सी सर्वर रिलीज़ उपलब्ध नहीं है।",
"mihon_proxy_server": "Mihon प्रॉक्सी सर्वर",
"android_proxy_server_mihon": "Android प्रॉक्सी सर्वर (Mihon)",
"select_extension_server_jar": "एक्सटेंशन सर्वर JAR चुनें",
"mass_migration_title": "मास माइग्रेशन",
"mass_migration_description": "अपनी लाइब्रेरी की प्रविष्टियों को एक स्रोत से दूसरे स्रोत पर माइग्रेट करें।",
"mass_migration_selection_title": "माइग्रेशन चयन",
"mass_migration_source_selecting_description": "माइग्रेट करने के लिए एक या अधिक स्रोतों का चयन करें।",
@ -868,11 +847,7 @@
"mass_migration_dialog_content": "यह चुनी हुई स्रोत की नई प्रविष्टियों के साथ वर्तमान प्रविष्टियों को बदल देगा।",
"mass_migration_help_title": "मास माइग्रेशन का उपयोग कैसे करें",
"mass_migration_help_content": "1. मूल स्रोत चुनें।\n2. उन शीर्षकों को चुनें जिन्हें आप माइग्रेट करना चाहते हैं।\n3. गंतव्य स्रोत चुनें।\n4. मिलान की जांच करें और माइग्रेशन शुरू करें।",
"mihon_proxy_server": "Mihon प्रॉक्सी सर्वर",
"android_proxy_server_mihon_description": "Mihon एक्सटेंशन का उपयोग करने के लिए आवश्यक प्रॉक्सी सर्वर डाउनलोड और कॉन्फ़िगर करें।",
"could_not_check_proxy_server_updates": "प्रॉक्सी सर्वर अपडेट की जांच नहीं की जा सकी।",
"proxy_server_ip_hint": "सर्वर IP (उदा: 10.0.0.5 या https://example.com)",
"no_newer_proxy_server_release_available": "कोई नया प्रॉक्सी सर्वर रिलीज़ उपलब्ध नहीं है।",
"statistics_title": "सांख्यिकी",
"statistics_total_manga": "कुल मांगा",
"statistics_total_anime": "कुल एनिमे",
@ -887,7 +862,52 @@
"statistics_reading_time_unit_minute": "{count, plural, one{1 मिनट} other{{count} मिनट}}",
"statistics_reading_time_unit_day": "{count, plural, one{1 दिन} other{{count} दिन}}",
"statistics_no_data": "सांख्यिकी दिखाने के लिए पर्याप्त डेटा नहीं है।",
"compression_level": "संपीड़न स्तर",
"split_epub_chapters": "अध्याय में विभाजित करें",
"split_epub_chapters_description": "प्रत्येक EPUB अध्याय को एक अलग प्रविष्टि के रूप में आयात करें"
"webtoon_disable_zoom_out": "वेबटून ज़ूम आउट अक्षम करें",
"webtoon_double_tap_zoom_enabled": "वेबटून ज़ूम के लिए डबल-टैप करें",
"navigate_to_pan": "पैन करने के लिए नेविगेट करें",
"navigate_to_pan_subtitle": "पेज पलटने से पहले ज़ूम की गई छवि को स्थानांतरित करें",
"split_wide_pages": "चौड़े पन्नों को विभाजित करें",
"dual_page_invert": "विभाजित पन्नों के हिस्सों को उलटें",
"dual_page_rotate_to_fit": "फिट करने के लिए घुमाएं",
"dual_page_rotate_to_fit_invert": "घूर्णन की दिशा उलटें",
"landscape_zoom": "स्वचालित लैंडस्केप ज़ूम",
"zoom_start_position": "ज़ूम प्रारंभ स्थिति",
"zoom_start_left": "बायें",
"zoom_start_right": "दायें",
"zoom_start_center": "केंद्र",
"automatic_background": "स्वचालित पृष्ठभूमि",
"tapping_inversion": "टैपिंग प्रतिलोम",
"tapping_inversion_none": "कोई नहीं",
"tapping_inversion_horizontal": "क्षैतिज",
"tapping_inversion_vertical": "लंबवत",
"tapping_inversion_both": "दोनों",
"flash_on_page_change": "पेज बदलने पर फ़्लैश",
"flash_on_page_change_subtitle": "AMOLED एंटी-रिटेंशन सहायक",
"flash_color": "फ़्लैश का रंग",
"flash_color_black": "काला",
"flash_color_white": "सफेद",
"flash_color_white_black": "सफेद और काला",
"flash_interval": "फ़्लैश अंतराल: {n} पन्ने",
"@flash_interval": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"flash_duration": "फ़्लैश अवधि: {n} ms",
"@flash_duration": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"show_navigation_overlay_on_start": "स्टार्टअप पर नेविगेशन ओवरले दिखाएं",
"reader_hide_threshold": "रीडर छिपाने की सीमा",
"reader_hide_threshold_highest": "उच्चतम (5 px)",
"reader_hide_threshold_high": "उच्च (13 px)",
"reader_hide_threshold_low": "निम्न (31 px)",
"reader_hide_threshold_lowest": "न्यूनतम (47 px)",
"error_no_pages_available": "त्रुटि: कोई पन्ने उपलब्ध नहीं हैं"
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -804,5 +804,53 @@
"checking_files": "Checking files",
"downloaded_bundle_missing_expected_files": "The downloaded bundle does not contain the expected files.",
"android_proxy_server_mihon": "Android Proxy Server (Mihon)",
"select_extension_server_jar": "Select extension server JAR"
"select_extension_server_jar": "Select extension server JAR",
"webtoon_disable_zoom_out": "Webtoonの縮小を無効化",
"webtoon_double_tap_zoom_enabled": "Webtoonダブルタップズーム",
"navigate_to_pan": "ナビゲーションをスクロールに変換",
"navigate_to_pan_subtitle": "ページをめくる前に拡大した画像を移動する",
"split_wide_pages": "見開きページを分割",
"dual_page_invert": "分割した半ページの左右を反転",
"dual_page_rotate_to_fit": "画面に合わせて回転",
"dual_page_rotate_to_fit_invert": "回転方向を反転",
"landscape_zoom": "横画面自動ズーム",
"zoom_start_position": "ズーム開始位置",
"zoom_start_left": "左",
"zoom_start_right": "右",
"zoom_start_center": "中央",
"automatic_background": "自動背景色",
"tapping_inversion": "タップ反転",
"tapping_inversion_none": "なし",
"tapping_inversion_horizontal": "横方向",
"tapping_inversion_vertical": "縦方向",
"tapping_inversion_both": "両方",
"flash_on_page_change": "ページ切り替え時に点滅",
"flash_on_page_change_subtitle": "AMOLED焼き付き防止アシスタント",
"flash_color": "点滅の色",
"flash_color_black": "黒",
"flash_color_white": "白",
"flash_color_white_black": "白 & 黒",
"flash_interval": "点滅間隔:{n}ページ",
"@flash_interval": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"flash_duration": "点滅時間:{n}ミリ秒",
"@flash_duration": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"show_navigation_overlay_on_start": "起動時にナビゲーションオーバーレイを表示",
"reader_hide_threshold": "リーダーを隠す閾値",
"reader_hide_threshold_highest": "最も高い (5 px)",
"reader_hide_threshold_high": "高い (13 px)",
"reader_hide_threshold_low": "低い (31 px)",
"reader_hide_threshold_lowest": "最も低い (47 px)",
"error_no_pages_available": "エラー:利用可能なページがありません"
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -698,5 +698,53 @@
"checking_files": "Checking files",
"downloaded_bundle_missing_expected_files": "The downloaded bundle does not contain the expected files.",
"android_proxy_server_mihon": "Android Proxy Server (Mihon)",
"select_extension_server_jar": "Select extension server JAR"
"select_extension_server_jar": "Select extension server JAR",
"webtoon_disable_zoom_out": "ปิดการใช้งานการซูมออกสำหรับเว็บตูน",
"webtoon_double_tap_zoom_enabled": "แตะสองครั้งเพื่อซูมเว็บตูน",
"navigate_to_pan": "เลื่อนภาพที่ซูมก่อนเปลี่ยนหน้า",
"navigate_to_pan_subtitle": "เลื่อนภาพที่ซูมก่อนจะทำการเปลี่ยนหน้าถัดไป",
"split_wide_pages": "แบ่งหน้าคู่",
"dual_page_invert": "สลับหน้าคู่ที่ถูกแบ่ง",
"dual_page_rotate_to_fit": "หมุนเพื่อปรับให้พอดี",
"dual_page_rotate_to_fit_invert": "กลับทิศทางการหมุน",
"landscape_zoom": "ซูมแนวนอนอัตโนมัติ",
"zoom_start_position": "ตำแหน่งเริ่มต้นของการซูม",
"zoom_start_left": "ซ้าย",
"zoom_start_right": "ขวา",
"zoom_start_center": "ตรงกลาง",
"automatic_background": "สีพื้นหลังอัตโนมัติ",
"tapping_inversion": "การกลับด้านการแตะ",
"tapping_inversion_none": "ไม่มี",
"tapping_inversion_horizontal": "แนวนอน",
"tapping_inversion_vertical": "แนวตั้ง",
"tapping_inversion_both": "ทั้งสองแบบ",
"flash_on_page_change": "กะพริบเมื่อเปลี่ยนหน้า",
"flash_on_page_change_subtitle": "ตัวช่วยป้องกันหน้าจอ AMOLED เบิร์น",
"flash_color": "สีของการกะพริบ",
"flash_color_black": "สีดำ",
"flash_color_white": "สีขาว",
"flash_color_white_black": "สีขาวและสีดำ",
"flash_interval": "ช่วงการกะพริบ: ทุก {n} หน้า",
"@flash_interval": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"flash_duration": "ระยะเวลาการกะพริบ: {n} มิลลิวินาที",
"@flash_duration": {
"placeholders": {
"n": {
"type": "String"
}
}
},
"show_navigation_overlay_on_start": "แสดงพื้นที่แตะเพื่อเลื่อนหน้าเมื่อเริ่มอ่าน",
"reader_hide_threshold": "ความเร็วการเลื่อนเพื่อซ่อนเมนู",
"reader_hide_threshold_highest": "เร็วที่สุด (5 px)",
"reader_hide_threshold_high": "เร็ว (13 px)",
"reader_hide_threshold_low": "ช้า (31 px)",
"reader_hide_threshold_lowest": "ช้าที่สุด (47 px)",
"error_no_pages_available": "เกิดข้อผิดพลาด: ไม่มีหน้าที่พร้อมใช้งาน"
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4342,6 +4342,210 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Default'**
String get tts_default;
/// No description provided for @webtoon_disable_zoom_out.
///
/// In en, this message translates to:
/// **'Webtoon disable zoom out'**
String get webtoon_disable_zoom_out;
/// No description provided for @webtoon_double_tap_zoom_enabled.
///
/// In en, this message translates to:
/// **'Webtoon double tap zoom'**
String get webtoon_double_tap_zoom_enabled;
/// No description provided for @navigate_to_pan.
///
/// In en, this message translates to:
/// **'Navigate to pan'**
String get navigate_to_pan;
/// No description provided for @navigate_to_pan_subtitle.
///
/// In en, this message translates to:
/// **'Move zoomed image before turning page'**
String get navigate_to_pan_subtitle;
/// No description provided for @split_wide_pages.
///
/// In en, this message translates to:
/// **'Split wide pages'**
String get split_wide_pages;
/// No description provided for @dual_page_invert.
///
/// In en, this message translates to:
/// **'Invert cut half pages'**
String get dual_page_invert;
/// No description provided for @dual_page_rotate_to_fit.
///
/// In en, this message translates to:
/// **'Rotate to fit'**
String get dual_page_rotate_to_fit;
/// No description provided for @dual_page_rotate_to_fit_invert.
///
/// In en, this message translates to:
/// **'Invert rotation direction'**
String get dual_page_rotate_to_fit_invert;
/// No description provided for @landscape_zoom.
///
/// In en, this message translates to:
/// **'Automatic landscape zoom'**
String get landscape_zoom;
/// No description provided for @zoom_start_position.
///
/// In en, this message translates to:
/// **'Zoom start position'**
String get zoom_start_position;
/// No description provided for @zoom_start_left.
///
/// In en, this message translates to:
/// **'Left'**
String get zoom_start_left;
/// No description provided for @zoom_start_right.
///
/// In en, this message translates to:
/// **'Right'**
String get zoom_start_right;
/// No description provided for @zoom_start_center.
///
/// In en, this message translates to:
/// **'Center'**
String get zoom_start_center;
/// No description provided for @automatic_background.
///
/// In en, this message translates to:
/// **'Automatic background'**
String get automatic_background;
/// No description provided for @tapping_inversion.
///
/// In en, this message translates to:
/// **'Tapping Inversion'**
String get tapping_inversion;
/// No description provided for @tapping_inversion_none.
///
/// In en, this message translates to:
/// **'None'**
String get tapping_inversion_none;
/// No description provided for @tapping_inversion_horizontal.
///
/// In en, this message translates to:
/// **'Horizontal'**
String get tapping_inversion_horizontal;
/// No description provided for @tapping_inversion_vertical.
///
/// In en, this message translates to:
/// **'Vertical'**
String get tapping_inversion_vertical;
/// No description provided for @tapping_inversion_both.
///
/// In en, this message translates to:
/// **'Both'**
String get tapping_inversion_both;
/// No description provided for @flash_on_page_change.
///
/// In en, this message translates to:
/// **'Flash on page change'**
String get flash_on_page_change;
/// No description provided for @flash_on_page_change_subtitle.
///
/// In en, this message translates to:
/// **'AMOLED anti-retention helper'**
String get flash_on_page_change_subtitle;
/// No description provided for @flash_color.
///
/// In en, this message translates to:
/// **'Flash Color'**
String get flash_color;
/// No description provided for @flash_color_black.
///
/// In en, this message translates to:
/// **'Black'**
String get flash_color_black;
/// No description provided for @flash_color_white.
///
/// In en, this message translates to:
/// **'White'**
String get flash_color_white;
/// No description provided for @flash_color_white_black.
///
/// In en, this message translates to:
/// **'White & Black'**
String get flash_color_white_black;
/// No description provided for @flash_interval.
///
/// In en, this message translates to:
/// **'Flash Interval: {n} pages'**
String flash_interval(String n);
/// No description provided for @flash_duration.
///
/// In en, this message translates to:
/// **'Flash Duration: {n} ms'**
String flash_duration(String n);
/// No description provided for @show_navigation_overlay_on_start.
///
/// In en, this message translates to:
/// **'Show Navigation Overlay on startup'**
String get show_navigation_overlay_on_start;
/// No description provided for @reader_hide_threshold.
///
/// In en, this message translates to:
/// **'Reader Hide Threshold'**
String get reader_hide_threshold;
/// No description provided for @reader_hide_threshold_highest.
///
/// In en, this message translates to:
/// **'Highest (5 px)'**
String get reader_hide_threshold_highest;
/// No description provided for @reader_hide_threshold_high.
///
/// In en, this message translates to:
/// **'High (13 px)'**
String get reader_hide_threshold_high;
/// No description provided for @reader_hide_threshold_low.
///
/// In en, this message translates to:
/// **'Low (31 px)'**
String get reader_hide_threshold_low;
/// No description provided for @reader_hide_threshold_lowest.
///
/// In en, this message translates to:
/// **'Lowest (47 px)'**
String get reader_hide_threshold_lowest;
/// No description provided for @error_no_pages_available.
///
/// In en, this message translates to:
/// **'Error: no pages available'**
String get error_no_pages_available;
}
class _AppLocalizationsDelegate

View file

@ -2349,4 +2349,111 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'تعطيل التصغير في الويب تون';
@override
String get webtoon_double_tap_zoom_enabled =>
'النقر المزدوج للتكبير في الويب تون';
@override
String get navigate_to_pan => 'التنقل للسحب';
@override
String get navigate_to_pan_subtitle => 'تحريك الصورة المكبرة قبل قلب الصفحة';
@override
String get split_wide_pages => 'تقسيم الصفحات العريضة';
@override
String get dual_page_invert => 'عكس نصفي الصفحات المقسمة';
@override
String get dual_page_rotate_to_fit => 'التدوير للملاءمة';
@override
String get dual_page_rotate_to_fit_invert => 'عكس اتجاه التدوير';
@override
String get landscape_zoom => 'تكبير تلقائي للوضع الأفقي';
@override
String get zoom_start_position => 'موضع بدء التكبير';
@override
String get zoom_start_left => 'اليسار';
@override
String get zoom_start_right => 'اليمين';
@override
String get zoom_start_center => 'الوسط';
@override
String get automatic_background => 'خلفية تلقائية';
@override
String get tapping_inversion => 'عكس النقر';
@override
String get tapping_inversion_none => 'لا شيء';
@override
String get tapping_inversion_horizontal => 'أفقي';
@override
String get tapping_inversion_vertical => 'عمودي';
@override
String get tapping_inversion_both => 'كلاهما';
@override
String get flash_on_page_change => 'وميض عند تغيير الصفحة';
@override
String get flash_on_page_change_subtitle => 'مساعد منع احتراق شاشة AMOLED';
@override
String get flash_color => 'لون الوميض';
@override
String get flash_color_black => 'أسود';
@override
String get flash_color_white => 'أبيض';
@override
String get flash_color_white_black => 'أبيض وأسود';
@override
String flash_interval(String n) {
return 'معدل الوميض: كل $n صفحات';
}
@override
String flash_duration(String n) {
return 'مدة الوميض: $n مللي ثانية';
}
@override
String get show_navigation_overlay_on_start => 'إظهار واجهة التنقل عند البدء';
@override
String get reader_hide_threshold => 'حد إخفاء القارئ';
@override
String get reader_hide_threshold_highest => 'الأعلى (5 بكسل)';
@override
String get reader_hide_threshold_high => 'مرتفع (13 بكسل)';
@override
String get reader_hide_threshold_low => 'منخفض (31 بكسل)';
@override
String get reader_hide_threshold_lowest => 'الأدنى (47 بكسل)';
@override
String get error_no_pages_available => 'خطأ: لا توجد صفحات متاحة';
}

View file

@ -2348,4 +2348,112 @@ class AppLocalizationsAs extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Webtoon disable zoom out';
@override
String get webtoon_double_tap_zoom_enabled => 'Webtoon double tap zoom';
@override
String get navigate_to_pan => 'Navigate to pan';
@override
String get navigate_to_pan_subtitle =>
'Move zoomed image before turning page';
@override
String get split_wide_pages => 'Split wide pages';
@override
String get dual_page_invert => 'Invert cut half pages';
@override
String get dual_page_rotate_to_fit => 'Rotate to fit';
@override
String get dual_page_rotate_to_fit_invert => 'Invert rotation direction';
@override
String get landscape_zoom => 'Automatic landscape zoom';
@override
String get zoom_start_position => 'Zoom start position';
@override
String get zoom_start_left => 'Left';
@override
String get zoom_start_right => 'Right';
@override
String get zoom_start_center => 'Center';
@override
String get automatic_background => 'Automatic background';
@override
String get tapping_inversion => 'Tapping Inversion';
@override
String get tapping_inversion_none => 'None';
@override
String get tapping_inversion_horizontal => 'Horizontal';
@override
String get tapping_inversion_vertical => 'Vertical';
@override
String get tapping_inversion_both => 'Both';
@override
String get flash_on_page_change => 'Flash on page change';
@override
String get flash_on_page_change_subtitle => 'AMOLED anti-retention helper';
@override
String get flash_color => 'Flash Color';
@override
String get flash_color_black => 'Black';
@override
String get flash_color_white => 'White';
@override
String get flash_color_white_black => 'White & Black';
@override
String flash_interval(String n) {
return 'Flash Interval: $n pages';
}
@override
String flash_duration(String n) {
return 'Flash Duration: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Show Navigation Overlay on startup';
@override
String get reader_hide_threshold => 'Reader Hide Threshold';
@override
String get reader_hide_threshold_highest => 'Highest (5 px)';
@override
String get reader_hide_threshold_high => 'High (13 px)';
@override
String get reader_hide_threshold_low => 'Low (31 px)';
@override
String get reader_hide_threshold_lowest => 'Lowest (47 px)';
@override
String get error_no_pages_available => 'Error: no pages available';
}

View file

@ -2362,4 +2362,113 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Webtoon-Herauszoomen deaktivieren';
@override
String get webtoon_double_tap_zoom_enabled =>
'Webtoon-Doppeltippen zum Zoomen';
@override
String get navigate_to_pan => 'Navigieren zum Schwenken';
@override
String get navigate_to_pan_subtitle =>
'Gezoomtes Bild vor dem Umblättern verschieben';
@override
String get split_wide_pages => 'Doppelseiten aufteilen';
@override
String get dual_page_invert => 'Geschnittene Seitenhälften umkehren';
@override
String get dual_page_rotate_to_fit => 'Zum Einpassen drehen';
@override
String get dual_page_rotate_to_fit_invert => 'Drehrichtung umkehren';
@override
String get landscape_zoom => 'Automatischer Querformat-Zoom';
@override
String get zoom_start_position => 'Zoom-Startposition';
@override
String get zoom_start_left => 'Links';
@override
String get zoom_start_right => 'Rechts';
@override
String get zoom_start_center => 'Mitte';
@override
String get automatic_background => 'Automatischer Hintergrund';
@override
String get tapping_inversion => 'Tipp-Invertierung';
@override
String get tapping_inversion_none => 'Keine';
@override
String get tapping_inversion_horizontal => 'Horizontal';
@override
String get tapping_inversion_vertical => 'Vertikal';
@override
String get tapping_inversion_both => 'Beide';
@override
String get flash_on_page_change => 'Blinken bei Seitenwechsel';
@override
String get flash_on_page_change_subtitle => 'AMOLED-Einbrennschutz-Assistent';
@override
String get flash_color => 'Blinkfarbe';
@override
String get flash_color_black => 'Schwarz';
@override
String get flash_color_white => 'Weiß';
@override
String get flash_color_white_black => 'Weiß & Schwarz';
@override
String flash_interval(String n) {
return 'Blinkintervall: $n Seiten';
}
@override
String flash_duration(String n) {
return 'Blinkdauer: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Navigations-Overlay beim Start anzeigen';
@override
String get reader_hide_threshold => 'Schwellenwert zum Ausblenden des Lesers';
@override
String get reader_hide_threshold_highest => 'Am höchsten (5 px)';
@override
String get reader_hide_threshold_high => 'Hoch (13 px)';
@override
String get reader_hide_threshold_low => 'Niedrig (31 px)';
@override
String get reader_hide_threshold_lowest => 'Am niedrigsten (47 px)';
@override
String get error_no_pages_available => 'Fehler: Keine Seiten verfügbar';
}

View file

@ -2341,4 +2341,112 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Webtoon disable zoom out';
@override
String get webtoon_double_tap_zoom_enabled => 'Webtoon double tap zoom';
@override
String get navigate_to_pan => 'Navigate to pan';
@override
String get navigate_to_pan_subtitle =>
'Move zoomed image before turning page';
@override
String get split_wide_pages => 'Split wide pages';
@override
String get dual_page_invert => 'Invert cut half pages';
@override
String get dual_page_rotate_to_fit => 'Rotate to fit';
@override
String get dual_page_rotate_to_fit_invert => 'Invert rotation direction';
@override
String get landscape_zoom => 'Automatic landscape zoom';
@override
String get zoom_start_position => 'Zoom start position';
@override
String get zoom_start_left => 'Left';
@override
String get zoom_start_right => 'Right';
@override
String get zoom_start_center => 'Center';
@override
String get automatic_background => 'Automatic background';
@override
String get tapping_inversion => 'Tapping Inversion';
@override
String get tapping_inversion_none => 'None';
@override
String get tapping_inversion_horizontal => 'Horizontal';
@override
String get tapping_inversion_vertical => 'Vertical';
@override
String get tapping_inversion_both => 'Both';
@override
String get flash_on_page_change => 'Flash on page change';
@override
String get flash_on_page_change_subtitle => 'AMOLED anti-retention helper';
@override
String get flash_color => 'Flash Color';
@override
String get flash_color_black => 'Black';
@override
String get flash_color_white => 'White';
@override
String get flash_color_white_black => 'White & Black';
@override
String flash_interval(String n) {
return 'Flash Interval: $n pages';
}
@override
String flash_duration(String n) {
return 'Flash Duration: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Show Navigation Overlay on startup';
@override
String get reader_hide_threshold => 'Reader Hide Threshold';
@override
String get reader_hide_threshold_highest => 'Highest (5 px)';
@override
String get reader_hide_threshold_high => 'High (13 px)';
@override
String get reader_hide_threshold_low => 'Low (31 px)';
@override
String get reader_hide_threshold_lowest => 'Lowest (47 px)';
@override
String get error_no_pages_available => 'Error: no pages available';
}

View file

@ -2372,6 +2372,115 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Desactivar zoom out de Webtoon';
@override
String get webtoon_double_tap_zoom_enabled =>
'Doble toque para zoom en Webtoon';
@override
String get navigate_to_pan => 'Navegar hacia el desplazamiento';
@override
String get navigate_to_pan_subtitle =>
'Mover la imagen ampliada antes de cambiar de página';
@override
String get split_wide_pages => 'Dividir páginas dobles';
@override
String get dual_page_invert => 'Invertir mitad de páginas cortadas';
@override
String get dual_page_rotate_to_fit => 'Rotar para ajustar';
@override
String get dual_page_rotate_to_fit_invert => 'Invertir dirección de rotación';
@override
String get landscape_zoom => 'Zoom automático en horizontal';
@override
String get zoom_start_position => 'Posición inicial del zoom';
@override
String get zoom_start_left => 'Izquierda';
@override
String get zoom_start_right => 'Derecha';
@override
String get zoom_start_center => 'Centro';
@override
String get automatic_background => 'Fondo automático';
@override
String get tapping_inversion => 'Inversión de toque';
@override
String get tapping_inversion_none => 'Ninguno';
@override
String get tapping_inversion_horizontal => 'Horizontal';
@override
String get tapping_inversion_vertical => 'Vertical';
@override
String get tapping_inversion_both => 'Ambos';
@override
String get flash_on_page_change => 'Destello al cambiar de página';
@override
String get flash_on_page_change_subtitle => 'Asistente anti-retención AMOLED';
@override
String get flash_color => 'Color del destello';
@override
String get flash_color_black => 'Negro';
@override
String get flash_color_white => 'Blanco';
@override
String get flash_color_white_black => 'Blanco y Negro';
@override
String flash_interval(String n) {
return 'Intervalo de destello: $n páginas';
}
@override
String flash_duration(String n) {
return 'Duración del destello: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Mostrar superposición de navegación al inicio';
@override
String get reader_hide_threshold => 'Límite para ocultar barra del lector';
@override
String get reader_hide_threshold_highest => 'El más alto (5 px)';
@override
String get reader_hide_threshold_high => 'Alto (13 px)';
@override
String get reader_hide_threshold_low => 'Bajo (31 px)';
@override
String get reader_hide_threshold_lowest => 'El más bajo (47 px)';
@override
String get error_no_pages_available => 'Error: no hay páginas disponibles';
}
/// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`).
@ -4697,4 +4806,113 @@ class AppLocalizationsEs419 extends AppLocalizationsEs {
@override
String get webview => 'Webview';
@override
String get webtoon_disable_zoom_out => 'Desactivar zoom out de Webtoon';
@override
String get webtoon_double_tap_zoom_enabled =>
'Doble toque para zoom en Webtoon';
@override
String get navigate_to_pan => 'Navegar hacia el desplazamiento';
@override
String get navigate_to_pan_subtitle =>
'Mover la imagen ampliada antes de cambiar de página';
@override
String get split_wide_pages => 'Dividir páginas dobles';
@override
String get dual_page_invert => 'Invertir mitad de páginas cortadas';
@override
String get dual_page_rotate_to_fit => 'Rotar para ajustar';
@override
String get dual_page_rotate_to_fit_invert => 'Invertir dirección de rotación';
@override
String get landscape_zoom => 'Zoom automático en horizontal';
@override
String get zoom_start_position => 'Posición inicial del zoom';
@override
String get zoom_start_left => 'Izquierda';
@override
String get zoom_start_right => 'Derecha';
@override
String get zoom_start_center => 'Centro';
@override
String get automatic_background => 'Fondo automático';
@override
String get tapping_inversion => 'Inversión de toque';
@override
String get tapping_inversion_none => 'Ninguno';
@override
String get tapping_inversion_horizontal => 'Horizontal';
@override
String get tapping_inversion_vertical => 'Vertical';
@override
String get tapping_inversion_both => 'Ambos';
@override
String get flash_on_page_change => 'Destello al cambiar de página';
@override
String get flash_on_page_change_subtitle => 'Asistente anti-retención AMOLED';
@override
String get flash_color => 'Color del destello';
@override
String get flash_color_black => 'Negro';
@override
String get flash_color_white => 'Blanco';
@override
String get flash_color_white_black => 'Blanco y Negro';
@override
String flash_interval(String n) {
return 'Intervalo de destello: $n páginas';
}
@override
String flash_duration(String n) {
return 'Duración del destello: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Mostrar superposición de navegación al inicio';
@override
String get reader_hide_threshold => 'Límite para ocultar barra del lector';
@override
String get reader_hide_threshold_highest => 'El más alto (5 px)';
@override
String get reader_hide_threshold_high => 'Alto (13 px)';
@override
String get reader_hide_threshold_low => 'Bajo (31 px)';
@override
String get reader_hide_threshold_lowest => 'El más bajo (47 px)';
@override
String get error_no_pages_available => 'Error: no hay páginas disponibles';
}

View file

@ -2372,4 +2372,113 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get tts_default => 'Par défaut';
@override
String get webtoon_disable_zoom_out => 'Désactiver le zoom arrière Webtoon';
@override
String get webtoon_double_tap_zoom_enabled =>
'Double-tap pour zoomer Webtoon';
@override
String get navigate_to_pan => 'Naviguer vers le panoramique';
@override
String get navigate_to_pan_subtitle =>
'Déplace d\'abord l\'image zoomée avant de tourner la page';
@override
String get split_wide_pages => 'Découper les pages doubles';
@override
String get dual_page_invert => 'Inverser les demi-pages découpées';
@override
String get dual_page_rotate_to_fit => 'Faire pivoter pour adapter';
@override
String get dual_page_rotate_to_fit_invert => 'Inverser le sens de rotation';
@override
String get landscape_zoom => 'Zoom paysage automatique';
@override
String get zoom_start_position => 'Position de départ du zoom';
@override
String get zoom_start_left => 'Gauche';
@override
String get zoom_start_right => 'Droite';
@override
String get zoom_start_center => 'Centre';
@override
String get automatic_background => 'Arrière-plan automatique';
@override
String get tapping_inversion => 'Inversion du tap';
@override
String get tapping_inversion_none => 'Aucune';
@override
String get tapping_inversion_horizontal => 'Horizontale';
@override
String get tapping_inversion_vertical => 'Verticale';
@override
String get tapping_inversion_both => 'Les deux';
@override
String get flash_on_page_change => 'Flash au changement de page';
@override
String get flash_on_page_change_subtitle => 'Assistant anti-rétention AMOLED';
@override
String get flash_color => 'Couleur du flash';
@override
String get flash_color_black => 'Noir';
@override
String get flash_color_white => 'Blanc';
@override
String get flash_color_white_black => 'Blanc & Noir';
@override
String flash_interval(String n) {
return 'Intervalle de flash : $n pages';
}
@override
String flash_duration(String n) {
return 'Durée de flash : $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Afficher les zones de navigation au démarrage';
@override
String get reader_hide_threshold => 'Seuil de masquage de l\'interface';
@override
String get reader_hide_threshold_highest => 'Le plus sensible (5 px)';
@override
String get reader_hide_threshold_high => 'Sensible (13 px)';
@override
String get reader_hide_threshold_low => 'Faible (31 px)';
@override
String get reader_hide_threshold_lowest => 'Le moins sensible (47 px)';
@override
String get error_no_pages_available => 'Erreur : aucune page disponible';
}

View file

@ -2345,4 +2345,113 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'वेबटून ज़ूम आउट अक्षम करें';
@override
String get webtoon_double_tap_zoom_enabled =>
'वेबटून ज़ूम के लिए डबल-टैप करें';
@override
String get navigate_to_pan => 'पैन करने के लिए नेविगेट करें';
@override
String get navigate_to_pan_subtitle =>
'पेज पलटने से पहले ज़ूम की गई छवि को स्थानांतरित करें';
@override
String get split_wide_pages => 'चौड़े पन्नों को विभाजित करें';
@override
String get dual_page_invert => 'विभाजित पन्नों के हिस्सों को उलटें';
@override
String get dual_page_rotate_to_fit => 'फिट करने के लिए घुमाएं';
@override
String get dual_page_rotate_to_fit_invert => 'घूर्णन की दिशा उलटें';
@override
String get landscape_zoom => 'स्वचालित लैंडस्केप ज़ूम';
@override
String get zoom_start_position => 'ज़ूम प्रारंभ स्थिति';
@override
String get zoom_start_left => 'बायें';
@override
String get zoom_start_right => 'दायें';
@override
String get zoom_start_center => 'केंद्र';
@override
String get automatic_background => 'स्वचालित पृष्ठभूमि';
@override
String get tapping_inversion => 'टैपिंग प्रतिलोम';
@override
String get tapping_inversion_none => 'कोई नहीं';
@override
String get tapping_inversion_horizontal => 'क्षैतिज';
@override
String get tapping_inversion_vertical => 'लंबवत';
@override
String get tapping_inversion_both => 'दोनों';
@override
String get flash_on_page_change => 'पेज बदलने पर फ़्लैश';
@override
String get flash_on_page_change_subtitle => 'AMOLED एंटी-रिटेंशन सहायक';
@override
String get flash_color => 'फ़्लैश का रंग';
@override
String get flash_color_black => 'काला';
@override
String get flash_color_white => 'सफेद';
@override
String get flash_color_white_black => 'सफेद और काला';
@override
String flash_interval(String n) {
return 'फ़्लैश अंतराल: $n पन्ने';
}
@override
String flash_duration(String n) {
return 'फ़्लैश अवधि: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'स्टार्टअप पर नेविगेशन ओवरले दिखाएं';
@override
String get reader_hide_threshold => 'रीडर छिपाने की सीमा';
@override
String get reader_hide_threshold_highest => 'उच्चतम (5 px)';
@override
String get reader_hide_threshold_high => 'उच्च (13 px)';
@override
String get reader_hide_threshold_low => 'निम्न (31 px)';
@override
String get reader_hide_threshold_lowest => 'न्यूनतम (47 px)';
@override
String get error_no_pages_available => 'त्रुटि: कोई पन्ने उपलब्ध नहीं हैं';
}

View file

@ -2350,4 +2350,114 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Nonaktifkan perkecil Webtoon';
@override
String get webtoon_double_tap_zoom_enabled =>
'Ketuk ganda untuk zoom Webtoon';
@override
String get navigate_to_pan => 'Navigasi untuk menggeser';
@override
String get navigate_to_pan_subtitle =>
'Pindahkan gambar dizoom sebelum membalik halaman';
@override
String get split_wide_pages => 'Bagi halaman ganda';
@override
String get dual_page_invert => 'Balikkan potongan setengah halaman';
@override
String get dual_page_rotate_to_fit => 'Putar agar pas';
@override
String get dual_page_rotate_to_fit_invert => 'Balikkan arah rotasi';
@override
String get landscape_zoom => 'Zoom lanskap otomatis';
@override
String get zoom_start_position => 'Posisi awal zoom';
@override
String get zoom_start_left => 'Kiri';
@override
String get zoom_start_right => 'Kanan';
@override
String get zoom_start_center => 'Tengah';
@override
String get automatic_background => 'Latar belakang otomatis';
@override
String get tapping_inversion => 'Inversi pengetukan';
@override
String get tapping_inversion_none => 'Tidak ada';
@override
String get tapping_inversion_horizontal => 'Horizontal';
@override
String get tapping_inversion_vertical => 'Vertikal';
@override
String get tapping_inversion_both => 'Keduanya';
@override
String get flash_on_page_change => 'Flash saat ganti halaman';
@override
String get flash_on_page_change_subtitle => 'Pembantu anti-retensi AMOLED';
@override
String get flash_color => 'Warna flash';
@override
String get flash_color_black => 'Hitam';
@override
String get flash_color_white => 'Putih';
@override
String get flash_color_white_black => 'Putih & Hitam';
@override
String flash_interval(String n) {
return 'Interval flash: $n halaman';
}
@override
String flash_duration(String n) {
return 'Durasi flash: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Tampilkan overlay navigasi saat memulai';
@override
String get reader_hide_threshold => 'Ambang batas menyembunyikan pembaca';
@override
String get reader_hide_threshold_highest => 'Tertinggi (5 px)';
@override
String get reader_hide_threshold_high => 'Tinggi (13 px)';
@override
String get reader_hide_threshold_low => 'Rendah (31 px)';
@override
String get reader_hide_threshold_lowest => 'Terendah (47 px)';
@override
String get error_no_pages_available =>
'Kesalahan: tidak ada halaman tersedia';
}

View file

@ -2371,4 +2371,115 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Disattiva zoom indietro Webtoon';
@override
String get webtoon_double_tap_zoom_enabled =>
'Doppio tocco per zoomare Webtoon';
@override
String get navigate_to_pan => 'Naviga per panoramica';
@override
String get navigate_to_pan_subtitle =>
'Sposta l\'immagine zoomata prima di girare pagina';
@override
String get split_wide_pages => 'Dividi pagine doppie';
@override
String get dual_page_invert => 'Inverti le mezze pagine tagliate';
@override
String get dual_page_rotate_to_fit => 'Ruota per adattare';
@override
String get dual_page_rotate_to_fit_invert =>
'Inverti la direzione di rotazione';
@override
String get landscape_zoom => 'Zoom automatico in orizzontale';
@override
String get zoom_start_position => 'Posisi di inizio zoom';
@override
String get zoom_start_left => 'Sinistra';
@override
String get zoom_start_right => 'Destra';
@override
String get zoom_start_center => 'Centro';
@override
String get automatic_background => 'Sfondo automatico';
@override
String get tapping_inversion => 'Inversione del tocco';
@override
String get tapping_inversion_none => 'Nessuna';
@override
String get tapping_inversion_horizontal => 'Orizzontale';
@override
String get tapping_inversion_vertical => 'Verticale';
@override
String get tapping_inversion_both => 'Entrambi';
@override
String get flash_on_page_change => 'Flash al cambio pagina';
@override
String get flash_on_page_change_subtitle =>
'Assistente anti-ritenzione AMOLED';
@override
String get flash_color => 'Colore del flash';
@override
String get flash_color_black => 'Nero';
@override
String get flash_color_white => 'Bianco';
@override
String get flash_color_white_black => 'Bianco e Nero';
@override
String flash_interval(String n) {
return 'Intervallo del flash: $n pagine';
}
@override
String flash_duration(String n) {
return 'Durata del flash: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Mostra l\'overlay di navigazione all\'avvio';
@override
String get reader_hide_threshold => 'Soglia per nascondere il lettore';
@override
String get reader_hide_threshold_highest => 'Massimo (5 px)';
@override
String get reader_hide_threshold_high => 'Alto (13 px)';
@override
String get reader_hide_threshold_low => 'Basso (31 px)';
@override
String get reader_hide_threshold_lowest => 'Minimo (47 px)';
@override
String get error_no_pages_available => 'Errore: nessuna pagina disponibile';
}

View file

@ -2312,4 +2312,110 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Webtoonの縮小を無効化';
@override
String get webtoon_double_tap_zoom_enabled => 'Webtoonダブルタップズーム';
@override
String get navigate_to_pan => 'ナビゲーションをスクロールに変換';
@override
String get navigate_to_pan_subtitle => 'ページをめくる前に拡大した画像を移動する';
@override
String get split_wide_pages => '見開きページを分割';
@override
String get dual_page_invert => '分割した半ページの左右を反転';
@override
String get dual_page_rotate_to_fit => '画面に合わせて回転';
@override
String get dual_page_rotate_to_fit_invert => '回転方向を反転';
@override
String get landscape_zoom => '横画面自動ズーム';
@override
String get zoom_start_position => 'ズーム開始位置';
@override
String get zoom_start_left => '';
@override
String get zoom_start_right => '';
@override
String get zoom_start_center => '中央';
@override
String get automatic_background => '自動背景色';
@override
String get tapping_inversion => 'タップ反転';
@override
String get tapping_inversion_none => 'なし';
@override
String get tapping_inversion_horizontal => '横方向';
@override
String get tapping_inversion_vertical => '縦方向';
@override
String get tapping_inversion_both => '両方';
@override
String get flash_on_page_change => 'ページ切り替え時に点滅';
@override
String get flash_on_page_change_subtitle => 'AMOLED焼き付き防止アシスタント';
@override
String get flash_color => '点滅の色';
@override
String get flash_color_black => '';
@override
String get flash_color_white => '';
@override
String get flash_color_white_black => '白 & 黒';
@override
String flash_interval(String n) {
return '点滅間隔:$nページ';
}
@override
String flash_duration(String n) {
return '点滅時間:$nミリ秒';
}
@override
String get show_navigation_overlay_on_start => '起動時にナビゲーションオーバーレイを表示';
@override
String get reader_hide_threshold => 'リーダーを隠す閾値';
@override
String get reader_hide_threshold_highest => '最も高い (5 px)';
@override
String get reader_hide_threshold_high => '高い (13 px)';
@override
String get reader_hide_threshold_low => '低い (31 px)';
@override
String get reader_hide_threshold_lowest => '最も低い (47 px)';
@override
String get error_no_pages_available => 'エラー:利用可能なページがありません';
}

View file

@ -2367,6 +2367,115 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Desativar zoom out do Webtoon';
@override
String get webtoon_double_tap_zoom_enabled =>
'Toque duplo para zoom no Webtoon';
@override
String get navigate_to_pan => 'Navegar para arrastar';
@override
String get navigate_to_pan_subtitle =>
'Mover a imagem ampliada antes de mudar de página';
@override
String get split_wide_pages => 'Dividir páginas duplas';
@override
String get dual_page_invert => 'Inverter metades de páginas cortadas';
@override
String get dual_page_rotate_to_fit => 'Rotacionar para caber';
@override
String get dual_page_rotate_to_fit_invert => 'Inverter direção de rotação';
@override
String get landscape_zoom => 'Zoom automático em modo paisagem';
@override
String get zoom_start_position => 'Posição inicial do zoom';
@override
String get zoom_start_left => 'Esquerda';
@override
String get zoom_start_right => 'Direita';
@override
String get zoom_start_center => 'Centro';
@override
String get automatic_background => 'Fundo automático';
@override
String get tapping_inversion => 'Inversão de toque';
@override
String get tapping_inversion_none => 'Nenhum';
@override
String get tapping_inversion_horizontal => 'Horizontal';
@override
String get tapping_inversion_vertical => 'Vertical';
@override
String get tapping_inversion_both => 'Ambos';
@override
String get flash_on_page_change => 'Piscar ao mudar de página';
@override
String get flash_on_page_change_subtitle => 'Assistente anti-retenção AMOLED';
@override
String get flash_color => 'Cor do flash';
@override
String get flash_color_black => 'Preto';
@override
String get flash_color_white => 'Branco';
@override
String get flash_color_white_black => 'Branco e Preto';
@override
String flash_interval(String n) {
return 'Intervalo do flash: $n páginas';
}
@override
String flash_duration(String n) {
return 'Duração do flash: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Mostrar sobreposição de navegação ao iniciar';
@override
String get reader_hide_threshold => 'Limite de ocultação do leitor';
@override
String get reader_hide_threshold_highest => 'Mais alto (5 px)';
@override
String get reader_hide_threshold_high => 'Alto (13 px)';
@override
String get reader_hide_threshold_low => 'Baixo (31 px)';
@override
String get reader_hide_threshold_lowest => 'Mais baixo (47 px)';
@override
String get error_no_pages_available => 'Erro: nenhuma página disponível';
}
/// The translations for Portuguese, as used in Brazil (`pt_BR`).
@ -4688,4 +4797,113 @@ class AppLocalizationsPtBr extends AppLocalizationsPt {
@override
String get webview => 'Webview';
@override
String get webtoon_disable_zoom_out => 'Desativar zoom out do Webtoon';
@override
String get webtoon_double_tap_zoom_enabled =>
'Toque duplo para zoom no Webtoon';
@override
String get navigate_to_pan => 'Navegar para arrastar';
@override
String get navigate_to_pan_subtitle =>
'Mover a imagem ampliada antes de mudar de página';
@override
String get split_wide_pages => 'Dividir páginas duplas';
@override
String get dual_page_invert => 'Inverter metades de páginas cortadas';
@override
String get dual_page_rotate_to_fit => 'Rotacionar para caber';
@override
String get dual_page_rotate_to_fit_invert => 'Inverter direção de rotação';
@override
String get landscape_zoom => 'Zoom automático em modo paisagem';
@override
String get zoom_start_position => 'Posição inicial do zoom';
@override
String get zoom_start_left => 'Esquerda';
@override
String get zoom_start_right => 'Direita';
@override
String get zoom_start_center => 'Centro';
@override
String get automatic_background => 'Fundo automático';
@override
String get tapping_inversion => 'Inversão de toque';
@override
String get tapping_inversion_none => 'Nenhum';
@override
String get tapping_inversion_horizontal => 'Horizontal';
@override
String get tapping_inversion_vertical => 'Vertical';
@override
String get tapping_inversion_both => 'Ambos';
@override
String get flash_on_page_change => 'Piscar ao mudar de página';
@override
String get flash_on_page_change_subtitle => 'Assistente anti-retenção AMOLED';
@override
String get flash_color => 'Cor do flash';
@override
String get flash_color_black => 'Preto';
@override
String get flash_color_white => 'Branco';
@override
String get flash_color_white_black => 'Branco e Preto';
@override
String flash_interval(String n) {
return 'Intervalo do flash: $n páginas';
}
@override
String flash_duration(String n) {
return 'Duração do flash: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Mostrar sobreposição de navegação ao iniciar';
@override
String get reader_hide_threshold => 'Limite de ocultação do leitor';
@override
String get reader_hide_threshold_highest => 'Mais alto (5 px)';
@override
String get reader_hide_threshold_high => 'Alto (13 px)';
@override
String get reader_hide_threshold_low => 'Baixo (31 px)';
@override
String get reader_hide_threshold_lowest => 'Mais baixo (47 px)';
@override
String get error_no_pages_available => 'Erro: nenhuma página disponível';
}

View file

@ -2380,4 +2380,115 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out =>
'Отключить уменьшение масштаба Webtoon';
@override
String get webtoon_double_tap_zoom_enabled =>
'Двойное касание для масштабирования Webtoon';
@override
String get navigate_to_pan => 'Навигация перетаскиванием';
@override
String get navigate_to_pan_subtitle =>
'Сначала сдвигать увеличенное изображение перед сменой страницы';
@override
String get split_wide_pages => 'Разделять двойные страницы';
@override
String get dual_page_invert => 'Инвертировать разделенные половины страниц';
@override
String get dual_page_rotate_to_fit => 'Поворачивать для соответствия';
@override
String get dual_page_rotate_to_fit_invert => 'Обратное направление вращения';
@override
String get landscape_zoom => 'Автоматическое масштабирование в альбоме';
@override
String get zoom_start_position => 'Начальная позиция масштабирования';
@override
String get zoom_start_left => 'Слева';
@override
String get zoom_start_right => 'Справа';
@override
String get zoom_start_center => 'По центру';
@override
String get automatic_background => 'Автоматический фон';
@override
String get tapping_inversion => 'Инверсия касаний';
@override
String get tapping_inversion_none => 'Нет';
@override
String get tapping_inversion_horizontal => 'По горизонтали';
@override
String get tapping_inversion_vertical => 'По вертикали';
@override
String get tapping_inversion_both => 'Оба';
@override
String get flash_on_page_change => 'Вспышка при смене страницы';
@override
String get flash_on_page_change_subtitle =>
'Помощник против выгорания AMOLED';
@override
String get flash_color => 'Цвет вспышки';
@override
String get flash_color_black => 'Черный';
@override
String get flash_color_white => 'Белый';
@override
String get flash_color_white_black => 'Белый и Черный';
@override
String flash_interval(String n) {
return 'Интервал вспышки: $n страниц';
}
@override
String flash_duration(String n) {
return 'Длительность вспышки: $n мс';
}
@override
String get show_navigation_overlay_on_start =>
'Показывать навигационную панель при запуске';
@override
String get reader_hide_threshold => 'Порог скрытия интерфейса';
@override
String get reader_hide_threshold_highest => 'Самый высокий (5 px)';
@override
String get reader_hide_threshold_high => 'Высокий (13 px)';
@override
String get reader_hide_threshold_low => 'Низкий (31 px)';
@override
String get reader_hide_threshold_lowest => 'Самый низкий (47 px)';
@override
String get error_no_pages_available => 'Ошибка: нет доступных страниц';
}

View file

@ -2341,4 +2341,114 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'ปิดการใช้งานการซูมออกสำหรับเว็บตูน';
@override
String get webtoon_double_tap_zoom_enabled => 'แตะสองครั้งเพื่อซูมเว็บตูน';
@override
String get navigate_to_pan => 'เลื่อนภาพที่ซูมก่อนเปลี่ยนหน้า';
@override
String get navigate_to_pan_subtitle =>
'เลื่อนภาพที่ซูมก่อนจะทำการเปลี่ยนหน้าถัดไป';
@override
String get split_wide_pages => 'แบ่งหน้าคู่';
@override
String get dual_page_invert => 'สลับหน้าคู่ที่ถูกแบ่ง';
@override
String get dual_page_rotate_to_fit => 'หมุนเพื่อปรับให้พอดี';
@override
String get dual_page_rotate_to_fit_invert => 'กลับทิศทางการหมุน';
@override
String get landscape_zoom => 'ซูมแนวนอนอัตโนมัติ';
@override
String get zoom_start_position => 'ตำแหน่งเริ่มต้นของการซูม';
@override
String get zoom_start_left => 'ซ้าย';
@override
String get zoom_start_right => 'ขวา';
@override
String get zoom_start_center => 'ตรงกลาง';
@override
String get automatic_background => 'สีพื้นหลังอัตโนมัติ';
@override
String get tapping_inversion => 'การกลับด้านการแตะ';
@override
String get tapping_inversion_none => 'ไม่มี';
@override
String get tapping_inversion_horizontal => 'แนวนอน';
@override
String get tapping_inversion_vertical => 'แนวตั้ง';
@override
String get tapping_inversion_both => 'ทั้งสองแบบ';
@override
String get flash_on_page_change => 'กะพริบเมื่อเปลี่ยนหน้า';
@override
String get flash_on_page_change_subtitle =>
'ตัวช่วยป้องกันหน้าจอ AMOLED เบิร์น';
@override
String get flash_color => 'สีของการกะพริบ';
@override
String get flash_color_black => 'สีดำ';
@override
String get flash_color_white => 'สีขาว';
@override
String get flash_color_white_black => 'สีขาวและสีดำ';
@override
String flash_interval(String n) {
return 'ช่วงการกะพริบ: ทุก $n หน้า';
}
@override
String flash_duration(String n) {
return 'ระยะเวลาการกะพริบ: $n มิลลิวินาที';
}
@override
String get show_navigation_overlay_on_start =>
'แสดงพื้นที่แตะเพื่อเลื่อนหน้าเมื่อเริ่มอ่าน';
@override
String get reader_hide_threshold => 'ความเร็วการเลื่อนเพื่อซ่อนเมนู';
@override
String get reader_hide_threshold_highest => 'เร็วที่สุด (5 px)';
@override
String get reader_hide_threshold_high => 'เร็ว (13 px)';
@override
String get reader_hide_threshold_low => 'ช้า (31 px)';
@override
String get reader_hide_threshold_lowest => 'ช้าที่สุด (47 px)';
@override
String get error_no_pages_available =>
'เกิดข้อผิดพลาด: ไม่มีหน้าที่พร้อมใช้งาน';
}

View file

@ -2353,4 +2353,114 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out =>
'Webtoon uzaklaştırmayı devre dışı bırak';
@override
String get webtoon_double_tap_zoom_enabled =>
'Webtoon yakınlaştırma için çift dokun';
@override
String get navigate_to_pan => 'Kaydırmak için gezin';
@override
String get navigate_to_pan_subtitle =>
'Sayfa çevirmeden önce yakınlaştırılmış resmi hareket ettir';
@override
String get split_wide_pages => 'Geniş sayfaları böl';
@override
String get dual_page_invert => 'Bölünen yarım sayfaları ters çevir';
@override
String get dual_page_rotate_to_fit => 'Sığdırmak için döndür';
@override
String get dual_page_rotate_to_fit_invert => 'Döndürme yönünü tersine çevir';
@override
String get landscape_zoom => 'Otomatik yatay yakınlaştırma';
@override
String get zoom_start_position => 'Yakınlaştırma başlangıç konumu';
@override
String get zoom_start_left => 'Sol';
@override
String get zoom_start_right => 'Sağ';
@override
String get zoom_start_center => 'Orta';
@override
String get automatic_background => 'Otomatik arka plan';
@override
String get tapping_inversion => 'Dokunma tersine çevirme';
@override
String get tapping_inversion_none => 'Hiçbiri';
@override
String get tapping_inversion_horizontal => 'Yatay';
@override
String get tapping_inversion_vertical => 'Dikey';
@override
String get tapping_inversion_both => 'Her ikisi';
@override
String get flash_on_page_change => 'Sayfa geçişinde flaş';
@override
String get flash_on_page_change_subtitle => 'AMOLED iz önleme yardımcısı';
@override
String get flash_color => 'Flaş Rengi';
@override
String get flash_color_black => 'Siyah';
@override
String get flash_color_white => 'Beyaz';
@override
String get flash_color_white_black => 'Beyaz & Siyah';
@override
String flash_interval(String n) {
return 'Flaş Aralığı: $n sayfa';
}
@override
String flash_duration(String n) {
return 'Flaş Süresi: $n ms';
}
@override
String get show_navigation_overlay_on_start =>
'Başlangıçta gezinme panelini göster';
@override
String get reader_hide_threshold => 'Okuyucu Gizleme Eşiği';
@override
String get reader_hide_threshold_highest => 'En yüksek (5 px)';
@override
String get reader_hide_threshold_high => 'Yüksek (13 px)';
@override
String get reader_hide_threshold_low => 'Düşük (31 px)';
@override
String get reader_hide_threshold_lowest => 'En düşük (47 px)';
@override
String get error_no_pages_available => 'Hata: kullanılabilir sayfa yok';
}

View file

@ -2274,4 +2274,110 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get tts_default => 'Default';
@override
String get webtoon_disable_zoom_out => 'Webtoon禁用缩小';
@override
String get webtoon_double_tap_zoom_enabled => 'Webtoon双击缩放';
@override
String get navigate_to_pan => '导航前移动已放大图像';
@override
String get navigate_to_pan_subtitle => '翻页前先移动放大的图像';
@override
String get split_wide_pages => '拆分双页';
@override
String get dual_page_invert => '反转拆分的半页';
@override
String get dual_page_rotate_to_fit => '旋转以适应';
@override
String get dual_page_rotate_to_fit_invert => '反转旋转方向';
@override
String get landscape_zoom => '横屏自动缩放';
@override
String get zoom_start_position => '缩放起始位置';
@override
String get zoom_start_left => '';
@override
String get zoom_start_right => '';
@override
String get zoom_start_center => '';
@override
String get automatic_background => '自动背景';
@override
String get tapping_inversion => '点击反转';
@override
String get tapping_inversion_none => '';
@override
String get tapping_inversion_horizontal => '水平';
@override
String get tapping_inversion_vertical => '垂直';
@override
String get tapping_inversion_both => '两者';
@override
String get flash_on_page_change => '翻页时闪烁';
@override
String get flash_on_page_change_subtitle => 'AMOLED 防烧屏辅助';
@override
String get flash_color => '闪烁颜色';
@override
String get flash_color_black => '黑色';
@override
String get flash_color_white => '白色';
@override
String get flash_color_white_black => '白与黑';
@override
String flash_interval(String n) {
return '闪烁间隔:$n页';
}
@override
String flash_duration(String n) {
return '闪烁持续时间:$n毫秒';
}
@override
String get show_navigation_overlay_on_start => '启动时显示导航浮层';
@override
String get reader_hide_threshold => '隐藏阅读器界面的阈值';
@override
String get reader_hide_threshold_highest => '最高 (5 px)';
@override
String get reader_hide_threshold_high => '高 (13 px)';
@override
String get reader_hide_threshold_low => '低 (31 px)';
@override
String get reader_hide_threshold_lowest => '最低 (47 px)';
@override
String get error_no_pages_available => '错误:无可用页面';
}

View file

@ -23,7 +23,6 @@ import 'package:mangayomi/models/track.dart' as track;
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart';
import 'package:mangayomi/modules/more/data_and_storage/providers/storage_usage.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/modules/more/settings/general/providers/general_state_provider.dart';
@ -51,6 +50,7 @@ import 'package:window_manager/window_manager.dart';
import 'package:path/path.dart' as p;
import 'package:flutter/services.dart' show rootBundle;
import 'package:mangayomi/utils/window_geometry.dart';
import 'package:mangayomi/modules/manga/reader/subsampling_scale_image_view/subsampling_scale_image_view.dart';
late Isar isar;
DiscordRPC? discordRpc;
@ -92,8 +92,8 @@ void main(List<String> args) async {
MediaKit.ensureInitialized();
await RustLib.init();
await imgCropIsolate.start();
await getIsolateService.start();
await ffiImageDecoder.start();
if (!isMobile) {
await windowManager.ensureInitialized();
await WindowGeometry.restore();

View file

@ -362,6 +362,25 @@ class Settings {
String? ttsVoice;
bool? splitWidePages;
bool? dualPageInvert;
bool? dualPageRotateToFit;
bool? dualPageRotateToFitInvert;
bool? landscapeZoom;
int? zoomStartPosition;
bool? automaticBackground;
bool? navigateToPan;
int? tappingInversion;
bool? flashOnPageChange;
int? flashDuration;
int? flashInterval;
int? flashColor;
bool? showNavigationOverlayOnStart;
bool? webtoonDisableZoomOut;
bool? webtoonDoubleTapZoomEnabled;
int? readerHideThreshold;
Settings({
this.id = 227,
this.updatedAt = 0,
@ -528,6 +547,23 @@ class Settings {
this.ttsPitch = 1.0,
this.ttsLanguage,
this.ttsVoice,
this.splitWidePages = false,
this.dualPageInvert = false,
this.dualPageRotateToFit = false,
this.dualPageRotateToFitInvert = false,
this.landscapeZoom = false,
this.zoomStartPosition = 1,
this.automaticBackground = false,
this.navigateToPan = true,
this.tappingInversion = 0,
this.flashOnPageChange = false,
this.flashDuration = 100,
this.flashInterval = 1,
this.flashColor = 0,
this.showNavigationOverlayOnStart = false,
this.webtoonDisableZoomOut = false,
this.webtoonDoubleTapZoomEnabled = true,
this.readerHideThreshold = 1,
});
Settings.fromJson(Map<String, dynamic> json) {
@ -812,6 +848,23 @@ class Settings {
ttsPitch = json['ttsPitch']?.toDouble();
ttsLanguage = json['ttsLanguage'];
ttsVoice = json['ttsVoice'];
splitWidePages = json['splitWidePages'];
dualPageInvert = json['dualPageInvert'];
dualPageRotateToFit = json['dualPageRotateToFit'];
dualPageRotateToFitInvert = json['dualPageRotateToFitInvert'];
landscapeZoom = json['landscapeZoom'];
zoomStartPosition = json['zoomStartPosition'];
automaticBackground = json['automaticBackground'];
navigateToPan = json['navigateToPan'];
tappingInversion = json['tappingInversion'];
flashOnPageChange = json['flashOnPageChange'];
flashDuration = json['flashDuration'];
flashInterval = json['flashInterval'];
flashColor = json['flashColor'];
showNavigationOverlayOnStart = json['showNavigationOverlayOnStart'];
webtoonDisableZoomOut = json['webtoonDisableZoomOut'];
webtoonDoubleTapZoomEnabled = json['webtoonDoubleTapZoomEnabled'];
readerHideThreshold = json['readerHideThreshold'];
}
Map<String, dynamic> toJson() => {
@ -1002,6 +1055,23 @@ class Settings {
'ttsPitch': ttsPitch,
'ttsLanguage': ttsLanguage,
'ttsVoice': ttsVoice,
'splitWidePages': splitWidePages,
'dualPageInvert': dualPageInvert,
'dualPageRotateToFit': dualPageRotateToFit,
'dualPageRotateToFitInvert': dualPageRotateToFitInvert,
'landscapeZoom': landscapeZoom,
'zoomStartPosition': zoomStartPosition,
'automaticBackground': automaticBackground,
'navigateToPan': navigateToPan,
'tappingInversion': tappingInversion,
'flashOnPageChange': flashOnPageChange,
'flashDuration': flashDuration,
'flashInterval': flashInterval,
'flashColor': flashColor,
'showNavigationOverlayOnStart': showNavigationOverlayOnStart,
'webtoonDisableZoomOut': webtoonDisableZoomOut,
'webtoonDoubleTapZoomEnabled': webtoonDoubleTapZoomEnabled,
'readerHideThreshold': readerHideThreshold,
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,93 +1,358 @@
import 'package:extended_image/extended_image.dart';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/utils/reg_exp_matcher.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/color_filter_widget.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/manga/reader/subsampling_scale_image_view/subsampling_scale_image_view.dart'
as ssiv;
import 'package:mangayomi/utils/extensions/others.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart';
class ImageViewPaged extends ConsumerWidget {
class ImageViewPaged extends ConsumerStatefulWidget {
final UChapDataPreload data;
final Function(UChapDataPreload data) onLongPressData;
final Widget? Function(ExtendedImageState state) loadStateChanged;
final Function(ExtendedImageGestureState state)? onDoubleTap;
final GestureConfig Function(ExtendedImageState state)?
initGestureConfigHandler;
final Widget? Function(ssiv.SubsamplingImageState state) loadStateChanged;
final bool zoomEnabled;
final bool panEnabled;
final void Function(int width, int height)? onImageLoaded;
final PageController? pageController;
final ssiv.SubsamplingScaleImageViewController? controller;
final bool isVisible;
const ImageViewPaged({
super.key,
required this.data,
required this.onLongPressData,
required this.loadStateChanged,
this.onDoubleTap,
this.initGestureConfigHandler,
this.zoomEnabled = true,
this.panEnabled = true,
this.onImageLoaded,
this.pageController,
this.controller,
this.isVisible = true,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final scaleType = ref.watch(scaleTypeStateProvider);
final image = data.getImageProvider(ref, true);
final (colorBlendMode, color) = chapterColorFIlterValues(context, ref);
final needsScaleOverride =
scaleType == ScaleType.fitWidth || scaleType == ScaleType.fitHeight;
final effectiveFit = needsScaleOverride
? BoxFit.contain
: getBoxFit(scaleType);
ConsumerState<ImageViewPaged> createState() => _ImageViewPagedState();
}
GestureConfig Function(ExtendedImageState)? effectiveGestureHandler;
if (needsScaleOverride) {
effectiveGestureHandler = (ExtendedImageState state) {
final base = initGestureConfigHandler?.call(state);
double initScale = base?.initialScale ?? 1.0;
InitialAlignment alignment =
base?.initialAlignment ?? InitialAlignment.center;
final info = state.extendedImageInfo;
if (info != null) {
final imgW = info.image.width.toDouble();
final imgH = info.image.height.toDouble();
final viewSize = MediaQuery.of(context).size;
final viewAspect = viewSize.width / viewSize.height;
final imgAspect = imgW / imgH;
if (scaleType == ScaleType.fitWidth && imgAspect < viewAspect) {
initScale = viewAspect / imgAspect;
alignment = InitialAlignment.topCenter;
} else if (scaleType == ScaleType.fitHeight &&
imgAspect > viewAspect) {
initScale = imgAspect / viewAspect;
alignment = InitialAlignment.centerLeft;
class _ImageViewPagedState extends ConsumerState<ImageViewPaged> {
Color? _autoBgColor;
bool _isDetecting = false;
bool _hasLandscapeZoomed = false;
String? _resolvedFilePath;
@override
void initState() {
super.initState();
widget.controller?.addListener(_onControllerChanged);
_resolvedFilePath = widget.data.resolvedFilePath;
_resolveFilePath();
}
@override
void didUpdateWidget(covariant ImageViewPaged oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?.removeListener(_onControllerChanged);
widget.controller?.addListener(_onControllerChanged);
}
if (widget.data != oldWidget.data) {
_hasLandscapeZoomed = false;
_resolvedFilePath = widget.data.resolvedFilePath;
_resolveFilePath();
}
if (widget.isVisible && !oldWidget.isVisible) {
_checkLandscapeZoom();
}
}
Future<void> _resolveFilePath() async {
if (widget.data.resolvedFilePath != null) {
if (mounted) {
setState(() {
_resolvedFilePath = widget.data.resolvedFilePath;
});
}
return;
}
final path = await widget.data.getLocalFilePath;
if (path != null) {
widget.data.resolvedFilePath = path;
if (mounted) {
setState(() {
_resolvedFilePath = path;
});
}
}
}
@override
void dispose() {
widget.controller?.removeListener(_onControllerChanged);
super.dispose();
}
void _onControllerChanged() {
if (widget.isVisible) {
_checkLandscapeZoom();
}
}
void _checkLandscapeZoom() {
if (!mounted) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_performLandscapeZoom();
});
}
void _performLandscapeZoom() {
final controller = widget.controller;
if (controller == null ||
!controller.isReady ||
!widget.isVisible ||
!mounted) {
return;
}
if (_hasLandscapeZoomed) return;
final landscapeZoom = ref.read(landscapeZoomStateProvider);
if (!landscapeZoom) return;
final scaleType = ref.read(scaleTypeStateProvider);
if (scaleType != ScaleType.fitScreen) return;
final sWidth = controller.sWidth;
final sHeight = controller.sHeight;
if (sWidth <= sHeight) return;
final currentScale = controller.scale;
final minScale = controller.minScale;
if (minScale <= 0.0) return;
if ((currentScale - minScale).abs() > 0.01) return;
final viewHeight =
context.size?.height ?? MediaQuery.of(context).size.height;
final targetScale = viewHeight / sHeight;
final zoomStart = ref.read(zoomStartPositionStateProvider);
final readerMode = ref
.read(readerControllerProvider(chapter: widget.data.chapter!).notifier)
.getReaderMode();
final isRightToLeft = readerMode == ReaderMode.rtl;
Offset targetPoint;
if (zoomStart == 0) {
targetPoint = const Offset(0, 0);
} else if (zoomStart == 1) {
targetPoint = Offset(sWidth.toDouble(), 0);
} else if (zoomStart == 2) {
targetPoint = Offset(sWidth / 2, sHeight / 2);
} else {
targetPoint = isRightToLeft
? Offset(sWidth.toDouble(), 0)
: const Offset(0, 0);
}
_hasLandscapeZoomed = true;
if (mounted) {
controller.animateScaleAndCenter(
targetScale,
targetPoint,
duration: const Duration(milliseconds: 500),
);
}
}
Duration _doubleTapAnimationDuration() {
final doubleTapAnimationValue =
isar.settings.getSync(227)?.doubleTapAnimationSpeed ?? 1;
return switch (doubleTapAnimationValue) {
0 => const Duration(milliseconds: 10),
1 => const Duration(milliseconds: 800),
_ => const Duration(milliseconds: 200),
};
}
void _detectBgColor(ImageProvider provider) async {
if (_isDetecting || _autoBgColor != null) return;
_isDetecting = true;
try {
final ImageStream stream = provider.resolve(ImageConfiguration.empty);
ImageStreamListener? listener;
listener = ImageStreamListener(
(ImageInfo info, bool syncCall) async {
try {
final byteData = await info.image.toByteData(
format: ui.ImageByteFormat.rawRgba,
);
if (byteData != null && byteData.lengthInBytes >= 4) {
final int r = byteData.getUint8(0);
final int g = byteData.getUint8(1);
final int b = byteData.getUint8(2);
final double brightness =
(r * 0.299 + g * 0.587 + b * 0.114) / 255.0;
if (mounted) {
setState(() {
_autoBgColor = brightness > 0.5 ? Colors.white : Colors.black;
});
}
}
} catch (_) {}
stream.removeListener(listener!);
},
onError: (err, stack) {
stream.removeListener(listener!);
},
);
stream.addListener(listener);
} catch (_) {}
}
@override
Widget build(BuildContext context) {
final scaleType = ref.watch(scaleTypeStateProvider);
final image = widget.data.getImageProvider(ref, true);
final (colorBlendMode, color) = chapterColorFIlterValues(context, ref);
final cropBorders = ref.watch(cropBordersStateProvider);
final automaticBackground = ref.watch(automaticBackgroundStateProvider);
final dualPageRotateToFit = ref.watch(dualPageRotateToFitStateProvider);
final dualPageRotateToFitInvert = ref.watch(
dualPageRotateToFitInvertStateProvider,
);
if (automaticBackground) {
_detectBgColor(image);
}
// Determine background color
Color? pageBgColor = _autoBgColor;
// rotation calculation
int rotation = 0;
if (dualPageRotateToFit &&
widget.data.loadedWidth != null &&
widget.data.loadedHeight != null) {
if (widget.data.loadedWidth! > widget.data.loadedHeight!) {
rotation = dualPageRotateToFitInvert ? 270 : 90;
}
}
// Mappe le scaleType de Mangayomi vers le BoxFit
final effectiveFit = switch (scaleType) {
ScaleType.fitWidth => BoxFit.fitWidth,
ScaleType.fitHeight => BoxFit.fitHeight,
ScaleType.stretch => BoxFit.fill,
_ => BoxFit.contain,
};
// Détection de GIF animé pour contourner le visualiseur de tuiles statiques
bool isAnimated = false;
if (widget.data.archiveImage != null &&
widget.data.archiveImage!.length > 3) {
final bytes = widget.data.archiveImage!;
if (bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46) {
isAnimated = true;
}
}
final url = widget.data.pageUrl?.url.trim().toLowerCase() ?? '';
if (url.contains('.gif')) isAnimated = true;
if (!isAnimated &&
widget.data.directory != null &&
widget.data.index != null) {
try {
final file = File(
'${widget.data.directory!.path}/${padIndex(widget.data.index!)}.jpg',
);
if (file.existsSync()) {
final raf = file.openSync();
try {
final fBytes = raf.readSync(3);
if (fBytes.length > 2 &&
fBytes[0] == 0x47 &&
fBytes[1] == 0x49 &&
fBytes[2] == 0x46) {
isAnimated = true;
}
} finally {
raf.closeSync();
}
}
return GestureConfig(
initialScale: initScale,
initialAlignment: alignment,
inertialSpeed: base?.inertialSpeed ?? 200,
inPageView: base?.inPageView ?? true,
maxScale: base?.maxScale ?? 8,
animationMaxScale: base?.animationMaxScale ?? 8,
cacheGesture: base?.cacheGesture ?? true,
hitTestBehavior: base?.hitTestBehavior ?? HitTestBehavior.translucent,
);
};
} catch (_) {}
}
if (_resolvedFilePath == null && !isAnimated) {
final Color bg =
pageBgColor ??
getBackgroundColor(ref.watch(backgroundColorStateProvider)) ??
Colors.black;
return Container(
color: bg,
child: const Center(
child: CircularProgressIndicatorAnimateRotate(progress: 0),
),
);
}
final ssivScaleType = switch (scaleType) {
ScaleType.fitScreen => ssiv.ScaleType.centerInside,
ScaleType.stretch => ssiv.ScaleType.centerCrop,
ScaleType.fitWidth => ssiv.ScaleType.fitWidth,
ScaleType.fitHeight => ssiv.ScaleType.fitHeight,
ScaleType.originalSize => ssiv.ScaleType.originalSize,
ScaleType.smartFit => ssiv.ScaleType.smartFit,
};
Widget content;
if (isAnimated) {
content = Image(
image: image,
fit: effectiveFit,
color: color,
colorBlendMode: colorBlendMode,
);
} else {
effectiveGestureHandler = initGestureConfigHandler;
content = ssiv.SubsamplingScaleImageView(
image: image,
resolvedFilePath: _resolvedFilePath,
preloadData: widget.data,
colorBlendMode: colorBlendMode,
color: color,
minimumScaleType: ssivScaleType,
filterQuality: FilterQuality.medium,
zoomEnabled: widget.zoomEnabled,
panEnabled: widget.panEnabled,
cropBorders: cropBorders,
srcRect: widget.data.srcRect,
onImageLoaded: widget.onImageLoaded,
loadStateChanged: widget.loadStateChanged,
pageController: widget.pageController,
rotation: rotation,
controller: widget.controller,
isVisible: widget.isVisible,
doubleTapZoomDuration: _doubleTapAnimationDuration(),
onReady: _checkLandscapeZoom,
);
}
if (pageBgColor != null) {
content = Container(color: pageBgColor, child: content);
}
return applyReaderColorFilter(
GestureDetector(
onLongPress: () => onLongPressData.call(data),
child: ExtendedImage(
image: image,
colorBlendMode: colorBlendMode,
color: color,
fit: effectiveFit,
filterQuality: FilterQuality.medium,
mode: ExtendedImageMode.gesture,
handleLoadingProgress: true,
loadStateChanged: loadStateChanged,
initGestureConfigHandler: effectiveGestureHandler,
onDoubleTap: onDoubleTap,
),
onLongPress: () => widget.onLongPressData.call(widget.data),
child: content,
),
ref,
);

View file

@ -1,22 +1,21 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/color_filter_widget.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/extensions/others.dart';
import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart';
import 'package:mangayomi/modules/manga/reader/subsampling_scale_image_view/src/min_subsampling_image_view.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/manga/reader/subsampling_scale_image_view/subsampling_scale_image_view.dart';
class ImageViewVertical extends ConsumerWidget {
final UChapDataPreload data;
final Function(UChapDataPreload data) onLongPressData;
final bool isHorizontal;
final ValueNotifier<bool> isScrolling;
final ValueNotifier<bool> isVisible;
final Function(bool) failedToLoadImage;
final Widget? Function(SubsamplingImageState)? loadStateChanged;
final int rotation;
final Function(double width, double height)? onImageLoaded;
const ImageViewVertical({
super.key,
@ -24,101 +23,31 @@ class ImageViewVertical extends ConsumerWidget {
required this.onLongPressData,
required this.failedToLoadImage,
required this.isHorizontal,
required this.isScrolling,
required this.isVisible,
this.loadStateChanged,
this.rotation = 0,
this.onImageLoaded,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final (colorBlendMode, color) = chapterColorFIlterValues(context, ref);
final imageWidget = ValueListenableBuilder<bool>(
valueListenable: isScrolling,
builder: (context, scrolling, _) => ExtendedImage(
colorBlendMode: colorBlendMode,
color: color,
image: data.getImageProvider(ref, true),
filterQuality: scrolling ? FilterQuality.low : FilterQuality.medium,
handleLoadingProgress: true,
fit: getBoxFit(ref.watch(scaleTypeStateProvider)),
enableLoadState: true,
loadStateChanged: (state) {
if (state.extendedImageLoadState == LoadState.completed) {
failedToLoadImage(false);
final rawSize = state.extendedImageInfo?.image;
if (rawSize != null && data.loadedHeight == null) {
final screenWidth = isHorizontal
? context.width(0.8)
: MediaQuery.of(context).size.width;
final aspect = rawSize.width / rawSize.height;
data.loadedWidth = screenWidth;
data.loadedHeight = screenWidth / aspect;
}
}
final placeholderHeight = data.loadedHeight ?? context.height(0.8);
final placeholderWidth = isHorizontal
? (data.loadedWidth ?? context.width(0.8))
: null;
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress = state.loadingProgress;
final double progress = loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: 0;
return Container(
color: Colors.black,
height: placeholderHeight,
width: placeholderWidth,
child: CircularProgressIndicatorAnimateRotate(progress: progress),
);
}
if (state.extendedImageLoadState == LoadState.failed) {
failedToLoadImage(true);
return Container(
color: Colors.black,
height: placeholderHeight,
width: placeholderWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
context.l10n.image_loading_error,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onLongPress: () {
state.reLoadImage();
failedToLoadImage(false);
},
onTap: () {
state.reLoadImage();
failedToLoadImage(false);
},
child: Container(
decoration: BoxDecoration(
color: context.primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: Text(context.l10n.retry),
),
),
),
),
],
),
);
}
return null;
},
),
final cropBorders = ref.watch(cropBordersStateProvider);
final imageWidget = MinSubsamplingImage(
data: data,
fit: getBoxFit(ref.watch(scaleTypeStateProvider)),
color: color,
colorBlendMode: colorBlendMode,
cropBorders: cropBorders,
isHorizontal: isHorizontal,
failedToLoadImage: failedToLoadImage,
isVisible: isVisible,
loadStateChanged: loadStateChanged,
rotation: rotation,
onImageLoaded: onImageLoaded,
);
return applyReaderColorFilter(
GestureDetector(
onLongPress: () => onLongPressData.call(data),

View file

@ -1,16 +1,18 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/modules/manga/reader/widgets/double_page_view.dart';
import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:mangayomi/models/settings.dart';
/// Main widget for virtual reading that replaces ScrollablePositionedList
class ImageViewWebtoon extends StatelessWidget {
class ImageViewWebtoon extends ConsumerStatefulWidget {
final List<UChapDataPreload> pages;
final ItemScrollController itemScrollController;
final ScrollOffsetController scrollOffsetController;
@ -25,16 +27,11 @@ class ImageViewWebtoon extends StatelessWidget {
final bool isDoublePageMode;
final bool isHorizontalContinuous;
final ReaderMode readerMode;
final PhotoViewController photoViewController;
final PhotoViewScaleStateController photoViewScaleStateController;
final Alignment scalePosition;
final Function(ScaleEndDetails) onScaleEnd;
final Function(Offset) onDoubleTapDown;
final VoidCallback onDoubleTap;
final int webtoonSidePadding;
final bool showPageGaps;
final bool reverse;
final ValueNotifier<bool> isScrolling;
final bool zoomOutDisabled;
final bool doubleTapZoomEnabled;
const ImageViewWebtoon({
super.key,
@ -52,37 +49,290 @@ class ImageViewWebtoon extends StatelessWidget {
required this.isDoublePageMode,
required this.isHorizontalContinuous,
required this.readerMode,
required this.photoViewController,
required this.photoViewScaleStateController,
required this.scalePosition,
required this.onScaleEnd,
required this.onDoubleTapDown,
required this.onDoubleTap,
required this.isScrolling,
this.webtoonSidePadding = 0,
this.showPageGaps = true,
this.reverse = false,
this.zoomOutDisabled = false,
this.doubleTapZoomEnabled = true,
this.onImageLoaded,
});
final Function(int index, double width, double height)? onImageLoaded;
@override
ConsumerState<ImageViewWebtoon> createState() => _ImageViewWebtoonState();
}
class _ImageViewWebtoonState extends ConsumerState<ImageViewWebtoon>
with TickerProviderStateMixin {
double _scale = 1.0;
double _baseScale = 1.0;
Offset _offset = Offset.zero;
Offset _baseOffset = Offset.zero;
Offset _pinchStartFocalPoint = Offset.zero;
late final AnimationController _zoomAnimationController;
double _animStartScale = 1.0;
double _animTargetScale = 1.0;
double _animFocalPointX = 0.0;
double _animFocalPointY = 0.0;
double _animStartOffsetDx = 0.0;
double _animStartOffsetDy = 0.0;
Set<int> _visibleIndices = {};
Offset _doubleTapPosition = Offset.zero;
@override
void initState() {
super.initState();
_zoomAnimationController = AnimationController(
vsync: this,
duration: _doubleTapAnimationDuration(),
);
_zoomAnimationController.addListener(() {
final t = _zoomAnimationController.value;
final curveVal = Curves.easeOutCubic.transform(t);
final currentScale =
_animStartScale + (_animTargetScale - _animStartScale) * curveVal;
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final targetDx =
_animFocalPointX -
(_animFocalPointX - _animStartOffsetDx) *
(currentScale / _animStartScale);
final targetDy =
_animFocalPointY -
(_animFocalPointY - _animStartOffsetDy) *
(currentScale / _animStartScale);
final maxDx = (screenWidth * (currentScale - 1)) / 2;
final maxDy = (screenHeight * (currentScale - 1)) / 2;
final clampedDx = currentScale > 1.0
? targetDx.clamp(-maxDx, maxDx)
: 0.0;
final clampedDy = currentScale > 1.0
? targetDy.clamp(-maxDy, maxDy)
: 0.0;
setState(() {
_scale = currentScale;
_offset = Offset(clampedDx, clampedDy);
});
});
widget.itemPositionsListener.itemPositions.addListener(
_updateVisibleIndices,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _updateVisibleIndices();
});
}
Duration _doubleTapAnimationDuration() {
final doubleTapAnimationValue =
isar.settings.getSync(227)?.doubleTapAnimationSpeed ?? 1;
return switch (doubleTapAnimationValue) {
0 => const Duration(milliseconds: 10),
1 => const Duration(milliseconds: 800),
_ => const Duration(milliseconds: 200),
};
}
void _handleScaleStart(ScaleStartDetails details) {
if (_zoomAnimationController.isAnimating) {
_zoomAnimationController.stop();
}
_baseScale = _scale;
_baseOffset = _offset;
_pinchStartFocalPoint = details.localFocalPoint;
}
void _handleScaleUpdate(ScaleUpdateDetails details) {
if (_zoomAnimationController.isAnimating) return;
final newScale = (_baseScale * details.scale).clamp(
widget.zoomOutDisabled ? 1.0 : 0.5,
5.0,
);
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
final isVertical = widget.scrollDirection == Axis.vertical;
double newDx;
double newDy;
final maxDx = (screenWidth * (newScale - 1)) / 2;
final maxDy = (screenHeight * (newScale - 1)) / 2;
if (details.pointerCount == 1) {
final dragDeltaX = details.localFocalPoint.dx - _pinchStartFocalPoint.dx;
final dragDeltaY = details.localFocalPoint.dy - _pinchStartFocalPoint.dy;
final tempDx = _baseOffset.dx + dragDeltaX;
final tempDy = _baseOffset.dy + dragDeltaY;
if (isVertical) {
newDx = tempDx.clamp(-maxDx, maxDx);
if (tempDy > maxDy) {
newDy = maxDy;
final overflowY = tempDy - maxDy;
try {
widget.scrollOffsetController.animateScroll(
offset: -overflowY,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
);
} catch (_) {}
} else if (tempDy < -maxDy) {
newDy = -maxDy;
final overflowY = tempDy - (-maxDy);
try {
widget.scrollOffsetController.animateScroll(
offset: -overflowY,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
);
} catch (_) {}
} else {
newDy = tempDy;
}
} else {
newDy = tempDy.clamp(-maxDy, maxDy);
if (tempDx > maxDx) {
newDx = maxDx;
final overflowX = tempDx - maxDx;
try {
widget.scrollOffsetController.animateScroll(
offset: -overflowX,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
);
} catch (_) {}
} else if (tempDx < -maxDx) {
newDx = -maxDx;
final overflowX = tempDx - (-maxDx);
try {
widget.scrollOffsetController.animateScroll(
offset: -overflowX,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
);
} catch (_) {}
} else {
newDx = tempDx;
}
}
} else {
final focalX = details.localFocalPoint.dx - screenWidth / 2;
final focalY = details.localFocalPoint.dy - screenHeight / 2;
newDx = focalX - (focalX - _baseOffset.dx) * (newScale / _baseScale);
newDy = focalY - (focalY - _baseOffset.dy) * (newScale / _baseScale);
}
final clampedDx = newScale > 1.0 ? newDx.clamp(-maxDx, maxDx) : 0.0;
final clampedDy = newScale > 1.0 ? newDy.clamp(-maxDy, maxDy) : 0.0;
setState(() {
_scale = newScale;
_offset = Offset(clampedDx, clampedDy);
});
}
void _handleScaleEnd(ScaleEndDetails details) {
if (_scale < 1.0) {
_animateZoom(
1.0,
Offset(
MediaQuery.of(context).size.width / 2,
MediaQuery.of(context).size.height / 2,
),
);
}
}
void _animateZoom(double targetScale, Offset localFocalPoint) {
if (_zoomAnimationController.isAnimating) {
_zoomAnimationController.stop();
}
_animStartScale = _scale;
_animTargetScale = targetScale;
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
_animFocalPointX = localFocalPoint.dx - screenWidth / 2;
_animFocalPointY = localFocalPoint.dy - screenHeight / 2;
_animStartOffsetDx = _offset.dx;
_animStartOffsetDy = _offset.dy;
_zoomAnimationController.duration = _doubleTapAnimationDuration();
_zoomAnimationController.forward(from: 0.0);
}
void _toggleScale(Offset localFocalPoint) {
if (!widget.doubleTapZoomEnabled || !mounted) return;
if (_zoomAnimationController.isAnimating) return;
if (_scale == 1.0) {
_animateZoom(2.0, localFocalPoint);
} else {
_animateZoom(1.0, localFocalPoint);
}
}
void _updateVisibleIndices() {
final positions = widget.itemPositionsListener.itemPositions.value;
if (positions.isEmpty) return;
final newVisible = positions.map((p) => p.index).toSet();
if (!setEquals(_visibleIndices, newVisible)) {
setState(() {
_visibleIndices = newVisible;
});
}
}
@override
void dispose() {
_zoomAnimationController.dispose();
widget.itemPositionsListener.itemPositions.removeListener(
_updateVisibleIndices,
);
super.dispose();
}
@override
Widget build(BuildContext context) {
return PhotoViewGallery.builder(
itemCount: 1,
builder: (_, _) => PhotoViewGalleryPageOptions.customChild(
controller: photoViewController,
scaleStateController: photoViewScaleStateController,
basePosition: scalePosition,
onScaleEnd: (context, details, controllerValue) => onScaleEnd(details),
return GestureDetector(
behavior: HitTestBehavior.translucent,
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
onScaleEnd: _handleScaleEnd,
onDoubleTapDown: (details) => _doubleTapPosition = details.localPosition,
onDoubleTap: () => _toggleScale(_doubleTapPosition),
child: Transform(
transform: Matrix4.diagonal3Values(_scale, _scale, 1.0)
..setTranslationRaw(_offset.dx, _offset.dy, 0.0),
alignment: Alignment.center,
child: ScrollablePositionedList.separated(
scrollDirection: scrollDirection,
reverse: reverse,
minCacheExtent: minCacheExtent,
initialScrollIndex: initialScrollIndex,
itemCount: pages.length,
physics: physics,
itemScrollController: itemScrollController,
scrollOffsetController: scrollOffsetController,
itemPositionsListener: itemPositionsListener,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
minCacheExtent: widget.minCacheExtent,
initialScrollIndex: widget.initialScrollIndex,
itemCount: widget.pages.length,
physics: widget.physics,
itemScrollController: widget.itemScrollController,
scrollOffsetController: widget.scrollOffsetController,
itemPositionsListener: widget.itemPositionsListener,
itemBuilder: (context, index) => _buildItem(context, index),
separatorBuilder: _buildSeparator,
),
@ -91,55 +341,63 @@ class ImageViewWebtoon extends StatelessWidget {
}
Widget _buildItem(BuildContext context, int index) {
final currentPage = pages[index];
final currentPage = widget.pages[index];
final uniqueKey = ValueKey(
'${currentPage.chapter?.id ?? "trans"}-${currentPage.index ?? index}',
);
return KeyedSubtree(
key: uniqueKey,
child: (isDoublePageMode && !isHorizontalContinuous)
child: (widget.isDoublePageMode && !widget.isHorizontalContinuous)
? _buildDoublePageItem(context, index)
: _buildSinglePageItem(context, index),
);
}
Widget _buildSinglePageItem(BuildContext context, int index) {
final currentPage = pages[index];
final double sidePad = webtoonSidePadding > 0
? MediaQuery.of(context).size.width * webtoonSidePadding / 100
final currentPage = widget.pages[index];
final double sidePad = widget.webtoonSidePadding > 0
? MediaQuery.of(context).size.width * widget.webtoonSidePadding / 100
: 0;
if (currentPage.isTransitionPage) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: TransitionViewVertical(data: currentPage),
);
return TransitionViewVertical(data: currentPage);
}
final bool isVisible = _visibleIndices.contains(index);
final dualPageRotateToFit = ref.watch(dualPageRotateToFitStateProvider);
final dualPageRotateToFitInvert = ref.watch(
dualPageRotateToFitInvertStateProvider,
);
int rotation = 0;
if (dualPageRotateToFit &&
currentPage.loadedWidth != null &&
currentPage.loadedHeight != null &&
currentPage.loadedWidth! > currentPage.loadedHeight!) {
rotation = dualPageRotateToFitInvert ? 270 : 90;
}
return Padding(
padding: isHorizontalContinuous
padding: widget.isHorizontalContinuous
? EdgeInsets.zero
: EdgeInsets.symmetric(horizontal: sidePad),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: ImageViewVertical(
data: currentPage,
failedToLoadImage: onFailedToLoadImage,
onLongPressData: onLongPressData,
isHorizontal: isHorizontalContinuous,
isScrolling: isScrolling,
),
child: ImageViewVertical(
data: currentPage,
failedToLoadImage: widget.onFailedToLoadImage,
onLongPressData: widget.onLongPressData,
isHorizontal: widget.isHorizontalContinuous,
isVisible: ValueNotifier<bool>(isVisible),
rotation: rotation,
onImageLoaded: (width, height) {
widget.onImageLoaded?.call(index, width, height);
},
),
);
}
Widget _buildDoublePageItem(BuildContext context, int index) {
final pageLength = pages.length;
final pageLength = widget.pages.length;
if (index >= pageLength) {
return const SizedBox.shrink();
}
@ -148,37 +406,35 @@ class ImageViewWebtoon extends StatelessWidget {
final int index2 = index1 + 1;
final List<UChapDataPreload?> datas = index == 0
? [pages[0], null]
? [widget.pages[0], null]
: [
index1 < pageLength ? pages[index1] : null,
index2 < pageLength ? pages[index2] : null,
index1 < pageLength ? widget.pages[index1] : null,
index2 < pageLength ? widget.pages[index2] : null,
];
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: DoublePageView.vertical(
pages: datas,
backgroundColor: backgroundColor,
onFailedToLoadImage: onFailedToLoadImage,
onLongPressData: onLongPressData,
),
return DoublePageView.vertical(
pages: datas,
backgroundColor: widget.backgroundColor,
onFailedToLoadImage: widget.onFailedToLoadImage,
onLongPressData: widget.onLongPressData,
);
}
Widget _buildSeparator(BuildContext context, int index) {
if (!showPageGaps || readerMode == ReaderMode.webtoon) {
if (!widget.showPageGaps || widget.readerMode == ReaderMode.webtoon) {
return const SizedBox.shrink();
}
if (isHorizontalContinuous) {
if (widget.isHorizontalContinuous) {
return VerticalDivider(
color: getBackgroundColor(backgroundColor),
color: getBackgroundColor(widget.backgroundColor),
width: 6,
);
} else {
return Divider(color: getBackgroundColor(backgroundColor), height: 6);
return Divider(
color: getBackgroundColor(widget.backgroundColor),
height: 6,
);
}
}
}

View file

@ -297,14 +297,6 @@ class ChapterPreloadManager {
return true;
}
/// Updates the cropImage for a page at the given index.
void updatePageCropImage(int index, Uint8List? cropImage) {
if (index >= 0 && index < _pages.length) {
_pages[index].cropImage = cropImage;
onPagesUpdated?.call();
}
}
/// Gets a unique identifier for a chapter.
String? _getChapterIdentifier(Chapter? chapter) {
if (chapter == null) return null;
@ -355,7 +347,9 @@ class ChapterPreloadManager {
if (page.isTransitionPage || page.chapter == null) continue;
if (_getChapterIdentifier(page.chapter) == id) {
page.archiveImage = null;
page.cropImage = null;
page.decodedImage?.dispose();
page.decodedImage = null;
page.resolvedFilePath = null;
evictedIndices.add(i);
}
}
@ -376,9 +370,23 @@ class ChapterPreloadManager {
}
}
/// Splits a single page into two pages dynamically.
void splitPage(int index, UChapDataPreload page1, UChapDataPreload page2) {
if (index < 0 || index >= _pages.length) return;
_pages.removeAt(index);
_pages.insert(index, page1);
_pages.insert(index + 1, page2);
onPagesUpdated?.call();
}
/// Disposes of all resources.
Future<void> dispose() async {
// Clear pages
for (final page in _pages) {
page.decodedImage?.dispose();
page.decodedImage = null;
page.resolvedFilePath = null;
}
_pages.clear();
_loadedChapterIds.clear();
_chapterLoadOrder.clear();

View file

@ -95,11 +95,6 @@ mixin ReaderMemoryManagement {
return _preloadManager.addLastChapterTransition(chapter);
}
/// Updates the cropImage for a page at the given index.
void updatePageCropImage(int index, Uint8List? cropImage) {
_preloadManager.updatePageCropImage(index, cropImage);
}
/// Disposes the preload manager and clears all cached data.
Future<void> disposePreloadManager() async {
if (!_isPreloadManagerInitialized) return;

View file

@ -1,140 +0,0 @@
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/src/rust/api/image.dart';
import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:mangayomi/utils/extensions/others.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'crop_borders_provider.g.dart';
@Riverpod(keepAlive: false)
Future<Uint8List?> cropBorders(
Ref ref, {
required UChapDataPreload data,
required bool cropBorder,
}) async {
Uint8List? imageBytes;
if (cropBorder) {
imageBytes = await data.getImageBytes;
if (imageBytes == null) {
return null;
}
return imgCropIsolate.process(imageBytes);
}
return null;
}
class ImageCropIsolate {
bool _isRunning = false;
Isolate? _rustIsolate;
ReceivePort? _receivePort;
SendPort? _sendPort;
Future<void> start() async {
if (!_isRunning) {
try {
await _initRustIsolate();
} catch (_) {
await stop();
}
}
}
Future<void> _initRustIsolate() async {
_receivePort = ReceivePort();
_rustIsolate = await Isolate.spawn(
_rustIsolateEntryPoint,
_receivePort!.sendPort,
);
final completer = Completer<SendPort>();
_receivePort!.listen((message) {
if (message is SendPort) {
completer.complete(message);
}
});
_sendPort = await completer.future;
_isRunning = true;
}
static Future<void> _rustIsolateEntryPoint(SendPort mainSendPort) async {
await RustLib.init();
final receivePort = ReceivePort();
mainSendPort.send(receivePort.sendPort);
receivePort.listen((message) async {
if (message is Map<String, dynamic>) {
try {
final imageBytes = message['imageBytes'] as Uint8List;
final responsePort = message['responsePort'] as SendPort;
final croppedImage = processCropImage(image: imageBytes);
responsePort.send({'success': true, 'data': croppedImage});
} catch (e) {
final responsePort = message['responsePort'] as SendPort;
responsePort.send({'success': false, 'error': e.toString()});
}
} else if (message == 'dispose') {
RustLib.dispose();
receivePort.close();
}
});
}
Future<Uint8List?> process(Uint8List imageBytes) async {
await start();
if (_sendPort == null) {
if (kDebugMode) {
print('Image crop isolate is not running');
}
return null;
}
final responsePort = ReceivePort();
final completer = Completer<Uint8List?>();
responsePort.listen((response) {
responsePort.close();
if (response is Map<String, dynamic>) {
if (response['success'] == true) {
completer.complete(response['data'] as Uint8List);
} else {
if (kDebugMode) {
print('Image cropping failed: ${response['error']}');
}
completer.complete(Future.value(null));
}
}
});
_sendPort!.send({
'imageBytes': imageBytes,
'responsePort': responsePort.sendPort,
});
return completer.future;
}
Future<void> stop() async {
if (!_isRunning) {
return;
}
_sendPort?.send('dispose');
_rustIsolate?.kill(priority: Isolate.immediate);
_receivePort?.close();
_sendPort = null;
_rustIsolate = null;
_receivePort = null;
_isRunning = false;
}
}
final imgCropIsolate = ImageCropIsolate();

View file

@ -1,98 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'crop_borders_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(cropBorders)
final cropBordersProvider = CropBordersFamily._();
final class CropBordersProvider
extends
$FunctionalProvider<
AsyncValue<Uint8List?>,
Uint8List?,
FutureOr<Uint8List?>
>
with $FutureModifier<Uint8List?>, $FutureProvider<Uint8List?> {
CropBordersProvider._({
required CropBordersFamily super.from,
required ({UChapDataPreload data, bool cropBorder}) super.argument,
}) : super(
retry: null,
name: r'cropBordersProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$cropBordersHash();
@override
String toString() {
return r'cropBordersProvider'
''
'$argument';
}
@$internal
@override
$FutureProviderElement<Uint8List?> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Uint8List?> create(Ref ref) {
final argument =
this.argument as ({UChapDataPreload data, bool cropBorder});
return cropBorders(
ref,
data: argument.data,
cropBorder: argument.cropBorder,
);
}
@override
bool operator ==(Object other) {
return other is CropBordersProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$cropBordersHash() => r'ec9c6fc8a8c88a2bb48bb54a497478bc61376291';
final class CropBordersFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<Uint8List?>,
({UChapDataPreload data, bool cropBorder})
> {
CropBordersFamily._()
: super(
retry: null,
name: r'cropBordersProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
CropBordersProvider call({
required UChapDataPreload data,
required bool cropBorder,
}) => CropBordersProvider._(
argument: (data: data, cropBorder: cropBorder),
from: this,
);
@override
String toString() => r'cropBordersProvider';
}

View file

@ -147,7 +147,7 @@ final class ReaderControllerProvider
}
}
String _$readerControllerHash() => r'43c6e1583c52c8fae01d606a51308b0e96a74a6b';
String _$readerControllerHash() => r'302fbe7c165af71a86a46004fdc7d50a174f5735';
final class ReaderControllerFamily extends $Family
with

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@ -10,7 +9,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
/// - Continuous modes (vertical continuous, webtoon, horizontal continuous)
class PageNavigationService {
final ItemScrollController itemScrollController;
final ExtendedPageController extendedController;
final PageController extendedController;
const PageNavigationService({
required this.itemScrollController,
@ -113,7 +112,7 @@ mixin PageNavigationMixin<T extends StatefulWidget> on State<T> {
/// Initializes the navigation service with the required controllers.
void initPageNavigation({
required ItemScrollController itemScrollController,
required ExtendedPageController extendedController,
required PageController extendedController,
}) {
_navigationService = PageNavigationService(
itemScrollController: itemScrollController,

View file

@ -0,0 +1,88 @@
import 'dart:ui' as ui;
/// Manages calculations and coordinate transformations between the following spaces:
/// - View (v): real pixels on the screen
/// - Source (s): logical coordinates in the image (oriented/rotated)
/// - File (f): raw physical coordinates in the original image file
class CoordinateTransformer {
final double scale;
final ui.Offset vTranslate;
final int rotation; // 0, 90, 180, 270
final int sWidth; // Physical width of the original file
final int sHeight; // Physical height of the original file
CoordinateTransformer({
required this.scale,
required this.vTranslate,
required this.rotation,
required this.sWidth,
required this.sHeight,
});
/// Converts a view (screen) coordinate to logical source (rotated image) coordinates
ui.Offset viewToSource(ui.Offset vCoord) {
final double sx = (vCoord.dx - vTranslate.dx) / scale;
final double sy = (vCoord.dy - vTranslate.dy) / scale;
return ui.Offset(sx, sy);
}
/// Public alias of viewToSource for the controller API
ui.Offset vCoordToSCoord(ui.Offset vCoord) => viewToSource(vCoord);
/// Converts a logical source (rotated image) coordinate to view (screen) coordinates
ui.Offset sourceToView(ui.Offset sCoord) {
final double vx = (sCoord.dx * scale) + vTranslate.dx;
final double vy = (sCoord.dy * scale) + vTranslate.dy;
return ui.Offset(vx, vy);
}
/// Public alias of sourceToView for the controller API
ui.Offset sCoordToVCoord(ui.Offset sCoord) => sourceToView(sCoord);
/// Converts a logical source rectangle to a display rectangle on the screen
ui.Rect sourceToViewRect(ui.Rect sRect) {
final ui.Offset topLeft = sourceToView(sRect.topLeft);
final ui.Offset bottomRight = sourceToView(sRect.bottomRight);
return ui.Rect.fromPoints(topLeft, bottomRight);
}
/// Converts a source coordinate rectangle (oriented) to raw physical coordinates in the original file
ui.Rect fileSRect(ui.Rect sRect) {
switch (rotation) {
case 90:
return ui.Rect.fromLTRB(
sRect.top,
sHeight - sRect.right,
sRect.bottom,
sHeight - sRect.left,
);
case 180:
return ui.Rect.fromLTRB(
sWidth - sRect.right,
sHeight - sRect.bottom,
sWidth - sRect.left,
sHeight - sRect.top,
);
case 270:
return ui.Rect.fromLTRB(
sWidth - sRect.bottom,
sRect.left,
sWidth - sRect.top,
sRect.right,
);
case 0:
default:
return sRect;
}
}
/// Effective width of the source, taking rotation into account
int get effectiveSWidth {
return (rotation == 90 || rotation == 270) ? sHeight : sWidth;
}
/// Effective height of the source, taking rotation into account
int get effectiveSHeight {
return (rotation == 90 || rotation == 270) ? sWidth : sHeight;
}
}

View file

@ -0,0 +1,521 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
/// Required parameters to decode an image region
class DecodeParams {
final String filePath;
final int left;
final int top;
final int right;
final int bottom;
final int sampleSize;
final bool cropBorders;
DecodeParams({
required this.filePath,
required this.left,
required this.top,
required this.right,
required this.bottom,
required this.sampleSize,
this.cropBorders = false,
});
Map<String, dynamic> toJson() => {
"filePath": filePath,
"left": left,
"top": top,
"right": right,
"bottom": bottom,
"sampleSize": sampleSize,
"cropBorders": cropBorders,
};
factory DecodeParams.fromJson(Map<String, dynamic> json) {
return DecodeParams(
bottom: json['bottom'],
filePath: json['filePath'],
left: json['left'],
top: json['top'],
right: json['right'],
sampleSize: json['sampleSize'],
cropBorders: json['cropBorders'] ?? false,
);
}
}
/// Result returned after decoding a region
class DecodeResult {
final int? pointerAddress;
final int width;
final int height;
final String? error;
DecodeResult({
this.pointerAddress,
this.width = 0,
this.height = 0,
this.error,
});
}
/// FFI interop interface with the native C++ library.
/// Manages loading the dynamic library and asynchronous execution
/// of decoding tasks in background Isolates.
class FfiImageDecoder {
static DynamicLibrary? _dylib;
bool _isRunning = false;
Isolate? _cIsolate;
ReceivePort? _receivePort;
SendPort? _sendPort;
final List<_DecodeJob> _queue = [];
bool _isProcessing = false;
void cancel(Object token) {
_queue.removeWhere((job) {
if (job.cancelToken == token) {
if (!job.completer.isCompleted) {
if (job.action == 'getDimensions') {
job.completer.complete(null);
} else {
job.completer.complete(DecodeResult(error: 'Cancelled'));
}
}
return true;
}
return false;
});
}
Future<void> start() async {
if (!_isRunning) {
try {
await _initCIsolate();
} catch (e, stackTrace) {
if (kDebugMode) {
print('Error starting FFI isolate: $e');
print(stackTrace);
}
await stop();
}
}
}
Future<void> _initCIsolate() async {
_receivePort = ReceivePort();
_cIsolate = await Isolate.spawn(
_cIsolateEntryPoint,
_receivePort!.sendPort,
);
final completer = Completer<SendPort>();
_receivePort!.listen((message) {
if (message is SendPort) {
completer.complete(message);
}
});
_sendPort = await completer.future;
_isRunning = true;
}
static DynamicLibrary get dylib {
if (_dylib == null) {
if (Platform.isAndroid) {
_dylib = DynamicLibrary.open("libsubsampling_scale_image_view.so");
} else if (Platform.isIOS || Platform.isMacOS) {
try {
_dylib = DynamicLibrary.open(
"subsampling_scale_image_view.framework/subsampling_scale_image_view",
);
} catch (_) {
_dylib = DynamicLibrary.process();
}
} else {
_dylib = DynamicLibrary.process();
}
}
return _dylib!;
}
static final Pointer<Opaque> Function(
Pointer<Utf8>,
bool,
Pointer<Int32>,
Pointer<Int32>,
)
_initDecoder = dylib
.lookupFunction<
Pointer<Opaque> Function(
Pointer<Utf8>,
Bool,
Pointer<Int32>,
Pointer<Int32>,
),
Pointer<Opaque> Function(
Pointer<Utf8>,
bool,
Pointer<Int32>,
Pointer<Int32>,
)
>('init_decoder');
static final bool Function(
Pointer<Opaque>,
int,
int,
int,
int,
int,
Pointer<Uint8>,
)
_decodeRegion = dylib
.lookupFunction<
Bool Function(
Pointer<Opaque>,
Int32,
Int32,
Int32,
Int32,
Int32,
Pointer<Uint8>,
),
bool Function(Pointer<Opaque>, int, int, int, int, int, Pointer<Uint8>)
>('decode_region');
static final void Function(Pointer<Opaque>) _freeDecoder = dylib
.lookupFunction<
Void Function(Pointer<Opaque>),
void Function(Pointer<Opaque>)
>('free_decoder');
/// Decodes a specific region of the image asynchronously in a background Isolate.
/// This prevents blocking the main UI thread during disk reads and decoding.
static Future<void> _cIsolateEntryPoint(SendPort mainSendPort) async {
final receivePort = ReceivePort();
mainSendPort.send(receivePort.sendPort);
// LRU Cache for active page decoders
final Map<String, Pointer<Opaque>> cachedContexts = {};
final List<String> lruList = [];
final Map<String, List<int>> cachedDimensions = {};
const int maxCacheSize = 10;
receivePort.listen((message) async {
if (message is Map<String, dynamic>) {
final responsePort = message['responsePort'] as SendPort;
final action = message['action'] as String?;
final params = DecodeParams.fromJson(message['params']);
final key = "${params.filePath}_${params.cropBorders}";
Pointer<Opaque> ctx = cachedContexts[key] ?? Pointer.fromAddress(0);
int imgWidth = 0;
int imgHeight = 0;
if (ctx.address == 0) {
// LRU Eviction
if (cachedContexts.length >= maxCacheSize) {
final oldestKey = lruList.removeAt(0);
final oldestCtx = cachedContexts.remove(oldestKey);
cachedDimensions.remove(oldestKey);
if (oldestCtx != null && oldestCtx.address != 0) {
_freeDecoder(oldestCtx);
}
}
final Pointer<Utf8> pathPtr = params.filePath.toNativeUtf8();
final Pointer<Int32> wPtr = calloc<Int32>();
final Pointer<Int32> hPtr = calloc<Int32>();
try {
final Pointer<Opaque> newCtx = _initDecoder(
pathPtr,
params.cropBorders,
wPtr,
hPtr,
);
if (newCtx.address == 0) {
responsePort.send({
'success': false,
'error':
"Failed to initialize decoder for file: ${params.filePath}",
});
return;
}
ctx = newCtx;
cachedContexts[key] = ctx;
lruList.add(key);
imgWidth = wPtr.value;
imgHeight = hPtr.value;
cachedDimensions[key] = [imgWidth, imgHeight];
} catch (e) {
responsePort.send({
'success': false,
'error': "Error initializing FFI decoder: $e",
});
return;
} finally {
calloc.free(pathPtr);
calloc.free(wPtr);
calloc.free(hPtr);
}
} else {
// Update LRU order
lruList.remove(key);
lruList.add(key);
final dims = cachedDimensions[key]!;
imgWidth = dims[0];
imgHeight = dims[1];
}
if (action == 'getDimensions') {
responsePort.send({
'success': true,
'width': imgWidth,
'height': imgHeight,
});
return;
}
// Calculates required size for destination pixel buffer (RGBA)
final int rectWidth = params.right - params.left;
final int rectHeight = params.bottom - params.top;
final int destWidth = rectWidth ~/ params.sampleSize;
final int destHeight = rectHeight ~/ params.sampleSize;
final int bufferSize = destWidth * destHeight * 4;
final Pointer<Uint8> rgbaBuffer = calloc<Uint8>(bufferSize);
try {
final bool success = _decodeRegion(
ctx,
params.left,
params.top,
params.right,
params.bottom,
params.sampleSize,
rgbaBuffer,
);
if (!success) {
calloc.free(rgbaBuffer);
responsePort.send({'error': "Region decoding failed."});
return;
}
responsePort.send({
'success': true,
'pointerAddress': rgbaBuffer.address,
'width': imgWidth,
'height': imgHeight,
});
} catch (e) {
calloc.free(rgbaBuffer);
responsePort.send({'success': false, 'error': "FFI error: $e"});
}
} else if (message == 'dispose') {
for (final ctx in cachedContexts.values) {
if (ctx.address != 0) {
_freeDecoder(ctx);
}
}
cachedContexts.clear();
lruList.clear();
cachedDimensions.clear();
receivePort.close();
}
});
}
Future<DecodeResult?> decodeRegionAsync(
DecodeParams params, {
Object? cancelToken,
}) async {
await start();
if (_sendPort == null) {
if (kDebugMode) {
print('FfiImageDecoder isolate is not running');
}
return null;
}
final completer = Completer<DecodeResult?>();
final job = _DecodeJob(
params: params,
completer: completer,
cancelToken: cancelToken,
);
_queue.add(job);
_processNext();
return completer.future;
}
Future<List<int>?> getImageDimensionsAsync(
String filePath, {
bool cropBorders = false,
Object? cancelToken,
}) async {
await start();
if (_sendPort == null) {
if (kDebugMode) {
print('FfiImageDecoder isolate is not running');
}
return null;
}
final completer = Completer<List<int>?>();
final job = _DecodeJob(
params: DecodeParams(
filePath: filePath,
left: 0,
top: 0,
right: 0,
bottom: 0,
sampleSize: 1,
cropBorders: cropBorders,
),
completer: completer,
cancelToken: cancelToken,
action: 'getDimensions',
);
_queue.add(job);
_processNext();
return completer.future;
}
void _processNext() {
if (_isProcessing || _queue.isEmpty) return;
final job = _queue.removeLast(); // LIFO processing
_isProcessing = true;
final responsePort = ReceivePort();
responsePort.listen((response) {
responsePort.close();
_isProcessing = false;
if (!job.completer.isCompleted) {
if (response is Map<String, dynamic>) {
if (response['success'] == true) {
if (job.action == 'getDimensions') {
job.completer.complete([
response['width'] as int,
response['height'] as int,
]);
} else {
final pointerAddress = response['pointerAddress'] as int?;
job.completer.complete(
DecodeResult(
pointerAddress: pointerAddress,
width: response['width'] as int,
height: response['height'] as int,
),
);
}
} else {
if (kDebugMode) {
print('Image job failed: ${response['error']}');
}
if (job.action == 'getDimensions') {
job.completer.complete(null);
} else {
job.completer.complete(DecodeResult(error: response['error']));
}
}
} else {
job.completer.complete(null);
}
}
_processNext();
});
_sendPort!.send({
'action': job.action,
'params': job.params.toJson(),
'responsePort': responsePort.sendPort,
});
}
bool getImageDimensions(
String filePath,
List<int> outSize, {
bool cropBorders = false,
}) {
final Pointer<Utf8> pathPtr = filePath.toNativeUtf8();
final Pointer<Int32> wPtr = calloc<Int32>();
final Pointer<Int32> hPtr = calloc<Int32>();
try {
final Pointer<Opaque> ctx = _initDecoder(
pathPtr,
cropBorders,
wPtr,
hPtr,
);
if (ctx.address == 0) return false;
outSize[0] = wPtr.value;
outSize[1] = hPtr.value;
_freeDecoder(ctx);
return true;
} catch (_) {
return false;
} finally {
calloc.free(pathPtr);
calloc.free(wPtr);
calloc.free(hPtr);
}
}
Future<void> stop() async {
if (!_isRunning) {
return;
}
_sendPort?.send('dispose');
_cIsolate?.kill(priority: Isolate.immediate);
_receivePort?.close();
_sendPort = null;
_cIsolate = null;
_receivePort = null;
_isRunning = false;
for (final job in _queue) {
if (!job.completer.isCompleted) {
if (job.action == 'getDimensions') {
job.completer.complete(null);
} else {
job.completer.complete(DecodeResult(error: 'Cancelled'));
}
}
}
_queue.clear();
_isProcessing = false;
}
}
class _DecodeJob {
final DecodeParams params;
final Completer<dynamic> completer;
final Object? cancelToken;
final String? action;
_DecodeJob({
required this.params,
required this.completer,
this.cancelToken,
this.action,
});
}
final ffiImageDecoder = FfiImageDecoder();

View file

@ -0,0 +1,368 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:ui' as ui;
import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/utils/extensions/others.dart';
import 'package:mangayomi/modules/manga/reader/subsampling_scale_image_view/subsampling_scale_image_view.dart';
class MinSubsamplingImage extends ConsumerStatefulWidget {
final UChapDataPreload data;
final BoxFit fit;
final Color? color;
final BlendMode? colorBlendMode;
final bool cropBorders;
final bool isHorizontal;
final Function(bool) failedToLoadImage;
final ValueNotifier<bool> isVisible;
final Widget? Function(SubsamplingImageState)? loadStateChanged;
final int rotation;
final Function(double width, double height)? onImageLoaded;
const MinSubsamplingImage({
super.key,
required this.data,
required this.fit,
this.color,
this.colorBlendMode,
required this.cropBorders,
required this.isHorizontal,
required this.failedToLoadImage,
required this.isVisible,
this.loadStateChanged,
this.rotation = 0,
this.onImageLoaded,
});
@override
ConsumerState<MinSubsamplingImage> createState() =>
_MinSubsamplingImageState();
}
class _MinSubsamplingImageState extends ConsumerState<MinSubsamplingImage> {
ui.Image? _uiImage;
bool _isLoading = true;
bool _hasError = false;
ImageStreamListener? _streamListener;
ImageStream? _imageStream;
ImageChunkEvent? _loadingProgress;
@override
void initState() {
super.initState();
_loadImage();
}
@override
void didUpdateWidget(MinSubsamplingImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data ||
widget.cropBorders != oldWidget.cropBorders) {
_loadImage(refresh: true);
}
}
@override
void dispose() {
_cleanStream();
ffiImageDecoder.cancel(this);
_uiImage?.dispose();
super.dispose();
}
void _cleanStream() {
if (_streamListener != null && _imageStream != null) {
_imageStream!.removeListener(_streamListener!);
}
_streamListener = null;
_imageStream = null;
}
Future<void> _loadImage({bool refresh = false}) async {
_cleanStream();
ffiImageDecoder.cancel(this);
if (widget.data.decodedImage != null && !refresh) {
_uiImage?.dispose();
_uiImage = widget.data.decodedImage!.clone();
if (mounted) {
setState(() {
_isLoading = false;
_hasError = false;
_loadingProgress = null;
});
widget.failedToLoadImage(false);
}
return;
}
if (mounted) {
setState(() {
_isLoading = true;
_hasError = false;
_loadingProgress = null;
});
}
_uiImage?.dispose();
_uiImage = null;
final String? path = await widget.data.getLocalFilePath;
if (path != null) {
await _loadFromPath(path);
} else {
final provider = widget.data.getImageProvider(ref, true);
_imageStream = provider.resolve(ImageConfiguration.empty);
_streamListener = ImageStreamListener(
(info, syncCall) async {
_cleanStream();
final cachedPath = await widget.data.getLocalFilePath;
if (cachedPath != null) {
await _loadFromPath(cachedPath);
} else {
if (mounted) {
widget.data.decodedImage = info.image.clone();
setState(() {
_uiImage = info.image.clone();
_isLoading = false;
_loadingProgress = null;
});
widget.failedToLoadImage(false);
}
}
},
onChunk: (ImageChunkEvent event) {
if (mounted) {
setState(() {
_loadingProgress = event;
});
}
},
onError: (err, stack) {
_cleanStream();
if (mounted) {
setState(() {
_hasError = true;
_isLoading = false;
_loadingProgress = null;
});
widget.failedToLoadImage(true);
}
},
);
_imageStream!.addListener(_streamListener!);
}
}
Future<void> _loadFromPath(String path) async {
if (!mounted) return;
try {
final dims = await ffiImageDecoder.getImageDimensionsAsync(
path,
cropBorders: widget.cropBorders,
cancelToken: this,
);
if (dims == null || dims.length < 2) {
throw Exception("Failed to get dimensions");
}
final int croppedWidth = dims[0];
final int croppedHeight = dims[1];
if (!mounted) return;
final bool isRotated = widget.rotation == 90 || widget.rotation == 270;
final double aspect = isRotated
? (croppedHeight / croppedWidth)
: (croppedWidth / croppedHeight);
double targetWidth;
double targetHeight;
if (widget.isHorizontal) {
final screenHeight =
MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom;
targetHeight = screenHeight;
targetWidth = screenHeight * aspect;
} else {
final screenWidth = MediaQuery.of(context).size.width;
targetWidth = screenWidth;
targetHeight = screenWidth / aspect;
}
widget.data.loadedWidth = targetWidth;
widget.data.loadedHeight = targetHeight;
const int sampleSize = 1;
final result = await ffiImageDecoder.decodeRegionAsync(
DecodeParams(
filePath: path,
left: 0,
top: 0,
right: croppedWidth,
bottom: croppedHeight,
sampleSize: sampleSize,
cropBorders: widget.cropBorders,
),
cancelToken: this,
);
if (result == null ||
result.pointerAddress == null ||
result.error != null) {
throw Exception(result?.error ?? "Failed to decode region");
}
final decodedWidth = croppedWidth ~/ sampleSize;
final decodedHeight = croppedHeight ~/ sampleSize;
final pointer = Pointer<Uint8>.fromAddress(result.pointerAddress!);
final bytes = pointer.asTypedList(decodedWidth * decodedHeight * 4);
final Completer<ui.Image> completer = Completer<ui.Image>();
try {
ui.decodeImageFromPixels(
bytes,
decodedWidth,
decodedHeight,
ui.PixelFormat.rgba8888,
(ui.Image img) {
calloc.free(pointer);
completer.complete(img);
},
);
} catch (e) {
calloc.free(pointer);
rethrow;
}
final ui.Image img = await completer.future;
if (mounted) {
widget.data.decodedImage = img.clone();
setState(() {
_uiImage = img;
_isLoading = false;
});
widget.failedToLoadImage(false);
widget.onImageLoaded?.call(
croppedWidth.toDouble(),
croppedHeight.toDouble(),
);
} else {
img.dispose();
}
} catch (e) {
if (mounted) {
setState(() {
_hasError = true;
_isLoading = false;
_loadingProgress = null;
});
widget.failedToLoadImage(true);
}
}
}
@override
Widget build(BuildContext context) {
final backgroundColor = ref.watch(backgroundColorStateProvider);
final subsamplingState = SubsamplingImageState(
loadState: _isLoading
? LoadState.loading
: _hasError
? LoadState.failed
: LoadState.completed,
loadingProgress: _loadingProgress,
reLoadCallback: _loadImage,
);
if (widget.loadStateChanged != null) {
final customWidget = widget.loadStateChanged!(subsamplingState);
if (customWidget != null) return customWidget;
}
final placeholderHeight = widget.data.loadedHeight ?? context.height(0.8);
final placeholderWidth = widget.isHorizontal
? (widget.data.loadedWidth ?? context.width(0.8))
: null;
if (_isLoading) {
final double progress = _loadingProgress?.expectedTotalBytes != null
? _loadingProgress!.cumulativeBytesLoaded /
_loadingProgress!.expectedTotalBytes!
: 0;
return Container(
color: getBackgroundColor(backgroundColor),
height: placeholderHeight,
width: placeholderWidth,
child: CircularProgressIndicatorAnimateRotate(progress: progress),
);
}
if (_hasError || _uiImage == null) {
final l10n = l10nLocalizations(context)!;
return Container(
color: getBackgroundColor(backgroundColor),
height: placeholderHeight,
width: placeholderWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.image_loading_error,
style: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: _loadImage,
child: Container(
decoration: BoxDecoration(
color: context.primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: Text(l10n.retry),
),
),
),
),
],
),
);
}
final Widget rawImage = RawImage(
image: _uiImage,
fit: widget.fit,
color: widget.color,
colorBlendMode: widget.colorBlendMode,
filterQuality: FilterQuality.medium,
);
if (widget.rotation != 0) {
final quarterTurns = (widget.rotation ~/ 90) % 4;
if (quarterTurns != 0) {
return RotatedBox(quarterTurns: quarterTurns, child: rawImage);
}
}
return rawImage;
}
}

View file

@ -0,0 +1,235 @@
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'tiling_engine.dart';
/// A lightweight render object widget that displays the subsampled image.
/// It replaces the use of CustomPaint to eliminate ephemeral allocations of CustomPainter.
class SubsamplingImageRenderWidget extends LeafRenderObjectWidget {
final TilingEngine tilingEngine;
final double scale;
final ui.Offset vTranslate;
final int rotation;
final int sWidth;
final int sHeight;
final bool showDebug;
final Color? color;
final BlendMode? colorBlendMode;
final FilterQuality filterQuality;
final Size preferredSize;
const SubsamplingImageRenderWidget({
super.key,
required this.tilingEngine,
required this.scale,
required this.vTranslate,
required this.rotation,
required this.sWidth,
required this.sHeight,
required this.showDebug,
this.color,
this.colorBlendMode,
required this.filterQuality,
required this.preferredSize,
});
@override
RenderSubsamplingImage createRenderObject(BuildContext context) {
return RenderSubsamplingImage(
tilingEngine: tilingEngine,
scale: scale,
vTranslate: vTranslate,
rotation: rotation,
sWidth: sWidth,
sHeight: sHeight,
color: color,
colorBlendMode: colorBlendMode,
filterQuality: filterQuality,
preferredSize: preferredSize,
);
}
@override
void updateRenderObject(
BuildContext context,
RenderSubsamplingImage renderObject,
) {
renderObject
..tilingEngine = tilingEngine
..scale = scale
..vTranslate = vTranslate
..rotation = rotation
..sWidth = sWidth
..sHeight = sHeight
..color = color
..colorBlendMode = colorBlendMode
..filterQuality = filterQuality
..preferredSize = preferredSize;
}
}
/// The RenderBox responsible for drawing the visible tiles on the Flutter Canvas.
/// It directly handles global transformations (translation, scale, rotation) without allocating intermediate objects at each frame.
class RenderSubsamplingImage extends RenderBox {
RenderSubsamplingImage({
required this._tilingEngine,
required this._scale,
required this._vTranslate,
required this._rotation,
required this._sWidth,
required this._sHeight,
this._color,
this._colorBlendMode,
required this._filterQuality,
required this._preferredSize,
});
TilingEngine _tilingEngine;
TilingEngine get tilingEngine => _tilingEngine;
set tilingEngine(TilingEngine value) {
if (_tilingEngine == value) return;
_tilingEngine = value;
markNeedsPaint();
}
double _scale;
double get scale => _scale;
set scale(double value) {
if (_scale == value) return;
_scale = value;
markNeedsPaint();
}
ui.Offset _vTranslate;
ui.Offset get vTranslate => _vTranslate;
set vTranslate(ui.Offset value) {
if (_vTranslate == value) return;
_vTranslate = value;
markNeedsPaint();
}
int _rotation;
int get rotation => _rotation;
set rotation(int value) {
if (_rotation == value) return;
_rotation = value;
markNeedsPaint();
}
int _sWidth;
int get sWidth => _sWidth;
set sWidth(int value) {
if (_sWidth == value) return;
_sWidth = value;
markNeedsLayout();
}
int _sHeight;
int get sHeight => _sHeight;
set sHeight(int value) {
if (_sHeight == value) return;
_sHeight = value;
markNeedsLayout();
}
Color? _color;
Color? get color => _color;
set color(Color? value) {
if (_color == value) return;
_color = value;
markNeedsPaint();
}
BlendMode? _colorBlendMode;
BlendMode? get colorBlendMode => _colorBlendMode;
set colorBlendMode(BlendMode? value) {
if (_colorBlendMode == value) return;
_colorBlendMode = value;
markNeedsPaint();
}
FilterQuality _filterQuality;
FilterQuality get filterQuality => _filterQuality;
set filterQuality(FilterQuality value) {
if (_filterQuality == value) return;
_filterQuality = value;
markNeedsPaint();
}
Size _preferredSize;
Size get preferredSize => _preferredSize;
set preferredSize(Size value) {
if (_preferredSize == value) return;
_preferredSize = value;
markNeedsLayout();
}
@override
void performLayout() {
size = constraints.constrain(_preferredSize);
}
@override
bool hitTestSelf(ui.Offset position) => true;
@override
void paint(PaintingContext context, ui.Offset offset) {
if (_sWidth == 0 || _sHeight == 0) return;
final canvas = context.canvas;
canvas.save();
// Applies the offset specific to the RenderBox
canvas.translate(offset.dx, offset.dy);
// 1. Applies base transformations (pan & zoom)
canvas.translate(_vTranslate.dx, _vTranslate.dy);
canvas.scale(_scale, _scale);
// 2. Handles the image rotation around its logical origin
if (_rotation == 90) {
canvas.rotate(pi / 2);
canvas.translate(0.0, -_sHeight.toDouble());
} else if (_rotation == 180) {
canvas.rotate(pi);
canvas.translate(-_sWidth.toDouble(), -_sHeight.toDouble());
} else if (_rotation == 270) {
canvas.rotate(-pi / 2);
canvas.translate(-_sWidth.toDouble(), 0.0);
}
final paint = Paint()
..isAntiAlias = true
..filterQuality = _filterQuality;
// Applies color blending if requested
if (_color != null) {
paint.colorFilter = ColorFilter.mode(
_color!,
_colorBlendMode ?? BlendMode.srcIn,
);
}
// 3. Draws all visible tiles currently loaded
final sortedKeys = _tilingEngine.tileMap.keys.toList()
..sort((a, b) => b.compareTo(a));
for (final sampleSize in sortedKeys) {
final tiles = _tilingEngine.tileMap[sampleSize] ?? [];
for (final tile in tiles) {
if (tile.visible && tile.image != null) {
final srcRect = ui.Rect.fromLTWH(
0.0,
0.0,
tile.image!.width.toDouble(),
tile.image!.height.toDouble(),
);
canvas.drawImageRect(tile.image!, srcRect, tile.sRect, paint);
}
}
}
canvas.restore();
}
}

View file

@ -0,0 +1,190 @@
import 'dart:ui' as ui;
import 'coordinate_transformer.dart';
import 'ffi_image_decoder.dart';
/// Represents an image tile in the grid
class Tile {
final ui.Rect sRect;
final int sampleSize;
bool visible = false;
bool loading = false;
ui.Image? image;
Tile({required this.sRect, required this.sampleSize});
/// Releases the graphical memory of the tile
void dispose() {
image?.dispose();
image = null;
if (loading) {
ffiImageDecoder.cancel(this);
loading = false;
}
}
}
/// Manages tiling partition and memory recycling
class TilingEngine {
Map<int, List<Tile>> tileMap = {};
int fullImageSampleSize = 1;
/// Initializes the tile grid for different sampleSizes (powers of 2)
void initialiseTileMap({
required int sWidth,
required int sHeight,
required double maxTileWidth,
required double maxTileHeight,
required int baseSampleSize,
required double viewWidth,
required double viewHeight,
}) {
// Clears the old grid
dispose();
tileMap = {};
fullImageSampleSize = baseSampleSize;
int sampleSize = fullImageSampleSize;
while (true) {
int xTiles = 1;
int yTiles = 1;
int sTileWidth = sWidth ~/ xTiles;
int sTileHeight = sHeight ~/ yTiles;
int subTileWidth = sTileWidth ~/ sampleSize;
int subTileHeight = sTileHeight ~/ sampleSize;
// Increases the number of horizontal tiles if necessary
while (subTileWidth + xTiles + 1 > maxTileWidth ||
(subTileWidth > viewWidth * 1.25 &&
sampleSize < fullImageSampleSize)) {
xTiles += 1;
sTileWidth = sWidth ~/ xTiles;
subTileWidth = sTileWidth ~/ sampleSize;
}
// Increases the number of vertical tiles if necessary
while (subTileHeight + yTiles + 1 > maxTileHeight ||
(subTileHeight > viewHeight * 1.25 &&
sampleSize < fullImageSampleSize)) {
yTiles += 1;
sTileHeight = sHeight ~/ yTiles;
subTileHeight = sTileHeight ~/ sampleSize;
}
List<Tile> tileGrid = [];
for (int x = 0; x < xTiles; x++) {
for (int y = 0; y < yTiles; y++) {
final tileLeft = x * sTileWidth;
final tileTop = y * sTileHeight;
final tileRight = (x == xTiles - 1) ? sWidth : (x + 1) * sTileWidth;
final tileBottom = (y == yTiles - 1)
? sHeight
: (y + 1) * sTileHeight;
final tile = Tile(
sRect: ui.Rect.fromLTRB(
tileLeft.toDouble(),
tileTop.toDouble(),
tileRight.toDouble(),
tileBottom.toDouble(),
),
sampleSize: sampleSize,
);
// The base layer is visible by default
tile.visible = (sampleSize == fullImageSampleSize);
tileGrid.add(tile);
}
}
tileMap[sampleSize] = tileGrid;
if (sampleSize == 1) {
break;
} else {
sampleSize = sampleSize ~/ 2;
}
}
}
/// Updates visibility and loads/recycles tiles based on the viewport
void refreshRequiredTiles({
required double scale,
required ui.Offset vTranslate,
required ui.Size viewSize,
required int rotation,
required int sWidth,
required int sHeight,
required int targetSampleSize,
required Function(Tile tile) loadTileCallback,
}) {
final transformer = CoordinateTransformer(
scale: scale,
vTranslate: vTranslate,
rotation: rotation,
sWidth: sWidth,
sHeight: sHeight,
);
// Screen viewport
final viewRect = ui.Rect.fromLTWH(0, 0, viewSize.width, viewSize.height);
for (final entry in tileMap.entries) {
final _ = entry.key;
final tiles = entry.value;
for (final tile in tiles) {
// Recycles tiles that are too detailed or too blurry (which are not the base layer)
if (tile.sampleSize < targetSampleSize ||
(tile.sampleSize > targetSampleSize &&
tile.sampleSize != fullImageSampleSize)) {
tile.visible = false;
tile.dispose();
continue;
}
if (tile.sampleSize == targetSampleSize) {
// Checks if the tile overlaps the screen
final tileViewRect = transformer.sourceToViewRect(tile.sRect);
final isVisibleOnScreen = viewRect.overlaps(tileViewRect);
if (isVisibleOnScreen) {
tile.visible = true;
if (!tile.loading && tile.image == null) {
loadTileCallback(tile);
}
} else if (tile.sampleSize != fullImageSampleSize) {
// Offscreen and not base layer -> recycle
tile.visible = false;
tile.dispose();
}
} else if (tile.sampleSize == fullImageSampleSize) {
// The base layer always remains visible in the background
tile.visible = true;
}
}
}
}
/// Determines if the base layer is completely loaded
bool isBaseLayerReady() {
final baseGrid = tileMap[fullImageSampleSize];
if (baseGrid == null || baseGrid.isEmpty) return false;
for (final tile in baseGrid) {
if (tile.image == null) return false;
}
return true;
}
/// Disposes of all tiles
void dispose() {
for (final grid in tileMap.values) {
for (final tile in grid) {
tile.dispose();
}
}
tileMap.clear();
}
}

View file

@ -0,0 +1,4 @@
export 'src/subsampling_scale_image_view.dart';
export 'src/coordinate_transformer.dart';
export 'src/ffi_image_decoder.dart'
show ffiImageDecoder, DecodeParams, DecodeResult;

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/page.dart';
@ -14,16 +15,22 @@ class UChapDataPreload {
int? index;
GetChapterPagesModel? chapterUrlModel;
int? pageIndex;
Uint8List? cropImage;
bool isTransitionPage;
Chapter? nextChapter;
String? mangaName;
bool? isLastChapter;
Rect? srcRect;
/// Cached rendered dimensions (set after image first loads)
double? loadedHeight;
double? loadedWidth;
/// Cached decoded image for MinSubsamplingImage to avoid re-decoding on scroll
Image? decodedImage;
/// Cached resolved local file path for SubsamplingScaleImageView to avoid resolving on scroll
String? resolvedFilePath;
UChapDataPreload(
this.chapter,
this.directory,
@ -33,11 +40,11 @@ class UChapDataPreload {
this.index,
this.chapterUrlModel,
this.pageIndex, {
this.cropImage,
this.isTransitionPage = false,
this.nextChapter,
this.mangaName,
this.isLastChapter = false,
this.srcRect,
});
UChapDataPreload.transition({
@ -54,5 +61,5 @@ class UChapDataPreload {
archiveImage = null,
index = null,
chapterUrlModel = null,
cropImage = null;
srcRect = null;
}

View file

@ -1,17 +1,17 @@
import 'package:extended_image/extended_image.dart';
import 'dart:async';
import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/reader/image_view_paged.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_paged.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:mangayomi/modules/manga/reader/subsampling_scale_image_view/subsampling_scale_image_view.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
/// Unified double page view for both paged and continuous reading modes.
///
@ -39,6 +39,15 @@ class DoublePageView extends StatefulWidget {
/// Whether to add top padding for the first page (vertical mode only).
final bool addTopPadding;
/// The scroll direction of the parent PageView (for gesture interception).
final Axis scrollDirection;
/// Callback when the zoom state changes.
final Function(bool isZoomed)? onZoomChanged;
/// Callback when the PhotoViewController is created/disposed.
final void Function(PhotoViewController? controller)? onControllerCreated;
const DoublePageView({
super.key,
required this.pages,
@ -47,6 +56,9 @@ class DoublePageView extends StatefulWidget {
this.onFailedToLoadImage,
this.isPagedMode = true,
this.addTopPadding = true,
this.scrollDirection = Axis.horizontal,
this.onZoomChanged,
this.onControllerCreated,
});
/// Creates a paged mode double page view.
@ -56,6 +68,9 @@ class DoublePageView extends StatefulWidget {
required this.backgroundColor,
this.onLongPressData,
this.onFailedToLoadImage,
required this.scrollDirection,
this.onZoomChanged,
this.onControllerCreated,
}) : isPagedMode = true,
addTopPadding = false;
@ -67,7 +82,10 @@ class DoublePageView extends StatefulWidget {
this.onLongPressData,
this.onFailedToLoadImage,
this.addTopPadding = true,
}) : isPagedMode = false;
}) : isPagedMode = false,
scrollDirection = Axis.vertical,
onZoomChanged = null,
onControllerCreated = null;
@override
State<DoublePageView> createState() => _DoublePageViewState();
@ -83,6 +101,7 @@ class _DoublePageViewState extends State<DoublePageView>
final PhotoViewController _photoViewController = PhotoViewController();
final PhotoViewScaleStateController _photoViewScaleStateController =
PhotoViewScaleStateController();
StreamSubscription? _zoomSubscription;
Duration _doubleTapAnimationDuration() {
final doubleTapAnimationValue =
@ -129,14 +148,25 @@ class _DoublePageViewState extends State<DoublePageView>
_photoViewController.scale = _animation.value;
};
_animation.addListener(_animationListener);
_zoomSubscription = _photoViewController.outputStateStream.listen((
value,
) {
final isZoomed = value.scale! > 1.01;
widget.onZoomChanged?.call(isZoomed);
});
widget.onControllerCreated?.call(_photoViewController);
}
}
@override
void dispose() {
if (widget.isPagedMode) {
widget.onControllerCreated?.call(null);
_animation.removeListener(_animationListener);
_scaleAnimationController.dispose();
_zoomSubscription?.cancel();
_photoViewController.dispose();
_photoViewScaleStateController.dispose();
}
@ -200,23 +230,20 @@ class _DoublePageViewState extends State<DoublePageView>
}
Widget _buildPagedMode() {
return PhotoViewGallery.builder(
backgroundDecoration: const BoxDecoration(color: Colors.transparent),
itemCount: 1,
builder: (context, _) {
return PhotoViewGalleryPageOptions.customChild(
controller: _photoViewController,
scaleStateController: _photoViewScaleStateController,
basePosition: _scalePosition,
onScaleEnd: _onScaleEnd,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => _toggleScale(details.globalPosition),
onDoubleTap: () {},
child: _buildPageRow(),
),
);
},
return PhotoViewGestureDetectorScope(
axis: widget.scrollDirection,
child: PhotoView.customChild(
controller: _photoViewController,
scaleStateController: _photoViewScaleStateController,
basePosition: _scalePosition,
onScaleEnd: _onScaleEnd,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => _toggleScale(details.globalPosition),
onDoubleTap: () {},
child: _buildPageRow(),
),
),
);
}
@ -248,10 +275,16 @@ class _DoublePageViewState extends State<DoublePageView>
final l10n = l10nLocalizations(context)!;
final onLongPress = widget.onLongPressData ?? (_) {};
return ImageViewPaged(
return ImageViewVertical(
data: pageData,
failedToLoadImage: (val) {
widget.onFailedToLoadImage?.call(val);
},
onLongPressData: onLongPress,
isVisible: ValueNotifier<bool>(true),
isHorizontal: true,
loadStateChanged: (state) {
switch (state.extendedImageLoadState) {
switch (state.loadState) {
case LoadState.loading:
return _buildLoadingState(state);
case LoadState.completed:
@ -260,11 +293,10 @@ class _DoublePageViewState extends State<DoublePageView>
return _buildFailedState(state, l10n);
}
},
onLongPressData: onLongPress,
);
}
Widget _buildLoadingState(ExtendedImageState state) {
Widget _buildLoadingState(SubsamplingImageState state) {
final loadingProgress = state.loadingProgress;
final progress = loadingProgress?.expectedTotalBytes != null
? loadingProgress!.cumulativeBytesLoaded /
@ -278,12 +310,12 @@ class _DoublePageViewState extends State<DoublePageView>
);
}
Widget _buildCompletedState(ExtendedImageState state) {
Widget? _buildCompletedState(SubsamplingImageState state) {
widget.onFailedToLoadImage?.call(false);
return Image(image: state.imageProvider);
return null; // Affiche l'image via MinSubsamplingImage
}
Widget _buildFailedState(ExtendedImageState state, dynamic l10n) {
Widget _buildFailedState(SubsamplingImageState state, dynamic l10n) {
widget.onFailedToLoadImage?.call(true);
return Container(
@ -305,7 +337,7 @@ class _DoublePageViewState extends State<DoublePageView>
);
}
Widget _buildRetryButton(ExtendedImageState state, dynamic l10n) {
Widget _buildRetryButton(SubsamplingImageState state, dynamic l10n) {
return GestureDetector(
onLongPress: () {
state.reLoadImage();

View file

@ -0,0 +1,205 @@
import 'package:flutter/material.dart';
class ReaderNavigationOverlay extends StatelessWidget {
final int navigationLayout;
final int tappingInversion;
final bool isRTL;
final VoidCallback onClose;
const ReaderNavigationOverlay({
super.key,
required this.navigationLayout,
required this.tappingInversion,
required this.isRTL,
required this.onClose,
});
bool get _shouldInvertHorizontal =>
tappingInversion == 1 || tappingInversion == 3;
bool get _shouldInvertVertical =>
tappingInversion == 2 || tappingInversion == 3;
Color get _menuColor => const Color(0xCC95818D);
Color get _prevColor => const Color(0xCCFF7733);
Color get _nextColor => const Color(0xCC84E296);
String get _menuText => "MENU";
String get _prevText => "PREV";
String get _nextText => "NEXT";
Widget _zone(Color color, String text) {
return Container(
decoration: BoxDecoration(
color: color,
border: Border.all(color: Colors.black26, width: 0.5),
),
child: Center(
child: Text(
text,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
shadows: [
Shadow(
blurRadius: 4.0,
color: Colors.black54,
offset: Offset(1.0, 1.0),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onClose,
child: IgnorePointer(
ignoring: false,
child: Container(
color: Colors.transparent,
child: switch (navigationLayout) {
1 => _buildLShaped(),
2 => _buildKindle(),
3 => _buildEdge(),
4 => _buildRightAndLeft(),
5 => _buildDisabled(),
_ => _buildDefault(),
},
),
),
);
}
// Helper values for LTR vs RTL
(Color, String) get _hLeft =>
isRTL ? (_nextColor, _nextText) : (_prevColor, _prevText);
(Color, String) get _hRight =>
isRTL ? (_prevColor, _prevText) : (_nextColor, _nextText);
(Color, String) get _leftZone => _shouldInvertHorizontal ? _hRight : _hLeft;
(Color, String) get _rightZone => _shouldInvertHorizontal ? _hLeft : _hRight;
Widget _buildDefault() {
return Stack(
children: [_buildDefaultHorizontalZones(), _buildDefaultVerticalZones()],
);
}
Widget _buildDefaultHorizontalZones() {
final left = _leftZone;
final right = _rightZone;
return Row(
children: [
Expanded(flex: 2, child: _zone(left.$1, left.$2)),
Expanded(flex: 2, child: _zone(_menuColor, _menuText)),
Expanded(flex: 2, child: _zone(right.$1, right.$2)),
],
);
}
Widget _buildDefaultVerticalZones() {
final topText = _shouldInvertVertical ? _nextText : _prevText;
final topColor = _shouldInvertVertical ? _nextColor : _prevColor;
final bottomText = _shouldInvertVertical ? _prevText : _nextText;
final bottomColor = _shouldInvertVertical ? _prevColor : _nextColor;
return Column(
children: [
Expanded(flex: 2, child: _zone(topColor, topText)),
const Expanded(flex: 5, child: SizedBox.shrink()),
Expanded(flex: 2, child: _zone(bottomColor, bottomText)),
],
);
}
Widget _buildLShaped() {
final leftAction = (_shouldInvertHorizontal ^ _shouldInvertVertical)
? _nextText
: _prevText;
final leftColor = (_shouldInvertHorizontal ^ _shouldInvertVertical)
? _nextColor
: _prevColor;
final rightAction = (_shouldInvertHorizontal ^ _shouldInvertVertical)
? _prevText
: _nextText;
final rightColor = (_shouldInvertHorizontal ^ _shouldInvertVertical)
? _prevColor
: _nextColor;
return Column(
children: [
Expanded(
flex: 1,
child: Row(
children: [
Expanded(flex: 1, child: _zone(leftColor, leftAction)),
Expanded(flex: 2, child: _zone(_menuColor, _menuText)),
],
),
),
Expanded(flex: 2, child: _zone(_menuColor, _menuText)),
Expanded(
flex: 1,
child: Row(
children: [
Expanded(flex: 2, child: _zone(_menuColor, _menuText)),
Expanded(flex: 1, child: _zone(rightColor, rightAction)),
],
),
),
],
);
}
Widget _buildKindle() {
final left = _leftZone;
final right = _rightZone;
return Column(
children: [
Expanded(flex: 1, child: _zone(_menuColor, _menuText)),
Expanded(
flex: 3,
child: Row(
children: [
Expanded(child: _zone(left.$1, left.$2)),
Expanded(child: _zone(right.$1, right.$2)),
],
),
),
],
);
}
Widget _buildEdge() {
final left = _leftZone;
final right = _rightZone;
return Row(
children: [
Expanded(flex: 1, child: _zone(left.$1, left.$2)),
Expanded(flex: 5, child: _zone(_menuColor, _menuText)),
Expanded(flex: 1, child: _zone(right.$1, right.$2)),
],
);
}
Widget _buildRightAndLeft() {
final left = _leftZone;
final right = _rightZone;
return Row(
children: [
Expanded(child: _zone(left.$1, left.$2)),
Expanded(child: _zone(right.$1, right.$2)),
],
);
}
Widget _buildDisabled() {
return SizedBox.expand(child: _zone(_menuColor, _menuText));
}
}

View file

@ -39,6 +39,9 @@ class ReaderGestureHandler extends StatelessWidget {
/// Navigation layout index (0-5), see class docs.
final int navigationLayout;
/// Tapping zone inversion mode (0 = None, 1 = Horizontal, 2 = Vertical, 3 = Both)
final int tappingInversion;
/// Callback when UI should be toggled
final VoidCallback onToggleUI;
@ -70,6 +73,7 @@ class ReaderGestureHandler extends StatelessWidget {
required this.onPreviousPage,
required this.onNextPage,
this.navigationLayout = 0,
this.tappingInversion = 0,
this.onDoubleTapDown,
this.onDoubleTap,
this.onSecondaryTapDown,
@ -90,6 +94,11 @@ class ReaderGestureHandler extends StatelessWidget {
// helpers
bool get _shouldInvertHorizontal =>
tappingInversion == 1 || tappingInversion == 3;
bool get _shouldInvertVertical =>
tappingInversion == 2 || tappingInversion == 3;
VoidCallback _prev() => isRTL ? onNextPage : onPreviousPage;
VoidCallback _next() => isRTL ? onPreviousPage : onNextPage;
@ -121,31 +130,32 @@ class ReaderGestureHandler extends StatelessWidget {
}
Widget _buildDefaultHorizontalZones(BuildContext context) {
final leftAction = _shouldInvertHorizontal ? _next() : _prev();
final rightAction = _shouldInvertHorizontal ? _prev() : _next();
return Row(
children: [
Expanded(flex: 2, child: _zone(_prev())),
Expanded(flex: 2, child: _zone(leftAction)),
Expanded(
flex: 2,
child: hasImageError
? SizedBox(width: context.width(1), height: context.height(0.7))
: _uiZone(),
),
Expanded(flex: 2, child: _zone(_next())),
Expanded(flex: 2, child: _zone(rightAction)),
],
);
}
Widget _buildDefaultVerticalZones(BuildContext context) {
final topAction = _shouldInvertVertical ? onNextPage : onPreviousPage;
final bottomAction = _shouldInvertVertical ? onPreviousPage : onNextPage;
return Column(
children: [
Expanded(
flex: 2,
child: _zone(hasImageError ? onToggleUI : onPreviousPage),
),
Expanded(flex: 2, child: _zone(hasImageError ? onToggleUI : topAction)),
const Expanded(flex: 5, child: SizedBox.shrink()),
Expanded(
flex: 2,
child: _zone(hasImageError ? onToggleUI : onNextPage),
child: _zone(hasImageError ? onToggleUI : bottomAction),
),
],
);
@ -161,13 +171,19 @@ class ReaderGestureHandler extends StatelessWidget {
//
Widget _buildLShaped(BuildContext context) {
final action1 = (_shouldInvertHorizontal ^ _shouldInvertVertical)
? _next()
: _prev();
final action2 = (_shouldInvertHorizontal ^ _shouldInvertVertical)
? _prev()
: _next();
return Column(
children: [
Expanded(
flex: 1,
child: Row(
children: [
Expanded(flex: 1, child: _zone(_prev())),
Expanded(flex: 1, child: _zone(action1)),
Expanded(flex: 2, child: _uiZone()),
],
),
@ -178,7 +194,7 @@ class ReaderGestureHandler extends StatelessWidget {
child: Row(
children: [
Expanded(flex: 2, child: _uiZone()),
Expanded(flex: 1, child: _zone(_next())),
Expanded(flex: 1, child: _zone(action2)),
],
),
),
@ -194,6 +210,8 @@ class ReaderGestureHandler extends StatelessWidget {
//
Widget _buildKindle(BuildContext context) {
final leftAction = _shouldInvertHorizontal ? _next() : _prev();
final rightAction = _shouldInvertHorizontal ? _prev() : _next();
return Column(
children: [
Expanded(flex: 1, child: _uiZone()),
@ -201,8 +219,8 @@ class ReaderGestureHandler extends StatelessWidget {
flex: 3,
child: Row(
children: [
Expanded(child: _zone(_prev())),
Expanded(child: _zone(_next())),
Expanded(child: _zone(leftAction)),
Expanded(child: _zone(rightAction)),
],
),
),
@ -219,11 +237,13 @@ class ReaderGestureHandler extends StatelessWidget {
//
Widget _buildEdge(BuildContext context) {
final leftAction = _shouldInvertHorizontal ? _next() : _prev();
final rightAction = _shouldInvertHorizontal ? _prev() : _next();
return Row(
children: [
Expanded(flex: 1, child: _zone(_prev())),
Expanded(flex: 1, child: _zone(leftAction)),
Expanded(flex: 5, child: _uiZone()),
Expanded(flex: 1, child: _zone(_next())),
Expanded(flex: 1, child: _zone(rightAction)),
],
);
}
@ -236,10 +256,12 @@ class ReaderGestureHandler extends StatelessWidget {
//
Widget _buildRightAndLeft(BuildContext context) {
final leftAction = _shouldInvertHorizontal ? _next() : _prev();
final rightAction = _shouldInvertHorizontal ? _prev() : _next();
return Row(
children: [
Expanded(child: _zone(_prev())),
Expanded(child: _zone(_next())),
Expanded(child: _zone(leftAction)),
Expanded(child: _zone(rightAction)),
],
);
}

View file

@ -127,9 +127,23 @@ class _ReadingModeTab extends ConsumerWidget {
final readerMode = ref.watch(currentReaderModeProvider);
final usePageTapZones = ref.watch(usePageTapZonesStateProvider);
final cropBorders = ref.watch(cropBordersStateProvider);
final splitWidePages = ref.watch(splitWidePagesStateProvider);
final keepScreenOn = ref.watch(keepScreenOnReaderStateProvider);
final showPageGaps = ref.watch(showPageGapsStateProvider);
final webtoonSidePadding = ref.watch(webtoonSidePaddingStateProvider);
final dualPageInvert = ref.watch(dualPageInvertStateProvider);
final dualPageRotateToFit = ref.watch(dualPageRotateToFitStateProvider);
final dualPageRotateToFitInvert = ref.watch(
dualPageRotateToFitInvertStateProvider,
);
final landscapeZoom = ref.watch(landscapeZoomStateProvider);
final zoomStartPosition = ref.watch(zoomStartPositionStateProvider);
final automaticBackground = ref.watch(automaticBackgroundStateProvider);
final webtoonDisableZoomOut = ref.watch(webtoonDisableZoomOutStateProvider);
final webtoonDoubleTapZoomEnabled = ref.watch(
webtoonDoubleTapZoomEnabledStateProvider,
);
final navigateToPan = ref.watch(navigateToPanStateProvider);
return SingleChildScrollView(
child: Padding(
@ -165,6 +179,185 @@ class _ReadingModeTab extends ConsumerWidget {
},
),
if (readerMode.isContinuous) ...[
SwitchListTile(
value: webtoonDisableZoomOut,
title: Text(
l10n.webtoon_disable_zoom_out,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref
.read(webtoonDisableZoomOutStateProvider.notifier)
.set(value);
},
),
SwitchListTile(
value: webtoonDoubleTapZoomEnabled,
title: Text(
l10n.webtoon_double_tap_zoom_enabled,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref
.read(webtoonDoubleTapZoomEnabledStateProvider.notifier)
.set(value);
},
),
],
if (!readerMode.isContinuous)
SwitchListTile(
value: navigateToPan,
title: Text(
l10n.navigate_to_pan,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
subtitle: Text(
l10n.navigate_to_pan_subtitle,
style: const TextStyle(fontSize: 11),
),
onChanged: (value) {
ref.read(navigateToPanStateProvider.notifier).set(value);
},
),
SwitchListTile(
value: splitWidePages,
title: Text(
l10n.split_wide_pages,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref.read(splitWidePagesStateProvider.notifier).set(value);
},
),
if (splitWidePages)
SwitchListTile(
value: dualPageInvert,
title: Text(
l10n.dual_page_invert,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref.read(dualPageInvertStateProvider.notifier).set(value);
},
),
SwitchListTile(
value: dualPageRotateToFit,
title: Text(
l10n.dual_page_rotate_to_fit,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref.read(dualPageRotateToFitStateProvider.notifier).set(value);
},
),
if (dualPageRotateToFit)
SwitchListTile(
value: dualPageRotateToFitInvert,
title: Text(
l10n.dual_page_rotate_to_fit_invert,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref
.read(dualPageRotateToFitInvertStateProvider.notifier)
.set(value);
},
),
if (!readerMode.isContinuous)
SwitchListTile(
value: landscapeZoom,
title: Text(
l10n.landscape_zoom,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref.read(landscapeZoomStateProvider.notifier).set(value);
},
),
if (!readerMode.isContinuous && landscapeZoom)
CustomPopupMenuButton<int>(
label: l10n.zoom_start_position,
title: switch (zoomStartPosition) {
0 => l10n.zoom_start_left,
1 => l10n.zoom_start_right,
_ => l10n.zoom_start_center,
},
onSelected: (value) {
ref.read(zoomStartPositionStateProvider.notifier).set(value);
},
value: zoomStartPosition,
list: const [0, 1, 2],
itemText: (pos) => switch (pos) {
0 => l10n.zoom_start_left,
1 => l10n.zoom_start_right,
_ => l10n.zoom_start_center,
},
),
SwitchListTile(
value: automaticBackground,
title: Text(
l10n.automatic_background,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref.read(automaticBackgroundStateProvider.notifier).set(value);
},
),
// Page Tap Zones
SwitchListTile(
value: usePageTapZones,
@ -313,6 +506,16 @@ class _GeneralTab extends ConsumerWidget {
final backgroundColor = ref.watch(backgroundColorStateProvider);
final navigationLayout = ref.watch(readerNavigationLayoutStateProvider);
final tappingInversion = ref.watch(tappingInversionStateProvider);
final flashOnPageChange = ref.watch(flashOnPageChangeStateProvider);
final flashColor = ref.watch(flashColorStateProvider);
final flashInterval = ref.watch(flashIntervalStateProvider);
final flashDuration = ref.watch(flashDurationStateProvider);
final showNavigationOverlayOnStart = ref.watch(
showNavigationOverlayOnStartStateProvider,
);
final readerHideThreshold = ref.watch(readerHideThresholdStateProvider);
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
@ -399,6 +602,261 @@ class _GeneralTab extends ConsumerWidget {
},
),
// Tapping Inversion
ListTile(
title: Text(
l10n.tapping_inversion,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
subtitle: Text(switch (tappingInversion) {
1 => l10n.tapping_inversion_horizontal,
2 => l10n.tapping_inversion_vertical,
3 => l10n.tapping_inversion_both,
_ => l10n.tapping_inversion_none,
}),
onTap: () {
showDialog(
context: context,
builder: (ctx) {
return SimpleDialog(
title: Text(l10n.tapping_inversion),
children: [
RadioGroup<int>(
groupValue: tappingInversion,
onChanged: (val) {
ref
.read(tappingInversionStateProvider.notifier)
.set(val!);
Navigator.pop(ctx);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<int>(
value: 0,
title: Text(l10n.tapping_inversion_none),
),
RadioListTile<int>(
value: 1,
title: Text(l10n.tapping_inversion_horizontal),
),
RadioListTile<int>(
value: 2,
title: Text(l10n.tapping_inversion_vertical),
),
RadioListTile<int>(
value: 3,
title: Text(l10n.tapping_inversion_both),
),
],
),
),
],
);
},
);
},
),
// Flash on Page Change
SwitchListTile(
value: flashOnPageChange,
title: Text(
l10n.flash_on_page_change,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
subtitle: Text(
l10n.flash_on_page_change_subtitle,
style: const TextStyle(fontSize: 11),
),
onChanged: (value) {
ref.read(flashOnPageChangeStateProvider.notifier).set(value);
},
),
if (flashOnPageChange) ...[
ListTile(
title: Text(
l10n.flash_color,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
ChoiceChip(
label: Text(l10n.flash_color_black),
selected: flashColor == 0,
onSelected: (val) {
if (val) {
ref.read(flashColorStateProvider.notifier).set(0);
}
},
),
const SizedBox(width: 8),
ChoiceChip(
label: Text(l10n.flash_color_white),
selected: flashColor == 1,
onSelected: (val) {
if (val) {
ref.read(flashColorStateProvider.notifier).set(1);
}
},
),
const SizedBox(width: 8),
ChoiceChip(
label: Text(l10n.flash_color_white_black),
selected: flashColor == 2,
onSelected: (val) {
if (val) {
ref.read(flashColorStateProvider.notifier).set(2);
}
},
),
],
),
),
),
ListTile(
title: Text(
l10n.flash_interval(flashInterval.toString()),
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
subtitle: Slider(
min: 1,
max: 10,
divisions: 9,
value: flashInterval.toDouble(),
onChanged: (val) {
ref
.read(flashIntervalStateProvider.notifier)
.set(val.toInt());
},
),
),
ListTile(
title: Text(
l10n.flash_duration(flashDuration.toString()),
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
subtitle: Slider(
min: 50,
max: 500,
divisions: 9,
value: flashDuration.toDouble(),
onChanged: (val) {
ref
.read(flashDurationStateProvider.notifier)
.set(val.toInt());
},
),
),
],
// Show Navigation Overlay
SwitchListTile(
value: showNavigationOverlayOnStart,
title: Text(
l10n.show_navigation_overlay_on_start,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
onChanged: (value) {
ref
.read(showNavigationOverlayOnStartStateProvider.notifier)
.set(value);
},
),
// Reader Hide Threshold
ListTile(
title: Text(
l10n.reader_hide_threshold,
style: TextStyle(
color: Theme.of(
context,
).textTheme.bodyLarge!.color!.withValues(alpha: 0.9),
fontSize: 14,
),
),
subtitle: Text(switch (readerHideThreshold) {
0 => l10n.reader_hide_threshold_highest,
1 => l10n.reader_hide_threshold_high,
2 => l10n.reader_hide_threshold_low,
_ => l10n.reader_hide_threshold_lowest,
}),
onTap: () {
showDialog(
context: context,
builder: (ctx) {
return SimpleDialog(
title: Text(l10n.reader_hide_threshold),
children: [
RadioGroup<int>(
groupValue: readerHideThreshold,
onChanged: (val) {
ref
.read(readerHideThresholdStateProvider.notifier)
.set(val!);
Navigator.pop(ctx);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<int>(
value: 0,
title: Text(l10n.reader_hide_threshold_highest),
),
RadioListTile<int>(
value: 1,
title: Text(l10n.reader_hide_threshold_high),
),
RadioListTile<int>(
value: 2,
title: Text(l10n.reader_hide_threshold_low),
),
RadioListTile<int>(
value: 3,
title: Text(l10n.reader_hide_threshold_lowest),
),
],
),
),
],
);
},
);
},
),
// Fullscreen
SwitchListTile(
value: fullScreenReader,

View file

@ -745,3 +745,345 @@ class TtsVoiceState extends _$TtsVoiceState {
);
}
}
@riverpod
class SplitWidePagesState extends _$SplitWidePagesState {
@override
bool build() {
return isar.settings.getSync(227)!.splitWidePages ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..splitWidePages = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class DualPageInvertState extends _$DualPageInvertState {
@override
bool build() {
return isar.settings.getSync(227)!.dualPageInvert ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..dualPageInvert = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class DualPageRotateToFitState extends _$DualPageRotateToFitState {
@override
bool build() {
return isar.settings.getSync(227)!.dualPageRotateToFit ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..dualPageRotateToFit = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class DualPageRotateToFitInvertState extends _$DualPageRotateToFitInvertState {
@override
bool build() {
return isar.settings.getSync(227)!.dualPageRotateToFitInvert ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..dualPageRotateToFitInvert = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class LandscapeZoomState extends _$LandscapeZoomState {
@override
bool build() {
return isar.settings.getSync(227)!.landscapeZoom ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..landscapeZoom = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class ZoomStartPositionState extends _$ZoomStartPositionState {
@override
int build() {
return isar.settings.getSync(227)!.zoomStartPosition ?? 1;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..zoomStartPosition = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class AutomaticBackgroundState extends _$AutomaticBackgroundState {
@override
bool build() {
return isar.settings.getSync(227)!.automaticBackground ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..automaticBackground = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class NavigateToPanState extends _$NavigateToPanState {
@override
bool build() {
return isar.settings.getSync(227)!.navigateToPan ?? true;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..navigateToPan = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class TappingInversionState extends _$TappingInversionState {
@override
int build() {
return isar.settings.getSync(227)!.tappingInversion ?? 0;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..tappingInversion = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class FlashOnPageChangeState extends _$FlashOnPageChangeState {
@override
bool build() {
return isar.settings.getSync(227)!.flashOnPageChange ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..flashOnPageChange = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class FlashDurationState extends _$FlashDurationState {
@override
int build() {
return isar.settings.getSync(227)!.flashDuration ?? 100;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..flashDuration = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class FlashIntervalState extends _$FlashIntervalState {
@override
int build() {
return isar.settings.getSync(227)!.flashInterval ?? 1;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..flashInterval = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class FlashColorState extends _$FlashColorState {
@override
int build() {
return isar.settings.getSync(227)!.flashColor ?? 0;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..flashColor = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class ShowNavigationOverlayOnStartState
extends _$ShowNavigationOverlayOnStartState {
@override
bool build() {
return isar.settings.getSync(227)!.showNavigationOverlayOnStart ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..showNavigationOverlayOnStart = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class WebtoonDisableZoomOutState extends _$WebtoonDisableZoomOutState {
@override
bool build() {
return isar.settings.getSync(227)!.webtoonDisableZoomOut ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..webtoonDisableZoomOut = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class WebtoonDoubleTapZoomEnabledState
extends _$WebtoonDoubleTapZoomEnabledState {
@override
bool build() {
return isar.settings.getSync(227)!.webtoonDoubleTapZoomEnabled ?? true;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..webtoonDoubleTapZoomEnabled = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class ReaderHideThresholdState extends _$ReaderHideThresholdState {
@override
int build() {
return isar.settings.getSync(227)!.readerHideThreshold ?? 1;
}
void set(int value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..readerHideThreshold = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}

View file

@ -1917,3 +1917,910 @@ abstract class _$TtsVoiceState extends $Notifier<String?> {
element.handleCreate(ref, build);
}
}
@ProviderFor(SplitWidePagesState)
final splitWidePagesStateProvider = SplitWidePagesStateProvider._();
final class SplitWidePagesStateProvider
extends $NotifierProvider<SplitWidePagesState, bool> {
SplitWidePagesStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'splitWidePagesStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$splitWidePagesStateHash();
@$internal
@override
SplitWidePagesState create() => SplitWidePagesState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$splitWidePagesStateHash() =>
r'c27b7a965cf7b0f5acbf993689185f3c788d67d6';
abstract class _$SplitWidePagesState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(DualPageInvertState)
final dualPageInvertStateProvider = DualPageInvertStateProvider._();
final class DualPageInvertStateProvider
extends $NotifierProvider<DualPageInvertState, bool> {
DualPageInvertStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dualPageInvertStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dualPageInvertStateHash();
@$internal
@override
DualPageInvertState create() => DualPageInvertState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$dualPageInvertStateHash() =>
r'9ddc66d5b712c5c122e4c264213e24b40a52b5e5';
abstract class _$DualPageInvertState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(DualPageRotateToFitState)
final dualPageRotateToFitStateProvider = DualPageRotateToFitStateProvider._();
final class DualPageRotateToFitStateProvider
extends $NotifierProvider<DualPageRotateToFitState, bool> {
DualPageRotateToFitStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dualPageRotateToFitStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dualPageRotateToFitStateHash();
@$internal
@override
DualPageRotateToFitState create() => DualPageRotateToFitState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$dualPageRotateToFitStateHash() =>
r'2fc96d6e7a33113d3a89c738c5df15193ae71b86';
abstract class _$DualPageRotateToFitState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(DualPageRotateToFitInvertState)
final dualPageRotateToFitInvertStateProvider =
DualPageRotateToFitInvertStateProvider._();
final class DualPageRotateToFitInvertStateProvider
extends $NotifierProvider<DualPageRotateToFitInvertState, bool> {
DualPageRotateToFitInvertStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dualPageRotateToFitInvertStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dualPageRotateToFitInvertStateHash();
@$internal
@override
DualPageRotateToFitInvertState create() => DualPageRotateToFitInvertState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$dualPageRotateToFitInvertStateHash() =>
r'44afe65591b107a15325fea77d36e07a26548021';
abstract class _$DualPageRotateToFitInvertState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(LandscapeZoomState)
final landscapeZoomStateProvider = LandscapeZoomStateProvider._();
final class LandscapeZoomStateProvider
extends $NotifierProvider<LandscapeZoomState, bool> {
LandscapeZoomStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'landscapeZoomStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$landscapeZoomStateHash();
@$internal
@override
LandscapeZoomState create() => LandscapeZoomState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$landscapeZoomStateHash() =>
r'b345f815068658747c1daee6d9af997ed354efb8';
abstract class _$LandscapeZoomState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(ZoomStartPositionState)
final zoomStartPositionStateProvider = ZoomStartPositionStateProvider._();
final class ZoomStartPositionStateProvider
extends $NotifierProvider<ZoomStartPositionState, int> {
ZoomStartPositionStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'zoomStartPositionStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$zoomStartPositionStateHash();
@$internal
@override
ZoomStartPositionState create() => ZoomStartPositionState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$zoomStartPositionStateHash() =>
r'4e42aabc355c9c7b879e309e2a7e05db1068a483';
abstract class _$ZoomStartPositionState extends $Notifier<int> {
int build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(AutomaticBackgroundState)
final automaticBackgroundStateProvider = AutomaticBackgroundStateProvider._();
final class AutomaticBackgroundStateProvider
extends $NotifierProvider<AutomaticBackgroundState, bool> {
AutomaticBackgroundStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'automaticBackgroundStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$automaticBackgroundStateHash();
@$internal
@override
AutomaticBackgroundState create() => AutomaticBackgroundState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$automaticBackgroundStateHash() =>
r'adeef482a7630c03d0eee4eef7bf33aeff71d053';
abstract class _$AutomaticBackgroundState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(NavigateToPanState)
final navigateToPanStateProvider = NavigateToPanStateProvider._();
final class NavigateToPanStateProvider
extends $NotifierProvider<NavigateToPanState, bool> {
NavigateToPanStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'navigateToPanStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$navigateToPanStateHash();
@$internal
@override
NavigateToPanState create() => NavigateToPanState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$navigateToPanStateHash() =>
r'f02ade437b1fe984cb465cd975a0fa0e19f365ca';
abstract class _$NavigateToPanState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(TappingInversionState)
final tappingInversionStateProvider = TappingInversionStateProvider._();
final class TappingInversionStateProvider
extends $NotifierProvider<TappingInversionState, int> {
TappingInversionStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tappingInversionStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tappingInversionStateHash();
@$internal
@override
TappingInversionState create() => TappingInversionState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$tappingInversionStateHash() =>
r'7dd5f3036fdb18d9063febbb82d86e04fb61de3d';
abstract class _$TappingInversionState extends $Notifier<int> {
int build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(FlashOnPageChangeState)
final flashOnPageChangeStateProvider = FlashOnPageChangeStateProvider._();
final class FlashOnPageChangeStateProvider
extends $NotifierProvider<FlashOnPageChangeState, bool> {
FlashOnPageChangeStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'flashOnPageChangeStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$flashOnPageChangeStateHash();
@$internal
@override
FlashOnPageChangeState create() => FlashOnPageChangeState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$flashOnPageChangeStateHash() =>
r'528e623f048eb868426b761482765cec0f1a0cad';
abstract class _$FlashOnPageChangeState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(FlashDurationState)
final flashDurationStateProvider = FlashDurationStateProvider._();
final class FlashDurationStateProvider
extends $NotifierProvider<FlashDurationState, int> {
FlashDurationStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'flashDurationStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$flashDurationStateHash();
@$internal
@override
FlashDurationState create() => FlashDurationState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$flashDurationStateHash() =>
r'c267a259b6eef748c6537e4a23f96ad4cd7b25eb';
abstract class _$FlashDurationState extends $Notifier<int> {
int build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(FlashIntervalState)
final flashIntervalStateProvider = FlashIntervalStateProvider._();
final class FlashIntervalStateProvider
extends $NotifierProvider<FlashIntervalState, int> {
FlashIntervalStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'flashIntervalStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$flashIntervalStateHash();
@$internal
@override
FlashIntervalState create() => FlashIntervalState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$flashIntervalStateHash() =>
r'8cee27191fcfcb703534d7998f7be4f027fae774';
abstract class _$FlashIntervalState extends $Notifier<int> {
int build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(FlashColorState)
final flashColorStateProvider = FlashColorStateProvider._();
final class FlashColorStateProvider
extends $NotifierProvider<FlashColorState, int> {
FlashColorStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'flashColorStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$flashColorStateHash();
@$internal
@override
FlashColorState create() => FlashColorState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$flashColorStateHash() => r'75930031561086c021f6069a44af12d68dde7538';
abstract class _$FlashColorState extends $Notifier<int> {
int build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(ShowNavigationOverlayOnStartState)
final showNavigationOverlayOnStartStateProvider =
ShowNavigationOverlayOnStartStateProvider._();
final class ShowNavigationOverlayOnStartStateProvider
extends $NotifierProvider<ShowNavigationOverlayOnStartState, bool> {
ShowNavigationOverlayOnStartStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'showNavigationOverlayOnStartStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() =>
_$showNavigationOverlayOnStartStateHash();
@$internal
@override
ShowNavigationOverlayOnStartState create() =>
ShowNavigationOverlayOnStartState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$showNavigationOverlayOnStartStateHash() =>
r'e847dc6c796d9e807d850ff92b093329607035ac';
abstract class _$ShowNavigationOverlayOnStartState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(WebtoonDisableZoomOutState)
final webtoonDisableZoomOutStateProvider =
WebtoonDisableZoomOutStateProvider._();
final class WebtoonDisableZoomOutStateProvider
extends $NotifierProvider<WebtoonDisableZoomOutState, bool> {
WebtoonDisableZoomOutStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'webtoonDisableZoomOutStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$webtoonDisableZoomOutStateHash();
@$internal
@override
WebtoonDisableZoomOutState create() => WebtoonDisableZoomOutState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$webtoonDisableZoomOutStateHash() =>
r'e5a2ad7e4307cfa47607cf9395becc70cf7a47c9';
abstract class _$WebtoonDisableZoomOutState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(WebtoonDoubleTapZoomEnabledState)
final webtoonDoubleTapZoomEnabledStateProvider =
WebtoonDoubleTapZoomEnabledStateProvider._();
final class WebtoonDoubleTapZoomEnabledStateProvider
extends $NotifierProvider<WebtoonDoubleTapZoomEnabledState, bool> {
WebtoonDoubleTapZoomEnabledStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'webtoonDoubleTapZoomEnabledStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$webtoonDoubleTapZoomEnabledStateHash();
@$internal
@override
WebtoonDoubleTapZoomEnabledState create() =>
WebtoonDoubleTapZoomEnabledState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$webtoonDoubleTapZoomEnabledStateHash() =>
r'2d1357a9dd732b7e5b8b99ec3f9d7d913bd70937';
abstract class _$WebtoonDoubleTapZoomEnabledState extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(ReaderHideThresholdState)
final readerHideThresholdStateProvider = ReaderHideThresholdStateProvider._();
final class ReaderHideThresholdStateProvider
extends $NotifierProvider<ReaderHideThresholdState, int> {
ReaderHideThresholdStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'readerHideThresholdStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$readerHideThresholdStateHash();
@$internal
@override
ReaderHideThresholdState create() => ReaderHideThresholdState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
}
String _$readerHideThresholdStateHash() =>
r'bf205952490921347c43288678e289686c985f11';
abstract class _$ReaderHideThresholdState extends $Notifier<int> {
int build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<int, int>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<int, int>,
int,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View file

@ -31,6 +31,21 @@ class ReaderScreen extends ConsumerWidget {
final showPageGaps = ref.watch(showPageGapsStateProvider);
final webtoonSidePadding = ref.watch(webtoonSidePaddingStateProvider);
final navigationLayout = ref.watch(readerNavigationLayoutStateProvider);
final navigateToPan = ref.watch(navigateToPanStateProvider);
final tappingInversion = ref.watch(tappingInversionStateProvider);
final flashOnPageChange = ref.watch(flashOnPageChangeStateProvider);
final flashDuration = ref.watch(flashDurationStateProvider);
final flashInterval = ref.watch(flashIntervalStateProvider);
final flashColor = ref.watch(flashColorStateProvider);
final showNavigationOverlayOnStart = ref.watch(
showNavigationOverlayOnStartStateProvider,
);
final webtoonDisableZoomOut = ref.watch(webtoonDisableZoomOutStateProvider);
final webtoonDoubleTapZoomEnabled = ref.watch(
webtoonDoubleTapZoomEnabledStateProvider,
);
final readerHideThreshold = ref.watch(readerHideThresholdStateProvider);
return Scaffold(
appBar: AppBar(title: Text(context.l10n.reader)),
body: SingleChildScrollView(
@ -474,6 +489,239 @@ class ReaderScreen extends ConsumerWidget {
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
SwitchListTile(
value: navigateToPan,
title: Text(context.l10n.navigate_to_pan),
subtitle: Text(context.l10n.navigate_to_pan_subtitle),
onChanged: (value) {
ref.read(navigateToPanStateProvider.notifier).set(value);
},
),
ListTile(
onTap: () {
showDialog(
context: context,
builder: (ctx) {
return SimpleDialog(
title: Text(context.l10n.tapping_inversion),
children: [
RadioGroup<int>(
groupValue: tappingInversion,
onChanged: (val) {
ref
.read(tappingInversionStateProvider.notifier)
.set(val!);
Navigator.pop(ctx);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<int>(
value: 0,
title: Text(
context.l10n.tapping_inversion_none,
),
),
RadioListTile<int>(
value: 1,
title: Text(
context.l10n.tapping_inversion_horizontal,
),
),
RadioListTile<int>(
value: 2,
title: Text(
context.l10n.tapping_inversion_vertical,
),
),
RadioListTile<int>(
value: 3,
title: Text(
context.l10n.tapping_inversion_both,
),
),
],
),
),
],
);
},
);
},
title: Text(context.l10n.tapping_inversion),
subtitle: Text(switch (tappingInversion) {
1 => context.l10n.tapping_inversion_horizontal,
2 => context.l10n.tapping_inversion_vertical,
3 => context.l10n.tapping_inversion_both,
_ => context.l10n.tapping_inversion_none,
}, style: TextStyle(fontSize: 11, color: context.secondaryColor)),
),
SwitchListTile(
value: webtoonDisableZoomOut,
title: Text(context.l10n.webtoon_disable_zoom_out),
onChanged: (value) {
ref
.read(webtoonDisableZoomOutStateProvider.notifier)
.set(value);
},
),
SwitchListTile(
value: webtoonDoubleTapZoomEnabled,
title: Text(context.l10n.webtoon_double_tap_zoom_enabled),
onChanged: (value) {
ref
.read(webtoonDoubleTapZoomEnabledStateProvider.notifier)
.set(value);
},
),
SwitchListTile(
value: flashOnPageChange,
title: Text(context.l10n.flash_on_page_change),
subtitle: Text(context.l10n.flash_on_page_change_subtitle),
onChanged: (value) {
ref.read(flashOnPageChangeStateProvider.notifier).set(value);
},
),
if (flashOnPageChange) ...[
ListTile(
title: Text(context.l10n.flash_color),
subtitle: Row(
children: [
ChoiceChip(
label: Text(context.l10n.flash_color_black),
selected: flashColor == 0,
onSelected: (val) {
if (val) {
ref.read(flashColorStateProvider.notifier).set(0);
}
},
),
const SizedBox(width: 8),
ChoiceChip(
label: Text(context.l10n.flash_color_white),
selected: flashColor == 1,
onSelected: (val) {
if (val) {
ref.read(flashColorStateProvider.notifier).set(1);
}
},
),
const SizedBox(width: 8),
ChoiceChip(
label: Text(context.l10n.flash_color_white_black),
selected: flashColor == 2,
onSelected: (val) {
if (val) {
ref.read(flashColorStateProvider.notifier).set(2);
}
},
),
],
),
),
ListTile(
title: Text(
context.l10n.flash_interval(flashInterval.toString()),
),
subtitle: Slider(
min: 1,
max: 10,
divisions: 9,
label: flashInterval.toString(),
value: flashInterval.toDouble(),
onChanged: (val) {
ref
.read(flashIntervalStateProvider.notifier)
.set(val.toInt());
},
),
),
ListTile(
title: Text(
context.l10n.flash_duration(flashDuration.toString()),
),
subtitle: Slider(
min: 50,
max: 500,
divisions: 9,
label: flashDuration.toString(),
value: flashDuration.toDouble(),
onChanged: (val) {
ref
.read(flashDurationStateProvider.notifier)
.set(val.toInt());
},
),
),
],
SwitchListTile(
value: showNavigationOverlayOnStart,
title: Text(context.l10n.show_navigation_overlay_on_start),
onChanged: (value) {
ref
.read(showNavigationOverlayOnStartStateProvider.notifier)
.set(value);
},
),
ListTile(
onTap: () {
showDialog(
context: context,
builder: (ctx) {
return SimpleDialog(
title: Text(context.l10n.reader_hide_threshold),
children: [
RadioGroup<int>(
groupValue: readerHideThreshold,
onChanged: (val) {
ref
.read(readerHideThresholdStateProvider.notifier)
.set(val!);
Navigator.pop(ctx);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<int>(
value: 0,
title: Text(
context.l10n.reader_hide_threshold_highest,
),
),
RadioListTile<int>(
value: 1,
title: Text(
context.l10n.reader_hide_threshold_high,
),
),
RadioListTile<int>(
value: 2,
title: Text(
context.l10n.reader_hide_threshold_low,
),
),
RadioListTile<int>(
value: 3,
title: Text(
context.l10n.reader_hide_threshold_lowest,
),
),
],
),
),
],
);
},
);
},
title: Text(context.l10n.reader_hide_threshold),
subtitle: Text(switch (readerHideThreshold) {
0 => context.l10n.reader_hide_threshold_highest,
1 => context.l10n.reader_hide_threshold_high,
2 => context.l10n.reader_hide_threshold_low,
_ => context.l10n.reader_hide_threshold_lowest,
}, style: TextStyle(fontSize: 11, color: context.secondaryColor)),
),
],
),
),

View file

@ -55,9 +55,8 @@ Future<StatisticsData> getStatistics(
final downloadedCount = await isar.downloads
.filter()
.chapter(
(q) => q.manga(
(m) => m.favoriteEqualTo(true).itemTypeEqualTo(itemType),
),
(q) =>
q.manga((m) => m.favoriteEqualTo(true).itemTypeEqualTo(itemType)),
)
.isDownloadEqualTo(true)
.count();
@ -76,10 +75,7 @@ Future<StatisticsData> getStatistics(
int completedItems = 0;
for (final id in completedFavouriteIds) {
final total = await isar.chapters
.filter()
.mangaIdEqualTo(id)
.count();
final total = await isar.chapters.filter().mangaIdEqualTo(id).count();
if (total == 0) continue;
final unread = await isar.chapters
.filter()

View file

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_qjs/quickjs/ffi.dart';
@ -108,7 +107,6 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
_ttsWordSub?.cancel();
_ttsProgress.dispose();
NovelTtsService.instance.stop();
clearGestureDetailsCache();
if (isDesktop) {
setFullScreen(value: false);
} else {

View file

@ -1,12 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `crop_image`
Uint8List processCropImage({required List<int> image}) =>
RustLib.instance.api.crateApiImageProcessCropImage(image: image);

View file

@ -4,7 +4,6 @@
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'api/epub.dart';
import 'api/image.dart';
import 'api/rhttp/client.dart';
import 'api/rhttp/error.dart';
import 'api/rhttp/http.dart';
@ -69,7 +68,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => 2140434025;
int get rustContentHash => -1109570704;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@ -128,8 +127,6 @@ abstract class RustLibApi extends BaseApi {
required bool fullData,
});
Uint8List crateApiImageProcessCropImage({required List<int> image});
Future<RequestClient> crateApiRhttpHttpRegisterClient({
required ClientSettings settings,
});
@ -534,29 +531,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["epubPath", "fullData"],
);
@override
Uint8List crateApiImageProcessCropImage({required List<int> image}) {
return handler.executeSync(
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_list_prim_u_8_loose(image, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: null,
),
constMeta: kCrateApiImageProcessCropImageConstMeta,
argValues: [image],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiImageProcessCropImageConstMeta =>
const TaskConstMeta(debugName: "process_crop_image", argNames: ["image"]);
@override
Future<RequestClient> crateApiRhttpHttpRegisterClient({
required ClientSettings settings,
@ -569,7 +543,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 12,
funcId: 11,
port: port_,
);
},
@ -597,7 +571,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_box_autoadd_client_settings(settings, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!;
},
codec: SseCodec(
decodeSuccessData:

View file

@ -4,7 +4,6 @@
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'api/epub.dart';
import 'api/image.dart';
import 'api/rhttp/client.dart';
import 'api/rhttp/error.dart';
import 'api/rhttp/http.dart';

View file

@ -7,7 +7,6 @@
// ignore_for_file: argument_type_not_assignable
import 'api/epub.dart';
import 'api/image.dart';
import 'api/rhttp/client.dart';
import 'api/rhttp/error.dart';
import 'api/rhttp/http.dart';

View file

@ -1,14 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:extended_image/extended_image.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/utils/headers.dart';
@ -76,6 +76,30 @@ extension ImageProviderExtension on ImageProvider {
}
extension UChapDataPreloadExtensions on UChapDataPreload {
Future<String?> get getLocalFilePath async {
if (isTransitionPage) return null;
if (archiveImage != null) {
final tempDir = Directory.systemTemp;
final tempFile = File(
p.join(tempDir.path, 'mangayomi_archive_${index ?? pageIndex}.jpg'),
);
if (!tempFile.existsSync()) {
tempFile.writeAsBytesSync(archiveImage!);
}
return tempFile.path;
}
if (isLocale == true && directory != null && index != null) {
return p.join(directory!.path, "${padIndex(index!)}.jpg");
}
if (pageUrl != null) {
final cachedImage = await _getCachedImageFile(pageUrl!.url);
if (cachedImage != null) {
return cachedImage.path;
}
}
return null;
}
Future<Uint8List?> get getImageBytes async {
Uint8List? imageBytes;
if (archiveImage != null) {
@ -111,37 +135,34 @@ extension UChapDataPreloadExtensions on UChapDataPreload {
final isLocale = data.isLocale!;
final archiveImage = data.archiveImage;
final cropBorders = ref.watch(cropBordersStateProvider);
return cropBorders && data.cropImage != null
? ExtendedMemoryImageProvider(data.cropImage!)
: (isLocale
? archiveImage != null
? ExtendedMemoryImageProvider(archiveImage)
: ExtendedFileImageProvider(
File(
p.join(
data.directory!.path,
"${padIndex(data.index!)}.jpg",
),
),
)
: CustomExtendedNetworkImageProvider(
data.pageUrl!.url.trim(),
cache: true,
cacheMaxAge: const Duration(days: 7),
showCloudFlareError: showCloudFlareError,
imageCacheFolderName: "cacheimagemanga",
headers: {
...data.pageUrl!.headers ?? {},
...ref.watch(
headersProvider(
source: data.chapter!.manga.value!.source!,
lang: data.chapter!.manga.value!.lang!,
sourceId: data.chapter!.manga.value!.sourceId,
),
),
},
))
return isLocale
? archiveImage != null
? MemoryImage(archiveImage)
: FileImage(
File(
p.join(
data.directory!.path,
"${padIndex(data.index!)}.jpg",
),
),
)
: CustomExtendedNetworkImageProvider(
data.pageUrl!.url.trim(),
cache: true,
cacheMaxAge: const Duration(days: 7),
showCloudFlareError: showCloudFlareError,
imageCacheFolderName: "cacheimagemanga",
headers: {
...data.pageUrl!.headers ?? {},
...ref.watch(
headersProvider(
source: data.chapter!.manga.value!.source!,
lang: data.chapter!.manga.value!.lang!,
sourceId: data.chapter!.manga.value!.sourceId,
),
),
},
)
as ImageProvider<Object>;
}
}
@ -163,3 +184,6 @@ Future<File?> _getCachedImageFile(String url, {String? cacheKey}) async {
}
return null;
}
/// get md5 from key
String keyToMd5(String key) => md5.convert(utf8.encode(key)).toString();

View file

@ -210,7 +210,7 @@ packages:
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
dependency: "direct main"
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
@ -325,10 +325,10 @@ packages:
dependency: transitive
description:
name: dbus
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91"
url: "https://pub.dev"
source: hosted
version: "0.7.14"
version: "0.7.13"
desktop_webview_window:
dependency: "direct main"
description:
@ -739,7 +739,7 @@ packages:
source: hosted
version: "1.1.0"
hooks:
dependency: transitive
dependency: "direct main"
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
@ -798,10 +798,10 @@ packages:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
url: "https://pub.dev"
source: hosted
version: "4.8.0"
version: "4.9.1"
infinite_listview:
dependency: transitive
description:
@ -1140,6 +1140,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.6.4"
native_toolchain_c:
dependency: "direct main"
description:
name: native_toolchain_c
sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
url: "https://pub.dev"
source: hosted
version: "0.19.2"
nm:
dependency: transitive
description:
@ -2004,10 +2012,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
version: "7.0.1"
xpath_selector:
dependency: transitive
description:
@ -2049,5 +2057,5 @@ packages:
source: hosted
version: "2.2.4"
sdks:
dart: ">=3.12.0 <4.0.0"
dart: ">=3.12.2 <4.0.0"
flutter: ">=3.44.0"

View file

@ -5,7 +5,7 @@ publish_to: "none"
version: 0.7.80+107
environment:
sdk: ^3.12.0
sdk: ^3.12.2
dependencies:
flutter:
@ -107,6 +107,9 @@ dependencies:
git:
url: https://github.com/kodjodevf/m_extension_server.git
flutter_tts: ^4.2.5
code_assets: ^1.2.1
hooks: ^2.0.2
native_toolchain_c: ^0.19.2
dependency_overrides:
ffi: ^2.1.3

652
rust/Cargo.lock generated
View file

@ -43,30 +43,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "aligned"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
dependencies = [
"as-slice",
]
[[package]]
name = "aligned-vec"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
[[package]]
name = "aligned-vec"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
dependencies = [
"equator",
]
[[package]]
name = "allo-isolate"
version = "0.1.26"
@ -135,32 +111,6 @@ dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-slice"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "async-compression"
version = "0.4.15"
@ -193,49 +143,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "av-scenechange"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
dependencies = [
"aligned",
"anyhow",
"arg_enum_proc_macro",
"arrayvec",
"log",
"num-rational",
"num-traits",
"pastey",
"rayon",
"thiserror 2.0.18",
"v_frame",
"y4m",
]
[[package]]
name = "av1-grain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
dependencies = [
"anyhow",
"arrayvec",
"log",
"nom",
"num-rational",
"v_frame",
]
[[package]]
name = "avif-serialize"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f"
dependencies = [
"arrayvec",
]
[[package]]
name = "aws-lc-rs"
version = "1.16.2"
@ -279,27 +186,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bitstream-io"
version = "4.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
dependencies = [
"core2",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -336,12 +228,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b"
[[package]]
name = "built"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
[[package]]
name = "bumpalo"
version = "3.19.1"
@ -360,12 +246,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.7.1"
@ -443,12 +323,6 @@ dependencies = [
"cc",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "combine"
version = "4.6.7"
@ -491,15 +365,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
dependencies = [
"memchr",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@ -518,37 +383,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -635,12 +469,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
[[package]]
name = "encoding_rs"
version = "0.8.34"
@ -673,76 +501,12 @@ dependencies = [
"zip 3.0.0",
]
[[package]]
name = "equator"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
dependencies = [
"equator-macro",
]
[[package]]
name = "equator-macro"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "exr"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
dependencies = [
"bit_field",
"half",
"lebe",
"miniz_oxide 0.8.9",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fax"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
dependencies = [
"fax_derive",
]
[[package]]
name = "fax_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "fdeflate"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
dependencies = [
"simd-adler32",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@ -971,16 +735,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gif"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gimli"
version = "0.28.1"
@ -1006,16 +760,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@ -1204,46 +948,6 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"exr",
"gif",
"image-webp",
"moxcms",
"num-traits",
"png",
"qoi",
"ravif",
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
name = "imgref"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
[[package]]
name = "indexmap"
version = "2.13.1"
@ -1265,17 +969,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "ipnet"
version = "2.10.0"
@ -1292,15 +985,6 @@ dependencies = [
"serde",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
@ -1384,12 +1068,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libbz2-rs-sys"
version = "0.2.2"
@ -1402,17 +1080,6 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libfuzzer-sys"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
dependencies = [
"arbitrary",
"cc",
"once_cell",
]
[[package]]
name = "libz-rs-sys"
version = "0.5.5"
@ -1438,15 +1105,6 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "loop9"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
dependencies = [
"imgref",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
@ -1462,16 +1120,6 @@ dependencies = [
"sha2",
]
[[package]]
name = "maybe-rayon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"rayon",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -1504,12 +1152,6 @@ dependencies = [
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@ -1541,85 +1183,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "num-bigint"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1694,18 +1263,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@ -1740,19 +1297,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "png"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.9",
]
[[package]]
name = "portable-atomic"
version = "1.9.0"
@ -1796,49 +1340,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "profiling"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "pxfm"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
dependencies = [
"num-traits",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quinn"
version = "0.11.9"
@ -1944,76 +1445,6 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rav1e"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
dependencies = [
"aligned-vec 0.6.4",
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av-scenechange",
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"interpolate_name",
"itertools",
"libc",
"libfuzzer-sys",
"log",
"maybe-rayon",
"new_debug_unreachable",
"noop_proc_macro",
"num-derive",
"num-traits",
"paste",
"profiling",
"rand",
"rand_chacha",
"simd_helpers",
"thiserror 2.0.18",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error",
"rav1e",
"rayon",
"rgb",
]
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.3"
@ -2094,12 +1525,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
[[package]]
name = "ring"
version = "0.17.8"
@ -2123,7 +1548,6 @@ dependencies = [
"epub",
"flutter_rust_bridge",
"futures-util",
"image",
"reqwest",
"tokio",
"tokio-util",
@ -2367,15 +1791,6 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_helpers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
"quote",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -2417,12 +1832,6 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "subtle"
version = "2.6.1"
@ -2498,20 +1907,6 @@ dependencies = [
"num_cpus",
]
[[package]]
name = "tiff"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg",
]
[[package]]
name = "time"
version = "0.3.47"
@ -2739,17 +2134,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "v_frame"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
dependencies = [
"aligned-vec 0.5.0",
"num-traits",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.4"
@ -2930,12 +2314,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8"
[[package]]
name = "winapi-util"
version = "0.1.11"
@ -3294,12 +2672,6 @@ dependencies = [
"xml",
]
[[package]]
name = "y4m"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]]
name = "zeroize"
version = "1.8.1"
@ -3392,27 +2764,3 @@ dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]

View file

@ -8,7 +8,6 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
flutter_rust_bridge = { version = "=2.12.0", features = ["chrono"] }
image = "0.25.10"
chrono = "0.4.44"
futures-util = "0.3.31"
reqwest = { version = "0.13.2", default-features = false, features = ["charset", "http2", "rustls", "stream", "multipart", "socks", "brotli", "gzip"] }

View file

@ -1,70 +0,0 @@
use flutter_rust_bridge::frb;
use image::{DynamicImage, GenericImageView, Pixel};
use std::io::Cursor;
fn crop_image(image: Vec<u8>) -> DynamicImage {
let mut decoded_image =
image::load_from_memory(image.as_slice()).expect(&format!("Failed to load image"));
let (width, height) = decoded_image.dimensions();
let mut left = width as u32;
let mut top = height as u32;
let mut right = 0;
let mut bottom = 0;
for y in 0..height {
for x in 0..width {
let pixel = decoded_image.get_pixel(x, y);
let channels = pixel.channels();
let alpha = channels[3] as u32;
let red = channels[0] as u32 + 1;
let green = channels[1] as u32 + 2;
let blue = channels[2] as u32 + 3;
// Crop transparent pixels
if alpha == 0 {
continue;
}
// Crop white pixels
if red > 0xAA && green > 0xAA && blue > 0xAA {
continue;
}
// Crop black pixels
if red < 0x05 && green < 0x05 && blue < 0x05 {
continue;
}
if x < left {
left = x;
}
if x > right {
right = x;
}
if y < top {
top = y;
}
if y > bottom {
bottom = y;
}
}
}
if left == width && top == height && right == 0 && bottom == 0 {
return decoded_image;
}
return decoded_image.crop(left, top, right - left + 1, bottom - top + 1);
}
#[frb(sync)]
pub fn process_crop_image(image: Vec<u8>) -> Vec<u8> {
let res = crop_image(image);
let mut image_data: Vec<u8> = Vec::new();
res.write_to(&mut Cursor::new(&mut image_data), image::ImageFormat::Png)
.unwrap();
return image_data;
}

View file

@ -3,7 +3,6 @@
//
pub mod epub;
pub mod image;
pub mod rhttp;
pub use epub::{EpubChapter, EpubNovel, EpubResource};

View file

@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 2140434025;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1109570704;
// Section: executor
@ -383,37 +383,6 @@ fn wire__crate__api__epub__parse_epub_from_path_impl(
},
)
}
fn wire__crate__api__image__process_crop_image_impl(
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "process_crop_image",
port: None,
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_image = <Vec<u8>>::sse_decode(&mut deserializer);
deserializer.end();
transform_result_sse::<_, ()>((move || {
let output_ok =
Result::<_, ()>::Ok(crate::api::image::process_crop_image(api_image))?;
Ok(output_ok)
})())
},
)
}
fn wire__crate__api__rhttp__http__register_client_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -1481,7 +1450,7 @@ fn pde_ffi_dispatcher_primary_impl(
),
9 => wire__crate__api__epub__parse_epub_from_bytes_impl(port, ptr, rust_vec_len, data_len),
10 => wire__crate__api__epub__parse_epub_from_path_impl(port, ptr, rust_vec_len, data_len),
12 => {
11 => {
wire__crate__api__rhttp__http__register_client_impl(port, ptr, rust_vec_len, data_len)
}
_ => unreachable!(),
@ -1506,8 +1475,7 @@ fn pde_ffi_dispatcher_sync_impl(
rust_vec_len,
data_len,
),
11 => wire__crate__api__image__process_crop_image_impl(ptr, rust_vec_len, data_len),
13 => wire__crate__api__rhttp__http__register_client_sync_impl(ptr, rust_vec_len, data_len),
12 => wire__crate__api__rhttp__http__register_client_sync_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}