mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-07-26 15:12:19 +00:00
feat: Implement tiling engine for image rendering in manga reader, more features, more optimizations
This commit is contained in:
parent
c130260876
commit
45ec2d1c8a
79 changed files with 22140 additions and 11788 deletions
27
hook/build.dart
Normal file
27
hook/build.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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
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
28
lib/ffi/image_decoder.h
Normal 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_
|
||||
1780
lib/l10n/app_ar.arb
1780
lib/l10n/app_ar.arb
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
1721
lib/l10n/app_de.arb
1721
lib/l10n/app_de.arb
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
1775
lib/l10n/app_es.arb
1775
lib/l10n/app_es.arb
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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": "त्रुटि: कोई पन्ने उपलब्ध नहीं हैं"
|
||||
}
|
||||
1714
lib/l10n/app_id.arb
1714
lib/l10n/app_id.arb
File diff suppressed because it is too large
Load diff
1714
lib/l10n/app_it.arb
1714
lib/l10n/app_it.arb
File diff suppressed because it is too large
Load diff
|
|
@ -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": "エラー:利用可能なページがありません"
|
||||
}
|
||||
1773
lib/l10n/app_pt.arb
1773
lib/l10n/app_pt.arb
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1777
lib/l10n/app_ru.arb
1777
lib/l10n/app_ru.arb
File diff suppressed because it is too large
Load diff
|
|
@ -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": "เกิดข้อผิดพลาด: ไม่มีหน้าที่พร้อมใช้งาน"
|
||||
}
|
||||
1714
lib/l10n/app_tr.arb
1714
lib/l10n/app_tr.arb
File diff suppressed because it is too large
Load diff
1781
lib/l10n/app_zh.arb
1781
lib/l10n/app_zh.arb
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 => 'خطأ: لا توجد صفحات متاحة';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => 'त्रुटि: कोई पन्ने उपलब्ध नहीं हैं';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => 'エラー:利用可能なページがありません';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => 'Ошибка: нет доступных страниц';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
'เกิดข้อผิดพลาด: ไม่มีหน้าที่พร้อมใช้งาน';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => '错误:无可用页面';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
205
lib/modules/manga/reader/widgets/navigation_overlay.dart
Normal file
205
lib/modules/manga/reader/widgets/navigation_overlay.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
26
pubspec.lock
26
pubspec.lock
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
652
rust/Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
pub mod epub;
|
||||
pub mod image;
|
||||
pub mod rhttp;
|
||||
|
||||
pub use epub::{EpubChapter, EpubNovel, EpubResource};
|
||||
|
|
|
|||
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue