├── .fvm └── fvm_config.json ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── release.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── README_cn.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-playstore.png │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-playstore.png │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── honjow │ │ │ │ └── eros_n │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── changelog ├── v1.0.0+1.md ├── v1.0.10+10.md ├── v1.0.11+11.md ├── v1.0.12+12.md ├── v1.0.13+13.md ├── v1.0.14+14.md ├── v1.0.15+15.md ├── v1.0.16+16.md ├── v1.0.17+17.md ├── v1.0.18+18.md ├── v1.0.19+19.md ├── v1.0.2+2.md ├── v1.0.20+20.md ├── v1.0.3+3.md ├── v1.0.4+4.md ├── v1.0.5+5.md ├── v1.0.6+6.md ├── v1.0.7+7.md ├── v1.0.8+8.md └── v1.0.9+9.md ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon.png │ │ ├── icon_20pt.png │ │ ├── icon_20pt@2x 1.png │ │ ├── icon_20pt@2x.png │ │ ├── icon_20pt@3x.png │ │ ├── icon_29pt 1.png │ │ ├── icon_29pt.png │ │ ├── icon_29pt@2x 1.png │ │ ├── icon_29pt@2x.png │ │ ├── icon_29pt@3x.png │ │ ├── icon_40pt.png │ │ ├── icon_40pt@2x 1.png │ │ ├── icon_40pt@2x.png │ │ ├── icon_40pt@3x.png │ │ ├── icon_60pt@2x.png │ │ ├── icon_60pt@3x.png │ │ ├── icon_76pt.png │ │ ├── icon_76pt@2x.png │ │ └── icon_83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── common │ ├── const │ │ └── const.dart │ ├── enum.dart │ ├── extension.dart │ ├── global.dart │ ├── parser │ │ ├── paese_user_page.dart │ │ ├── parse_gallery_detail.dart │ │ ├── parse_gallery_image.dart │ │ ├── parse_gallery_list.dart │ │ ├── parse_info.dart │ │ ├── parse_login_page.dart │ │ └── parser.dart │ └── provider │ │ ├── palette_generator.dart │ │ ├── receive_sharing_provider.dart │ │ ├── settings_provider.dart │ │ ├── tag_translate_provider.dart │ │ └── theme_provider.dart ├── component │ ├── converter │ │ └── locale_converter.dart │ ├── dialog │ │ └── cf_dialog.dart │ ├── exception │ │ └── error.dart │ ├── models │ │ ├── comment.dart │ │ ├── comment.freezed.dart │ │ ├── comment.g.dart │ │ ├── comment_poster.dart │ │ ├── comment_poster.freezed.dart │ │ ├── comment_poster.g.dart │ │ ├── gallery.dart │ │ ├── gallery.freezed.dart │ │ ├── gallery.g.dart │ │ ├── gallery_images.dart │ │ ├── gallery_images.freezed.dart │ │ ├── gallery_images.g.dart │ │ ├── gallery_search.dart │ │ ├── gallery_search.freezed.dart │ │ ├── gallery_search.g.dart │ │ ├── gallery_set.dart │ │ ├── gallery_set.freezed.dart │ │ ├── gallery_set.g.dart │ │ ├── gallery_title.dart │ │ ├── gallery_title.freezed.dart │ │ ├── gallery_title.g.dart │ │ ├── image.dart │ │ ├── image.freezed.dart │ │ ├── image.g.dart │ │ ├── index.dart │ │ ├── settings.dart │ │ ├── settings.freezed.dart │ │ ├── settings.g.dart │ │ ├── tag.dart │ │ ├── tag.freezed.dart │ │ ├── tag.g.dart │ │ ├── tag_translate_info.dart │ │ ├── tag_translate_info.freezed.dart │ │ ├── tag_translate_info.g.dart │ │ ├── user.dart │ │ ├── user.freezed.dart │ │ └── user.g.dart │ ├── theme │ │ └── theme.dart │ └── widget │ │ ├── blur_image.dart │ │ ├── broken_shield.dart │ │ ├── buttons.dart │ │ ├── desktop.dart │ │ ├── eros_cached_network_image.dart │ │ ├── gallery_image │ │ ├── _image_loader.dart │ │ ├── gallery_image_platform_interface.dart │ │ ├── gallery_image_provider.dart │ │ ├── gallery_image_web.dart │ │ └── multi_image_stream_completer.dart │ │ ├── preload_photo_view_gallery.dart │ │ ├── scroll.dart │ │ ├── scrolling_fab.dart │ │ ├── sliver.dart │ │ ├── sys_title.dart │ │ ├── system_ui_overlay.dart │ │ └── web_view.dart ├── generated │ ├── intl │ │ ├── messages_all.dart │ │ ├── messages_en.dart │ │ └── messages_zh_CN.dart │ └── l10n.dart ├── l10n │ ├── intl_en.arb │ └── intl_zh_CN.arb ├── main.dart ├── network │ ├── api.dart │ ├── app_dio │ │ ├── app_dio.dart │ │ ├── dio_file_service.dart │ │ ├── dio_http_cli.dart │ │ ├── dio_through.dart │ │ ├── dio_user_agent.dart │ │ ├── exception.dart │ │ ├── http_config.dart │ │ ├── http_response.dart │ │ ├── http_transformer.dart │ │ └── pdio.dart │ └── request.dart ├── pages │ ├── enum.dart │ ├── gallery │ │ ├── comments_page.dart │ │ ├── gallery_page_state.dart │ │ ├── gallery_page_state.freezed.dart │ │ ├── gallery_provider.dart │ │ ├── gallery_view.dart │ │ └── thumb_page.dart │ ├── list_view │ │ ├── item │ │ │ ├── item_base.dart │ │ │ ├── item_grid_card.dart │ │ │ ├── item_list_card.dart │ │ │ └── item_waterfall_flow_card.dart │ │ └── list_view.dart │ ├── nav │ │ ├── favorite │ │ │ ├── favorite_provider.dart │ │ │ └── favorite_view.dart │ │ ├── front │ │ │ ├── front_provider.dart │ │ │ ├── front_view.dart │ │ │ ├── list_view_state.dart │ │ │ ├── list_view_state.freezed.dart │ │ │ └── list_view_state.g.dart │ │ ├── history │ │ │ ├── history_provider.dart │ │ │ └── history_view.dart │ │ ├── index │ │ │ ├── index_provider.dart │ │ │ ├── index_state.dart │ │ │ └── index_view.dart │ │ ├── more │ │ │ ├── more_state.dart │ │ │ └── more_view.dart │ │ └── search │ │ │ ├── search_provider.dart │ │ │ └── search_view.dart │ ├── read │ │ ├── read_provider.dart │ │ ├── read_state.dart │ │ ├── read_state.freezed.dart │ │ ├── read_state.g.dart │ │ ├── read_view.dart │ │ └── read_widget.dart │ ├── setting │ │ ├── about_page.dart │ │ ├── advanced_setting_page.dart │ │ ├── appearance_setting_page.dart │ │ ├── general_setting_page.dart │ │ ├── license_page.dart │ │ ├── read_setting_page.dart │ │ ├── setting_base.dart │ │ └── settings_page.dart │ ├── splash │ │ └── splash_view.dart │ ├── user │ │ ├── login_page.dart │ │ ├── user_provider.dart │ │ └── web_login_page.dart │ └── webview │ │ └── webview.dart ├── routes │ ├── routes.dart │ └── routes.gr.dart ├── store │ ├── db │ │ ├── entity │ │ │ ├── gallery_history.dart │ │ │ ├── gallery_history.g.dart │ │ │ ├── nh_tag.dart │ │ │ ├── nh_tag.g.dart │ │ │ ├── tag_translate.dart │ │ │ └── tag_translate.g.dart │ │ ├── isar.dart │ │ └── isar_helper.dart │ └── kv │ │ └── hive.dart └── utils │ ├── clipboard_helper.dart │ ├── eros_utils.dart │ ├── get_utils │ ├── extensions │ │ ├── context_extensions.dart │ │ ├── double_extensions.dart │ │ ├── duration_extensions.dart │ │ ├── export.dart │ │ ├── iterable_extensions.dart │ │ ├── num_extensions.dart │ │ ├── string_extensions.dart │ │ └── widget_extensions.dart │ ├── get_utils.dart │ ├── get_utils │ │ └── get_utils.dart │ ├── platform │ │ ├── platform.dart │ │ ├── platform_io.dart │ │ └── platform_web.dart │ └── queue │ │ └── get_queue.dart │ ├── logger.dart │ ├── logger │ └── pretty_printer.dart │ └── toast.dart ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── app_icon_1024.png │ │ ├── app_icon_128.png │ │ ├── app_icon_16.png │ │ ├── app_icon_256.png │ │ ├── app_icon_32.png │ │ ├── app_icon_512.png │ │ └── app_icon_64.png │ ├── Base.lproj │ └── MainMenu.xib │ ├── Configs │ ├── AppInfo.xcconfig │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── comment_1.jpg ├── gallery_1.jpg ├── gallery_2.jpg ├── history_1.jpg └── home_1.jpg ├── scripts └── thin-payload.sh ├── test ├── model_test.dart ├── network_test.dart ├── nhapi_test.dart └── widget_test.dart └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "stable", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Always perform LF normalization 5 | *.dart text 6 | *.gradle text 7 | *.html text 8 | *.java text 9 | *.json text 10 | *.md text 11 | *.py text 12 | *.sh text 13 | *.txt text 14 | *.xml text 15 | *.yaml text 16 | 17 | # Make sure that these Windows files always have CRLF line endings at checkout 18 | *.bat text eol=crlf 19 | *.ps1 text eol=crlf 20 | *.rc text eol=crlf 21 | *.sln text eol=crlf 22 | *.props text eol=crlf 23 | *.vcxproj text eol=crlf 24 | *.vcxproj.filters text eol=crlf 25 | # Including templates 26 | *.sln.tmpl text eol=crlf 27 | *.props.tmpl text eol=crlf 28 | *.vcxproj.tmpl text eol=crlf 29 | 30 | # Never perform LF normalization 31 | *.ico binary 32 | *.jar binary 33 | *.png binary 34 | *.zip binary 35 | *.ttf binary 36 | *.otf binary 37 | 38 | 39 | GoogleService-Info.plist filter=git-crypt diff=git-crypt binary 40 | google-service.json filter=git-crypt diff=git-crypt binary 41 | /lib/firebase_options.dart filter=git-crypt diff=git-crypt binary 42 | /lib/config/config.dart filter=git-crypt diff=git-crypt binary -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "gradle" 10 | directory: "/android" 11 | schedule: 12 | interval: "daily" 13 | 14 | - package-ecosystem: "pub" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | GPUCache/ 47 | .fvm/flutter_sdk 48 | 49 | .local-chromium/ 50 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 8 | channel: unknown 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 17 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 18 | - platform: windows 19 | create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 20 | base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eros-N 2 | English | [简体中文](https://github.com/honjow/eros_n/blob/master/README_cn.md) 3 | 4 | [![](https://img.shields.io/github/downloads/honjow/eros_n/total.svg)](https://gitHub.com/honjow/eros_n/releases) 5 | [![](https://img.shields.io/github/downloads/honjow/eros_n/latest/total)](https://github.com/honjow/eros_n/releases/latest) 6 | [![](https://img.shields.io/github/v/release/honjow/eros_n)](https://github.com/honjow/eros_n/releases/latest) 7 | [![](https://img.shields.io/github/stars/honjow/eros_n)]() 8 | [![Telegram](https://img.shields.io/badge/chat-on%20Telegram-blue.svg)](https://t.me/joinchat/AEj27KMQe0JiMmUx) 9 | 10 | ## Introduction 11 | An unofficial Nhentai app. 12 | 13 | ## Features 14 | - [x] Front page 15 | - [x] User login 16 | - [x] Favorites 17 | - [x] View gallery 18 | - [x] Gallery tags 19 | - [ ] More view settings 20 | - [ ] Download gallery 21 | - [x] Similar galleries 22 | - [x] Gallery comments 23 | - [x] Post comments 24 | - [x] History 25 | - [x] Search 26 | - [ ] More settings 27 | 28 | ## Windows platform requirements 29 | - [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) 30 | - [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170) 31 | - Windows 10 1809+ 32 | 33 | 34 | ## Screenshots 35 | | | | | 36 | |:--------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------:| 37 | | Front![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/home_1.jpg) | Gallery![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/gallery_1.jpg) | Gallery![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/gallery_2.jpg) | 38 | | Comment![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/comment_1.jpg) | History![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/history_1.jpg) || 39 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # Eros-N 2 | [English](https://github.com/honjow/eros_n/blob/master/README.md) | 简体中文 3 | 4 | [![](https://img.shields.io/github/downloads/honjow/eros_n/total.svg)](https://gitHub.com/honjow/eros_n/releases) 5 | [![](https://img.shields.io/github/downloads/honjow/eros_n/latest/total)](https://github.com/honjow/eros_n/releases/latest) 6 | [![](https://img.shields.io/github/v/release/honjow/eros_n)](https://github.com/honjow/eros_n/releases/latest) 7 | [![](https://img.shields.io/github/stars/honjow/eros_n)]() 8 | [![Telegram](https://img.shields.io/badge/chat-on%20Telegram-blue.svg)](https://t.me/joinchat/AEj27KMQe0JiMmUx) 9 | 10 | 11 | ## 应用简介 12 | 一个第三方nh客户端. 13 | 14 | ## 功能 15 | - [x] 首页 16 | - [x] 用户登录 17 | - [x] 收藏列表 18 | - [x] 查看画廊 19 | - [x] 画廊标签 (翻译) 20 | - [ ] 更多查看设置 21 | - [ ] 下载画廊 22 | - [x] 相似画廊 23 | - [x] 画廊评论 24 | - [x] 发布评论 25 | - [x] 画廊历史 26 | - [x] 搜索 27 | - [ ] 更多设置 28 | 29 | ## Windows 平台需求 30 | - [WebView2 运行时](https://developer.microsoft.com/zh-CN/microsoft-edge/webview2/) 31 | - [Microsoft Visual C++ 可再发行程序包](https://learn.microsoft.com/zh-CN/cpp/windows/latest-supported-vc-redist?view=msvc-170) 32 | - Windows 10 1809+ 33 | 34 | ## 截图 35 | | | | | 36 | |:---------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:| 37 | | 首页![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/home_1.jpg) | 画廊![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/gallery_1.jpg) | 画廊![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/gallery_2.jpg) | 38 | | 评论![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/comment_1.jpg) | 历史![](https://raw.githubusercontent.com/honjow/eros_n/master/screenshots/history_1.jpg) || 39 | 40 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | keystore.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/debug/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/debug/res/drawable-v24/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /android/app/src/debug/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 8 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/honjow/eros_n/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.honjow.eros_n 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import androidx.core.view.WindowCompat 6 | import io.flutter.embedding.android.FlutterFragmentActivity 7 | import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownFragmentActivity 8 | 9 | class MainActivity: FlutterAndroidVolumeKeydownFragmentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | WindowCompat.setDecorFitsSystemWindows(getWindow(), false) 12 | 13 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 14 | // Disable the Android splash screen fade out animation to avoid 15 | // a flicker before the similar frame is drawn in Flutter. 16 | splashScreen.setOnExitAnimationListener { splashScreenView -> splashScreenView.remove() } 17 | } 18 | 19 | super.onCreate(savedInstanceState) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /changelog/v1.0.0+1.md: -------------------------------------------------------------------------------- 1 | # Eros-N 2 | 一个第三方nh客户端 3 | ## 初版发布 4 | - [x] 首页 5 | - [x] 用户登录 6 | - [x] 收藏列表 7 | - [x] 查看画廊 8 | - [ ] 画廊TAG 9 | - [ ] 更多查看模式设置 10 | - [ ] 下载画廊 11 | - [x] 相似画廊 12 | - [ ] 画廊评论 13 | - [ ] 画廊历史 14 | - [ ] 搜索 15 | - [ ] 更多设置 16 | -------------------------------------------------------------------------------- /changelog/v1.0.10+10.md: -------------------------------------------------------------------------------- 1 | # Fix 2 | - 修正iOS上部分非预期滚动效果 3 | - 优化web登陆响应速度,避免长时间等待 4 | - 优化cf过盾速度 5 | # Feature 6 | - 添加使用封面色调着色功能 7 | - 添加评论发布功能 -------------------------------------------------------------------------------- /changelog/v1.0.11+11.md: -------------------------------------------------------------------------------- 1 | # Fix 2 | - 修正webtoon等垂直阅读模式缩放问题 3 | # Feature 4 | - 外部链接打开画廊功能 5 | - 音量键翻页 6 | - 历史记录搜索 -------------------------------------------------------------------------------- /changelog/v1.0.12+12.md: -------------------------------------------------------------------------------- 1 | # Feature 2 | - 自动阅读 -------------------------------------------------------------------------------- /changelog/v1.0.13+13.md: -------------------------------------------------------------------------------- 1 | # Feature 2 | - 为首页添加语言筛选和排序 3 | --- 4 | 5 | - Add language filtering and sorting to the front page -------------------------------------------------------------------------------- /changelog/v1.0.14+14.md: -------------------------------------------------------------------------------- 1 | # Feature 2 | - 修复网页登陆问题 3 | --- 4 | 5 | - Fix web login bug -------------------------------------------------------------------------------- /changelog/v1.0.15+15.md: -------------------------------------------------------------------------------- 1 | # Feature 2 | - 搜索添加语言过滤按钮 3 | - 增加检测剪贴板画廊链接功能(默认关闭) -------------------------------------------------------------------------------- /changelog/v1.0.16+16.md: -------------------------------------------------------------------------------- 1 | # Feature 2 | - 搜索添加popular_month排序 3 | 4 | ---- 5 | 6 | - add popular_month sort -------------------------------------------------------------------------------- /changelog/v1.0.17+17.md: -------------------------------------------------------------------------------- 1 | - 过滤器切换加载指示 2 | - 优化通过外部url打开画廊的场景 -------------------------------------------------------------------------------- /changelog/v1.0.18+18.md: -------------------------------------------------------------------------------- 1 | ## Fix 2 | - 修复通过外部url冷启动app 3 | 4 | ## Feature 5 | - 画廊页大屏布局,缩略图显示在右侧 -------------------------------------------------------------------------------- /changelog/v1.0.19+19.md: -------------------------------------------------------------------------------- 1 | - 更新Flutter3.7 -------------------------------------------------------------------------------- /changelog/v1.0.2+2.md: -------------------------------------------------------------------------------- 1 | - 修正iOS不能正常载入的问题 2 | - 修正不能从历史记录正常进入画廊的问题 3 | - 中文翻译更新 4 | - 历史记录按天分组 5 | - 其它小调整 6 | 7 | 其余基本功能在慢慢开发中 8 | -------------------------------------------------------------------------------- /changelog/v1.0.20+20.md: -------------------------------------------------------------------------------- 1 | - 优化过盾检测 -------------------------------------------------------------------------------- /changelog/v1.0.3+3.md: -------------------------------------------------------------------------------- 1 | - 添加画廊分享,种子下载分享 2 | - 添加评论页 (还不能发表) -------------------------------------------------------------------------------- /changelog/v1.0.4+4.md: -------------------------------------------------------------------------------- 1 | - 画廊页添加tag,设置添加tag翻译开关(默认开关的时候自动下载最新数据库,长按强制更新) 2 | 3 | 接下来的工作是重构部分列表请求逻辑。放假比较懒,慢慢写 -------------------------------------------------------------------------------- /changelog/v1.0.5+5.md: -------------------------------------------------------------------------------- 1 | - 添加搜索页,基本的搜索功能 2 | - 画廊语言标识(仅标注中文,英文) 3 | 4 | 本来打算用nh的api来进行搜索的,为此重构了大部分model,但是发现nh的api有比较奇怪的bug,所以还是用网页的搜索来做 -------------------------------------------------------------------------------- /changelog/v1.0.6+6.md: -------------------------------------------------------------------------------- 1 | - 在画廊列表显示标签(默认关闭) -------------------------------------------------------------------------------- /changelog/v1.0.7+7.md: -------------------------------------------------------------------------------- 1 | - 优化启用列表显示标签后,列表的滑动流畅性 2 | - 列表标签默认使用Wrap布局,水平布局作为可选切换 -------------------------------------------------------------------------------- /changelog/v1.0.8+8.md: -------------------------------------------------------------------------------- 1 | - 完善搜索,可通过点击标签跳转搜索页 2 | - 调整搜索文本框样式 3 | - 完善阅读页,增加全屏选项,阅读方向设置,滑动控制条等 4 | - 添加深色模式手动设置选项 5 | - 列表布局样式增加列表,网格布局 -------------------------------------------------------------------------------- /changelog/v1.0.9+9.md: -------------------------------------------------------------------------------- 1 | - 搜索标签提示 2 | - 更多主题色 3 | - 应用语言设置 -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | $iOSVersion = '11.0' 3 | platform :ios, $iOSVersion 4 | 5 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 6 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 7 | 8 | project 'Runner', { 9 | 'Debug' => :debug, 10 | 'Profile' => :release, 11 | 'Release' => :release, 12 | } 13 | 14 | def flutter_root 15 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 16 | unless File.exist?(generated_xcode_build_settings_path) 17 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 18 | end 19 | 20 | File.foreach(generated_xcode_build_settings_path) do |line| 21 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 22 | return matches[1].strip if matches 23 | end 24 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 25 | end 26 | 27 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 28 | 29 | flutter_ios_podfile_setup 30 | 31 | target 'Runner' do 32 | use_frameworks! 33 | use_modular_headers! 34 | 35 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 36 | end 37 | 38 | post_install do |installer| 39 | installer.pods_project.targets.each do |target| 40 | flutter_additional_ios_build_settings(target) 41 | target.build_configurations.each do |config| 42 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion # required by simple_permission 43 | # config.build_settings['ENABLE_BITCODE'] = 'NO' 44 | # config.build_settings['CODE_SIGNING_REQUIRED'] = "NO" 45 | # config.build_settings['CODE_SIGNING_ALLOWED'] = "NO" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_20pt@2x 1.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "icon_20pt@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon_29pt.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon_29pt@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon_29pt@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "icon_40pt@2x 1.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon_40pt@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "icon_60pt@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "icon_60pt@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "icon_20pt.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "icon_20pt@2x.png", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "icon_29pt 1.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "icon_29pt@2x 1.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "icon_40pt.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "icon_40pt@2x.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "icon_76pt.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "icon_76pt@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "icon_83.5@2x.png", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "Icon.png", 113 | "idiom" : "ios-marketing", 114 | "scale" : "1x", 115 | "size" : "1024x1024" 116 | } 117 | ], 118 | "info" : { 119 | "author" : "xcode", 120 | "version" : 1 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x 1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt 1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x 1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x 1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_76pt.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Eros N 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | eros_n 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UIApplicationSupportsIndirectInputEvents 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UIViewControllerBasedStatusBarAppearance 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/common/const/const.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:eros_n/store/db/entity/nh_tag.dart'; 4 | 5 | class NHConst { 6 | static const String userAgent = 'ErosN'; 7 | 8 | static const String accept = 9 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9'; 10 | 11 | static const String acceptLanguage = 12 | 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6'; 13 | 14 | static const String baseUrl = 'https://nhentai.net'; 15 | static const String baseHost = 'nhentai.net'; 16 | 17 | static const String loginUrl = 'https://nhentai.net/login/'; 18 | static const String registerUrl = 'https://nhentai.net/register/'; 19 | static const String infoUrl = 'https://nhentai.net/info/'; 20 | 21 | // 瀑布流视图参数 22 | // static const double waterfallFlowCrossAxisSpacing = 4.0; 23 | // static const double waterfallFlowMainAxisSpacing = 4.0; 24 | // static const double waterfallFlowMaxCrossAxisExtent = 150.0; 25 | // static const double waterfallFlowMaxCrossAxisExtentTablet = 220.0; 26 | 27 | // 瀑布流视图参数 large 28 | static const double waterfallFlowLargeCrossAxisSpacing = 12.0; 29 | static const double waterfallFlowLargeMainAxisSpacing = 12.0; 30 | static const double waterfallFlowLargeMaxCrossAxisExtent = 190.0; 31 | 32 | // Grid视图参数 33 | static const double gridCrossAxisSpacing = 6.0; 34 | static const double gridMainAxisSpacing = 6.0; 35 | static const double gridMaxCrossAxisExtent = 150.0; 36 | static const double gridChildAspectRatio = 1 / 1.8; 37 | 38 | // 扩展名map 39 | static const Map extMap = { 40 | 'j': 'jpg', 41 | 'p': 'png', 42 | 'g': 'gif', 43 | }; 44 | 45 | static const Map localeMap = { 46 | 'en_': 'English', 47 | 'zh_CN': '简体中文' 48 | }; 49 | 50 | static List internalNhTags = [ 51 | NhTag( 52 | id: 6346, 53 | name: 'japanese', 54 | type: 'language', 55 | ), 56 | NhTag( 57 | id: 12227, 58 | name: 'english', 59 | type: 'language', 60 | ), 61 | NhTag( 62 | id: 29963, 63 | name: 'chinese', 64 | type: 'language', 65 | ), 66 | NhTag( 67 | id: 17249, 68 | name: 'translated', 69 | type: 'language', 70 | ), 71 | ]; 72 | } 73 | -------------------------------------------------------------------------------- /lib/common/enum.dart: -------------------------------------------------------------------------------- 1 | enum TagCategory { 2 | tags('tags'), 3 | artists('artists'), 4 | groups('groups'), 5 | parodies('parodies'), 6 | characters('characters'), 7 | languages('languages'); 8 | 9 | const TagCategory(this.value); 10 | final String value; 11 | } 12 | 13 | enum TagLayoutOnItem { 14 | singleLine, 15 | 16 | // row, 17 | 18 | wrap, 19 | } 20 | 21 | enum ReadModel { 22 | leftToRight, 23 | rightToLeft, 24 | curlLeftToRight, 25 | curlRightToLeft, 26 | vertical, 27 | curlVertical, 28 | webtoon, 29 | defaultModel, 30 | } 31 | 32 | enum ListModel { 33 | list, 34 | grid, 35 | waterfall, 36 | waterfallCompact, 37 | } 38 | 39 | enum SearchSort { 40 | popularMonth('popular-month'), 41 | popularWeek('popular-week'), 42 | popularToday('popular-today'), 43 | popular('popular'), 44 | recent(''); 45 | 46 | const SearchSort(this.value); 47 | 48 | final String value; 49 | } 50 | 51 | enum LanguagesFilter { 52 | japanese('japanese'), 53 | chinese('chinese'), 54 | english('english'), 55 | translated('translated'), 56 | all(''); 57 | 58 | const LanguagesFilter(this.value); 59 | final String value; 60 | } 61 | -------------------------------------------------------------------------------- /lib/common/extension.dart: -------------------------------------------------------------------------------- 1 | extension DateHelpers on DateTime { 2 | bool isToday() { 3 | final now = DateTime.now(); 4 | return now.day == day && now.month == month && now.year == year; 5 | } 6 | 7 | bool isYesterday() { 8 | final yesterday = DateTime.now().subtract(const Duration(days: 1)); 9 | return yesterday.day == day && 10 | yesterday.month == month && 11 | yesterday.year == year; 12 | } 13 | } 14 | 15 | extension ExtString on String { 16 | String get prettyTitle => 17 | replaceAll(RegExp(r'(\[.*?\]|\(.*?\))|{.*?}'), '').trim(); 18 | 19 | String get processApi => replaceAllMapped( 20 | RegExp(r'"id":\s*"(\d+)",'), (match) => '"id":${match.group(1)},') 21 | .replaceAllMapped(RegExp(r'"media_id":\s+(\d+),'), 22 | (match) => '"id": "${match.group(1)}",') 23 | .replaceAllMapped( 24 | RegExp(r'"w":\s*"(\d+)",'), (match) => '"w":${match.group(1)},') 25 | .replaceAllMapped( 26 | RegExp(r'"h":\s*"(\d+)",'), (match) => '"h":${match.group(1)},') 27 | .replaceAllMapped(RegExp(r'"count":\s*"(\d+)",'), 28 | (match) => '"count":${match.group(1)},'); 29 | } 30 | 31 | extension IterableExtensions on Iterable { 32 | Iterable> chunked(int chunkSize) sync* { 33 | if (length <= 0) { 34 | yield []; 35 | return; 36 | } 37 | int skip = 0; 38 | while (skip < length) { 39 | final chunk = this.skip(skip).take(chunkSize); 40 | yield chunk.toList(growable: false); 41 | skip += chunkSize; 42 | if (chunk.length < chunkSize) return; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/common/parser/paese_user_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/component/models/index.dart'; 2 | import 'package:eros_n/utils/logger.dart'; 3 | import 'package:html/dom.dart'; 4 | import 'package:html/parser.dart' show parse; 5 | 6 | User? parseUserPage(String html) { 7 | final Document document = parse(html); 8 | 9 | const avatarSelector = '.bigavatar > img'; 10 | final avatarElm = document.querySelector(avatarSelector); 11 | final avatarUrl = avatarElm?.attributes['src']; 12 | return User( 13 | avatarUrl: avatarUrl, 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/common/parser/parse_gallery_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/component/models/index.dart'; 2 | import 'package:eros_n/utils/logger.dart'; 3 | import 'package:html/dom.dart'; 4 | import 'package:html/parser.dart' show parse; 5 | 6 | GalleryImage parseGalleryImage(String html) { 7 | final Document document = parse(html); 8 | 9 | const selectorImage = '#image-container > a > img'; 10 | 11 | final imageElm = document.querySelector(selectorImage); 12 | final imageUrl = imageElm?.attributes['src']; 13 | final imageHeight = imageElm?.attributes['height']; 14 | final imageWidth = imageElm?.attributes['width']; 15 | 16 | logger.d('imageUrl: $imageUrl'); 17 | 18 | return GalleryImage( 19 | imageUrl: imageUrl, 20 | imgHeight: int.tryParse(imageHeight ?? ''), 21 | imgWidth: int.tryParse(imageWidth ?? ''), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/common/parser/parse_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/component/models/index.dart'; 2 | import 'package:eros_n/utils/logger.dart'; 3 | import 'package:html/dom.dart'; 4 | import 'package:html/parser.dart' show parse; 5 | 6 | User? parseInfo(String html) { 7 | final Document document = parse(html); 8 | 9 | const usernameSelector = '.username'; 10 | final usernameElm = document.querySelector(usernameSelector); 11 | final username = usernameElm?.text; 12 | 13 | final hrefElem = usernameElm?.parent; 14 | logger.d('hrefElem: ${hrefElem?.outerHtml}'); 15 | final href = hrefElem?.attributes['href']; 16 | final userId = RegExp(r'\d+').firstMatch(href ?? '')?.group(0); 17 | 18 | return User( 19 | userName: username, 20 | userId: userId, 21 | userUrl: href, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/common/parser/parse_login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/component/exception/error.dart'; 2 | import 'package:eros_n/component/models/index.dart'; 3 | import 'package:eros_n/utils/logger.dart'; 4 | import 'package:html/dom.dart'; 5 | import 'package:html/parser.dart' show parse; 6 | 7 | String? parseLoginPage(String html) { 8 | final doc = parse(html); 9 | 10 | // final regExpToken = RegExp(r'csrf_token:\s+"(\w+)"'); 11 | // final regExpTokenMatch = regExpToken.firstMatch(html); 12 | // final token = regExpTokenMatch?.group(1); 13 | 14 | const tokenSelector = '#content > form > input[type=hidden]'; 15 | final tokenElement = doc.querySelector(tokenSelector); 16 | final tokenValue = tokenElement?.attributes['value']; 17 | 18 | const captchaSelector = '#content > form > div > div.captcha'; 19 | 20 | final captchaElement = doc.querySelector(captchaSelector); 21 | 22 | logger.d('captchaElement ${captchaElement?.outerHtml}'); 23 | 24 | final error = doc.querySelector('#errors')?.text ?? ''; 25 | 26 | if (error.contains('CAPTCHA') || captchaElement != null) { 27 | logger.e('CAPTCHA'); 28 | throw LoginCaptchaError(); 29 | } 30 | 31 | if (error.contains('Invalid')) { 32 | logger.e('Invalid'); 33 | throw LoginInvalidError(); 34 | } 35 | 36 | return tokenValue; 37 | } 38 | -------------------------------------------------------------------------------- /lib/common/parser/parser.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | library parser; 4 | 5 | export 'paese_user_page.dart'; 6 | export 'parse_gallery_detail.dart'; 7 | export 'parse_gallery_image.dart'; 8 | export 'parse_gallery_list.dart'; 9 | export 'parse_info.dart'; 10 | export 'parse_login_page.dart'; 11 | -------------------------------------------------------------------------------- /lib/common/provider/palette_generator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:eros_n/component/widget/eros_cached_network_image.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | import 'package:palette_generator/palette_generator.dart'; 8 | 9 | final paletteGeneratorProvider = FutureProvider.autoDispose 10 | .family((ref, imageUrl) async { 11 | final imageProvider = ResizeImage(getErosImageProvider(imageUrl), width: 32); 12 | 13 | final completer = Completer(); 14 | imageProvider.resolve(const ImageConfiguration()).addListener( 15 | ImageStreamListener( 16 | (imageInfo, _) { 17 | final image = imageInfo.image; 18 | final paletteGenerator = PaletteGenerator.fromImage( 19 | image, 20 | // region: Rect.fromCenter( 21 | // center: Offset(image.width / 2, image.height / 2), 22 | // width: image.width * 1, 23 | // height: image.height * 1, 24 | // ), 25 | ); 26 | completer.complete(paletteGenerator); 27 | }, 28 | ), 29 | ); 30 | 31 | return completer.future; 32 | }); 33 | -------------------------------------------------------------------------------- /lib/common/provider/receive_sharing_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:eros_n/common/global.dart'; 4 | import 'package:eros_n/routes/routes.dart'; 5 | import 'package:eros_n/utils/get_utils/get_utils.dart'; 6 | import 'package:eros_n/utils/logger.dart'; 7 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 8 | import 'package:receive_sharing_intent/receive_sharing_intent.dart'; 9 | 10 | class ReceiveSharingNotifier extends StateNotifier { 11 | ReceiveSharingNotifier(this.ref) : super(null); 12 | 13 | final Ref ref; 14 | 15 | String? sharedText; 16 | 17 | Future _goPage( 18 | String url, 19 | WidgetRef widgetRef, { 20 | bool replace = true, 21 | }) async { 22 | if (url.isEmpty || 23 | !(await RouteUtil.goGalleryByUrl(widgetRef, url, replace: replace))) { 24 | 0 25 | .milliseconds 26 | .delay() 27 | .then((value) => erosRouter.replaceNamed(NHRoutes.home)); 28 | } 29 | } 30 | 31 | void listenReceiveSharing(WidgetRef widgetRef) { 32 | // For sharing or opening urls/text coming from outside the app while the app is in the memory 33 | ReceiveSharingIntent.getTextStream().listen((String value) { 34 | sharedText = value; 35 | logger.e('getTextStream Shared: $sharedText'); 36 | _goPage(sharedText ?? '', widgetRef, replace: false); 37 | }, onError: (err) { 38 | logger.e('getTextStream error: $err'); 39 | }); 40 | 41 | // For sharing or opening urls/text coming from outside the app while the app is closed 42 | ReceiveSharingIntent.getInitialText().then((String? value) { 43 | logger.e('getInitialText Shared: $value'); 44 | 45 | if (value == sharedText) { 46 | return; 47 | } 48 | 49 | sharedText = value ?? ''; 50 | logger.v('Shared: $sharedText'); 51 | _goPage(sharedText ?? '', widgetRef, replace: false); 52 | }); 53 | } 54 | } 55 | 56 | final receiveSharingProvider = 57 | StateNotifierProvider.autoDispose((ref) { 58 | return ReceiveSharingNotifier(ref); 59 | }); 60 | -------------------------------------------------------------------------------- /lib/common/provider/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/component/theme/theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | class ThemeNotifier extends StateNotifier { 6 | ThemeNotifier() : super(ThemeConfig.lightTheme); 7 | } 8 | 9 | final themeProvider = StateNotifierProvider((ref) { 10 | return ThemeNotifier(); 11 | }); 12 | -------------------------------------------------------------------------------- /lib/component/converter/locale_converter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | 5 | class CustomLocaleConverter implements JsonConverter { 6 | const CustomLocaleConverter(); 7 | 8 | @override 9 | Locale fromJson(String json) { 10 | final parts = json.split('_'); 11 | return Locale(parts[0], parts[1]); 12 | } 13 | 14 | @override 15 | String toJson(Locale object) { 16 | return '${object.languageCode}_${object.countryCode}'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/component/exception/error.dart: -------------------------------------------------------------------------------- 1 | enum NhErrorType { 2 | def, 3 | 4 | loginCaptcha, 5 | 6 | loginInvalid, 7 | } 8 | 9 | class NhError implements Exception { 10 | NhError({ 11 | this.type = NhErrorType.def, 12 | this.error, 13 | }); 14 | 15 | NhErrorType type; 16 | 17 | dynamic error; 18 | 19 | String get message => error?.toString() ?? ''; 20 | 21 | @override 22 | String toString() { 23 | var msg = 'NhError [$type]: $message'; 24 | if (error is Error) { 25 | msg += '\n${error.stackTrace}'; 26 | } 27 | return msg; 28 | } 29 | } 30 | 31 | class LoginCaptchaError extends NhError { 32 | LoginCaptchaError() : super(type: NhErrorType.loginCaptcha); 33 | } 34 | 35 | class LoginInvalidError extends NhError { 36 | LoginInvalidError() : super(type: NhErrorType.loginInvalid); 37 | } 38 | -------------------------------------------------------------------------------- /lib/component/models/comment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'comment_poster.dart'; 4 | 5 | part 'comment.freezed.dart'; 6 | part 'comment.g.dart'; 7 | 8 | @freezed 9 | class Comment with _$Comment { 10 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 11 | const factory Comment({ 12 | @JsonKey(name: 'id') int? commentId, 13 | @JsonKey(name: 'gallery_id') int? gid, 14 | @JsonKey(name: 'post_date') int? postDate, 15 | @JsonKey(name: 'body') String? commentText, 16 | @JsonKey(name: 'poster') CommentPoster? poster, 17 | }) = _Comment; 18 | 19 | factory Comment.fromJson(Map json) => 20 | _$CommentFromJson(json); 21 | } 22 | -------------------------------------------------------------------------------- /lib/component/models/comment.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'comment.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Comment _$$_CommentFromJson(Map json) => _$_Comment( 10 | commentId: json['id'] as int?, 11 | gid: json['gallery_id'] as int?, 12 | postDate: json['post_date'] as int?, 13 | commentText: json['body'] as String?, 14 | poster: json['poster'] == null 15 | ? null 16 | : CommentPoster.fromJson(json['poster'] as Map), 17 | ); 18 | 19 | Map _$$_CommentToJson(_$_Comment instance) => 20 | { 21 | 'id': instance.commentId, 22 | 'gallery_id': instance.gid, 23 | 'post_date': instance.postDate, 24 | 'body': instance.commentText, 25 | 'poster': instance.poster?.toJson(), 26 | }; 27 | -------------------------------------------------------------------------------- /lib/component/models/comment_poster.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'comment_poster.freezed.dart'; 5 | part 'comment_poster.g.dart'; 6 | 7 | @freezed 8 | class CommentPoster with _$CommentPoster { 9 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 10 | const factory CommentPoster({ 11 | @JsonKey(name: 'id') int? posterId, 12 | @JsonKey(name: 'username') String? username, 13 | @JsonKey(name: 'avatar_url') String? avatarUrl, 14 | @JsonKey(name: 'is_superuser') bool? isSuperuser, 15 | @JsonKey(name: 'is_staff') bool? isStaff, 16 | }) = _CommentPoster; 17 | 18 | factory CommentPoster.fromJson(Map json) => 19 | _$CommentPosterFromJson(json); 20 | } 21 | -------------------------------------------------------------------------------- /lib/component/models/comment_poster.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'comment_poster.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_CommentPoster _$$_CommentPosterFromJson(Map json) => 10 | _$_CommentPoster( 11 | posterId: json['id'] as int?, 12 | username: json['username'] as String?, 13 | avatarUrl: json['avatar_url'] as String?, 14 | isSuperuser: json['is_superuser'] as bool?, 15 | isStaff: json['is_staff'] as bool?, 16 | ); 17 | 18 | Map _$$_CommentPosterToJson(_$_CommentPoster instance) => 19 | { 20 | 'id': instance.posterId, 21 | 'username': instance.username, 22 | 'avatar_url': instance.avatarUrl, 23 | 'is_superuser': instance.isSuperuser, 24 | 'is_staff': instance.isStaff, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/component/models/gallery.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/common/const/const.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | import 'comment.dart'; 5 | import 'gallery_images.dart'; 6 | import 'gallery_title.dart'; 7 | import 'tag.dart'; 8 | 9 | part 'gallery.freezed.dart'; 10 | 11 | part 'gallery.g.dart'; 12 | 13 | @freezed 14 | class Gallery with _$Gallery { 15 | const factory Gallery({ 16 | @JsonKey(name: 'id') @Default(0) int gid, 17 | @JsonKey(name: 'media_id') String? mediaId, 18 | @Default(GalleryTitle()) GalleryTitle title, 19 | @Default(GalleryImages()) GalleryImages images, 20 | String? scanlator, 21 | @JsonKey(name: 'upload_date') int? uploadDate, 22 | @JsonKey(name: 'num_pages') int? numPages, 23 | @JsonKey(name: 'num_favorites') int? numFavorites, 24 | @Default([]) List tags, 25 | @JsonKey(ignore: true) @Default([]) List simpleTags, 26 | @JsonKey(ignore: true) String? languageCode, 27 | @JsonKey(ignore: true) int? viewTime, 28 | @JsonKey(ignore: true) @Default([]) List comments, 29 | @JsonKey(ignore: true) @Default([]) List moreLikeGallerys, 30 | @JsonKey(ignore: true) @Default(0) int currentPageIndex, 31 | @JsonKey(ignore: true) bool? isFavorited, 32 | @JsonKey(ignore: true) String? uploadedDateTime, 33 | @JsonKey(ignore: true) String? csrfToken, 34 | }) = _Gallery; 35 | 36 | const Gallery._(); 37 | 38 | factory Gallery.fromJson(Map json) => 39 | _$GalleryFromJson(json); 40 | 41 | @JsonKey(ignore: true) 42 | DateTime? get uploadedDate => 43 | DateTime.tryParse(uploadedDateTime ?? '')?.toLocal(); 44 | 45 | @JsonKey(ignore: true) 46 | String? get thumbUrl => mediaId != null 47 | ? 'https://t.nhentai.net/galleries/$mediaId/thumb.${NHConst.extMap[images.thumbnail.type]}' 48 | : null; 49 | @JsonKey(ignore: true) 50 | String? get coverUrl => mediaId != null 51 | ? 'https://t.nhentai.net/galleries/$mediaId/cover.${NHConst.extMap[images.cover.type]}' 52 | : null; 53 | @JsonKey(ignore: true) 54 | String get url => 'https://nhentai.net/g/$gid/'; 55 | } 56 | -------------------------------------------------------------------------------- /lib/component/models/gallery.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'gallery.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Gallery _$$_GalleryFromJson(Map json) => _$_Gallery( 10 | gid: json['id'] as int? ?? 0, 11 | mediaId: json['media_id'] as String?, 12 | title: json['title'] == null 13 | ? const GalleryTitle() 14 | : GalleryTitle.fromJson(json['title'] as Map), 15 | images: json['images'] == null 16 | ? const GalleryImages() 17 | : GalleryImages.fromJson(json['images'] as Map), 18 | scanlator: json['scanlator'] as String?, 19 | uploadDate: json['upload_date'] as int?, 20 | numPages: json['num_pages'] as int?, 21 | numFavorites: json['num_favorites'] as int?, 22 | tags: (json['tags'] as List?) 23 | ?.map((e) => Tag.fromJson(e as Map)) 24 | .toList() ?? 25 | const [], 26 | ); 27 | 28 | Map _$$_GalleryToJson(_$_Gallery instance) => 29 | { 30 | 'id': instance.gid, 31 | 'media_id': instance.mediaId, 32 | 'title': instance.title, 33 | 'images': instance.images, 34 | 'scanlator': instance.scanlator, 35 | 'upload_date': instance.uploadDate, 36 | 'num_pages': instance.numPages, 37 | 'num_favorites': instance.numFavorites, 38 | 'tags': instance.tags, 39 | }; 40 | -------------------------------------------------------------------------------- /lib/component/models/gallery_images.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | import 'image.dart'; 5 | 6 | part 'gallery_images.freezed.dart'; 7 | 8 | part 'gallery_images.g.dart'; 9 | 10 | @freezed 11 | class GalleryImages with _$GalleryImages { 12 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 13 | const factory GalleryImages({ 14 | @JsonKey(name: 'pages') @Default([]) List pages, 15 | @JsonKey(name: 'cover') @Default(GalleryImage()) GalleryImage cover, 16 | @JsonKey(name: 'thumbnail') @Default(GalleryImage()) GalleryImage thumbnail, 17 | }) = _GalleryImages; 18 | 19 | factory GalleryImages.fromJson(Map json) => 20 | _$GalleryImagesFromJson(json); 21 | } 22 | -------------------------------------------------------------------------------- /lib/component/models/gallery_images.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'gallery_images.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_GalleryImages _$$_GalleryImagesFromJson(Map json) => 10 | _$_GalleryImages( 11 | pages: (json['pages'] as List?) 12 | ?.map((e) => GalleryImage.fromJson(e as Map)) 13 | .toList() ?? 14 | const [], 15 | cover: json['cover'] == null 16 | ? const GalleryImage() 17 | : GalleryImage.fromJson(json['cover'] as Map), 18 | thumbnail: json['thumbnail'] == null 19 | ? const GalleryImage() 20 | : GalleryImage.fromJson(json['thumbnail'] as Map), 21 | ); 22 | 23 | Map _$$_GalleryImagesToJson(_$_GalleryImages instance) => 24 | { 25 | 'pages': instance.pages.map((e) => e.toJson()).toList(), 26 | 'cover': instance.cover.toJson(), 27 | 'thumbnail': instance.thumbnail.toJson(), 28 | }; 29 | -------------------------------------------------------------------------------- /lib/component/models/gallery_search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | import 'gallery.dart'; 5 | 6 | part 'gallery_search.freezed.dart'; 7 | 8 | part 'gallery_search.g.dart'; 9 | 10 | @freezed 11 | class GallerySearch with _$GallerySearch { 12 | // @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 13 | const factory GallerySearch({ 14 | @JsonKey(name: 'result') @Default([]) List result, 15 | @JsonKey(name: 'num_pages') int? numPages, 16 | @JsonKey(name: 'per_page') int? perPage, 17 | }) = _GallerySearch; 18 | 19 | factory GallerySearch.fromJson(Map json) => 20 | _$GallerySearchFromJson(json); 21 | } 22 | -------------------------------------------------------------------------------- /lib/component/models/gallery_search.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'gallery_search.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_GallerySearch _$$_GallerySearchFromJson(Map json) => 10 | _$_GallerySearch( 11 | result: (json['result'] as List?) 12 | ?.map((e) => Gallery.fromJson(e as Map)) 13 | .toList() ?? 14 | const [], 15 | numPages: json['num_pages'] as int?, 16 | perPage: json['per_page'] as int?, 17 | ); 18 | 19 | Map _$$_GallerySearchToJson(_$_GallerySearch instance) => 20 | { 21 | 'result': instance.result, 22 | 'num_pages': instance.numPages, 23 | 'per_page': instance.perPage, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/component/models/gallery_set.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'gallery.dart'; 4 | 5 | part 'gallery_set.freezed.dart'; 6 | 7 | part 'gallery_set.g.dart'; 8 | 9 | @freezed 10 | class GallerySet with _$GallerySet { 11 | const factory GallerySet({ 12 | List? gallerys, 13 | List? populars, 14 | List? favorites, 15 | int? maxPage, 16 | int? currentPage, 17 | bool? fromCache, 18 | }) = _GallerySet; 19 | 20 | factory GallerySet.fromJson(Map json) => 21 | _$GallerySetFromJson(json); 22 | } 23 | -------------------------------------------------------------------------------- /lib/component/models/gallery_set.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'gallery_set.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_GallerySet _$$_GallerySetFromJson(Map json) => 10 | _$_GallerySet( 11 | gallerys: (json['gallerys'] as List?) 12 | ?.map((e) => Gallery.fromJson(e as Map)) 13 | .toList(), 14 | populars: (json['populars'] as List?) 15 | ?.map((e) => Gallery.fromJson(e as Map)) 16 | .toList(), 17 | favorites: (json['favorites'] as List?) 18 | ?.map((e) => Gallery.fromJson(e as Map)) 19 | .toList(), 20 | maxPage: json['maxPage'] as int?, 21 | currentPage: json['currentPage'] as int?, 22 | fromCache: json['fromCache'] as bool?, 23 | ); 24 | 25 | Map _$$_GallerySetToJson(_$_GallerySet instance) => 26 | { 27 | 'gallerys': instance.gallerys, 28 | 'populars': instance.populars, 29 | 'favorites': instance.favorites, 30 | 'maxPage': instance.maxPage, 31 | 'currentPage': instance.currentPage, 32 | 'fromCache': instance.fromCache, 33 | }; 34 | -------------------------------------------------------------------------------- /lib/component/models/gallery_title.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'gallery_title.freezed.dart'; 5 | 6 | part 'gallery_title.g.dart'; 7 | 8 | @freezed 9 | class GalleryTitle with _$GalleryTitle { 10 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 11 | const factory GalleryTitle({ 12 | @JsonKey(name: 'english') String? englishTitle, 13 | @JsonKey(name: 'japanese') String? japaneseTitle, 14 | @JsonKey(name: 'pretty') String? prettyTitle, 15 | }) = _GalleryTitle; 16 | 17 | factory GalleryTitle.fromJson(Map json) => 18 | _$GalleryTitleFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /lib/component/models/gallery_title.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'gallery_title.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_GalleryTitle _$$_GalleryTitleFromJson(Map json) => 10 | _$_GalleryTitle( 11 | englishTitle: json['english'] as String?, 12 | japaneseTitle: json['japanese'] as String?, 13 | prettyTitle: json['pretty'] as String?, 14 | ); 15 | 16 | Map _$$_GalleryTitleToJson(_$_GalleryTitle instance) => 17 | { 18 | 'english': instance.englishTitle, 19 | 'japanese': instance.japaneseTitle, 20 | 'pretty': instance.prettyTitle, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/component/models/image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'image.freezed.dart'; 5 | 6 | part 'image.g.dart'; 7 | 8 | @freezed 9 | class GalleryImage with _$GalleryImage { 10 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 11 | const factory GalleryImage({ 12 | @Default('j') @JsonKey(name: 't') String type, 13 | @JsonKey(name: 'h') int? imgHeight, 14 | @JsonKey(name: 'w') int? imgWidth, 15 | @JsonKey(ignore: true) String? imageUrl, 16 | @JsonKey(ignore: true) String? href, 17 | }) = _GalleryImage; 18 | 19 | factory GalleryImage.fromJson(Map json) => 20 | _$GalleryImageFromJson(json); 21 | } 22 | -------------------------------------------------------------------------------- /lib/component/models/image.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'image.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_GalleryImage _$$_GalleryImageFromJson(Map json) => 10 | _$_GalleryImage( 11 | type: json['t'] as String? ?? 'j', 12 | imgHeight: json['h'] as int?, 13 | imgWidth: json['w'] as int?, 14 | ); 15 | 16 | Map _$$_GalleryImageToJson(_$_GalleryImage instance) => 17 | { 18 | 't': instance.type, 19 | 'h': instance.imgHeight, 20 | 'w': instance.imgWidth, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/component/models/index.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | library index; 4 | 5 | export 'comment.dart'; 6 | export 'comment_poster.dart'; 7 | export 'gallery.dart'; 8 | export 'gallery_images.dart'; 9 | export 'gallery_search.dart'; 10 | export 'gallery_set.dart'; 11 | export 'gallery_title.dart'; 12 | export 'image.dart'; 13 | export 'settings.dart'; 14 | export 'tag.dart'; 15 | export 'tag_translate_info.dart'; 16 | export 'user.dart'; 17 | -------------------------------------------------------------------------------- /lib/component/models/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/common/enum.dart'; 2 | import 'package:eros_n/component/theme/theme.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:freezed_annotation/freezed_annotation.dart'; 5 | 6 | part 'settings.freezed.dart'; 7 | part 'settings.g.dart'; 8 | 9 | @freezed 10 | class Settings with _$Settings { 11 | const factory Settings({ 12 | @Default(false) bool isCoverBlur, 13 | @Default(false) bool isTagTranslate, 14 | @Default(true) bool dynamicColor, 15 | @Default(SearchSort.recent) SearchSort searchSort, 16 | @Default(false) bool showTags, 17 | @Default(TagLayoutOnItem.wrap) TagLayoutOnItem tagLayoutOnItem, 18 | @Default(ThemeMode.system) ThemeMode themeMode, 19 | @Default(false) bool fullScreenReader, 20 | @Default(ReadModel.leftToRight) ReadModel readModel, 21 | @Default(ListModel.waterfall) ListModel listModel, 22 | @Default('') String localeCode, 23 | @Default(ThemeConfig.dynamicThemeColorLabel) String themeColorLabel, 24 | @Default(false) bool supportDynamicColors, 25 | @Default(true) bool hideBottomNavigationOnScroll, 26 | @Default(true) bool useGalleryTint, 27 | @Default(false) bool volumeKeyTurnPage, 28 | @Default(2.0) double autoReadInterval, 29 | @Default(3) int preloadPagesCount, 30 | @Default(SearchSort.recent) SearchSort searchSortOnFrontPage, 31 | @Default(LanguagesFilter.all) LanguagesFilter frontLanguagesFilter, 32 | @Default(LanguagesFilter.all) LanguagesFilter searchLanguagesFilter, 33 | @Default(false) bool clipboardDetection, 34 | }) = _Settings; 35 | 36 | const Settings._(); 37 | 38 | factory Settings.fromJson(Map json) => 39 | _$SettingsFromJson(json); 40 | 41 | @JsonKey(ignore: true) 42 | bool get readModelPageView => 43 | readModel == ReadModel.leftToRight || 44 | readModel == ReadModel.rightToLeft || 45 | readModel == ReadModel.vertical; 46 | } 47 | -------------------------------------------------------------------------------- /lib/component/models/tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'tag.freezed.dart'; 5 | 6 | part 'tag.g.dart'; 7 | 8 | @freezed 9 | class Tag with _$Tag { 10 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 11 | const factory Tag({ 12 | int? id, 13 | String? type, 14 | String? name, 15 | String? url, 16 | int? count, 17 | @JsonKey(ignore: true) String? translatedName, 18 | }) = _Tag; 19 | 20 | factory Tag.fromJson(Map json) => _$TagFromJson(json); 21 | } 22 | -------------------------------------------------------------------------------- /lib/component/models/tag.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'tag.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_Tag _$$_TagFromJson(Map json) => _$_Tag( 10 | id: json['id'] as int?, 11 | type: json['type'] as String?, 12 | name: json['name'] as String?, 13 | url: json['url'] as String?, 14 | count: json['count'] as int?, 15 | ); 16 | 17 | Map _$$_TagToJson(_$_Tag instance) => { 18 | 'id': instance.id, 19 | 'type': instance.type, 20 | 'name': instance.name, 21 | 'url': instance.url, 22 | 'count': instance.count, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/component/models/tag_translate_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'tag_translate_info.freezed.dart'; 5 | part 'tag_translate_info.g.dart'; 6 | 7 | @freezed 8 | class TagTranslateInfo with _$TagTranslateInfo { 9 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 10 | const factory TagTranslateInfo({ 11 | String? version, 12 | String? remoteVersion, 13 | String? lastReleaseUrl, 14 | String? nhTagVersion, 15 | }) = _TagTranslateInfo; 16 | 17 | factory TagTranslateInfo.fromJson(Map json) => 18 | _$TagTranslateInfoFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /lib/component/models/tag_translate_info.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'tag_translate_info.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_TagTranslateInfo _$$_TagTranslateInfoFromJson(Map json) => 10 | _$_TagTranslateInfo( 11 | version: json['version'] as String?, 12 | remoteVersion: json['remote_version'] as String?, 13 | lastReleaseUrl: json['last_release_url'] as String?, 14 | nhTagVersion: json['nh_tag_version'] as String?, 15 | ); 16 | 17 | Map _$$_TagTranslateInfoToJson(_$_TagTranslateInfo instance) => 18 | { 19 | 'version': instance.version, 20 | 'remote_version': instance.remoteVersion, 21 | 'last_release_url': instance.lastReleaseUrl, 22 | 'nh_tag_version': instance.nhTagVersion, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/component/models/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'user.freezed.dart'; 5 | part 'user.g.dart'; 6 | 7 | @freezed 8 | class User with _$User { 9 | const factory User({ 10 | String? userName, 11 | String? avatarUrl, 12 | String? userId, 13 | String? userUrl, 14 | String? sessionid, 15 | }) = _User; 16 | 17 | factory User.fromJson(Map json) => _$UserFromJson(json); 18 | 19 | const User._(); 20 | 21 | bool get isLogin => sessionid != null && sessionid!.isNotEmpty; 22 | } 23 | -------------------------------------------------------------------------------- /lib/component/models/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_User _$$_UserFromJson(Map json) => _$_User( 10 | userName: json['userName'] as String?, 11 | avatarUrl: json['avatarUrl'] as String?, 12 | userId: json['userId'] as String?, 13 | userUrl: json['userUrl'] as String?, 14 | sessionid: json['sessionid'] as String?, 15 | ); 16 | 17 | Map _$$_UserToJson(_$_User instance) => { 18 | 'userName': instance.userName, 19 | 'avatarUrl': instance.avatarUrl, 20 | 'userId': instance.userId, 21 | 'userUrl': instance.userUrl, 22 | 'sessionid': instance.sessionid, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/component/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ThemeConfig { 4 | static late ColorScheme lightColorScheme; 5 | static late ColorScheme darkColorScheme; 6 | 7 | static late ThemeData lightTheme; 8 | static late ThemeData darkTheme; 9 | 10 | static const defaultThemeColorLabel = 'blue'; 11 | static const dynamicThemeColorLabel = 'dynamic'; 12 | static const Map colorMap = { 13 | 'red': Colors.red, 14 | 'pink': Colors.pink, 15 | 'purple': Colors.purple, 16 | 'deep_purple': Colors.deepPurple, 17 | 'indigo': Colors.indigo, 18 | 'blue': Colors.blue, 19 | 'light_blue': Colors.lightBlue, 20 | 'cyan': Colors.cyan, 21 | 'teal': Colors.teal, 22 | 'green': Colors.green, 23 | 'light_green': Colors.lightGreen, 24 | 'lime': Colors.lime, 25 | 'yellow': Colors.yellow, 26 | 'amber': Colors.amber, 27 | 'orange': Colors.orange, 28 | 'deep_orange': Colors.deepOrange, 29 | 'brown': Colors.brown, 30 | 'grey': Colors.grey, 31 | 'blue_grey': Colors.blueGrey, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /lib/component/widget/blur_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | class BlurImage extends StatelessWidget { 7 | const BlurImage({ 8 | super.key, 9 | required this.child, 10 | this.blur = true, 11 | this.sigma = 5.0, 12 | this.fit = StackFit.expand, 13 | this.color = Colors.transparent, 14 | }); 15 | 16 | final Widget child; 17 | final bool blur; 18 | final double sigma; 19 | final StackFit fit; 20 | final Color color; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Stack( 25 | alignment: Alignment.center, 26 | fit: fit, 27 | children: [ 28 | Container(child: child), 29 | if (blur) 30 | BackdropFilter( 31 | filter: ImageFilter.blur( 32 | sigmaX: sigma, 33 | sigmaY: sigma, 34 | ), 35 | child: ColoredBox( 36 | color: color, 37 | ), 38 | ), 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/component/widget/gallery_image/gallery_image_platform_interface.dart: -------------------------------------------------------------------------------- 1 | library cached_network_image_platform_interface; 2 | 3 | import 'dart:async'; 4 | import 'dart:ui' as ui; 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 8 | 9 | /// Render options for images on the web platform. 10 | enum ImageRenderMethodForWeb { 11 | /// HtmlImage uses a default web image including default browser caching. 12 | /// This is the recommended and default choice. 13 | HtmlImage, // ignore: constant_identifier_names 14 | 15 | /// HttpGet uses an http client to fetch an image. It enables the use of 16 | /// headers, but loses some default web functionality. 17 | HttpGet, // ignore: constant_identifier_names 18 | } 19 | 20 | /// ImageLoader class to load images differently on various platforms. 21 | class ImageLoader { 22 | /// loads the images async and gives the resulted codecs on a Stream. The 23 | /// Stream gives the option to show multiple images after each other. 24 | Stream loadBufferAsync( 25 | String url, 26 | String? cacheKey, 27 | StreamController chunkEvents, 28 | DecoderBufferCallback decode, 29 | BaseCacheManager cacheManager, 30 | int? maxHeight, 31 | int? maxWidth, 32 | Map? headers, 33 | Function()? errorListener, 34 | ImageRenderMethodForWeb imageRenderMethodForWeb, 35 | Function() evictImage, 36 | ) { 37 | throw UnimplementedError(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/component/widget/scroll.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomScrollPhysics extends BouncingScrollPhysics { 4 | const CustomScrollPhysics({ScrollPhysics? parent}) : super(parent: parent); 5 | 6 | @override 7 | CustomScrollPhysics applyTo(ScrollPhysics? ancestor) { 8 | return CustomScrollPhysics(parent: buildParent(ancestor)); 9 | } 10 | 11 | @override 12 | SpringDescription get spring => SpringDescription.withDampingRatio( 13 | mass: 0.5, 14 | stiffness: 300.0, // Increase this value as you wish. 15 | ratio: 1.1, 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /lib/component/widget/scrolling_fab.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/utils/logger.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/rendering.dart'; 4 | 5 | class ScrollingFab extends StatefulWidget { 6 | const ScrollingFab({ 7 | Key? key, 8 | this.duration, 9 | this.curve, 10 | this.scrollController, 11 | required this.label, 12 | required this.icon, 13 | this.extendedIconLabelSpacing, 14 | this.onPressed, 15 | }) : super(key: key); 16 | 17 | final Duration? duration; 18 | final Curve? curve; 19 | final ScrollController? scrollController; 20 | final double? extendedIconLabelSpacing; 21 | final Widget label; 22 | final Widget icon; 23 | final VoidCallback? onPressed; 24 | 25 | @override 26 | State createState() => _ScrollingFabState(); 27 | } 28 | 29 | class _ScrollingFabState extends State { 30 | late bool _isExtended; 31 | 32 | void _scrollListener() { 33 | if (widget.scrollController!.position.userScrollDirection == 34 | ScrollDirection.reverse) { 35 | if (!mounted || !_isExtended) { 36 | return; 37 | } 38 | logger.v('ScrollingFab: ScrollDirection.reverse'); 39 | setState(() { 40 | _isExtended = false; 41 | }); 42 | } 43 | if (widget.scrollController!.position.userScrollDirection == 44 | ScrollDirection.forward) { 45 | if (!mounted || _isExtended) { 46 | return; 47 | } 48 | logger.v('ScrollingFab: _scrollListener: forward'); 49 | setState(() { 50 | _isExtended = true; 51 | }); 52 | } 53 | } 54 | 55 | @override 56 | void initState() { 57 | super.initState(); 58 | _isExtended = true; 59 | widget.scrollController?.addListener(_scrollListener); 60 | } 61 | 62 | @override 63 | void dispose() { 64 | widget.scrollController?.removeListener(_scrollListener); 65 | super.dispose(); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | final labelSizeAnimation = AnimatedSize( 71 | duration: widget.duration ?? const Duration(milliseconds: 300), 72 | curve: widget.curve ?? Curves.easeInOut, 73 | child: SizedBox( 74 | width: _isExtended ? null : 0, 75 | child: _isExtended ? widget.label : const SizedBox(), 76 | ), 77 | ); 78 | 79 | return AnimatedSize( 80 | duration: widget.duration ?? const Duration(milliseconds: 300), 81 | curve: widget.curve ?? Curves.easeInOut, 82 | child: FloatingActionButton.extended( 83 | onPressed: widget.onPressed, 84 | label: labelSizeAnimation, 85 | icon: widget.icon, 86 | isExtended: true, 87 | extendedIconLabelSpacing: 88 | _isExtended ? widget.extendedIconLabelSpacing : 0, 89 | extendedPadding: _isExtended 90 | ? const EdgeInsetsDirectional.only(start: 16, end: 20) 91 | : const EdgeInsetsDirectional.only(start: 16, end: 16), 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/component/widget/sliver.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../utils/logger.dart'; 4 | 5 | class SGDelegatePack { 6 | SGDelegatePack(this.gridDelegate, this.size); 7 | SliverGridDelegateWithFixedCrossAxisCount gridDelegate; 8 | Size size; 9 | } 10 | 11 | SGDelegatePack sliverGridDelegateWithMaxToCount( 12 | double width, SliverGridDelegateWithMaxCrossAxisExtent sliverGridDelegate, 13 | [double extendHeight = 0]) { 14 | int crossAxisCount = ((width + sliverGridDelegate.crossAxisSpacing) / 15 | (sliverGridDelegate.maxCrossAxisExtent + 16 | sliverGridDelegate.crossAxisSpacing)) 17 | .ceil(); 18 | 19 | final double usableCrossAxisExtent = 20 | width - sliverGridDelegate.crossAxisSpacing * (crossAxisCount - 1); 21 | if (crossAxisCount < 1) { 22 | crossAxisCount = 1; 23 | } 24 | final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount; 25 | double childMainAxisExtent = 26 | childCrossAxisExtent / sliverGridDelegate.childAspectRatio; 27 | childMainAxisExtent += extendHeight; 28 | final childAspectRatio = childCrossAxisExtent / childMainAxisExtent; 29 | return SGDelegatePack( 30 | SliverGridDelegateWithFixedCrossAxisCount( 31 | crossAxisCount: crossAxisCount, 32 | mainAxisSpacing: sliverGridDelegate.mainAxisSpacing, 33 | crossAxisSpacing: sliverGridDelegate.crossAxisSpacing, 34 | childAspectRatio: childAspectRatio, 35 | ), 36 | Size(childCrossAxisExtent, childMainAxisExtent)); 37 | } 38 | -------------------------------------------------------------------------------- /lib/component/widget/sys_title.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | // todo: Windows 标题 8 | // 没有找到好的方案 暂时搁置 9 | 10 | class DesktopTitle { 11 | DesktopTitle(this.title, { 12 | this.titleVisible = true, 13 | this.buttonVisible = true, 14 | }); 15 | final String title; 16 | final bool titleVisible; 17 | final bool buttonVisible; 18 | } 19 | 20 | class SysTitle extends StatefulWidget { 21 | 22 | SysTitle({ 23 | required this.title, 24 | required this.child, 25 | this.desktopTitleVisible = true, 26 | this.desktopButtonVisible = true, 27 | this.color, 28 | super.key 29 | }); 30 | 31 | final bool desktopTitleVisible; 32 | final bool desktopButtonVisible; 33 | 34 | static DesktopTitle? current; 35 | static final StreamController _currentController = StreamController.broadcast(); 36 | static Stream get currentStream => _currentController.stream; 37 | 38 | 39 | /// A one-line description of this app for use in the window manager. 40 | /// Must not be null. 41 | final String title; 42 | 43 | /// A color that the window manager should use to identify this app. Must be 44 | /// an opaque color (i.e. color.alpha must be 255 (0xFF)), and must not be 45 | /// null. 46 | final Color? color; 47 | 48 | /// The widget below this widget in the tree. 49 | /// 50 | /// {@macro flutter.widgets.ProxyWidget.child} 51 | final Widget child; 52 | 53 | @override 54 | State createState() => _SysTitleState(); 55 | } 56 | 57 | class _SysTitleState extends State { 58 | @override 59 | Widget build(BuildContext context) { 60 | final desktopTitle = DesktopTitle(widget.title, titleVisible: widget.desktopTitleVisible, buttonVisible: widget.desktopButtonVisible); 61 | SysTitle.current = desktopTitle; 62 | SysTitle._currentController.sink.add(desktopTitle); 63 | SystemChrome.setApplicationSwitcherDescription( 64 | ApplicationSwitcherDescription( 65 | label: widget.title, 66 | primaryColor: widget.color?.value, 67 | ), 68 | ); 69 | return widget.child; 70 | } 71 | 72 | @override 73 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 74 | super.debugFillProperties(properties); 75 | properties.add(StringProperty('title', widget.title, defaultValue: '')); 76 | properties.add(ColorProperty('color', widget.color, defaultValue: null)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/component/widget/system_ui_overlay.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | class SystemUIOverlay extends StatelessWidget { 6 | const SystemUIOverlay({Key? key, this.child}) : super(key: key); 7 | final Widget? child; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final brightnessLight = Theme.of(context).brightness == Brightness.light; 12 | 13 | return AnnotatedRegion( 14 | value: SystemUiOverlayStyle( 15 | systemNavigationBarColor: Colors.transparent, 16 | systemNavigationBarDividerColor: Colors.transparent, 17 | statusBarColor: Colors.transparent, 18 | systemNavigationBarContrastEnforced: false, 19 | systemNavigationBarIconBrightness: 20 | brightnessLight ? Brightness.dark : Brightness.light, 21 | ), 22 | child: child ?? Container(), 23 | ); 24 | } 25 | 26 | static TransitionBuilder? init({TransitionBuilder? builder}) { 27 | return (BuildContext context, Widget? child) => SystemUIOverlay( 28 | child: builder?.call(context, child), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:flutter/foundation.dart'; 15 | import 'package:intl/intl.dart'; 16 | import 'package:intl/message_lookup_by_library.dart'; 17 | import 'package:intl/src/intl_helpers.dart'; 18 | 19 | import 'messages_en.dart' as messages_en; 20 | import 'messages_zh_CN.dart' as messages_zh_cn; 21 | 22 | typedef Future LibraryLoader(); 23 | Map _deferredLibraries = { 24 | 'en': () => new SynchronousFuture(null), 25 | 'zh_CN': () => new SynchronousFuture(null), 26 | }; 27 | 28 | MessageLookupByLibrary? _findExact(String localeName) { 29 | switch (localeName) { 30 | case 'en': 31 | return messages_en.messages; 32 | case 'zh_CN': 33 | return messages_zh_cn.messages; 34 | default: 35 | return null; 36 | } 37 | } 38 | 39 | /// User programs should call this before using [localeName] for messages. 40 | Future initializeMessages(String localeName) { 41 | var availableLocale = Intl.verifiedLocale( 42 | localeName, (locale) => _deferredLibraries[locale] != null, 43 | onFailure: (_) => null); 44 | if (availableLocale == null) { 45 | return new SynchronousFuture(false); 46 | } 47 | var lib = _deferredLibraries[availableLocale]; 48 | lib == null ? new SynchronousFuture(false) : lib(); 49 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 50 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 51 | return new SynchronousFuture(true); 52 | } 53 | 54 | bool _messagesExistFor(String locale) { 55 | try { 56 | return _findExact(locale) != null; 57 | } catch (e) { 58 | return false; 59 | } 60 | } 61 | 62 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 63 | var actualLocale = 64 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 65 | if (actualLocale == null) return null; 66 | return _findExact(actualLocale); 67 | } 68 | -------------------------------------------------------------------------------- /lib/l10n/intl_zh_CN.arb: -------------------------------------------------------------------------------- 1 | { 2 | "language_name": "简体中文", 3 | "app_title": "Eros-N", 4 | "popular": "热门", 5 | "gallery": "画廊", 6 | "newest": "最新", 7 | "home": "主页", 8 | "favorites": "收藏", 9 | "history": "历史", 10 | "more": "更多", 11 | "search": "搜索", 12 | "settings": "设置", 13 | "appearance": "外观", 14 | "about": "关于", 15 | "download": "下载", 16 | "login": "登录", 17 | "logout": "登出", 18 | "register": "注册", 19 | "username": "用户名", 20 | "password": "密码", 21 | "login_by_web": "通过网页登陆", 22 | "today": "今天", 23 | "yesterday": "昨天", 24 | "thumbs": "缩略图", 25 | "more_like_this": "更多相似", 26 | "read": "阅读", 27 | "resume": "继续", 28 | "share": "分享", 29 | "please_login_first": "请先登录", 30 | "login_need_captcha": "登录需要验证码", 31 | "login_invalid": "无效登录信息", 32 | "please_login_web": "请通过网页登录", 33 | "ok": "确定", 34 | "cancel": "取消", 35 | "never": "从不", 36 | "always": "始终", 37 | "auto": "自动", 38 | "comments": "评论", 39 | "tag_type_parodies": "原作", 40 | "tag_type_characters": "角色", 41 | "tag_type_tags": "标签", 42 | "tag_type_artists": "作者", 43 | "tag_type_groups": "团队", 44 | "tag_type_languages": "语言", 45 | "tag_type_categories": "类别", 46 | "recent": "最新发布", 47 | "popular_today": "本日热门", 48 | "popular_week": "本周热门", 49 | "popular_month": "本月热门", 50 | "popular_all": "所有热门", 51 | "clear_history": "清除历史", 52 | "clear_history_tip": "确定要清除所有历史记录吗?", 53 | "left_to_right": "从左到右", 54 | "right_to_left": "从右到左", 55 | "webtoon": "条漫", 56 | "vertical": "垂直", 57 | "curl_vertical": "垂直(连续)", 58 | "full_screen": "全屏", 59 | "display": "显示", 60 | "theme_mode": "主题模式", 61 | "light": "浅色", 62 | "dark": "深色", 63 | "system": "系统", 64 | "dynamic_color": "动态颜色", 65 | "dynamic_color_tip": "根据壁纸自动调整主题颜色", 66 | "theme": "主题", 67 | "list_style": "列表样式", 68 | "tag_translation": "标签翻译", 69 | "tag_translation_tip": "需要下载翻译数据,当前版本: {version}", 70 | "tags_data": "标签数据", 71 | "show_tags": "显示标签", 72 | "show_tags_tip": "在列表中显示标签, 需要下载额外标签数据", 73 | "tag_layout_on_item": "列表项标签布局", 74 | "wrap": "换行", 75 | "single_line": "单行", 76 | "cover_blur": "封面模糊", 77 | "list_model": "列表模式", 78 | "list": "列表", 79 | "grid": "网格", 80 | "waterfall": "瀑布流", 81 | "waterfall_compact": "瀑布流 (紧凑)", 82 | "advanced": "高级", 83 | "tag": "标签", 84 | "read_model": "阅读模式", 85 | "general": "通用", 86 | "language": "语言", 87 | "hide_bottom_navigation_on_scroll": "滚动时隐藏底部导航栏", 88 | "use_gallery_tint": "使用画廊色调", 89 | "use_gallery_tint_tip": "根据封面的颜色给画廊页面着色", 90 | "comment_length_error": "评论内容需要超过10个字符", 91 | "open_supported_links": "打开支持的链接", 92 | "open_supported_links_tip": "从 Android 12 开始, 应用只有在获得许可的情况下,才能作为网络链接的处理应用。否则会使用默认浏览器处理。您可以在此手动许可", 93 | "volume_key_turn_page": "音量键翻页", 94 | "auto_read_interval": "自动阅读间隔", 95 | "preload_pages_count": "预载页数", 96 | "japanese": "日语", 97 | "chinese": "汉语", 98 | "english": "英语", 99 | "translated": "翻译", 100 | "none": "无", 101 | "all": "全部", 102 | "clipboard_detection": "剪贴板链接检测" 103 | } -------------------------------------------------------------------------------- /lib/network/api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; 2 | import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; 3 | import 'package:eros_n/common/global.dart'; 4 | 5 | class Api { 6 | static CacheOptions cacheOption = CacheOptions( 7 | store: BackupCacheStore( 8 | primary: MemCacheStore(), 9 | secondary: HiveCacheStore(Global.appSupportPath), 10 | ), 11 | policy: CachePolicy.forceCache, 12 | hitCacheOnErrorExcept: [401, 403, 304, 503], 13 | maxStale: const Duration(days: 1), 14 | priority: CachePriority.normal, 15 | cipher: null, 16 | keyBuilder: CacheOptions.defaultCacheKeyBuilder, 17 | allowPostMethod: false, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /lib/network/app_dio/dio_file_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:eros_n/common/global.dart'; 3 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_cache_manager/src/web/mime_converter.dart'; 6 | 7 | import '../../network/app_dio/dio_http_cli.dart'; 8 | import 'package:clock/clock.dart'; 9 | 10 | import '../../network/app_dio/http_response.dart'; 11 | import '../../network/app_dio/http_transformer.dart'; 12 | 13 | 14 | class DioFileService extends FileService { 15 | DioFileService(); 16 | 17 | @override 18 | Future get(String url, 19 | {Map? headers}) async { 20 | DioHttpClient dioHttpClient = DioHttpClient(dioConfig: globalDioConfig); 21 | 22 | final req = await dioHttpClient.get(url, 23 | options: Options( 24 | headers: headers, 25 | responseType: ResponseType.stream, 26 | ), httpTransformer: HttpTransformerBuilder( 27 | (response) { 28 | return DioHttpResponse.success(response); 29 | }, 30 | )); 31 | 32 | return DioGetResponse(req.data as Response); 33 | } 34 | } 35 | 36 | class DioGetResponse implements FileServiceResponse { 37 | DioGetResponse(this._response); 38 | 39 | final DateTime _receivedTime = clock.now(); 40 | 41 | final Response _response; 42 | 43 | @override 44 | int get statusCode => _response.statusCode!; 45 | 46 | String? _header(String name) { 47 | return _response.headers.map[name]?.first; 48 | } 49 | 50 | @override 51 | Stream> get content { 52 | return (_response.data as ResponseBody).stream.cast>(); 53 | } 54 | 55 | @override 56 | int? get contentLength { 57 | final contentLength = _response.headers[Headers.contentLengthHeader]?.first; 58 | return contentLength != null ? int.tryParse(contentLength) : null; 59 | } 60 | 61 | @override 62 | DateTime get validTill { 63 | // Without a cache-control header we keep the file for a week 64 | var ageDuration = const Duration(days: 7); 65 | final controlHeader = _header(HttpHeaders.cacheControlHeader); 66 | if (controlHeader != null) { 67 | final controlSettings = controlHeader.split(','); 68 | for (final setting in controlSettings) { 69 | final sanitizedSetting = setting.trim().toLowerCase(); 70 | if (sanitizedSetting == 'no-cache') { 71 | ageDuration = const Duration(); 72 | } 73 | if (sanitizedSetting.startsWith('max-age=')) { 74 | var validSeconds = int.tryParse(sanitizedSetting.split('=')[1]) ?? 0; 75 | if (validSeconds > 0) { 76 | ageDuration = Duration(seconds: validSeconds); 77 | } 78 | } 79 | } 80 | } 81 | 82 | return _receivedTime.add(ageDuration); 83 | } 84 | 85 | @override 86 | String? get eTag => _header(HttpHeaders.etagHeader); 87 | 88 | @override 89 | String get fileExtension { 90 | var fileExtension = ''; 91 | final contentTypeHeader = _header(HttpHeaders.contentTypeHeader); 92 | if (contentTypeHeader != null) { 93 | final contentType = ContentType.parse(contentTypeHeader); 94 | fileExtension = contentType.fileExtension; 95 | } 96 | return fileExtension; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/network/app_dio/dio_through.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | class DioThroughInterceptor extends Interceptor { 6 | DioThroughInterceptor({required this.dio, required this.throughHandler}); 7 | 8 | Dio dio; 9 | Future Function(DioError err) throughHandler; 10 | 11 | @override 12 | Future onError(DioError err, ErrorInterceptorHandler handler) async { 13 | if (!err.requestOptions.disableThrough) { 14 | if (err.response?.statusCode == 403 || err.response?.statusCode == 503) { 15 | print('DioThroughInterceptor onError'); 16 | print(err); 17 | final ok = await throughHandler(err); 18 | if (ok) { 19 | final headers = err.requestOptions.headers; 20 | headers.remove(HttpHeaders.cookieHeader); 21 | final request = err.requestOptions.copyWith(headers: headers); 22 | await dio 23 | .fetch(request) 24 | .then((value) => handler.resolve(value)); 25 | return; 26 | } 27 | } 28 | } 29 | return super.onError(err, handler); 30 | } 31 | } 32 | 33 | const _disableThroughKey = '__dio_through_disable'; 34 | 35 | extension RequestOptionsThrough on RequestOptions { 36 | bool get disableThrough => (extra[_disableThroughKey] as bool?) ?? false; 37 | 38 | set disableThrough(bool value) => extra[_disableThroughKey] = value; 39 | } 40 | 41 | extension OptionsThrough on Options { 42 | bool get disableRetry => (extra?[_disableThroughKey] as bool?) ?? false; 43 | 44 | set disableRetry(bool value) => extra?[_disableThroughKey] = value; 45 | } 46 | -------------------------------------------------------------------------------- /lib/network/app_dio/dio_user_agent.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:dio/dio.dart'; 5 | 6 | class DioUserAgentInterceptor extends Interceptor { 7 | DioUserAgentInterceptor(this.getUserAgent); 8 | FutureOr Function(RequestOptions) getUserAgent; 9 | 10 | @override 11 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { 12 | final ua = await getUserAgent(options); 13 | if(ua != null) { 14 | options.headers[HttpHeaders.userAgentHeader] = ua; 15 | } 16 | handler.next(options); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /lib/network/app_dio/exception.dart: -------------------------------------------------------------------------------- 1 | class HttpException implements Exception { 2 | HttpException([this._message, this._code, this._data]); 3 | 4 | final String? _message; 5 | 6 | String get message => _message ?? runtimeType.toString(); 7 | 8 | final int? _code; 9 | 10 | int get code => _code ?? -1; 11 | 12 | final dynamic _data; 13 | dynamic get data => _data; 14 | 15 | @override 16 | String toString() { 17 | return 'HttpException{_message: $_message, _code: $_code, _data: $_data}'; 18 | } 19 | } 20 | 21 | /// 客户端请求错误 22 | class BadRequestException extends HttpException { 23 | BadRequestException({String? message, int? code, dynamic data}) 24 | : super(message, code, data); 25 | } 26 | 27 | /// 服务端响应错误 28 | class BadServiceException extends HttpException { 29 | BadServiceException({String? message, int? code}) : super(message, code); 30 | } 31 | 32 | class UnknownException extends HttpException { 33 | UnknownException([String? message]) : super(message); 34 | } 35 | 36 | class CancelException extends HttpException { 37 | CancelException([String? message]) : super(message); 38 | } 39 | 40 | class Image509Exception extends HttpException { 41 | Image509Exception([String? message]) : super(message); 42 | } 43 | 44 | class NetworkException extends HttpException { 45 | NetworkException({String? message, int? code}) : super(message, code); 46 | } 47 | 48 | /// 401 49 | class UnauthorisedException extends HttpException { 50 | UnauthorisedException({String? message, int? code = 401}) 51 | : super(message, code); 52 | } 53 | 54 | class BadResponseException extends HttpException { 55 | BadResponseException([this.data]) : super(); 56 | 57 | dynamic data; 58 | } 59 | 60 | /// 列表样式 61 | class ListDisplayModeException extends HttpException { 62 | ListDisplayModeException({String? message, int? code, this.params}) 63 | : super(message, code); 64 | Map? params; 65 | } 66 | 67 | class FavOrderException extends HttpException { 68 | FavOrderException({String? message, int? code, this.order}) 69 | : super(message, code); 70 | String? order; 71 | } 72 | -------------------------------------------------------------------------------- /lib/network/app_dio/http_transformer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | import 'http_response.dart'; 6 | 7 | typedef HttpTransformerBuilderCallback = FutureOr Function( 8 | Response response); 9 | 10 | /// Response 解析 11 | abstract class HttpTransformer { 12 | FutureOr> parse(Response response); 13 | } 14 | 15 | class DefaultHttpTransformer extends HttpTransformer { 16 | // 假设接口返回类型 17 | // { 18 | // "code": 100, 19 | // "data": {}, 20 | // "message": "success" 21 | // } 22 | /// 内部构造方法,可避免外部暴露构造函数,进行实例化 23 | DefaultHttpTransformer._internal(); 24 | 25 | /// 工厂构造方法,这里使用命名构造函数方式进行声明 26 | factory DefaultHttpTransformer.getInstance() => _instance; 27 | 28 | /// 单例对象 29 | static final DefaultHttpTransformer _instance = 30 | DefaultHttpTransformer._internal(); 31 | 32 | @override 33 | FutureOr> parse(Response response) { 34 | // if (response.data["code"] == 100) { 35 | // return HttpResponse.success(response.data["data"]); 36 | // } else { 37 | // return HttpResponse.failure(errorMsg:response.data["message"],errorCode: response.data["code"]); 38 | // } 39 | // return DioHttpResponse.success(response.data['data']); 40 | return DioHttpResponse.success(response.data); 41 | } 42 | } 43 | 44 | class HttpTransformerBuilder extends HttpTransformer { 45 | HttpTransformerBuilder(this.parseCallback); 46 | 47 | final HttpTransformerBuilderCallback parseCallback; 48 | 49 | @override 50 | FutureOr parse(Response response) { 51 | return parseCallback(response); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/network/app_dio/pdio.dart: -------------------------------------------------------------------------------- 1 | export 'app_dio.dart'; 2 | export 'dio_http_cli.dart'; 3 | export 'exception.dart'; 4 | export 'http_config.dart'; 5 | export 'http_response.dart'; 6 | export 'http_transformer.dart'; 7 | -------------------------------------------------------------------------------- /lib/pages/enum.dart: -------------------------------------------------------------------------------- 1 | enum LoadStatus { 2 | none, 3 | loading, 4 | loadingMore, 5 | empty, 6 | success, 7 | error, 8 | getToken, 9 | } 10 | 11 | enum PageStatus { 12 | none, 13 | loading, 14 | empty, 15 | error, 16 | } 17 | -------------------------------------------------------------------------------- /lib/pages/gallery/gallery_page_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/pages/enum.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'gallery_page_state.freezed.dart'; 5 | 6 | @freezed 7 | class GalleryViewState with _$GalleryViewState { 8 | const factory GalleryViewState({ 9 | PageStatus? pageStatus, 10 | @Default(true) bool appBartTansparent, 11 | }) = _GalleryViewState; 12 | } 13 | -------------------------------------------------------------------------------- /lib/pages/nav/front/list_view_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import '../../enum.dart'; 4 | 5 | part 'list_view_state.freezed.dart'; 6 | part 'list_view_state.g.dart'; 7 | 8 | @freezed 9 | class ListViewState with _$ListViewState { 10 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 11 | const factory ListViewState({ 12 | @Default(LoadStatus.none) LoadStatus status, 13 | @Default(1) int curPage, 14 | @Default(1) int maxPage, 15 | @Default(true) bool floatingAppBar, 16 | String? errorMessage, 17 | @Default(false) bool appBarSearch, 18 | }) = _ListViewState; 19 | 20 | factory ListViewState.fromJson(Map json) => 21 | _$ListViewStateFromJson(json); 22 | 23 | const ListViewState._(); 24 | 25 | bool get isNone => status == LoadStatus.none; 26 | bool get isLoadMore => status == LoadStatus.loadingMore; 27 | bool get isLoading => status == LoadStatus.loading; 28 | bool get isLoadEmpty => status == LoadStatus.empty; 29 | bool get isLoadError => status == LoadStatus.error; 30 | bool get isLoadSuccess => status == LoadStatus.success; 31 | } 32 | -------------------------------------------------------------------------------- /lib/pages/nav/front/list_view_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'list_view_state.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_ListViewState _$$_ListViewStateFromJson(Map json) => 10 | _$_ListViewState( 11 | status: $enumDecodeNullable(_$LoadStatusEnumMap, json['status']) ?? 12 | LoadStatus.none, 13 | curPage: json['cur_page'] as int? ?? 1, 14 | maxPage: json['max_page'] as int? ?? 1, 15 | floatingAppBar: json['floating_app_bar'] as bool? ?? true, 16 | errorMessage: json['error_message'] as String?, 17 | appBarSearch: json['app_bar_search'] as bool? ?? false, 18 | ); 19 | 20 | Map _$$_ListViewStateToJson(_$_ListViewState instance) => 21 | { 22 | 'status': _$LoadStatusEnumMap[instance.status]!, 23 | 'cur_page': instance.curPage, 24 | 'max_page': instance.maxPage, 25 | 'floating_app_bar': instance.floatingAppBar, 26 | 'error_message': instance.errorMessage, 27 | 'app_bar_search': instance.appBarSearch, 28 | }; 29 | 30 | const _$LoadStatusEnumMap = { 31 | LoadStatus.none: 'none', 32 | LoadStatus.loading: 'loading', 33 | LoadStatus.loadingMore: 'loadingMore', 34 | LoadStatus.empty: 'empty', 35 | LoadStatus.success: 'success', 36 | LoadStatus.error: 'error', 37 | LoadStatus.getToken: 'getToken', 38 | }; 39 | -------------------------------------------------------------------------------- /lib/pages/nav/index/index_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/common/provider/settings_provider.dart'; 2 | import 'package:eros_n/utils/get_utils/extensions/export.dart'; 3 | import 'package:eros_n/utils/logger.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import 'index_state.dart'; 8 | 9 | class IndexNotifier extends StateNotifier { 10 | IndexNotifier(this.ref) : super(IndexState()); 11 | 12 | final Map scrollControllerMap = {}; 13 | bool tapAwait = false; 14 | 15 | final Ref ref; 16 | 17 | Future setIndex(int index, 18 | {required BuildContext context, bool jumpToPage = false}) async { 19 | if (index == state.selectedIndex) { 20 | // logger.d('state.scrollControllerMap len ${scrollControllerMap.length}'); 21 | await doubleTapBar( 22 | duration: const Duration(milliseconds: 800), 23 | awaitComplete: false, 24 | onTap: () { 25 | scrollControllerMap[state.selectedIndex] 26 | ?.animateTo(0, duration: 300.milliseconds, curve: Curves.ease); 27 | }, 28 | ); 29 | } 30 | 31 | if (jumpToPage) { 32 | state.pageController.jumpToPage(index); 33 | } 34 | state = state.copyWith(selectedIndex: index); 35 | } 36 | 37 | void hideNavigationBar() { 38 | final hideBottomNavigationOnScroll = ref.read(settingsProvider 39 | .select((settings) => settings.hideBottomNavigationOnScroll)); 40 | if (!state.hideNavigationBar && hideBottomNavigationOnScroll) { 41 | state = state.copyWith(hideNavigationBar: true); 42 | } 43 | } 44 | 45 | void showNavigationBar() { 46 | if (state.hideNavigationBar) { 47 | state = state.copyWith(hideNavigationBar: false); 48 | } 49 | } 50 | 51 | void addScrollController(ScrollController scrollController) { 52 | scrollControllerMap[state.selectedIndex] = scrollController; 53 | logger.v('state.scrollControllerMap len ${scrollControllerMap.length}'); 54 | } 55 | 56 | /// 双击bar的处理 57 | Future doubleTapBar({ 58 | required VoidCallback onTap, 59 | VoidCallback? onDoubleTap, 60 | required Duration duration, 61 | required bool awaitComplete, 62 | }) async { 63 | if (!tapAwait) { 64 | tapAwait = true; 65 | 66 | if (awaitComplete) { 67 | await Future.delayed(duration); 68 | if (tapAwait) { 69 | // loggerNoStack.v('等待结束 执行单击事件'); 70 | tapAwait = false; 71 | onTap(); 72 | } 73 | } else { 74 | onTap(); 75 | await Future.delayed(duration); 76 | tapAwait = false; 77 | } 78 | } else { 79 | tapAwait = false; 80 | onDoubleTap?.call(); 81 | } 82 | } 83 | } 84 | 85 | final indexProvider = StateNotifierProvider((ref) { 86 | return IndexNotifier(ref); 87 | }); 88 | -------------------------------------------------------------------------------- /lib/pages/nav/index/index_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | @immutable 4 | class IndexState { 5 | IndexState({this.selectedIndex = 0, this.hideNavigationBar = false}); 6 | 7 | final int selectedIndex; 8 | 9 | final bool hideNavigationBar; 10 | 11 | final PageController pageController = PageController(); 12 | 13 | 14 | 15 | IndexState copyWith({int? selectedIndex, bool? hideNavigationBar}) { 16 | return IndexState( 17 | selectedIndex: selectedIndex ?? this.selectedIndex, 18 | hideNavigationBar: hideNavigationBar ?? this.hideNavigationBar 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/pages/nav/more/more_state.dart: -------------------------------------------------------------------------------- 1 | class MoreState { 2 | MoreState() { 3 | ///Initialize variables 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/pages/read/read_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'read_state.freezed.dart'; 4 | part 'read_state.g.dart'; 5 | 6 | @freezed 7 | class ReadState with _$ReadState { 8 | @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true) 9 | const factory ReadState({ 10 | @Default(false) bool showAppBar, 11 | @Default(0.0) double topBarOffset, 12 | @Default(0.0) double bottomBarOffset, 13 | @Default(0.0) double bottomBarHeight, 14 | @Default(false) bool showThumbList, 15 | @Default(0.0) double paddingTop, 16 | @Default(0.0) double paddingBottom, 17 | @Default(false) bool autoRead, 18 | @Default({}) Set loadCompleteIndexSet, 19 | }) = _ReadState; 20 | 21 | const ReadState._(); 22 | 23 | factory ReadState.fromJson(Map json) => 24 | _$ReadStateFromJson(json); 25 | } 26 | -------------------------------------------------------------------------------- /lib/pages/read/read_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'read_state.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$_ReadState _$$_ReadStateFromJson(Map json) => _$_ReadState( 10 | showAppBar: json['show_app_bar'] as bool? ?? false, 11 | topBarOffset: (json['top_bar_offset'] as num?)?.toDouble() ?? 0.0, 12 | bottomBarOffset: (json['bottom_bar_offset'] as num?)?.toDouble() ?? 0.0, 13 | bottomBarHeight: (json['bottom_bar_height'] as num?)?.toDouble() ?? 0.0, 14 | showThumbList: json['show_thumb_list'] as bool? ?? false, 15 | paddingTop: (json['padding_top'] as num?)?.toDouble() ?? 0.0, 16 | paddingBottom: (json['padding_bottom'] as num?)?.toDouble() ?? 0.0, 17 | autoRead: json['auto_read'] as bool? ?? false, 18 | loadCompleteIndexSet: (json['load_complete_index_set'] as List?) 19 | ?.map((e) => e as int) 20 | .toSet() ?? 21 | const {}, 22 | ); 23 | 24 | Map _$$_ReadStateToJson(_$_ReadState instance) => 25 | { 26 | 'show_app_bar': instance.showAppBar, 27 | 'top_bar_offset': instance.topBarOffset, 28 | 'bottom_bar_offset': instance.bottomBarOffset, 29 | 'bottom_bar_height': instance.bottomBarHeight, 30 | 'show_thumb_list': instance.showThumbList, 31 | 'padding_top': instance.paddingTop, 32 | 'padding_bottom': instance.paddingBottom, 33 | 'auto_read': instance.autoRead, 34 | 'load_complete_index_set': instance.loadCompleteIndexSet.toList(), 35 | }; 36 | -------------------------------------------------------------------------------- /lib/pages/setting/about_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:eros_n/common/global.dart'; 3 | import 'package:eros_n/routes/routes.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:url_launcher/url_launcher_string.dart'; 6 | 7 | @RoutePage() 8 | class AboutPage extends StatelessWidget { 9 | const AboutPage({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text('About'), 16 | ), 17 | body: SingleChildScrollView( 18 | child: Column( 19 | children: [ 20 | ListTile( 21 | title: Text('Version'), 22 | subtitle: Text( 23 | '${Global.packageInfo.version} (${Global.packageInfo.buildNumber})'), 24 | ), 25 | ListTile( 26 | title: Text('Author'), 27 | subtitle: Text('Honjow'), 28 | onTap: () => launchUrlString( 29 | 'https://github.com/honjow', 30 | mode: LaunchMode.externalApplication, 31 | ), 32 | ), 33 | ListTile( 34 | title: Text('Email'), 35 | subtitle: Text('honjow311@gmail.com'), 36 | onTap: () => launchUrlString('mailto:honjow311@gmail.com'), 37 | ), 38 | ListTile( 39 | title: Text('Github'), 40 | subtitle: Text('https://github.com/erosTeam/eros_n'), 41 | onTap: () => launchUrlString( 42 | 'https://github.com/erosTeam/eros_n', 43 | mode: LaunchMode.externalApplication, 44 | ), 45 | ), 46 | ListTile( 47 | title: Text('Telegram'), 48 | onTap: () => launchUrlString( 49 | 'https://t.me/joinchat/AEj27KMQe0JiMmUx', 50 | mode: LaunchMode.externalApplication, 51 | ), 52 | ), 53 | ListTile( 54 | title: Text('Telegram Channel'), 55 | onTap: () => launchUrlString( 56 | 'https://t.me/fehviewer', 57 | mode: LaunchMode.externalApplication, 58 | ), 59 | ), 60 | ListTile( 61 | title: Text('License'), 62 | onTap: () { 63 | erosRouter.pushNamed(NHRoutes.license); 64 | }, 65 | ), 66 | ], 67 | ), 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/pages/setting/advanced_setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:eros_n/common/provider/tag_translate_provider.dart'; 3 | import 'package:eros_n/generated/l10n.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | import 'setting_base.dart'; 8 | 9 | @RoutePage() 10 | class AdvancedSettingPage extends StatelessWidget { 11 | const AdvancedSettingPage({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: Text(L10n.of(context).advanced), 18 | ), 19 | body: ListView( 20 | children: [ 21 | SmallTitle(title: L10n.of(context).tag), 22 | Consumer(builder: (context, ref, child) { 23 | final allNhTag = ref.watch(allNhTagProvider); 24 | return ListTile( 25 | title: Text(L10n.of(context).tags_data), 26 | // subtitle: Text('total: ${ref.watch(tagTranslateProvider).total}, long press to update'), 27 | subtitle: allNhTag.when( 28 | data: (data) => 29 | Text('total: ${data.length}, long press to update'), 30 | loading: () => Text('loading...'), 31 | error: (error, stack) => Text('error: $error'), 32 | ), 33 | onLongPress: () { 34 | ref.read(tagTranslateProvider.notifier).updateNhTags(); 35 | }, 36 | ); 37 | }), 38 | ], 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/pages/setting/license_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | @RoutePage() 5 | class MyLicensePage extends StatelessWidget { 6 | const MyLicensePage({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return const LicensePage(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/pages/setting/setting_base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:eros_n/generated/l10n.dart'; 4 | import 'package:eros_n/utils/get_utils/extensions/context_extensions.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class SmallTitle extends StatelessWidget { 8 | const SmallTitle({Key? key, required this.title}) : super(key: key); 9 | final String title; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | padding: EdgeInsets.only( 15 | left: max(context.mediaQueryPadding.left, 16), top: 16, bottom: 6), 16 | child: Text( 17 | title, 18 | style: Theme.of(context).textTheme.labelLarge?.copyWith( 19 | color: Theme.of(context).colorScheme.primary, 20 | fontWeight: FontWeight.w500, 21 | ), 22 | ), 23 | ); 24 | } 25 | } 26 | 27 | class RadioDialogListTile extends StatelessWidget { 28 | const RadioDialogListTile( 29 | {Key? key, 30 | this.title, 31 | this.subtitle, 32 | this.onChanged, 33 | this.dialogTitle, 34 | this.groupValue, 35 | required this.radioTitleMap}) 36 | : super(key: key); 37 | 38 | final Widget? title; 39 | final Widget? subtitle; 40 | final ValueChanged? onChanged; 41 | final Widget? dialogTitle; 42 | final T? groupValue; 43 | final Map radioTitleMap; 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | void onChanged(T? value) { 48 | if (value != null) { 49 | this.onChanged?.call(value); 50 | Navigator.of(context).pop(); 51 | } 52 | } 53 | 54 | final children = radioTitleMap.entries 55 | .map((e) => RadioListTile( 56 | title: e.value, 57 | value: e.key, 58 | groupValue: groupValue, 59 | onChanged: onChanged, 60 | activeColor: Theme.of(context).colorScheme.primary, 61 | )) 62 | .toList(); 63 | 64 | return ListTile( 65 | title: title, 66 | subtitle: subtitle ?? radioTitleMap[groupValue], 67 | onTap: () { 68 | showDialog( 69 | context: context, 70 | builder: (context) { 71 | return AlertDialog( 72 | contentPadding: const EdgeInsets.only(top: 16), 73 | title: dialogTitle ?? title, 74 | content: Column( 75 | mainAxisSize: MainAxisSize.min, 76 | children: children, 77 | ), 78 | actions: [ 79 | TextButton( 80 | onPressed: () { 81 | Navigator.of(context).pop(); 82 | }, 83 | child: Text(L10n.of(context).cancel), 84 | ), 85 | ], 86 | ); 87 | }, 88 | ); 89 | }, 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/pages/setting/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:eros_n/common/global.dart'; 3 | import 'package:eros_n/generated/l10n.dart'; 4 | import 'package:eros_n/routes/routes.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | @RoutePage() 9 | class SettingsPage extends StatelessWidget { 10 | const SettingsPage({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text(L10n.of(context).settings), 17 | ), 18 | body: ListView( 19 | children: [ 20 | ListTile( 21 | iconColor: Theme.of(context).colorScheme.primary, 22 | leading: const Icon(Icons.tune_outlined), 23 | title: Text(L10n.of(context).general), 24 | onTap: () { 25 | erosRouter.pushNamed(NHRoutes.generalSetting); 26 | }, 27 | ), 28 | ListTile( 29 | iconColor: Theme.of(context).colorScheme.primary, 30 | leading: const Icon(Icons.color_lens_outlined), 31 | title: Text(L10n.of(context).appearance), 32 | onTap: () { 33 | erosRouter.pushNamed(NHRoutes.appearanceSetting); 34 | }, 35 | ), 36 | // read setting 37 | ListTile( 38 | iconColor: Theme.of(context).colorScheme.primary, 39 | leading: const Icon(Icons.chrome_reader_mode_outlined), 40 | title: Text(L10n.of(context).read), 41 | onTap: () { 42 | erosRouter.pushNamed(NHRoutes.readSetting); 43 | }, 44 | ), 45 | // advance setting 46 | ListTile( 47 | iconColor: Theme.of(context).colorScheme.primary, 48 | leading: const Icon(Icons.data_object_outlined), 49 | title: Text(L10n.of(context).advanced), 50 | onTap: () { 51 | erosRouter.pushNamed(NHRoutes.advancedSetting); 52 | }, 53 | ), 54 | ], 55 | )); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/pages/splash/splash_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:eros_n/common/global.dart'; 3 | import 'package:eros_n/routes/routes.dart'; 4 | import 'package:eros_n/utils/get_utils/get_utils.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | @RoutePage() 9 | class SplashPage extends StatefulHookConsumerWidget { 10 | const SplashPage({super.key}); 11 | 12 | @override 13 | ConsumerState createState() => _SplashPageState(); 14 | } 15 | 16 | class _SplashPageState extends ConsumerState { 17 | @override 18 | void initState() { 19 | super.initState(); 20 | 21 | 10 22 | .milliseconds 23 | .delay() 24 | .then((value) => erosRouter.replaceNamed(NHRoutes.home)); 25 | } 26 | 27 | @override 28 | void dispose() { 29 | super.dispose(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return const Scaffold( 35 | body: Center( 36 | child: Text( 37 | 'Eros-N', 38 | style: TextStyle(fontSize: 40), 39 | ), 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/pages/user/user_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:eros_n/common/const/const.dart'; 3 | import 'package:eros_n/common/global.dart'; 4 | import 'package:eros_n/component/models/index.dart'; 5 | import 'package:eros_n/network/app_dio/pdio.dart'; 6 | import 'package:eros_n/network/request.dart'; 7 | import 'package:eros_n/utils/logger.dart'; 8 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 9 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 10 | 11 | class UserNotifier extends StateNotifier { 12 | UserNotifier(super.state); 13 | 14 | Future login({ 15 | required String username, 16 | required String password, 17 | }) async { 18 | late final String csrfToken; 19 | 20 | try { 21 | csrfToken = await getLoginToken() ?? ''; 22 | } on HttpException catch (e) { 23 | rethrow; 24 | } 25 | 26 | logger.d('csrfToken $csrfToken'); 27 | final loginResult = await loginNhentai( 28 | username: username, password: password, csrfToken: csrfToken); 29 | 30 | return loginResult; 31 | } 32 | 33 | Future loginGetMore() async { 34 | logger.d('loginWithCookie'); 35 | 36 | final cookie = 37 | await Global.cookieJar.loadForRequest(Uri.parse(NHConst.baseUrl)); 38 | final sessionid = 39 | cookie.firstWhereOrNull((element) => element.name == 'sessionid'); 40 | 41 | state = state.copyWith(sessionid: sessionid?.value); 42 | hiveHelper.setUser(state); 43 | 44 | late User userFromIndex; 45 | try { 46 | userFromIndex = await getInfoFromIndex(refresh: true); 47 | } on HttpException catch (e) { 48 | rethrow; 49 | } 50 | // log userFromIndex 51 | logger.d('userFromIndex $userFromIndex'); 52 | 53 | state = state.copyWith( 54 | userUrl: userFromIndex.userUrl, 55 | userName: userFromIndex.userName, 56 | userId: userFromIndex.userId, 57 | ); 58 | hiveHelper.setUser(state); 59 | 60 | if (userFromIndex.userUrl == null) { 61 | return; 62 | } 63 | late User userFromProfile; 64 | try { 65 | userFromProfile = await getInfoFromUserPage( 66 | url: userFromIndex.userUrl ?? '', refresh: true); 67 | } on HttpException catch (e) { 68 | rethrow; 69 | } 70 | 71 | state = state.copyWith( 72 | avatarUrl: userFromProfile.avatarUrl, 73 | ); 74 | hiveHelper.setUser(state); 75 | } 76 | 77 | void logout() { 78 | Global.cookieJar.deleteAll(); 79 | CookieManager.instance().deleteAllCookies(); 80 | state = const User(); 81 | hiveHelper.setUser(state); 82 | } 83 | } 84 | 85 | final userProvider = StateNotifierProvider((ref) { 86 | return UserNotifier(hiveHelper.getUser()); 87 | }); 88 | -------------------------------------------------------------------------------- /lib/pages/webview/webview.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 4 | 5 | @RoutePage() 6 | class NhWebViewPage extends StatelessWidget { 7 | const NhWebViewPage({Key? key, required this.initialUrl, this.title}) 8 | : super(key: key); 9 | final String initialUrl; 10 | final String? title; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text(title ?? ''), 17 | ), 18 | body: InAppWebView( 19 | initialUrlRequest: URLRequest( 20 | url: Uri.parse(initialUrl), 21 | ), 22 | initialOptions: inAppWebViewOptions, 23 | ), 24 | ); 25 | } 26 | } 27 | 28 | final InAppWebViewGroupOptions inAppWebViewOptions = InAppWebViewGroupOptions( 29 | crossPlatform: InAppWebViewOptions( 30 | useShouldOverrideUrlLoading: true, 31 | mediaPlaybackRequiresUserGesture: false, 32 | // clearCache: true, 33 | // userAgent: NHConst.userAgent, 34 | ), 35 | android: AndroidInAppWebViewOptions( 36 | useHybridComposition: true, 37 | ), 38 | ios: IOSInAppWebViewOptions( 39 | allowsInlineMediaPlayback: true, 40 | ), 41 | ); 42 | -------------------------------------------------------------------------------- /lib/store/db/entity/gallery_history.dart: -------------------------------------------------------------------------------- 1 | import 'package:isar/isar.dart'; 2 | 3 | part 'gallery_history.g.dart'; 4 | 5 | @Collection() 6 | class GalleryHistory { 7 | @Index(unique: true, replace: true) 8 | Id gid = 0; 9 | String? mediaId; 10 | String? csrfToken; 11 | String? title; 12 | String? japaneseTitle; 13 | String? url; 14 | String? thumbUrl; 15 | int? coverImgHeight; 16 | int? coverImgWidth; 17 | @Index() 18 | int? lastReadTime; 19 | } 20 | -------------------------------------------------------------------------------- /lib/store/db/entity/nh_tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:copy_with_extension/copy_with_extension.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:isar/isar.dart'; 4 | 5 | part 'nh_tag.g.dart'; 6 | 7 | @CopyWith() 8 | @Collection() 9 | @JsonSerializable() 10 | class NhTag { 11 | NhTag({ 12 | required this.id, 13 | this.name, 14 | this.count, 15 | this.type, 16 | this.lastUseTime = 0, 17 | this.translateName, 18 | }); 19 | 20 | factory NhTag.fromJson(Map json) => _$NhTagFromJson(json); 21 | Map toJson() => _$NhTagToJson(this); 22 | 23 | @Index(unique: true, replace: true) 24 | final Id? id; 25 | @Index() 26 | @JsonKey(ignore: true) 27 | final String? type; 28 | @Index() 29 | final String? name; 30 | @Index() 31 | final String? translateName; 32 | int? count; 33 | @Index() 34 | final int lastUseTime; 35 | 36 | @override 37 | String toString() { 38 | return 'NhTag{id: $id, type: $type, name: $name, translateName: $translateName, count: $count, lastUseTime: $lastUseTime}'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/store/db/entity/tag_translate.dart: -------------------------------------------------------------------------------- 1 | import 'package:copy_with_extension/copy_with_extension.dart'; 2 | import 'package:isar/isar.dart'; 3 | 4 | part 'tag_translate.g.dart'; 5 | 6 | @CopyWith() 7 | @Collection() 8 | class TagTranslate { 9 | TagTranslate({ 10 | required this.namespace, 11 | required this.name, 12 | this.translateName, 13 | this.intro, 14 | this.links, 15 | this.lastUseTime = 0, 16 | }) : id = Isar.autoIncrement; 17 | Id id; 18 | @Index() 19 | final String namespace; 20 | @Index(composite: [CompositeIndex('namespace')], unique: true, replace: true) 21 | @Index() 22 | final String name; 23 | @Index() 24 | final String? translateName; 25 | final String? intro; 26 | final String? links; 27 | @Index() 28 | final int lastUseTime; 29 | 30 | String? get translateNameNotMD { 31 | final reg = RegExp(r'!\[(\S+)?\]\(.+?\)(\S+)'); 32 | final match = reg.allMatches(translateName ?? ''); 33 | if (match.isNotEmpty) { 34 | return translateName?.replaceAllMapped( 35 | reg, (match) => match.group(2) ?? '') ?? 36 | translateName; 37 | } else { 38 | return translateName; 39 | } 40 | } 41 | 42 | @override 43 | String toString() { 44 | return 'TagTranslate{id: $id, namespace: $namespace, name: $name, translateName: $translateName, intro: $intro, links: $links, lastUseTime: $lastUseTime}'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/store/db/isar.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/common/global.dart'; 2 | import 'package:isar/isar.dart'; 3 | 4 | import 'entity/gallery_history.dart'; 5 | import 'entity/nh_tag.dart'; 6 | import 'entity/tag_translate.dart'; 7 | 8 | Future openIsar({String? path}) async { 9 | final dirPath = path ?? Global.appSupportPath; 10 | 11 | final isar = await Isar.open( 12 | [ 13 | GalleryHistorySchema, 14 | TagTranslateSchema, 15 | NhTagSchema, 16 | ], 17 | directory: dirPath, 18 | name: 'eros_n', 19 | ); 20 | 21 | return isar; 22 | } 23 | -------------------------------------------------------------------------------- /lib/store/kv/hive.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:eros_n/component/models/index.dart'; 4 | import 'package:eros_n/utils/logger.dart'; 5 | import 'package:hive_flutter/hive_flutter.dart'; 6 | 7 | const String configBox = 'config_box'; 8 | 9 | const String userAgentKey = 'user_agent'; 10 | const String settingsKey = 'settings'; 11 | const String userKey = 'user'; 12 | const String tagTranslateInfoKey = 'tag_translate_info'; 13 | 14 | class HiveHelper { 15 | HiveHelper(); 16 | 17 | static final _configBox = Hive.box(configBox); 18 | static final _userBox = Hive.box(userKey); 19 | 20 | static Future init() async { 21 | await Hive.initFlutter(); 22 | await Hive.openBox( 23 | configBox, 24 | compactionStrategy: (int entries, int deletedEntries) { 25 | logger.v('entries $entries'); 26 | return entries > 2; 27 | }, 28 | ); 29 | await Hive.openBox( 30 | userKey, 31 | compactionStrategy: (int entries, int deletedEntries) { 32 | logger.v('entries $entries'); 33 | return true; 34 | }, 35 | ); 36 | } 37 | 38 | String? getString(String key) { 39 | return _configBox.get(key, defaultValue: ''); 40 | } 41 | 42 | Future setString(String key, String value) async { 43 | await _configBox.put(key, value); 44 | } 45 | 46 | String? getUserAgent() { 47 | return _configBox.get(userAgentKey, defaultValue: ''); 48 | } 49 | 50 | Future setUserAgent(String? value) async { 51 | if (value != null) { 52 | await _configBox.put(userAgentKey, value); 53 | } 54 | } 55 | 56 | Settings? getSettings() { 57 | final settings = _configBox.get(settingsKey, defaultValue: '{}') ?? '{}'; 58 | if (settings.isNotEmpty) { 59 | return Settings.fromJson(jsonDecode(settings) as Map); 60 | } 61 | return null; 62 | } 63 | 64 | Future setSettings(Settings settings) async { 65 | await _configBox.put(settingsKey, jsonEncode(settings.toJson())); 66 | } 67 | 68 | User getUser() { 69 | final user = _userBox.get(userKey, defaultValue: '{}') ?? '{}'; 70 | if (user.isNotEmpty) { 71 | return User.fromJson(jsonDecode(user) as Map); 72 | } 73 | return const User(); 74 | } 75 | 76 | Future setUser(User user) async { 77 | logger.d('setUser $user'); 78 | await _userBox.put(userKey, jsonEncode(user.toJson())); 79 | } 80 | 81 | TagTranslateInfo getTagTranslateInfo() { 82 | final tagTranslateInfo = 83 | _configBox.get(tagTranslateInfoKey, defaultValue: '{}') ?? '{}'; 84 | if (tagTranslateInfo.isNotEmpty) { 85 | return TagTranslateInfo.fromJson( 86 | jsonDecode(tagTranslateInfo) as Map); 87 | } 88 | return const TagTranslateInfo(); 89 | } 90 | 91 | Future setTagTranslateInfo(TagTranslateInfo tagTranslateInfo) async { 92 | await _configBox.put( 93 | tagTranslateInfoKey, jsonEncode(tagTranslateInfo.toJson())); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/utils/clipboard_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/common/provider/settings_provider.dart'; 2 | import 'package:eros_n/routes/routes.dart'; 3 | import 'package:eros_n/utils/toast.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | class ClipboardHelper { 9 | Future getClipboardData() async { 10 | ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); 11 | return data?.text; 12 | } 13 | 14 | void setClipboardData(String text) { 15 | Clipboard.setData(ClipboardData(text: text)); 16 | } 17 | 18 | Future chkClipboardLink(BuildContext context, WidgetRef ref) async { 19 | final clipboardDetection = 20 | ref.read(settingsProvider.select((s) => s.clipboardDetection)); 21 | if (!clipboardDetection) { 22 | return; 23 | } 24 | 25 | final String? text = await getClipboardData(); 26 | if (text != null) { 27 | _showClipboardLinkToast(text, ref); 28 | } 29 | } 30 | 31 | Future _showClipboardLinkToast(String text, WidgetRef ref) async { 32 | final RegExp regGalleryPageUrl = RegExp(r'https://nhentai.net/g/(\d+)/\d+'); 33 | final RegExp regGalleryUrl = RegExp(r'https?://nhentai.net/g/(\d+)/?'); 34 | 35 | final url = regGalleryPageUrl.firstMatch(text)?.group(0) ?? 36 | regGalleryUrl.firstMatch(text)?.group(0) ?? 37 | ''; 38 | 39 | if (regGalleryUrl.hasMatch(text) || regGalleryPageUrl.hasMatch(text)) { 40 | showActionToast( 41 | url, 42 | icon: CupertinoIcons.link, 43 | onPressed: () { 44 | RouteUtil.goGalleryByUrl(ref, text); 45 | }, 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/utils/eros_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:eros_n/common/const/const.dart'; 4 | 5 | T radomList(Iterable srcList) { 6 | final index = Random().nextInt(srcList.length); 7 | return srcList.toList()[index]; 8 | } 9 | 10 | String buildImageCacheKey(String url) { 11 | final uri = Uri.parse(url); 12 | final host = uri.host; 13 | if (host.endsWith(NHConst.baseHost)) { 14 | return uri.path; 15 | } 16 | return url; 17 | } 18 | 19 | String? getTagNamespace(String tagType) { 20 | switch (tagType) { 21 | case 'Parodies': 22 | return 'parody'; 23 | case 'Characters': 24 | return 'character'; 25 | case 'Tags': 26 | return null; 27 | case 'Artists': 28 | return 'artist'; 29 | case 'Groups': 30 | return 'group'; 31 | case 'Languages': 32 | return 'language'; 33 | case 'Categories': 34 | return null; 35 | default: 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/utils/get_utils/extensions/double_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | extension Precision on double { 4 | double toPrecision(int fractionDigits) { 5 | var mod = pow(10, fractionDigits.toDouble()).toDouble(); 6 | return ((this * mod).round().toDouble() / mod); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/get_utils/extensions/duration_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | /// Duration utilities. 4 | extension GetDurationUtils on Duration { 5 | /// Utility to delay some callback (or code execution). 6 | /// 7 | /// Sample: 8 | /// ``` 9 | /// void main() async { 10 | /// final _delay = 3.seconds; 11 | /// print('+ wait $_delay'); 12 | /// await _delay.delay(); 13 | /// print('- finish wait $_delay'); 14 | /// print('+ callback in 700ms'); 15 | /// await 0.7.seconds.delay(() { 16 | /// } 17 | ///``` 18 | Future delay([FutureOr Function()? callback]) async => 19 | Future.delayed(this, callback); 20 | } 21 | -------------------------------------------------------------------------------- /lib/utils/get_utils/extensions/export.dart: -------------------------------------------------------------------------------- 1 | export 'context_extensions.dart'; 2 | export 'double_extensions.dart'; 3 | export 'duration_extensions.dart'; 4 | export 'iterable_extensions.dart'; 5 | export 'num_extensions.dart'; 6 | export 'string_extensions.dart'; 7 | export 'widget_extensions.dart'; 8 | -------------------------------------------------------------------------------- /lib/utils/get_utils/extensions/iterable_extensions.dart: -------------------------------------------------------------------------------- 1 | extension IterableExtensions on Iterable { 2 | Iterable mapMany( 3 | Iterable? Function(T item) selector) sync* { 4 | for (var item in this) { 5 | final res = selector(item); 6 | if (res != null) yield* res; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/get_utils/extensions/num_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../get_utils/get_utils.dart'; 4 | 5 | extension GetNumUtils on num { 6 | bool isLowerThan(num b) => GetUtils.isLowerThan(this, b); 7 | 8 | bool isGreaterThan(num b) => GetUtils.isGreaterThan(this, b); 9 | 10 | bool isEqual(num b) => GetUtils.isEqual(this, b); 11 | 12 | /// Utility to delay some callback (or code execution). 13 | /// TODO: Add a separated implementation of delay() with the ability 14 | /// to stop it. 15 | /// 16 | /// Sample: 17 | /// ``` 18 | /// void main() async { 19 | /// print('+ wait for 2 seconds'); 20 | /// await 2.delay(); 21 | /// print('- 2 seconds completed'); 22 | /// print('+ callback in 1.2sec'); 23 | /// 1.delay(() => print('- 1.2sec callback called')); 24 | /// print('currently running callback 1.2sec'); 25 | /// } 26 | ///``` 27 | Future delay([FutureOr Function()? callback]) async => Future.delayed( 28 | Duration(milliseconds: (this * 1000).round()), 29 | callback, 30 | ); 31 | 32 | /// Easy way to make Durations from numbers. 33 | /// 34 | /// Sample: 35 | /// ``` 36 | /// print(1.seconds + 200.milliseconds); 37 | /// print(1.hours + 30.minutes); 38 | /// print(1.5.hours); 39 | ///``` 40 | Duration get milliseconds => Duration(microseconds: (this * 1000).round()); 41 | 42 | Duration get seconds => Duration(milliseconds: (this * 1000).round()); 43 | 44 | Duration get minutes => 45 | Duration(seconds: (this * Duration.secondsPerMinute).round()); 46 | 47 | Duration get hours => 48 | Duration(minutes: (this * Duration.minutesPerHour).round()); 49 | 50 | Duration get days => Duration(hours: (this * Duration.hoursPerDay).round()); 51 | 52 | //final _delayMaps = {}; 53 | // TODO: create a proper Future and control the Timer. 54 | // Future delay([double seconds = 0, VoidCallback callback]) async { 55 | // final ms = (seconds * 1000).round(); 56 | // return Future.delayed(Duration(milliseconds: ms), callback); 57 | // return _delayMaps[callback]; 58 | // } 59 | //killDelay(VoidCallback callback) { 60 | // if (_delayMaps.containsKey(callback)) { 61 | // _delayMaps[callback]?.timeout(Duration.zero, onTimeout: () { 62 | // print('callbacl eliminado!'); 63 | // }); 64 | // _delayMaps.remove(callback); 65 | // } 66 | //} 67 | 68 | } 69 | -------------------------------------------------------------------------------- /lib/utils/get_utils/extensions/string_extensions.dart: -------------------------------------------------------------------------------- 1 | import '../get_utils/get_utils.dart'; 2 | 3 | extension GetStringUtils on String { 4 | bool get isNum => GetUtils.isNum(this); 5 | 6 | bool get isNumericOnly => GetUtils.isNumericOnly(this); 7 | 8 | bool get isAlphabetOnly => GetUtils.isAlphabetOnly(this); 9 | 10 | bool get isBool => GetUtils.isBool(this); 11 | 12 | bool get isVectorFileName => GetUtils.isVector(this); 13 | 14 | bool get isImageFileName => GetUtils.isImage(this); 15 | 16 | bool get isAudioFileName => GetUtils.isAudio(this); 17 | 18 | bool get isVideoFileName => GetUtils.isVideo(this); 19 | 20 | bool get isTxtFileName => GetUtils.isTxt(this); 21 | 22 | bool get isDocumentFileName => GetUtils.isWord(this); 23 | 24 | bool get isExcelFileName => GetUtils.isExcel(this); 25 | 26 | bool get isPPTFileName => GetUtils.isPPT(this); 27 | 28 | bool get isAPKFileName => GetUtils.isAPK(this); 29 | 30 | bool get isPDFFileName => GetUtils.isPDF(this); 31 | 32 | bool get isHTMLFileName => GetUtils.isHTML(this); 33 | 34 | bool get isURL => GetUtils.isURL(this); 35 | 36 | bool get isEmail => GetUtils.isEmail(this); 37 | 38 | bool get isPhoneNumber => GetUtils.isPhoneNumber(this); 39 | 40 | bool get isDateTime => GetUtils.isDateTime(this); 41 | 42 | bool get isMD5 => GetUtils.isMD5(this); 43 | 44 | bool get isSHA1 => GetUtils.isSHA1(this); 45 | 46 | bool get isSHA256 => GetUtils.isSHA256(this); 47 | 48 | bool get isBinary => GetUtils.isBinary(this); 49 | 50 | bool get isIPv4 => GetUtils.isIPv4(this); 51 | 52 | bool get isIPv6 => GetUtils.isIPv6(this); 53 | 54 | bool get isHexadecimal => GetUtils.isHexadecimal(this); 55 | 56 | bool get isPalindrom => GetUtils.isPalindrom(this); 57 | 58 | bool get isPassport => GetUtils.isPassport(this); 59 | 60 | bool get isCurrency => GetUtils.isCurrency(this); 61 | 62 | bool get isCpf => GetUtils.isCpf(this); 63 | 64 | bool get isCnpj => GetUtils.isCnpj(this); 65 | 66 | bool isCaseInsensitiveContains(String b) => 67 | GetUtils.isCaseInsensitiveContains(this, b); 68 | 69 | bool isCaseInsensitiveContainsAny(String b) => 70 | GetUtils.isCaseInsensitiveContainsAny(this, b); 71 | 72 | String? get capitalize => GetUtils.capitalize(this); 73 | 74 | String? get capitalizeFirst => GetUtils.capitalizeFirst(this); 75 | 76 | String get removeAllWhitespace => GetUtils.removeAllWhitespace(this); 77 | 78 | String? get camelCase => GetUtils.camelCase(this); 79 | 80 | String? get paramCase => GetUtils.paramCase(this); 81 | 82 | String numericOnly({bool firstWordOnly = false}) => 83 | GetUtils.numericOnly(this, firstWordOnly: firstWordOnly); 84 | 85 | String createPath([Iterable? segments]) { 86 | final path = startsWith('/') ? this : '/$this'; 87 | return GetUtils.createPath(path, segments); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/utils/get_utils/extensions/widget_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | /// add Padding Property to widget 4 | extension WidgetPaddingX on Widget { 5 | Widget paddingAll(double padding) => 6 | Padding(padding: EdgeInsets.all(padding), child: this); 7 | 8 | Widget paddingSymmetric({double horizontal = 0.0, double vertical = 0.0}) => 9 | Padding( 10 | padding: 11 | EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), 12 | child: this); 13 | 14 | Widget paddingOnly({ 15 | double left = 0.0, 16 | double top = 0.0, 17 | double right = 0.0, 18 | double bottom = 0.0, 19 | }) => 20 | Padding( 21 | padding: EdgeInsets.only( 22 | top: top, left: left, right: right, bottom: bottom), 23 | child: this); 24 | 25 | Widget get paddingZero => Padding(padding: EdgeInsets.zero, child: this); 26 | } 27 | 28 | /// Add margin property to widget 29 | extension WidgetMarginX on Widget { 30 | Widget marginAll(double margin) => 31 | Container(margin: EdgeInsets.all(margin), child: this); 32 | 33 | Widget marginSymmetric({double horizontal = 0.0, double vertical = 0.0}) => 34 | Container( 35 | margin: 36 | EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), 37 | child: this); 38 | 39 | Widget marginOnly({ 40 | double left = 0.0, 41 | double top = 0.0, 42 | double right = 0.0, 43 | double bottom = 0.0, 44 | }) => 45 | Container( 46 | margin: EdgeInsets.only( 47 | top: top, left: left, right: right, bottom: bottom), 48 | child: this); 49 | 50 | Widget get marginZero => Container(margin: EdgeInsets.zero, child: this); 51 | } 52 | 53 | /// Allows you to insert widgets inside a CustomScrollView 54 | extension WidgetSliverBoxX on Widget { 55 | Widget get sliverBox => SliverToBoxAdapter(child: this); 56 | } 57 | -------------------------------------------------------------------------------- /lib/utils/get_utils/get_utils.dart: -------------------------------------------------------------------------------- 1 | export 'extensions/export.dart'; 2 | export 'get_utils/get_utils.dart'; 3 | export 'platform/platform.dart'; 4 | export 'queue/get_queue.dart'; 5 | -------------------------------------------------------------------------------- /lib/utils/get_utils/platform/platform.dart: -------------------------------------------------------------------------------- 1 | import 'platform_web.dart' if (dart.library.io) 'platform_io.dart'; 2 | 3 | // ignore: avoid_classes_with_only_static_members 4 | class GetPlatform { 5 | static bool get isWeb => GeneralPlatform.isWeb; 6 | 7 | static bool get isMacOS => GeneralPlatform.isMacOS; 8 | 9 | static bool get isWindows => GeneralPlatform.isWindows; 10 | 11 | static bool get isLinux => GeneralPlatform.isLinux; 12 | 13 | static bool get isAndroid => GeneralPlatform.isAndroid; 14 | 15 | static bool get isIOS => GeneralPlatform.isIOS; 16 | 17 | static bool get isFuchsia => GeneralPlatform.isFuchsia; 18 | 19 | static bool get isMobile => GetPlatform.isIOS || GetPlatform.isAndroid; 20 | 21 | static bool get isDesktop => 22 | GetPlatform.isMacOS || GetPlatform.isWindows || GetPlatform.isLinux; 23 | } 24 | -------------------------------------------------------------------------------- /lib/utils/get_utils/platform/platform_io.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | // ignore: avoid_classes_with_only_static_members 4 | class GeneralPlatform { 5 | static bool get isWeb => false; 6 | 7 | static bool get isMacOS => Platform.isMacOS; 8 | 9 | static bool get isWindows => Platform.isWindows; 10 | 11 | static bool get isLinux => Platform.isLinux; 12 | 13 | static bool get isAndroid => Platform.isAndroid; 14 | 15 | static bool get isIOS => Platform.isIOS; 16 | 17 | static bool get isFuchsia => Platform.isFuchsia; 18 | 19 | static bool get isDesktop => 20 | Platform.isMacOS || Platform.isWindows || Platform.isLinux; 21 | } 22 | -------------------------------------------------------------------------------- /lib/utils/get_utils/platform/platform_web.dart: -------------------------------------------------------------------------------- 1 | // TODO: resolve platform/desktop by JS browser agent. 2 | // ignore: avoid_web_libraries_in_flutter 3 | import 'dart:html' as html; 4 | 5 | import '../get_utils.dart'; 6 | 7 | html.Navigator _navigator = html.window.navigator; 8 | 9 | // ignore: avoid_classes_with_only_static_members 10 | class GeneralPlatform { 11 | static bool get isWeb => true; 12 | 13 | static bool get isMacOS => 14 | _navigator.appVersion.contains('Mac OS') && !GeneralPlatform.isIOS; 15 | 16 | static bool get isWindows => _navigator.appVersion.contains('Win'); 17 | 18 | static bool get isLinux => 19 | (_navigator.appVersion.contains('Linux') || 20 | _navigator.appVersion.contains('x11')) && 21 | !isAndroid; 22 | 23 | // @check https://developer.chrome.com/multidevice/user-agent 24 | static bool get isAndroid => _navigator.appVersion.contains('Android '); 25 | 26 | static bool get isIOS { 27 | // maxTouchPoints is needed to separate iPad iOS13 vs new MacOS 28 | return GetUtils.hasMatch(_navigator.platform, r'/iPad|iPhone|iPod/') || 29 | (_navigator.platform == 'MacIntel' && _navigator.maxTouchPoints! > 1); 30 | } 31 | 32 | static bool get isFuchsia => false; 33 | 34 | static bool get isDesktop => isMacOS || isWindows || isLinux; 35 | } 36 | -------------------------------------------------------------------------------- /lib/utils/get_utils/queue/get_queue.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class GetMicrotask { 4 | int _version = 0; 5 | int _microtask = 0; 6 | 7 | int get microtask => _microtask; 8 | int get version => _version; 9 | 10 | void exec(Function callback) { 11 | if (_microtask == _version) { 12 | _microtask++; 13 | scheduleMicrotask(() { 14 | _version++; 15 | _microtask = _version; 16 | callback(); 17 | }); 18 | } 19 | } 20 | } 21 | 22 | class GetQueue { 23 | final List<_Item> _queue = []; 24 | bool _active = false; 25 | 26 | Future add(Function job) { 27 | var completer = Completer(); 28 | _queue.add(_Item(completer, job)); 29 | _check(); 30 | return completer.future; 31 | } 32 | 33 | void cancelAllJobs() { 34 | _queue.clear(); 35 | } 36 | 37 | void _check() async { 38 | if (!_active && _queue.isNotEmpty) { 39 | _active = true; 40 | var item = _queue.removeAt(0); 41 | try { 42 | item.completer.complete(await item.job()); 43 | } on Exception catch (e) { 44 | item.completer.completeError(e); 45 | } 46 | _active = false; 47 | _check(); 48 | } 49 | } 50 | } 51 | 52 | class _Item { 53 | final dynamic completer; 54 | final dynamic job; 55 | 56 | _Item(this.completer, this.job); 57 | } 58 | -------------------------------------------------------------------------------- /lib/utils/toast.dart: -------------------------------------------------------------------------------- 1 | import 'package:eros_n/utils/get_utils/get_utils.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 4 | 5 | void showActionToast(String msg, {IconData? icon, VoidCallback? onPressed}) { 6 | SmartDialog.show( 7 | alignment: Alignment.bottomCenter, 8 | useAnimation: true, 9 | displayTime: 5.seconds, 10 | // consumeEvent: true, 11 | usePenetrate: true, 12 | clickMaskDismiss: false, 13 | maskColor: Colors.transparent, 14 | builder: (context) => Container( 15 | margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 120), 16 | child: Material( 17 | elevation: 6, 18 | borderRadius: BorderRadius.circular(16), 19 | type: MaterialType.card, 20 | child: Container( 21 | padding: const EdgeInsets.symmetric(horizontal: 20), 22 | child: Row( 23 | mainAxisSize: MainAxisSize.min, 24 | children: [ 25 | Expanded( 26 | child: Container( 27 | padding: const EdgeInsets.symmetric(vertical: 12), 28 | child: Text( 29 | msg, 30 | textScaleFactor: 1.0, 31 | style: Theme.of(context).textTheme.bodySmall, 32 | softWrap: true, 33 | ), 34 | ), 35 | ), 36 | IconButton( 37 | icon: Icon( 38 | icon, 39 | size: 18, 40 | ), 41 | padding: const EdgeInsets.only(left: 28), 42 | onPressed: () { 43 | SmartDialog.dismiss(); 44 | onPressed?.call(); 45 | }, 46 | ), 47 | ], 48 | ), 49 | ), 50 | ), 51 | ), 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import bitsdojo_window_macos 9 | import dynamic_color 10 | import isar_flutter_libs 11 | import package_info_plus 12 | import path_provider_foundation 13 | import sqflite 14 | import url_launcher_macos 15 | import wakelock_macos 16 | import window_size 17 | 18 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 19 | BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) 20 | DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) 21 | IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) 22 | FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) 23 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 24 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 25 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 26 | WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) 27 | WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin")) 28 | } 29 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.13' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - bitsdojo_window_macos (0.0.1): 3 | - FlutterMacOS 4 | - dynamic_color (0.0.2): 5 | - FlutterMacOS 6 | - FlutterMacOS (1.0.0) 7 | - FMDB (2.7.5): 8 | - FMDB/standard (= 2.7.5) 9 | - FMDB/standard (2.7.5) 10 | - isar_flutter_libs (1.0.0): 11 | - FlutterMacOS 12 | - package_info_plus_macos (0.0.1): 13 | - FlutterMacOS 14 | - path_provider_macos (0.0.1): 15 | - FlutterMacOS 16 | - sqflite (0.0.2): 17 | - FlutterMacOS 18 | - FMDB (>= 2.7.5) 19 | - url_launcher_macos (0.0.1): 20 | - FlutterMacOS 21 | - wakelock_macos (0.0.1): 22 | - FlutterMacOS 23 | - window_size (0.0.2): 24 | - FlutterMacOS 25 | 26 | DEPENDENCIES: 27 | - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) 28 | - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) 29 | - FlutterMacOS (from `Flutter/ephemeral`) 30 | - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) 31 | - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) 32 | - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) 33 | - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) 34 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 35 | - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) 36 | - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) 37 | 38 | SPEC REPOS: 39 | trunk: 40 | - FMDB 41 | 42 | EXTERNAL SOURCES: 43 | bitsdojo_window_macos: 44 | :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos 45 | dynamic_color: 46 | :path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos 47 | FlutterMacOS: 48 | :path: Flutter/ephemeral 49 | isar_flutter_libs: 50 | :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos 51 | package_info_plus_macos: 52 | :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos 53 | path_provider_macos: 54 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos 55 | sqflite: 56 | :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos 57 | url_launcher_macos: 58 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 59 | wakelock_macos: 60 | :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos 61 | window_size: 62 | :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos 63 | 64 | SPEC CHECKSUMS: 65 | bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 66 | dynamic_color: 394d6a888650f8534e029b27d2f8bc5c64e44008 67 | FlutterMacOS: 85f90bfb3f1703249cf1539e4dfbff31e8584698 68 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 69 | isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a 70 | package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c 71 | path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 72 | sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea 73 | url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 74 | wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 75 | window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 76 | 77 | PODFILE CHECKSUM: a884f6dd3f7494f3892ee6c81feea3a3abbf9153 78 | 79 | COCOAPODS: 1.11.3 80 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = eros_n 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.honjow.erosn 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2022 com.honjow. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /screenshots/comment_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/screenshots/comment_1.jpg -------------------------------------------------------------------------------- /screenshots/gallery_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/screenshots/gallery_1.jpg -------------------------------------------------------------------------------- /screenshots/gallery_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/screenshots/gallery_2.jpg -------------------------------------------------------------------------------- /screenshots/history_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/screenshots/history_1.jpg -------------------------------------------------------------------------------- /screenshots/home_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/screenshots/home_1.jpg -------------------------------------------------------------------------------- /scripts/thin-payload.sh: -------------------------------------------------------------------------------- 1 | # 精简Payload文件夹 (上传到AppStore会自动区分平台, 此代码仅用于构建非签名ipa) 2 | 3 | foreachThin(){ 4 | for file in $1/* 5 | do 6 | if test -f $file 7 | then 8 | mime=$(file --mime-type -b $file) 9 | if [ "$mime" == 'application/x-mach-binary' ] || [ "${file##*.}"x = "dylib"x ] 10 | then 11 | echo thin $file 12 | xcrun -sdk iphoneos lipo "$file" -thin arm64 -output "$file" 13 | xcrun -sdk iphoneos bitcode_strip "$file" -r -o "$file" 14 | strip -S -x "$file" -o "$file" 15 | fi 16 | fi 17 | if test -d $file 18 | then 19 | foreachThin $file 20 | fi 21 | done 22 | } 23 | 24 | if [ $# eq 0 ]; then 25 | ehco "no argument" 26 | else 27 | foreachThin $1 28 | fi -------------------------------------------------------------------------------- /test/network_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:http/http.dart' as http; 3 | 4 | void main() { 5 | test('NH', () async { 6 | var headers = { 7 | 'cookie': 8 | 'cf_clearance=6dsrFOlTMeE8iDVcta_FDAUPs466BtRi2VTLoy4Wuz0-1662493545-0-150; csrftoken=tPal8XhvGycU22LwiOhLZ4KRqBndr519zLORNMpOTJpw8pEBbw7TEdkXZipHYDXZ', 9 | 'User-Agent': 10 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.27' 11 | }; 12 | var request = http.Request('GET', Uri.parse('https://nhentai.net/')); 13 | 14 | request.headers.addAll(headers); 15 | 16 | http.StreamedResponse response = await request.send(); 17 | 18 | if (response.statusCode == 200) { 19 | print(await response.stream.bytesToString()); 20 | } else { 21 | print(response.reasonPhrase); 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/nhapi_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:nhentai/nhentai.dart'; 3 | 4 | void main() { 5 | final api = API(); 6 | test('api search', () { 7 | 8 | }); 9 | 10 | test('get book', () async { 11 | final Book? book = await api.getBook(177013); 12 | if (book == null) { 13 | throw Exception('Something went wrong.'); 14 | } 15 | 16 | // Short book summary 17 | print( 18 | 'Book: $book\n' 19 | 'Artists: ${book.tags.artists.join(', ')}\n' 20 | 'Languages: ${book.tags.languages.join(', ')}\n' 21 | 'Cover: ${book.cover.getUrl(api: api)}\n' 22 | 'First page: ${book.pages.first.getUrl(api: api)}\n' 23 | 'First page thumbnail: ${book.pages.first.thumbnail.getUrl( 24 | api: api)}\n', 25 | ); 26 | }); 27 | } -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:eros_n/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | void RegisterPlugins(flutter::PluginRegistry* registry) { 17 | BitsdojoWindowPluginRegisterWithRegistrar( 18 | registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); 19 | DynamicColorPluginCApiRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); 21 | IsarFlutterLibsPluginRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); 23 | UrlLauncherWindowsRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 25 | WebviewWindowsPluginRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("WebviewWindowsPlugin")); 27 | WindowSizePluginRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("WindowSizePlugin")); 29 | } 30 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | bitsdojo_window_windows 7 | dynamic_color 8 | isar_flutter_libs 9 | url_launcher_windows 10 | webview_windows 11 | window_size 12 | ) 13 | 14 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 15 | ) 16 | 17 | set(PLUGIN_BUNDLED_LIBRARIES) 18 | 19 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 20 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 21 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 24 | endforeach(plugin) 25 | 26 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 27 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 28 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 29 | endforeach(ffi_plugin) 30 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 37 | 38 | # Run the Flutter tool portions of the build. This must not be removed. 39 | add_dependencies(${BINARY_NAME} flutter_assemble) 40 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | // #include "webview_cef/webview_cef_plugin_c_api.h" 5 | 6 | #include "flutter_window.h" 7 | #include "utils.h" 8 | 9 | #include 10 | auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); 11 | 12 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 13 | _In_ wchar_t *command_line, _In_ int show_command) { 14 | //start cef deamon processes. MUST CALL FIRST 15 | //initCEFProcesses(); 16 | 17 | // Attach to console when present (e.g., 'flutter run') or create a 18 | // new console when running with a debugger. 19 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 20 | CreateAndAttachConsole(); 21 | } 22 | 23 | // Initialize COM, so that it is available for use in the library and/or 24 | // plugins. 25 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 26 | 27 | flutter::DartProject project(L"data"); 28 | 29 | std::vector command_line_arguments = 30 | GetCommandLineArguments(); 31 | 32 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 33 | 34 | FlutterWindow window(project); 35 | Win32Window::Point origin(10, 10); 36 | Win32Window::Size size(1280, 720); 37 | if (!window.CreateAndShow(L"eros_n", origin, size)) { 38 | return EXIT_FAILURE; 39 | } 40 | window.SetQuitOnClose(true); 41 | 42 | ::MSG msg; 43 | while (::GetMessage(&msg, nullptr, 0, 0)) { 44 | ::TranslateMessage(&msg); 45 | ::DispatchMessage(&msg); 46 | } 47 | 48 | ::CoUninitialize(); 49 | return EXIT_SUCCESS; 50 | } 51 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erosTeam/eros_n/b23a7d50f1a84de672ba9e157c53cf6ac946515b/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | --------------------------------------------------------------------------------