├── lib
├── l10n
│ └── app_zh.arb
├── utils
│ ├── http.dart
│ ├── launch.dart
│ ├── datetime.dart
│ ├── string.dart
│ ├── css.dart
│ ├── cookie.dart
│ ├── firebase.dart
│ ├── key_event.g.dart
│ ├── notification.dart
│ └── key_event.dart
├── modules
│ ├── search
│ │ ├── types.dart
│ │ ├── dao.g.dart
│ │ ├── widgets
│ │ │ ├── screen.g.dart
│ │ │ ├── rating_bottom_sheet.g.dart
│ │ │ ├── filter_bottom_sheet.g.dart
│ │ │ ├── category_bottom_sheet.g.dart
│ │ │ ├── rating_bottom_sheet.dart
│ │ │ ├── text_field.dart
│ │ │ ├── filter.dart
│ │ │ ├── filter_bottom_sheet.dart
│ │ │ ├── category_bottom_sheet.dart
│ │ │ └── screen.dart
│ │ └── dao.dart
│ ├── setting
│ │ ├── types.dart
│ │ ├── widgets
│ │ │ ├── log_out_confirm.g.dart
│ │ │ ├── tab.g.dart
│ │ │ ├── select_list_tile.g.dart
│ │ │ ├── log_out_confirm.dart
│ │ │ ├── confirm_list_tile.g.dart
│ │ │ ├── select_list_tile.dart
│ │ │ ├── confirm_list_tile.dart
│ │ │ └── screen.g.dart
│ │ ├── enum_adapter.dart
│ │ └── store.dart
│ ├── common
│ │ └── widgets
│ │ │ ├── full_width.dart
│ │ │ ├── bottom_sheet_container.dart
│ │ │ ├── rating_bar.g.dart
│ │ │ ├── full_width.g.dart
│ │ │ ├── rating_bar.dart
│ │ │ ├── bottom_sheet_container.g.dart
│ │ │ ├── app_lifecycle_observer.dart
│ │ │ ├── stateful_wrapper.dart
│ │ │ ├── brightness_observer.dart
│ │ │ ├── loading_dialog.dart
│ │ │ ├── controlled_text_field.dart
│ │ │ └── orientation_setter.dart
│ ├── home
│ │ ├── widgets
│ │ │ ├── body.dart
│ │ │ ├── bottom_nav.g.dart
│ │ │ ├── body.g.dart
│ │ │ ├── bottom_nav.dart
│ │ │ └── screen.dart
│ │ ├── store.dart
│ │ ├── store.g.dart
│ │ └── tabs.dart
│ ├── download
│ │ ├── utils.dart
│ │ ├── daos
│ │ │ ├── image.g.dart
│ │ │ ├── task.g.dart
│ │ │ └── image.dart
│ │ ├── widgets
│ │ │ ├── confirm_bottom_sheet.g.dart
│ │ │ ├── tab.g.dart
│ │ │ ├── menu_bottom_sheet.g.dart
│ │ │ ├── list.g.dart
│ │ │ ├── confirm_bottom_sheet.dart
│ │ │ └── tab.dart
│ │ └── types.dart
│ ├── gallery
│ │ ├── dao.g.dart
│ │ ├── widgets
│ │ │ ├── category_icon.g.dart
│ │ │ ├── category_label.g.dart
│ │ │ ├── title.g.dart
│ │ │ ├── network_list.g.dart
│ │ │ ├── square_thumbnail.dart
│ │ │ ├── title.dart
│ │ │ ├── category_icon.dart
│ │ │ ├── square_thumbnail.g.dart
│ │ │ ├── category_label.dart
│ │ │ ├── thumbnail.g.dart
│ │ │ ├── thumbnail.dart
│ │ │ ├── list.g.dart
│ │ │ ├── tab.dart
│ │ │ └── network_list.dart
│ │ ├── types.g.dart
│ │ ├── dao.dart
│ │ └── stores
│ │ │ └── network_list.g.dart
│ ├── history
│ │ ├── dao.g.dart
│ │ ├── widgets
│ │ │ ├── tab.g.dart
│ │ │ └── tab.dart
│ │ └── dao.dart
│ ├── favorite
│ │ └── widgets
│ │ │ ├── icon.g.dart
│ │ │ ├── icon.dart
│ │ │ ├── tab.g.dart
│ │ │ ├── bottom_sheet.g.dart
│ │ │ └── tab.dart
│ ├── app
│ │ └── widgets
│ │ │ ├── theme_data_builder.g.dart
│ │ │ └── theme_data_builder.dart
│ ├── image
│ │ ├── widgets
│ │ │ ├── screen.g.dart
│ │ │ ├── tap_event_detector.g.dart
│ │ │ ├── app_bar.g.dart
│ │ │ ├── tap_event_detector.dart
│ │ │ ├── body.g.dart
│ │ │ ├── animated_navigation.dart
│ │ │ ├── key_event_detector.dart
│ │ │ ├── system_overlay_setter.dart
│ │ │ └── bottom_nav.dart
│ │ └── types.dart
│ ├── check_update
│ │ ├── widgets
│ │ │ └── screen.g.dart
│ │ ├── types.dart
│ │ ├── types.g.dart
│ │ └── store.dart
│ ├── session
│ │ ├── store.dart
│ │ └── store.g.dart
│ └── login
│ │ └── widgets
│ │ └── screen.dart
├── database
│ └── converter.dart
├── tasks
│ └── handler.dart
└── main.dart
├── ios
├── Flutter
│ ├── Debug.xcconfig
│ ├── Release.xcconfig
│ └── AppFrameworkInfo.plist
├── Runner
│ ├── Runner-Bridging-Header.h
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── 16.png
│ │ │ ├── 20.png
│ │ │ ├── 29.png
│ │ │ ├── 32.png
│ │ │ ├── 40.png
│ │ │ ├── 48.png
│ │ │ ├── 50.png
│ │ │ ├── 55.png
│ │ │ ├── 57.png
│ │ │ ├── 58.png
│ │ │ ├── 60.png
│ │ │ ├── 64.png
│ │ │ ├── 72.png
│ │ │ ├── 76.png
│ │ │ ├── 80.png
│ │ │ ├── 87.png
│ │ │ ├── 88.png
│ │ │ ├── 100.png
│ │ │ ├── 1024.png
│ │ │ ├── 114.png
│ │ │ ├── 120.png
│ │ │ ├── 128.png
│ │ │ ├── 144.png
│ │ │ ├── 152.png
│ │ │ ├── 167.png
│ │ │ ├── 172.png
│ │ │ ├── 180.png
│ │ │ ├── 196.png
│ │ │ ├── 216.png
│ │ │ ├── 256.png
│ │ │ └── 512.png
│ │ └── LaunchImage.imageset
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ ├── README.md
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ ├── Main.storyboard
│ │ └── LaunchScreen.storyboard
│ └── Info.plist
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── IDEWorkspaceChecks.plist
├── Runner.xcodeproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── IDEWorkspaceChecks.plist
└── .gitignore
├── l10n.yaml
├── docs
├── download-qrcode.png
├── screenshot-info.png
├── screenshot-list.png
├── screenshot-view.png
├── screenshot-search.png
└── screenshot-download.png
├── test
├── repositories
│ └── fixtures
│ │ ├── gallery_list_incorrect_token.json
│ │ ├── gallery_list_not_found.json
│ │ └── gallery_list.json
└── utils
│ ├── cookie_test.dart
│ ├── css_test.dart
│ ├── string_test.dart
│ └── datetime_test.dart
├── android
├── gradle.properties
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── drawable-hdpi
│ │ │ │ │ ├── app_icon.png
│ │ │ │ │ └── download.png
│ │ │ │ ├── drawable-mdpi
│ │ │ │ │ ├── app_icon.png
│ │ │ │ │ └── download.png
│ │ │ │ ├── drawable-xhdpi
│ │ │ │ │ ├── app_icon.png
│ │ │ │ │ └── download.png
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── drawable-xxhdpi
│ │ │ │ │ ├── app_icon.png
│ │ │ │ │ └── download.png
│ │ │ │ ├── drawable-xxxhdpi
│ │ │ │ │ ├── app_icon.png
│ │ │ │ │ └── download.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── raw
│ │ │ │ │ └── keep.xml
│ │ │ │ ├── xml
│ │ │ │ │ └── backup_rules.xml
│ │ │ │ └── drawable
│ │ │ │ │ └── launch_background.xml
│ │ │ └── AndroidManifest.xml
│ │ ├── debug
│ │ │ ├── res
│ │ │ │ └── values
│ │ │ │ │ └── strings.xml
│ │ │ └── AndroidManifest.xml
│ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── proguard-rules.pro
│ ├── google-services.json
│ └── build.gradle
├── .gitignore
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── settings.gradle
└── build.gradle
├── analysis_options.yaml
├── .metadata
├── CONTRIBUTING.md
├── .github
├── workflows
│ ├── build.yml
│ ├── test.yml
│ └── release.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── README.md
└── pubspec.yaml
/lib/l10n/app_zh.arb:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/l10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: lib/l10n
2 | template-arb-file: app_en.arb
3 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/docs/download-qrcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/docs/download-qrcode.png
--------------------------------------------------------------------------------
/docs/screenshot-info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/docs/screenshot-info.png
--------------------------------------------------------------------------------
/docs/screenshot-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/docs/screenshot-list.png
--------------------------------------------------------------------------------
/docs/screenshot-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/docs/screenshot-view.png
--------------------------------------------------------------------------------
/docs/screenshot-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/docs/screenshot-search.png
--------------------------------------------------------------------------------
/docs/screenshot-download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/docs/screenshot-download.png
--------------------------------------------------------------------------------
/lib/utils/http.dart:
--------------------------------------------------------------------------------
1 | bool isStatusCodeOk(int statusCode) {
2 | return statusCode >= 200 && statusCode < 300;
3 | }
4 |
--------------------------------------------------------------------------------
/test/repositories/fixtures/gallery_list_incorrect_token.json:
--------------------------------------------------------------------------------
1 | {"gmetadata":[{"gid":1663615,"error":"Key missing, or incorrect key provided."}]}
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableR8=true
3 | android.useAndroidX=true
4 | android.enableJetifier=true
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-hdpi/app_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-hdpi/download.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-mdpi/app_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-mdpi/download.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-xhdpi/app_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-xhdpi/download.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-xxhdpi/app_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-xxhdpi/download.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-xxxhdpi/app_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/drawable-xxxhdpi/download.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | EH Redux
4 |
5 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | /key.properties
8 | GeneratedPluginRegistrant.java
9 |
--------------------------------------------------------------------------------
/android/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | EH Redux (Debug)
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/raw/keep.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommy351/eh-redux/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/lib/utils/launch.dart:
--------------------------------------------------------------------------------
1 | import 'package:url_launcher/url_launcher.dart';
2 |
3 | Future tryLaunch(String url) async {
4 | if (await canLaunch(url)) {
5 | await launch(url);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/repositories/fixtures/gallery_list_not_found.json:
--------------------------------------------------------------------------------
1 | {"gmetadata":[{"gid":1673615,"error":"Gallery not found. If you just added this gallery, you may have to wait a short while before it becomes available."}]}
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/lib/utils/datetime.dart:
--------------------------------------------------------------------------------
1 | DateTime tryParseSecondsSinceEpoch(String s) {
2 | final n = int.tryParse(s);
3 | if (n == null) return null;
4 |
5 | return DateTime.fromMillisecondsSinceEpoch(n * 1000, isUtc: true);
6 | }
7 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
7 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lint/analysis_options_package.yaml
2 |
3 | linter:
4 | rules:
5 | prefer_single_quotes: true
6 | sort_constructors_first: true
7 |
8 | analyzer:
9 | exclude:
10 | - "lib/generated/**"
11 | - "**/*.g.dart"
12 | - "**/*.freezed.dart"
13 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lib/modules/search/types.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'types.freezed.dart';
4 |
5 | @freezed
6 | abstract class SearchArguments with _$SearchArguments {
7 | const factory SearchArguments({
8 | @Default('') String query,
9 | }) = _SearchArguments;
10 | }
11 |
--------------------------------------------------------------------------------
/ios/Runner.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.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.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 and should not be manually edited.
5 |
6 | version:
7 | revision: 5f21edf8b66e31a39133177319414395cc5b5f48
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/lib/modules/setting/types.dart:
--------------------------------------------------------------------------------
1 | enum OrientationSetting {
2 | auto,
3 | landscape,
4 | portrait,
5 | }
6 |
7 | enum ThemeSetting {
8 | system,
9 | light,
10 | dark,
11 | black,
12 | }
13 |
14 | enum SettingKey {
15 | displayJapaneseTitle,
16 | orientation,
17 | turnPagesWithVolumeKeys,
18 | theme,
19 | displayContentWarning,
20 | }
21 |
--------------------------------------------------------------------------------
/test/utils/cookie_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/utils/cookie.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 |
4 | void main() {
5 | test('parse cookies', () {
6 | expect(
7 | parseCookies('foo=bar; bar=baz'),
8 | equals({
9 | 'foo': 'bar',
10 | 'bar': 'baz',
11 | }));
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils/string.dart:
--------------------------------------------------------------------------------
1 | String trimPrefix(String s, String prefix) {
2 | if (s.startsWith(prefix)) {
3 | return s.substring(prefix.length);
4 | }
5 |
6 | return s;
7 | }
8 |
9 | String trimSuffix(String s, String suffix) {
10 | if (s.endsWith(suffix)) {
11 | return s.substring(0, s.length - suffix.length);
12 | }
13 |
14 | return s;
15 | }
16 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/full_width.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
3 |
4 | part 'full_width.g.dart';
5 |
6 | @swidget
7 | Widget fullWidth(BuildContext context, {@required Widget child}) {
8 | return SizedBox(
9 | width: double.infinity,
10 | child: child,
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/lib/modules/home/widgets/body.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
3 |
4 | part 'body.g.dart';
5 |
6 | @swidget
7 | Widget homeBody(BuildContext context, {@required Widget child}) {
8 | return MediaQuery.removePadding(
9 | context: context,
10 | removeTop: true,
11 | child: child,
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils/css.dart:
--------------------------------------------------------------------------------
1 | import 'dart:collection';
2 |
3 | Map parseRules(String style) {
4 | final rules = HashMap();
5 |
6 | for (final rule in style.split(';')) {
7 | final index = rule.indexOf(':');
8 |
9 | if (index > -1) {
10 | rules[rule.substring(0, index).trim()] = rule.substring(index + 1).trim();
11 | }
12 | }
13 |
14 | return rules;
15 | }
16 |
--------------------------------------------------------------------------------
/lib/modules/download/utils.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:path/path.dart' as p;
4 | import 'package:path_provider/path_provider.dart';
5 |
6 | Future getGalleryDownloadDirectory(int galleryId) async {
7 | final docDir = await getApplicationDocumentsDirectory();
8 | if (docDir == null) return null;
9 | return Directory(p.join(docDir.path, 'downloads', galleryId.toString()));
10 | }
11 |
--------------------------------------------------------------------------------
/lib/modules/gallery/dao.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'dao.dart';
4 |
5 | // **************************************************************************
6 | // DaoGenerator
7 | // **************************************************************************
8 |
9 | mixin _$GalleriesDaoMixin on DatabaseAccessor {
10 | $GalleriesTable get galleries => attachedDatabase.galleries;
11 | }
12 |
--------------------------------------------------------------------------------
/lib/modules/search/dao.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'dao.dart';
4 |
5 | // **************************************************************************
6 | // DaoGenerator
7 | // **************************************************************************
8 |
9 | mixin _$SearchHistoriesDaoMixin on DatabaseAccessor {
10 | $SearchHistoriesTable get searchHistories => attachedDatabase.searchHistories;
11 | }
12 |
--------------------------------------------------------------------------------
/test/utils/css_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/utils/css.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 |
4 | void main() {
5 | test('parse CSS rules', () {
6 | expect(
7 | parseRules(
8 | 'width: 300px; height: 400px; font-family: Arial, sans-serif;'),
9 | equals({
10 | 'width': '300px',
11 | 'height': '400px',
12 | 'font-family': 'Arial, sans-serif',
13 | }));
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/modules/download/daos/image.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'image.dart';
4 |
5 | // **************************************************************************
6 | // DaoGenerator
7 | // **************************************************************************
8 |
9 | mixin _$DownloadedImagesDaoMixin on DatabaseAccessor {
10 | $DownloadedImagesTable get downloadedImages =>
11 | attachedDatabase.downloadedImages;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/utils/cookie.dart:
--------------------------------------------------------------------------------
1 | import 'dart:collection';
2 |
3 | Map parseCookies(String cookies) {
4 | final result = HashMap();
5 |
6 | for (final cookie in cookies.split(';')) {
7 | final index = cookie.indexOf('=');
8 |
9 | if (index > -1) {
10 | final key = cookie.substring(0, index).trim();
11 | final value = cookie.substring(index + 1).trim();
12 | result[key] = value;
13 | }
14 | }
15 |
16 | return result;
17 | }
18 |
--------------------------------------------------------------------------------
/test/utils/string_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/utils/string.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 |
4 | void main() {
5 | test('trimPrefix', () {
6 | expect(trimPrefix('abcdefg', 'abc'), equals('defg'));
7 | expect(trimPrefix('abcdefg', 'bcd'), equals('abcdefg'));
8 | });
9 |
10 | test('trimSuffix', () {
11 | expect(trimSuffix('abcdefg', 'efg'), equals('abcd'));
12 | expect(trimSuffix('abcdefg', 'bcd'), equals('abcdefg'));
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/lib/modules/history/dao.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'dao.dart';
4 |
5 | // **************************************************************************
6 | // DaoGenerator
7 | // **************************************************************************
8 |
9 | mixin _$HistoryDaoMixin on DatabaseAccessor {
10 | $GalleriesTable get galleries => attachedDatabase.galleries;
11 | $DownloadTasksTable get downloadTasks => attachedDatabase.downloadTasks;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/modules/download/daos/task.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'task.dart';
4 |
5 | // **************************************************************************
6 | // DaoGenerator
7 | // **************************************************************************
8 |
9 | mixin _$DownloadTasksDaoMixin on DatabaseAccessor {
10 | $DownloadTasksTable get downloadTasks => attachedDatabase.downloadTasks;
11 | $GalleriesTable get galleries => attachedDatabase.galleries;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/bottom_sheet_container.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
3 |
4 | part 'bottom_sheet_container.g.dart';
5 |
6 | @swidget
7 | Widget bottomSheetContainer(
8 | BuildContext context, {
9 | @required Widget child,
10 | }) {
11 | final mediaQuery = MediaQuery.of(context);
12 |
13 | return Padding(
14 | padding: mediaQuery.padding + mediaQuery.viewInsets,
15 | child: child,
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/test/utils/datetime_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/utils/datetime.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 |
4 | void main() {
5 | group('tryParseSecondsSinceEpoch', () {
6 | test('should return null if invalid', () {
7 | expect(tryParseSecondsSinceEpoch('ajaoerjwoei'), isNull);
8 | });
9 |
10 | test('should return DateTime', () {
11 | expect(tryParseSecondsSinceEpoch('1592589710'),
12 | equals(DateTime.parse('2020-06-19T18:01:50Z')));
13 | });
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/lib/modules/home/widgets/bottom_nav.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'bottom_nav.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class HomeBottomNav extends StatelessWidget {
10 | const HomeBottomNav({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => homeBottomNav(_context);
14 | }
15 |
--------------------------------------------------------------------------------
/lib/modules/setting/widgets/log_out_confirm.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'log_out_confirm.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class LogOutConfirm extends StatelessWidget {
10 | const LogOutConfirm({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => logOutConfirm(_context);
14 | }
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/modules/home/widgets/body.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'body.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class HomeBody extends StatelessWidget {
10 | const HomeBody({Key key, @required this.child}) : super(key: key);
11 |
12 | final Widget child;
13 |
14 | @override
15 | Widget build(BuildContext _context) => homeBody(_context, child: child);
16 | }
17 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/rating_bar.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'rating_bar.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class RatingBar extends StatelessWidget {
10 | const RatingBar(this.rating, {Key key}) : super(key: key);
11 |
12 | final double rating;
13 |
14 | @override
15 | Widget build(BuildContext _context) => ratingBar(_context, rating);
16 | }
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Getting Started
4 |
5 | Before getting started, you have to [install Flutter](https://flutter.dev/docs/get-started/install) first.
6 |
7 | ## Install Dependencies
8 |
9 | ```sh
10 | flutter pub get
11 | ```
12 |
13 | ## Code Generation
14 |
15 | We use [build_runner](https://pub.dev/packages/build_runner) to generate models and stores.
16 |
17 | ```sh
18 | flutter pub run build_runner build
19 | ```
20 |
21 | ## Lint
22 |
23 | We use [lint](https://pub.dev/packages/lint) as linter rules.
24 |
25 | ```sh
26 | flutter analyze
27 | ```
28 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/full_width.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'full_width.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class FullWidth extends StatelessWidget {
10 | const FullWidth({Key key, @required this.child}) : super(key: key);
11 |
12 | final Widget child;
13 |
14 | @override
15 | Widget build(BuildContext _context) => fullWidth(_context, child: child);
16 | }
17 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/rating_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
3 |
4 | part 'rating_bar.g.dart';
5 |
6 | @swidget
7 | Widget ratingBar(BuildContext context, double rating) {
8 | return Row(
9 | children: Iterable.generate(5)
10 | .map((e) => Icon(_getIconData(rating - e)))
11 | .toList(),
12 | );
13 | }
14 |
15 | IconData _getIconData(double n) {
16 | if (n >= 0.75) return Icons.star;
17 | if (n >= 0.25) return Icons.star_half;
18 | return Icons.star_border;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/modules/favorite/widgets/icon.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'icon.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class FavoriteIcon extends StatelessWidget {
10 | const FavoriteIcon({Key key, @required this.favorite}) : super(key: key);
11 |
12 | final int favorite;
13 |
14 | @override
15 | Widget build(BuildContext _context) =>
16 | favoriteIcon(_context, favorite: favorite);
17 | }
18 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/category_icon.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'category_icon.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class CategoryIcon extends StatelessWidget {
10 | const CategoryIcon({Key key, @required this.category}) : super(key: key);
11 |
12 | final String category;
13 |
14 | @override
15 | Widget build(BuildContext _context) =>
16 | categoryIcon(_context, category: category);
17 | }
18 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/category_label.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'category_label.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class CategoryLabel extends StatelessWidget {
10 | const CategoryLabel({Key key, @required this.category}) : super(key: key);
11 |
12 | final String category;
13 |
14 | @override
15 | Widget build(BuildContext _context) =>
16 | categoryLabel(_context, category: category);
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | pull_request:
5 |
6 | jobs:
7 | android:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v1
11 | - uses: actions/setup-java@v1
12 | with:
13 | java-version: "12.x"
14 | - uses: subosito/flutter-action@v1
15 | with:
16 | flutter-version: "1.22.x"
17 | channel: stable
18 | - run: flutter pub get
19 | - run: flutter build apk --debug
20 | - uses: actions/upload-artifact@v2
21 | with:
22 | name: apk-debug
23 | path: build/app/outputs/apk/debug/
24 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/bottom_sheet_container.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'bottom_sheet_container.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class BottomSheetContainer extends StatelessWidget {
10 | const BottomSheetContainer({Key key, @required this.child}) : super(key: key);
11 |
12 | final Widget child;
13 |
14 | @override
15 | Widget build(BuildContext _context) =>
16 | bottomSheetContainer(_context, child: child);
17 | }
18 |
--------------------------------------------------------------------------------
/lib/modules/app/widgets/theme_data_builder.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'theme_data_builder.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class ThemeDataBuilder extends StatelessWidget {
10 | const ThemeDataBuilder({Key key, @required this.builder}) : super(key: key);
11 |
12 | final Widget Function(BuildContext, ThemeData) builder;
13 |
14 | @override
15 | Widget build(BuildContext _context) =>
16 | themeDataBuilder(_context, builder: builder);
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | pull_request:
5 |
6 | jobs:
7 | test:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v1
11 | - uses: actions/setup-java@v1
12 | with:
13 | java-version: "12.x"
14 | - uses: subosito/flutter-action@v1
15 | with:
16 | flutter-version: "1.22.x"
17 | channel: stable
18 | - run: flutter pub get
19 | - run: flutter analyze
20 | - run: flutter test --coverage
21 | - uses: codecov/codecov-action@v1
22 | with:
23 | token: ${{ secrets.CODECOV_TOKEN }}
24 | file: coverage/lcov.info
25 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/title.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'title.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class GalleryTitle extends StatelessWidget {
10 | const GalleryTitle({Key key, @required this.title, this.titleJpn = ''})
11 | : super(key: key);
12 |
13 | final String title;
14 |
15 | final String titleJpn;
16 |
17 | @override
18 | Widget build(BuildContext _context) =>
19 | galleryTitle(_context, title: title, titleJpn: titleJpn);
20 | }
21 |
--------------------------------------------------------------------------------
/lib/modules/download/widgets/confirm_bottom_sheet.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'confirm_bottom_sheet.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class DownloadConfirmBottomSheet extends StatelessWidget {
10 | const DownloadConfirmBottomSheet({Key key, @required this.gallery})
11 | : super(key: key);
12 |
13 | final Gallery gallery;
14 |
15 | @override
16 | Widget build(BuildContext _context) =>
17 | downloadConfirmBottomSheet(_context, gallery: gallery);
18 | }
19 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/screen.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'screen.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _Content extends StatelessWidget {
10 | const _Content({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => _content(_context);
14 | }
15 |
16 | class _Body extends StatelessWidget {
17 | const _Body({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _body(_context);
21 | }
22 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/screen.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'screen.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _AppBar extends StatelessWidget {
10 | const _AppBar({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => _appBar(_context);
14 | }
15 |
16 | class _Body extends StatelessWidget {
17 | const _Body({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _body(_context);
21 | }
22 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Flutter Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | include ':app'
6 |
7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
8 | def properties = new Properties()
9 |
10 | assert localPropertiesFile.exists()
11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
12 |
13 | def flutterSdkPath = properties.getProperty("flutter.sdk")
14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
16 |
--------------------------------------------------------------------------------
/lib/modules/check_update/widgets/screen.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'screen.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _Body extends StatelessWidget {
10 | const _Body({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => _body(_context);
14 | }
15 |
16 | class _DownloadButton extends StatelessWidget {
17 | const _DownloadButton({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _downloadButton(_context);
21 | }
22 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/network_list.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'network_list.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _Placeholder extends StatelessWidget {
10 | const _Placeholder({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => _placeholder(_context);
14 | }
15 |
16 | class _Footer extends StatelessWidget {
17 | const _Footer({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _footer(_context);
21 | }
22 |
--------------------------------------------------------------------------------
/lib/modules/setting/enum_adapter.dart:
--------------------------------------------------------------------------------
1 | import 'package:enum_to_string/enum_to_string.dart';
2 | import 'package:shared_preferences/shared_preferences.dart';
3 | import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
4 |
5 | class EnumAdapter extends PreferenceAdapter {
6 | const EnumAdapter(this.enumValues);
7 |
8 | final List enumValues;
9 |
10 | @override
11 | T getValue(SharedPreferences preferences, String key) {
12 | final value = preferences.getString(key);
13 | return EnumToString.fromString(enumValues, value);
14 | }
15 |
16 | @override
17 | Future setValue(SharedPreferences preferences, String key, T value) {
18 | return preferences.setString(key, EnumToString.convertToString(value));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/modules/home/store.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/home/widgets/screen.dart';
2 | import 'package:eh_redux/utils/firebase.dart';
3 | import 'package:mobx/mobx.dart';
4 |
5 | import 'tabs.dart';
6 |
7 | part 'store.g.dart';
8 |
9 | class HomeStore = _HomeStoreBase with _$HomeStore;
10 |
11 | abstract class _HomeStoreBase with Store {
12 | @observable
13 | int currentTab = 0;
14 |
15 | @action
16 | void setCurrentTab(int value) {
17 | if (currentTab == value) return;
18 |
19 | assert(value < tabs.length);
20 | currentTab = value;
21 | sendCurrentTabToAnalytics();
22 | }
23 |
24 | void sendCurrentTabToAnalytics() {
25 | analytics.setCurrentScreen(
26 | screenName: '${HomeScreen.route}/${tabs[currentTab].name}',
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/rating_bottom_sheet.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'rating_bottom_sheet.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class RatingBottomSheet extends StatelessWidget {
10 | const RatingBottomSheet({Key key, @required this.store}) : super(key: key);
11 |
12 | final SearchStore store;
13 |
14 | @override
15 | Widget build(BuildContext _context) =>
16 | ratingBottomSheet(_context, store: store);
17 | }
18 |
19 | class _Body extends StatelessWidget {
20 | const _Body({Key key}) : super(key: key);
21 |
22 | @override
23 | Widget build(BuildContext _context) => _body(_context);
24 | }
25 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/square_thumbnail.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
3 |
4 | import 'thumbnail.dart';
5 |
6 | part 'square_thumbnail.g.dart';
7 |
8 | @swidget
9 | Widget gallerySquareThumbnail(
10 | BuildContext context, {
11 | @required int galleryId,
12 | @required String fallbackUrl,
13 | double size,
14 | BorderRadius borderRadius,
15 | }) {
16 | return AspectRatio(
17 | aspectRatio: 1,
18 | child: ClipRRect(
19 | borderRadius: borderRadius ?? BorderRadius.circular(16),
20 | child: GalleryThumbnail(
21 | galleryId: galleryId,
22 | fallbackUrl: fallbackUrl,
23 | width: size,
24 | height: size,
25 | fit: BoxFit.cover,
26 | ),
27 | ),
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Device (please complete the following information):**
27 | - Device: [e.g. Pixel 3]
28 | - OS: [e.g. Android 9]
29 | - App Version: [e.g. 0.5.0]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/lib/modules/favorite/widgets/icon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
3 |
4 | part 'icon.g.dart';
5 |
6 | @swidget
7 | Widget favoriteIcon(
8 | BuildContext context, {
9 | @required int favorite,
10 | }) {
11 | final data = IconTheme.of(context);
12 | const colors = [
13 | Colors.grey,
14 | Colors.red,
15 | Colors.orange,
16 | Colors.yellow,
17 | Colors.green,
18 | Colors.lightGreen,
19 | Colors.lightBlue,
20 | Colors.blueAccent,
21 | Colors.purple,
22 | Colors.pink,
23 | ];
24 |
25 | return Container(
26 | width: data.size,
27 | height: data.size,
28 | decoration: BoxDecoration(
29 | shape: BoxShape.circle,
30 | color: colors[favorite] ?? Colors.grey,
31 | ),
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/title.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/setting/store.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
4 | import 'package:provider/provider.dart';
5 | import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
6 |
7 | part 'title.g.dart';
8 |
9 | @swidget
10 | Widget galleryTitle(
11 | BuildContext context, {
12 | @required String title,
13 | String titleJpn = '',
14 | }) {
15 | final settingStore = Provider.of(context);
16 |
17 | return PreferenceBuilder(
18 | preference: settingStore.displayJapaneseTitle,
19 | builder: (context, displayJapaneseTitle) {
20 | return Text(
21 | displayJapaneseTitle && titleJpn.isNotEmpty ? titleJpn : title);
22 | },
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/tap_event_detector.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'tap_event_detector.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class TapEventDetector extends StatelessWidget {
10 | const TapEventDetector(
11 | {Key key,
12 | @required this.onPrevious,
13 | @required this.onNext,
14 | @required this.child})
15 | : super(key: key);
16 |
17 | final void Function() onPrevious;
18 |
19 | final void Function() onNext;
20 |
21 | final Widget child;
22 |
23 | @override
24 | Widget build(BuildContext _context) => tapEventDetector(_context,
25 | onPrevious: onPrevious, onNext: onNext, child: child);
26 | }
27 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.50'
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.5.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | classpath 'com.google.gms:google-services:4.3.3'
12 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | google()
19 | jcenter()
20 | }
21 | }
22 |
23 | rootProject.buildDir = '../build'
24 | subprojects {
25 | project.buildDir = "${rootProject.buildDir}/${project.name}"
26 | }
27 | subprojects {
28 | project.evaluationDependsOn(':app')
29 | }
30 |
31 | task clean(type: Delete) {
32 | delete rootProject.buildDir
33 | }
34 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/app_bar.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'app_bar.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _ShareButton extends StatelessWidget {
10 | const _ShareButton({Key key, @required this.image}) : super(key: key);
11 |
12 | final GalleryImage image;
13 |
14 | @override
15 | Widget build(BuildContext _context) => _shareButton(_context, image: image);
16 | }
17 |
18 | class _PopupButton extends StatelessWidget {
19 | const _PopupButton({Key key, @required this.image}) : super(key: key);
20 |
21 | final GalleryImage image;
22 |
23 | @override
24 | Widget build(BuildContext _context) => _popupButton(_context, image: image);
25 | }
26 |
--------------------------------------------------------------------------------
/lib/modules/favorite/widgets/tab.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'tab.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _AppBar extends StatelessWidget {
10 | const _AppBar({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => _appBar(_context);
14 | }
15 |
16 | class _LoginHint extends StatelessWidget {
17 | const _LoginHint({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _loginHint(_context);
21 | }
22 |
23 | class _Content extends StatelessWidget {
24 | const _Content({Key key}) : super(key: key);
25 |
26 | @override
27 | Widget build(BuildContext _context) => _content(_context);
28 | }
29 |
--------------------------------------------------------------------------------
/lib/modules/history/widgets/tab.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'tab.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class HistoryTab extends StatelessWidget {
10 | const HistoryTab({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => historyTab(_context);
14 | }
15 |
16 | class _AppBar extends StatelessWidget {
17 | const _AppBar({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _appBar(_context);
21 | }
22 |
23 | class _Content extends StatelessWidget {
24 | const _Content({Key key}) : super(key: key);
25 |
26 | @override
27 | Widget build(BuildContext _context) => _content(_context);
28 | }
29 |
--------------------------------------------------------------------------------
/lib/modules/setting/widgets/tab.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'tab.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class SettingTab extends StatelessWidget {
10 | const SettingTab({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => settingTab(_context);
14 | }
15 |
16 | class _LoginTile extends StatelessWidget {
17 | const _LoginTile({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _loginTile(_context);
21 | }
22 |
23 | class _VersionTile extends StatelessWidget {
24 | const _VersionTile({Key key}) : super(key: key);
25 |
26 | @override
27 | Widget build(BuildContext _context) => _versionTile(_context);
28 | }
29 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
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 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/modules/setting/widgets/select_list_tile.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'select_list_tile.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class SelectListTile extends StatelessWidget {
10 | const SelectListTile(
11 | {Key key,
12 | this.title,
13 | @required this.items,
14 | @required this.value,
15 | @required this.onChanged})
16 | : super(key: key);
17 |
18 | final Widget title;
19 |
20 | final List> items;
21 |
22 | final T value;
23 |
24 | final dynamic Function(T) onChanged;
25 |
26 | @override
27 | Widget build(BuildContext _context) => selectListTile(_context,
28 | title: title, items: items, value: value, onChanged: onChanged);
29 | }
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | .dart_tool/
26 | .flutter-plugins
27 | .flutter-plugins-dependencies
28 | .packages
29 | .pub-cache/
30 | .pub/
31 | /build/
32 |
33 | # Web related
34 | lib/generated_plugin_registrant.dart
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Exceptions to above rules.
43 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
44 |
45 | /.gradle/
46 | /coverage/
47 | /.vscode/
48 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/category_icon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
3 |
4 | part 'category_icon.g.dart';
5 |
6 | @swidget
7 | Widget categoryIcon(
8 | BuildContext context, {
9 | @required String category,
10 | }) {
11 | final data = IconTheme.of(context);
12 | const colors = {
13 | 'Doujinshi': Colors.red,
14 | 'Manga': Colors.orange,
15 | 'Artist CG': Colors.yellow,
16 | 'Game CG': Colors.green,
17 | 'Western': Colors.lightGreen,
18 | 'Non-H': Colors.lightBlue,
19 | 'Image Set': Colors.indigo,
20 | 'Cosplay': Colors.purple,
21 | 'Asian Porn': Colors.deepPurple,
22 | };
23 |
24 | return Container(
25 | width: data.size,
26 | height: data.size,
27 | decoration: BoxDecoration(
28 | shape: BoxShape.circle,
29 | color: colors[category] ?? Colors.grey,
30 | ),
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/lib/modules/image/types.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/gallery/types.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'types.freezed.dart';
5 |
6 | @freezed
7 | abstract class ImageId implements _$ImageId {
8 | const factory ImageId({
9 | @required GalleryId galleryId,
10 | @required int page,
11 | @required String key,
12 | }) = _ImageId;
13 |
14 | const ImageId._();
15 |
16 | String get path => '/s/$key/${galleryId.id}-$page';
17 | }
18 |
19 | @freezed
20 | abstract class GalleryImage with _$GalleryImage {
21 | const factory GalleryImage.network({
22 | @required ImageId id,
23 | int width,
24 | int height,
25 | @required String url,
26 | String reloadKey,
27 | }) = NetworkGalleryImage;
28 |
29 | const factory GalleryImage.local({
30 | @required ImageId id,
31 | @required int width,
32 | @required int height,
33 | @required String path,
34 | }) = LocalGalleryImage;
35 | }
36 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/tap_event_detector.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/image/store.dart';
2 | import 'package:flutter/widgets.dart';
3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
4 | import 'package:provider/provider.dart';
5 |
6 | part 'tap_event_detector.g.dart';
7 |
8 | @swidget
9 | Widget tapEventDetector(
10 | BuildContext context, {
11 | @required void Function() onPrevious,
12 | @required void Function() onNext,
13 | @required Widget child,
14 | }) {
15 | final width = MediaQuery.of(context).size.width;
16 | final store = Provider.of(context);
17 |
18 | return GestureDetector(
19 | onTapUp: (details) {
20 | final dx = details.localPosition.dx;
21 |
22 | if (dx < width / 3) {
23 | onPrevious();
24 | } else if (dx > width / 3 * 2) {
25 | onNext();
26 | } else {
27 | store.toggleNav();
28 | }
29 | },
30 | child: child,
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/square_thumbnail.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'square_thumbnail.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class GallerySquareThumbnail extends StatelessWidget {
10 | const GallerySquareThumbnail(
11 | {Key key,
12 | @required this.galleryId,
13 | @required this.fallbackUrl,
14 | this.size,
15 | this.borderRadius})
16 | : super(key: key);
17 |
18 | final int galleryId;
19 |
20 | final String fallbackUrl;
21 |
22 | final double size;
23 |
24 | final BorderRadius borderRadius;
25 |
26 | @override
27 | Widget build(BuildContext _context) => gallerySquareThumbnail(_context,
28 | galleryId: galleryId,
29 | fallbackUrl: fallbackUrl,
30 | size: size,
31 | borderRadius: borderRadius);
32 | }
33 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/filter_bottom_sheet.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'filter_bottom_sheet.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class FilterBottomSheet extends StatelessWidget {
10 | const FilterBottomSheet({Key key, @required this.store}) : super(key: key);
11 |
12 | final SearchStore store;
13 |
14 | @override
15 | Widget build(BuildContext _context) =>
16 | filterBottomSheet(_context, store: store);
17 | }
18 |
19 | class _OptionTile extends StatelessWidget {
20 | const _OptionTile({Key key, @required this.name, @required this.label})
21 | : super(key: key);
22 |
23 | final String name;
24 |
25 | final String label;
26 |
27 | @override
28 | Widget build(BuildContext _context) =>
29 | _optionTile(_context, name: name, label: label);
30 | }
31 |
--------------------------------------------------------------------------------
/lib/database/converter.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:enum_to_string/enum_to_string.dart';
4 | import 'package:moor/moor.dart';
5 |
6 | class ListConverter extends TypeConverter, String> {
7 | @override
8 | String mapToSql(List value) {
9 | if (value == null) return null;
10 | return jsonEncode(value);
11 | }
12 |
13 | @override
14 | List mapToDart(String fromDb) {
15 | if (fromDb == null) return null;
16 | return (jsonDecode(fromDb) as List).map((e) => e as T).toList();
17 | }
18 | }
19 |
20 | class EnumStringConverter extends TypeConverter {
21 | const EnumStringConverter(this.values);
22 |
23 | final List values;
24 |
25 | @override
26 | String mapToSql(T value) {
27 | if (value == null) return null;
28 | return EnumToString.convertToString(value);
29 | }
30 |
31 | @override
32 | T mapToDart(String fromDb) {
33 | if (fromDb == null) return null;
34 | return EnumToString.fromString(values, fromDb);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/category_bottom_sheet.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'category_bottom_sheet.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class CategoryBottomSheet extends StatelessWidget {
10 | const CategoryBottomSheet({Key key, @required this.store}) : super(key: key);
11 |
12 | final SearchStore store;
13 |
14 | @override
15 | Widget build(BuildContext _context) =>
16 | categoryBottomSheet(_context, store: store);
17 | }
18 |
19 | class _CategoryTile extends StatelessWidget {
20 | const _CategoryTile({Key key, @required this.category, @required this.value})
21 | : super(key: key);
22 |
23 | final String category;
24 |
25 | final int value;
26 |
27 | @override
28 | Widget build(BuildContext _context) =>
29 | _categoryTile(_context, category: category, value: value);
30 | }
31 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/lib/utils/firebase.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_analytics/firebase_analytics.dart';
2 | import 'package:firebase_analytics/observer.dart';
3 | import 'package:firebase_core/firebase_core.dart';
4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart';
5 | import 'package:flutter/foundation.dart';
6 |
7 | final FirebaseAnalytics analytics = FirebaseAnalytics();
8 | final FirebaseAnalyticsObserver firebaseAnalyticsObserver =
9 | FirebaseAnalyticsObserver(analytics: analytics);
10 |
11 | Future initializeFirebase() async {
12 | // Wait for Firebase to initialize
13 | await Firebase.initializeApp();
14 |
15 | // Only enable crashlytics collection on non-debug builds
16 | await FirebaseCrashlytics.instance
17 | .setCrashlyticsCollectionEnabled(!kDebugMode);
18 |
19 | // Pass all uncaught errors to Crashlytics.
20 | final originalOnError = FlutterError.onError;
21 | FlutterError.onError = (FlutterErrorDetails errorDetails) async {
22 | await FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
23 | // Forward to original handler.
24 | originalOnError(errorDetails);
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/category_label.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
4 |
5 | part 'category_label.g.dart';
6 |
7 | @swidget
8 | Widget categoryLabel(
9 | BuildContext context, {
10 | @required String category,
11 | }) {
12 | final labels = {
13 | 'Doujinshi': AppLocalizations.of(context).categoryDoujinshi,
14 | 'Manga': AppLocalizations.of(context).categoryManga,
15 | 'Artist CG': AppLocalizations.of(context).categoryArtistCG,
16 | 'Game CG': AppLocalizations.of(context).categoryGameCG,
17 | 'Western': AppLocalizations.of(context).categoryWestern,
18 | 'Non-H': AppLocalizations.of(context).categoryNonH,
19 | 'Image Set': AppLocalizations.of(context).categoryImageSet,
20 | 'Cosplay': AppLocalizations.of(context).categoryCosplay,
21 | 'Asian Porn': AppLocalizations.of(context).categoryAsianPorn,
22 | 'Misc': AppLocalizations.of(context).categoryMisc,
23 | };
24 |
25 | return Text(labels[category] ?? category);
26 | }
27 |
--------------------------------------------------------------------------------
/lib/utils/key_event.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'key_event.dart';
4 |
5 | // **************************************************************************
6 | // BuiltValueGenerator
7 | // **************************************************************************
8 |
9 | const KeyCode _$volumeDown = const KeyCode._('volumeDown');
10 | const KeyCode _$volumeUp = const KeyCode._('volumeUp');
11 |
12 | KeyCode _$keyCodeValueOf(String name) {
13 | switch (name) {
14 | case 'volumeDown':
15 | return _$volumeDown;
16 | case 'volumeUp':
17 | return _$volumeUp;
18 | default:
19 | throw new ArgumentError(name);
20 | }
21 | }
22 |
23 | final BuiltSet _$keyCodeValues = new BuiltSet(const [
24 | _$volumeDown,
25 | _$volumeUp,
26 | ]);
27 |
28 | // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new
29 |
--------------------------------------------------------------------------------
/lib/modules/check_update/types.dart:
--------------------------------------------------------------------------------
1 | import 'package:built_collection/built_collection.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 |
4 | part 'types.freezed.dart';
5 | part 'types.g.dart';
6 |
7 | @freezed
8 | abstract class GitHubAsset with _$GitHubAsset {
9 | @JsonSerializable(fieldRename: FieldRename.snake)
10 | const factory GitHubAsset({
11 | String name,
12 | String contentType,
13 | String state,
14 | int size,
15 | String browserDownloadUrl,
16 | }) = _GitHubAsset;
17 |
18 | factory GitHubAsset.fromJson(Map json) =>
19 | _$GitHubAssetFromJson(json);
20 | }
21 |
22 | @freezed
23 | abstract class GitHubRelease with _$GitHubRelease {
24 | @JsonSerializable(fieldRename: FieldRename.snake)
25 | const factory GitHubRelease({
26 | String name,
27 | String body,
28 | String tagName,
29 | String htmlUrl,
30 | BuiltList assets,
31 | }) = _GitHubRelease;
32 |
33 | factory GitHubRelease.fromJson(Map json) =>
34 | _$GitHubReleaseFromJson(json);
35 | }
36 |
37 | enum UpdateStatus {
38 | pending,
39 | failed,
40 | canUpdate,
41 | noUpdate,
42 | }
43 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/thumbnail.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'thumbnail.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class GalleryThumbnail extends StatelessWidget {
10 | const GalleryThumbnail(
11 | {Key key,
12 | @required this.galleryId,
13 | @required this.fallbackUrl,
14 | this.width,
15 | this.height,
16 | this.fit})
17 | : super(key: key);
18 |
19 | final int galleryId;
20 |
21 | final String fallbackUrl;
22 |
23 | final double width;
24 |
25 | final double height;
26 |
27 | final BoxFit fit;
28 |
29 | @override
30 | Widget build(BuildContext _context) => galleryThumbnail(_context,
31 | galleryId: galleryId,
32 | fallbackUrl: fallbackUrl,
33 | width: width,
34 | height: height,
35 | fit: fit);
36 | }
37 |
38 | class _Placeholder extends StatelessWidget {
39 | const _Placeholder({Key key}) : super(key: key);
40 |
41 | @override
42 | Widget build(BuildContext _context) => _placeholder(_context);
43 | }
44 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/app_lifecycle_observer.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 |
3 | class AppLifecycleObserver extends StatefulWidget {
4 | const AppLifecycleObserver({
5 | Key key,
6 | @required this.didChange,
7 | @required this.child,
8 | }) : assert(didChange != null),
9 | assert(child != null),
10 | super(key: key);
11 |
12 | final void Function(AppLifecycleState) didChange;
13 | final Widget child;
14 |
15 | @override
16 | _AppLifecycleObserverState createState() => _AppLifecycleObserverState();
17 | }
18 |
19 | class _AppLifecycleObserverState extends State
20 | with WidgetsBindingObserver {
21 | @override
22 | void initState() {
23 | super.initState();
24 | WidgetsBinding.instance.addObserver(this);
25 | }
26 |
27 | @override
28 | void dispose() {
29 | WidgetsBinding.instance.removeObserver(this);
30 | super.dispose();
31 | }
32 |
33 | @override
34 | void didChangeAppLifecycleState(AppLifecycleState state) {
35 | super.didChangeAppLifecycleState(state);
36 | widget.didChange(state);
37 | }
38 |
39 | @override
40 | Widget build(BuildContext context) {
41 | return widget.child;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/tasks/handler.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:async/async.dart';
4 | import 'package:eh_redux/database/database.dart';
5 | import 'package:eh_redux/modules/download/tasks/download.dart';
6 | import 'package:enum_to_string/enum_to_string.dart';
7 | import 'package:logging/logging.dart';
8 |
9 | enum TaskTag {
10 | download,
11 | }
12 |
13 | class BackgroundTaskHandler {
14 | static final _log = Logger('BackgroundTaskHandler');
15 |
16 | final _databaseMemo = AsyncMemoizer();
17 |
18 | Future get _database => _databaseMemo.runOnce(() async {
19 | final isolate =
20 | await Database.reuseIsolate() ?? await Database.createIsolate();
21 | return Database.connect(await isolate.connect());
22 | });
23 |
24 | Future handle(
25 | String taskName,
26 | Map inputData,
27 | ) async {
28 | _log.fine('Handle: name=$taskName, data=$inputData');
29 |
30 | switch (EnumToString.fromString(TaskTag.values, taskName)) {
31 | case TaskTag.download:
32 | final handler = DownloadTaskHandler(
33 | database: await _database,
34 | );
35 | return handler.handle(inputData);
36 | }
37 |
38 | return true;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/modules/history/dao.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/database.dart';
2 | import 'package:eh_redux/modules/download/daos/task.dart';
3 | import 'package:eh_redux/modules/gallery/dao.dart';
4 | import 'package:eh_redux/modules/gallery/types.dart';
5 | import 'package:logging/logging.dart';
6 | import 'package:moor/moor.dart';
7 |
8 | part 'dao.g.dart';
9 |
10 | @UseDao(tables: [Galleries, DownloadTasks])
11 | class HistoryDao extends DatabaseAccessor with _$HistoryDaoMixin {
12 | HistoryDao(Database db) : super(db);
13 |
14 | static final _log = Logger('HistoryDao');
15 |
16 | Stream> watchAll() {
17 | _log.fine('watchAll');
18 | final query = select(galleries)
19 | ..where((t) => isNotNull(t.lastReadAt))
20 | ..orderBy([
21 | (t) => OrderingTerm.desc(t.lastReadAt),
22 | ]);
23 |
24 | return query.map((e) => Gallery.fromEntry(e)).watch();
25 | }
26 |
27 | Future deleteAll() async {
28 | _log.fine('deleteAll');
29 |
30 | final taskIds = await select(downloadTasks).map((t) => t.galleryId).get();
31 | _log.finer('Download task IDs: $taskIds');
32 |
33 | final query = delete(galleries)..where((t) => t.id.isNotIn(taskIds));
34 |
35 | await query.go();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/modules/setting/widgets/log_out_confirm.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:eh_redux/modules/session/store.dart';
3 | import 'package:eh_redux/utils/firebase.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
6 | import 'package:provider/provider.dart';
7 |
8 | part 'log_out_confirm.g.dart';
9 |
10 | @swidget
11 | Widget logOutConfirm(BuildContext context) {
12 | final sessionStore = Provider.of(context);
13 |
14 | return AlertDialog(
15 | title: Text(AppLocalizations.of(context).logOutDialogTitle),
16 | content: Text(AppLocalizations.of(context).logOutDialogContent),
17 | actions: [
18 | TextButton(
19 | onPressed: () {
20 | Navigator.pop(context);
21 | },
22 | child: Text(AppLocalizations.of(context).cancelButtonLabel),
23 | ),
24 | TextButton(
25 | onPressed: () async {
26 | await sessionStore.deleteSession();
27 | analytics.logEvent(name: 'logout');
28 | Navigator.pop(context);
29 | },
30 | child: Text(AppLocalizations.of(context).logOutButtonLabel),
31 | ),
32 | ],
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/stateful_wrapper.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 |
3 | class StatefulWrapper extends StatefulWidget {
4 | const StatefulWrapper({
5 | Key key,
6 | @required this.builder,
7 | this.onInit,
8 | this.onDispose,
9 | }) : assert(builder != null),
10 | super(key: key);
11 |
12 | final Widget Function(BuildContext) builder;
13 | final Function Function(BuildContext) onInit;
14 | final void Function() onDispose;
15 |
16 | @override
17 | _StatefulWrapperState createState() => _StatefulWrapperState();
18 | }
19 |
20 | class _StatefulWrapperState extends State {
21 | final _disposes = [];
22 |
23 | @override
24 | void initState() {
25 | super.initState();
26 |
27 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
28 | if (widget.onInit != null) {
29 | _disposes.add(widget.onInit(context));
30 | }
31 | });
32 | }
33 |
34 | @override
35 | void dispose() {
36 | for (final dispose in _disposes) {
37 | dispose();
38 | }
39 |
40 | if (widget.onDispose != null) {
41 | widget.onDispose();
42 | }
43 |
44 | super.dispose();
45 | }
46 |
47 | @override
48 | Widget build(BuildContext context) {
49 | return widget.builder(context);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/modules/history/widgets/tab.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/database.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 | import 'package:eh_redux/modules/gallery/types.dart';
4 | import 'package:eh_redux/modules/gallery/widgets/list.dart';
5 | import 'package:eh_redux/modules/home/widgets/body.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
8 | import 'package:provider/provider.dart';
9 |
10 | part 'tab.g.dart';
11 |
12 | @swidget
13 | Widget historyTab(BuildContext context) {
14 | final database = Provider.of(context);
15 |
16 | return StreamProvider>(
17 | create: (_) => database.historyDao.watchAll(),
18 | child: NestedScrollView(
19 | headerSliverBuilder: (context, _) => const [_AppBar()],
20 | body: const _Content(),
21 | ),
22 | );
23 | }
24 |
25 | @swidget
26 | Widget _appBar(BuildContext context) {
27 | return SliverAppBar(
28 | title: Text(AppLocalizations.of(context).homeTabTitleHistory),
29 | pinned: true,
30 | );
31 | }
32 |
33 | @swidget
34 | Widget _content(BuildContext context) {
35 | final data = Provider.of>(context);
36 |
37 | return HomeBody(
38 | child: GalleryList(data: data ?? []),
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/lib/modules/download/widgets/tab.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'tab.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class DownloadTab extends StatelessWidget {
10 | const DownloadTab({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => downloadTab(_context);
14 | }
15 |
16 | class _AppBar extends StatelessWidget {
17 | const _AppBar({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _appBar(_context);
21 | }
22 |
23 | class _ResumeAllButton extends StatelessWidget {
24 | const _ResumeAllButton({Key key}) : super(key: key);
25 |
26 | @override
27 | Widget build(BuildContext _context) => _resumeAllButton(_context);
28 | }
29 |
30 | class _PauseAllButton extends StatelessWidget {
31 | const _PauseAllButton({Key key}) : super(key: key);
32 |
33 | @override
34 | Widget build(BuildContext _context) => _pauseAllButton(_context);
35 | }
36 |
37 | class _Content extends StatelessWidget {
38 | const _Content({Key key}) : super(key: key);
39 |
40 | @override
41 | Widget build(BuildContext _context) => _content(_context);
42 | }
43 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/brightness_observer.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/services.dart';
2 | import 'package:flutter/widgets.dart';
3 |
4 | class BrightnessObserver extends StatefulWidget {
5 | const BrightnessObserver({
6 | Key key,
7 | @required this.builder,
8 | }) : assert(builder != null),
9 | super(key: key);
10 |
11 | final Widget Function(BuildContext, Brightness) builder;
12 |
13 | @override
14 | _BrightnessObserverState createState() => _BrightnessObserverState();
15 | }
16 |
17 | class _BrightnessObserverState extends State
18 | with WidgetsBindingObserver {
19 | Brightness _brightness;
20 |
21 | @override
22 | void initState() {
23 | super.initState();
24 | WidgetsBinding.instance.addObserver(this);
25 | _update();
26 | }
27 |
28 | @override
29 | void dispose() {
30 | WidgetsBinding.instance.removeObserver(this);
31 | super.dispose();
32 | }
33 |
34 | @override
35 | void didChangePlatformBrightness() {
36 | super.didChangePlatformBrightness();
37 | setState(() {
38 | _update();
39 | });
40 | }
41 |
42 | @override
43 | Widget build(BuildContext context) {
44 | return widget.builder(context, _brightness);
45 | }
46 |
47 | void _update() {
48 | _brightness = WidgetsBinding.instance.window.platformBrightness;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/loading_dialog.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class LoadingDialog extends StatefulWidget {
4 | const LoadingDialog({
5 | Key key,
6 | @required this.future,
7 | }) : assert(future != null),
8 | super(key: key);
9 |
10 | final Future future;
11 |
12 | @override
13 | _LoadingDialogState createState() => _LoadingDialogState();
14 | }
15 |
16 | class _LoadingDialogState extends State {
17 | @override
18 | void initState() {
19 | super.initState();
20 | widget.future.then((value) => Navigator.pop(context, value));
21 | }
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | return const SimpleDialog(
26 | children: [
27 | Center(
28 | child: Padding(
29 | padding: EdgeInsets.all(16),
30 | child: CircularProgressIndicator(),
31 | ),
32 | ),
33 | ],
34 | );
35 | }
36 | }
37 |
38 | Future showLoadingDialog({
39 | @required BuildContext context,
40 | @required Future future,
41 | }) async {
42 | assert(future != null);
43 |
44 | return showDialog(
45 | context: context,
46 | barrierDismissible: false,
47 | builder: (context) {
48 | return LoadingDialog(
49 | future: future,
50 | );
51 | },
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/lib/modules/home/widgets/bottom_nav.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/home/store.dart';
2 | import 'package:eh_redux/modules/home/tabs.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_mobx/flutter_mobx.dart';
5 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
6 | import 'package:provider/provider.dart';
7 |
8 | part 'bottom_nav.g.dart';
9 |
10 | @swidget
11 | Widget homeBottomNav(BuildContext context) {
12 | final homeStore = Provider.of(context);
13 | final theme = Theme.of(context);
14 |
15 | return Observer(
16 | builder: (context) {
17 | final items = tabs
18 | .map((tab) => BottomNavigationBarItem(
19 | icon: Icon(tab.icon),
20 | label: tab.title(context),
21 | ))
22 | .toList();
23 |
24 | return BottomNavigationBar(
25 | items: items,
26 | currentIndex: homeStore.currentTab,
27 | selectedItemColor: theme.accentColor,
28 | unselectedItemColor: theme.hintColor,
29 | onTap: (index) {
30 | homeStore.setCurrentTab(index);
31 | PrimaryScrollController.of(context).animateTo(
32 | 0,
33 | duration: const Duration(milliseconds: 500),
34 | curve: Curves.easeOutCubic,
35 | );
36 | },
37 | );
38 | },
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/lib/modules/setting/widgets/confirm_list_tile.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'confirm_list_tile.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class ConfirmListTile extends StatelessWidget {
10 | const ConfirmListTile(
11 | {Key key,
12 | this.title,
13 | this.leading,
14 | this.trailing,
15 | this.dialogTitle,
16 | this.dialogContent,
17 | this.disabled = false,
18 | @required this.confirmActionChild,
19 | @required this.onConfirm})
20 | : super(key: key);
21 |
22 | final Widget title;
23 |
24 | final Widget leading;
25 |
26 | final Widget trailing;
27 |
28 | final Widget dialogTitle;
29 |
30 | final Widget dialogContent;
31 |
32 | final bool disabled;
33 |
34 | final Widget confirmActionChild;
35 |
36 | final dynamic Function() onConfirm;
37 |
38 | @override
39 | Widget build(BuildContext _context) => confirmListTile(_context,
40 | title: title,
41 | leading: leading,
42 | trailing: trailing,
43 | dialogTitle: dialogTitle,
44 | dialogContent: dialogContent,
45 | disabled: disabled,
46 | confirmActionChild: confirmActionChild,
47 | onConfirm: onConfirm);
48 | }
49 |
--------------------------------------------------------------------------------
/lib/modules/session/store.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_secure_storage/flutter_secure_storage.dart';
2 | import 'package:mobx/mobx.dart';
3 |
4 | part 'store.g.dart';
5 |
6 | enum LoginStatus {
7 | pending,
8 | notLoggedIn,
9 | loggedIn,
10 | }
11 |
12 | class SessionStore = _SessionStoreBase with _$SessionStore;
13 |
14 | abstract class _SessionStoreBase with Store {
15 | _SessionStoreBase({
16 | FlutterSecureStorage storage,
17 | }) {
18 | _storage = storage ?? const FlutterSecureStorage();
19 | session = ObservableFuture(_storage.read(key: _sessionKey));
20 | }
21 |
22 | static const _sessionKey = 'session';
23 |
24 | FlutterSecureStorage _storage;
25 |
26 | @observable
27 | ObservableFuture session;
28 |
29 | @computed
30 | LoginStatus get loginStatus {
31 | if (session.value != null && session.value.isNotEmpty) {
32 | return LoginStatus.loggedIn;
33 | }
34 |
35 | return session.status == FutureStatus.pending
36 | ? LoginStatus.pending
37 | : LoginStatus.notLoggedIn;
38 | }
39 |
40 | @action
41 | Future setSession(String value) async {
42 | await _storage.write(key: _sessionKey, value: value);
43 | session = ObservableFuture.value(value);
44 | }
45 |
46 | @action
47 | Future deleteSession() async {
48 | await _storage.delete(key: _sessionKey);
49 | session = ObservableFuture.value('');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/modules/search/dao.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/database.dart';
2 | import 'package:logging/logging.dart';
3 | import 'package:moor/moor.dart';
4 |
5 | part 'dao.g.dart';
6 |
7 | @DataClassName('SearchHistoryEntry')
8 | class SearchHistories extends Table {
9 | TextColumn get query => text()();
10 | DateTimeColumn get lastQueriedAt => dateTime()();
11 |
12 | @override
13 | Set get primaryKey => {query};
14 | }
15 |
16 | @UseDao(tables: [SearchHistories])
17 | class SearchHistoriesDao extends DatabaseAccessor
18 | with _$SearchHistoriesDaoMixin {
19 | SearchHistoriesDao(Database db) : super(db);
20 |
21 | static final _log = Logger('SearchHistoriesDao');
22 |
23 | Future insertEntry(SearchHistoryEntry entry) async {
24 | _log.fine('insertEntry: $entry');
25 | await into(searchHistories).insertOnConflictUpdate(entry);
26 | }
27 |
28 | Future> listEntries(String pattern) async {
29 | _log.fine('listEntries: $pattern');
30 | final query = select(searchHistories)
31 | ..where((t) => t.query.like('%$pattern%'))
32 | ..orderBy([
33 | (e) =>
34 | OrderingTerm(expression: e.lastQueriedAt, mode: OrderingMode.desc),
35 | ]);
36 |
37 | return query.get();
38 | }
39 |
40 | Future deleteAllEntries() async {
41 | _log.fine('deleteAllEntries');
42 | await delete(searchHistories).go();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/modules/setting/widgets/select_list_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
3 |
4 | part 'select_list_tile.g.dart';
5 |
6 | @swidget
7 | Widget selectListTile(
8 | BuildContext context, {
9 | Widget title,
10 | @required List> items,
11 | @required T value,
12 | @required Function(T) onChanged,
13 | }) {
14 | final index = items.indexWhere((e) => e.value == value);
15 |
16 | return ListTile(
17 | title: title,
18 | subtitle: items[index]?.child,
19 | onTap: () {
20 | showDialog(
21 | context: context,
22 | builder: (context) {
23 | return SimpleDialog(
24 | title: title,
25 | children: items
26 | .map((e) => RadioListTile(
27 | key: e.key,
28 | value: e.value,
29 | groupValue: items[index]?.value,
30 | title: e.child,
31 | onChanged: (value) {
32 | if (e.onTap != null) {
33 | e.onTap();
34 | }
35 |
36 | onChanged(value);
37 | Navigator.pop(context);
38 | },
39 | ))
40 | .toList(),
41 | );
42 | },
43 | );
44 | },
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | ## Flutter wrapper
2 | -keep class io.flutter.app.** { *; }
3 | -keep class io.flutter.plugin.** { *; }
4 | -keep class io.flutter.util.** { *; }
5 | -keep class io.flutter.view.** { *; }
6 | -keep class io.flutter.** { *; }
7 | -keep class io.flutter.plugins.** { *; }
8 | -dontwarn io.flutter.embedding.**
9 |
10 | ## Gson rules
11 | # Gson uses generic type information stored in a class file when working with fields. Proguard
12 | # removes such information by default, so configure it to keep all of it.
13 | -keepattributes Signature
14 |
15 | # For using GSON @Expose annotation
16 | -keepattributes *Annotation*
17 |
18 | # Gson specific classes
19 | -dontwarn sun.misc.**
20 | #-keep class com.google.gson.stream.** { *; }
21 |
22 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
23 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
24 | -keep class * implements com.google.gson.TypeAdapter
25 | -keep class * implements com.google.gson.TypeAdapterFactory
26 | -keep class * implements com.google.gson.JsonSerializer
27 | -keep class * implements com.google.gson.JsonDeserializer
28 |
29 | # Prevent R8 from leaving Data object members always null
30 | -keepclassmembers,allowobfuscation class * {
31 | @com.google.gson.annotations.SerializedName ;
32 | }
33 |
34 | ## flutter_local_notification plugin rules
35 | -keep class com.dexterous.** { *; }
36 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/body.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'body.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _StatusBar extends StatelessWidget {
10 | const _StatusBar({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => _statusBar(_context);
14 | }
15 |
16 | class _ImageLoading extends StatelessWidget {
17 | const _ImageLoading({Key key, this.event}) : super(key: key);
18 |
19 | final ImageChunkEvent event;
20 |
21 | @override
22 | Widget build(BuildContext _context) => _imageLoading(_context, event: event);
23 | }
24 |
25 | class _ImageView extends StatelessWidget {
26 | const _ImageView({Key key, @required this.page}) : super(key: key);
27 |
28 | final int page;
29 |
30 | @override
31 | Widget build(BuildContext _context) => _imageView(_context, page: page);
32 | }
33 |
34 | class _ImageError extends StatelessWidget {
35 | const _ImageError({Key key, @required this.page, this.error, this.reloadKey})
36 | : super(key: key);
37 |
38 | final int page;
39 |
40 | final ImageError error;
41 |
42 | final String reloadKey;
43 |
44 | @override
45 | Widget build(BuildContext _context) =>
46 | _imageError(_context, page: page, error: error, reloadKey: reloadKey);
47 | }
48 |
--------------------------------------------------------------------------------
/lib/modules/setting/store.dart:
--------------------------------------------------------------------------------
1 | import 'package:enum_to_string/enum_to_string.dart';
2 | import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
3 |
4 | import 'enum_adapter.dart';
5 | import 'types.dart';
6 |
7 | const _parse = EnumToString.convertToString;
8 |
9 | class SettingStore {
10 | SettingStore(StreamingSharedPreferences prefs)
11 | : displayJapaneseTitle = prefs.getBool(
12 | _parse(SettingKey.displayJapaneseTitle),
13 | defaultValue: false),
14 | orientation = prefs.getCustomValue(
15 | _parse(SettingKey.orientation),
16 | defaultValue: OrientationSetting.auto,
17 | adapter: const EnumAdapter(OrientationSetting.values)),
18 | turnPagesWithVolumeKeys = prefs.getBool(
19 | _parse(SettingKey.turnPagesWithVolumeKeys),
20 | defaultValue: false),
21 | theme = prefs.getCustomValue(_parse(SettingKey.theme),
22 | defaultValue: ThemeSetting.system,
23 | adapter: const EnumAdapter(ThemeSetting.values)),
24 | displayContentWarning = prefs.getBool(
25 | _parse(SettingKey.displayContentWarning),
26 | defaultValue: true);
27 |
28 | final Preference displayJapaneseTitle;
29 | final Preference orientation;
30 | final Preference turnPagesWithVolumeKeys;
31 | final Preference theme;
32 | final Preference displayContentWarning;
33 | }
34 |
--------------------------------------------------------------------------------
/lib/modules/download/widgets/menu_bottom_sheet.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'menu_bottom_sheet.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class DownloadMenuBottomSheet extends StatelessWidget {
10 | const DownloadMenuBottomSheet({Key key, @required this.task})
11 | : super(key: key);
12 |
13 | final DownloadTask task;
14 |
15 | @override
16 | Widget build(BuildContext _context) =>
17 | downloadMenuBottomSheet(_context, task: task);
18 | }
19 |
20 | class _PauseButton extends StatelessWidget {
21 | const _PauseButton({Key key}) : super(key: key);
22 |
23 | @override
24 | Widget build(BuildContext _context) => _pauseButton(_context);
25 | }
26 |
27 | class _ResumeButton extends StatelessWidget {
28 | const _ResumeButton({Key key}) : super(key: key);
29 |
30 | @override
31 | Widget build(BuildContext _context) => _resumeButton(_context);
32 | }
33 |
34 | class _RetryButton extends StatelessWidget {
35 | const _RetryButton({Key key}) : super(key: key);
36 |
37 | @override
38 | Widget build(BuildContext _context) => _retryButton(_context);
39 | }
40 |
41 | class _DeleteButton extends StatelessWidget {
42 | const _DeleteButton({Key key}) : super(key: key);
43 |
44 | @override
45 | Widget build(BuildContext _context) => _deleteButton(_context);
46 | }
47 |
--------------------------------------------------------------------------------
/lib/modules/home/store.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'store.dart';
4 |
5 | // **************************************************************************
6 | // StoreGenerator
7 | // **************************************************************************
8 |
9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
10 |
11 | mixin _$HomeStore on _HomeStoreBase, Store {
12 | final _$currentTabAtom = Atom(name: '_HomeStoreBase.currentTab');
13 |
14 | @override
15 | int get currentTab {
16 | _$currentTabAtom.reportRead();
17 | return super.currentTab;
18 | }
19 |
20 | @override
21 | set currentTab(int value) {
22 | _$currentTabAtom.reportWrite(value, super.currentTab, () {
23 | super.currentTab = value;
24 | });
25 | }
26 |
27 | final _$_HomeStoreBaseActionController =
28 | ActionController(name: '_HomeStoreBase');
29 |
30 | @override
31 | void setCurrentTab(int value) {
32 | final _$actionInfo = _$_HomeStoreBaseActionController.startAction(
33 | name: '_HomeStoreBase.setCurrentTab');
34 | try {
35 | return super.setCurrentTab(value);
36 | } finally {
37 | _$_HomeStoreBaseActionController.endAction(_$actionInfo);
38 | }
39 | }
40 |
41 | @override
42 | String toString() {
43 | return '''
44 | currentTab: ${currentTab}
45 | ''';
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EH Redux
2 |
3 | [](https://github.com/tommy351/eh-redux/releases)    [](https://codecov.io/gh/tommy351/eh-redux)
4 |
5 | A E-Hentai reader written in Flutter. This project is still under development. Some features are not implemented yet.
6 |
7 | ## Requirements
8 |
9 | Android 4.4 and above
10 |
11 | ## Download
12 |
13 | [Latest APK]((https://github.com/tommy351/eh-redux/releases/latest/download/app-arm64-v8a-release.apk))
14 |
15 | [](https://github.com/tommy351/eh-redux/releases/latest/download/app-arm64-v8a-release.apk)
16 |
17 | Using devices that do not support ARM64? See [other versions](https://github.com/tommy351/eh-redux/releases).
18 |
19 | ## Screenshots
20 |
21 |     
22 |
23 | ## Contributing
24 |
25 | See [CONTRIBUTING.md](CONTRIBUTING.md).
26 |
27 | ## License
28 |
29 | Apache License 2.0
30 |
31 | Logo is made by [Good Ware](https://www.flaticon.com/authors/good-ware) from [www.flaticon.com](https://www.flaticon.com/free-icon/panda_2675616).
32 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/thumbnail.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:eh_redux/database/database.dart';
4 | import 'package:eh_redux/modules/image/file_fallback_image.dart';
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter_cache_manager/flutter_cache_manager.dart';
7 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
8 | import 'package:octo_image/octo_image.dart';
9 | import 'package:provider/provider.dart';
10 |
11 | part 'thumbnail.g.dart';
12 |
13 | @swidget
14 | Widget galleryThumbnail(
15 | BuildContext context, {
16 | @required int galleryId,
17 | @required String fallbackUrl,
18 | double width,
19 | double height,
20 | BoxFit fit,
21 | }) {
22 | final database = Provider.of(context);
23 |
24 | return OctoImage(
25 | image: FileFallbackImage(
26 | getFile: () async {
27 | final task = await database.downloadTasksDao.getSingle(galleryId);
28 | final thumbnail = task?.thumbnail;
29 |
30 | if (thumbnail != null && thumbnail.isNotEmpty) {
31 | return File(thumbnail);
32 | }
33 |
34 | return null;
35 | },
36 | url: fallbackUrl,
37 | cacheManager: DefaultCacheManager(),
38 | ),
39 | width: width,
40 | height: height,
41 | fit: fit,
42 | placeholderBuilder: (context) => const _Placeholder(),
43 | );
44 | }
45 |
46 | @swidget
47 | Widget _placeholder(BuildContext context) {
48 | return SizedBox.expand(
49 | child: Container(
50 | color: Colors.grey.withOpacity(0.4),
51 | ),
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/lib/modules/setting/widgets/confirm_list_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
4 |
5 | part 'confirm_list_tile.g.dart';
6 |
7 | @swidget
8 | Widget confirmListTile(
9 | BuildContext context, {
10 | Widget title,
11 | Widget leading,
12 | Widget trailing,
13 | Widget dialogTitle,
14 | Widget dialogContent,
15 | bool disabled = false,
16 | @required Widget confirmActionChild,
17 | @required Function() onConfirm,
18 | }) {
19 | return ListTile(
20 | title: title,
21 | leading: leading,
22 | trailing: trailing,
23 | onTap: disabled
24 | ? null
25 | : () {
26 | showDialog(
27 | context: context,
28 | builder: (context) => AlertDialog(
29 | title: dialogTitle,
30 | content: dialogContent,
31 | actions: [
32 | TextButton(
33 | onPressed: () {
34 | Navigator.pop(context);
35 | },
36 | child: Text(AppLocalizations.of(context).cancelButtonLabel),
37 | ),
38 | TextButton(
39 | onPressed: () async {
40 | await onConfirm();
41 | Navigator.pop(context);
42 | },
43 | child: confirmActionChild,
44 | ),
45 | ],
46 | ),
47 | );
48 | },
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/lib/modules/setting/widgets/screen.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'screen.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _Body extends StatelessWidget {
10 | const _Body({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => _body(_context);
14 | }
15 |
16 | class _Title extends StatelessWidget {
17 | const _Title(this.title, {Key key}) : super(key: key);
18 |
19 | final String title;
20 |
21 | @override
22 | Widget build(BuildContext _context) => _title(_context, title);
23 | }
24 |
25 | class _SelectTile extends StatelessWidget {
26 | const _SelectTile(
27 | {Key key,
28 | @required this.title,
29 | @required this.preference,
30 | @required this.items})
31 | : super(key: key);
32 |
33 | final String title;
34 |
35 | final Preference preference;
36 |
37 | final List> items;
38 |
39 | @override
40 | Widget build(BuildContext _context) => _selectTile(_context,
41 | title: title, preference: preference, items: items);
42 | }
43 |
44 | class _SwitchTile extends StatelessWidget {
45 | const _SwitchTile({Key key, @required this.title, @required this.preference})
46 | : super(key: key);
47 |
48 | final String title;
49 |
50 | final Preference preference;
51 |
52 | @override
53 | Widget build(BuildContext _context) =>
54 | _switchTile(_context, title: title, preference: preference);
55 | }
56 |
--------------------------------------------------------------------------------
/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 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | eh_redux
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(FLUTTER_BUILD_NAME)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UIViewControllerBasedStatusBarAppearance
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/lib/modules/gallery/types.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'types.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$_GalleryResponse _$_$_GalleryResponseFromJson(Map json) {
10 | return _$_GalleryResponse(
11 | id: json['gid'] as int,
12 | token: json['token'] as String,
13 | title: json['title'] as String,
14 | titleJpn: json['title_jpn'] as String,
15 | category: json['category'] as String,
16 | thumbnail: json['thumb'] as String,
17 | uploader: json['uploader'] as String,
18 | fileCount: int.tryParse(json['filecount'] as String),
19 | fileSize: json['filesize'] as int,
20 | expunged: json['expunged'] as bool,
21 | rating: double.tryParse(json['rating'] as String),
22 | tags: (json['tags'] as List)?.map((e) => e as String)?.toList(),
23 | posted: tryParseSecondsSinceEpoch(json['posted'] as String),
24 | );
25 | }
26 |
27 | Map _$_$_GalleryResponseToJson(_$_GalleryResponse instance) =>
28 | {
29 | 'gid': instance.id,
30 | 'token': instance.token,
31 | 'title': instance.title,
32 | 'title_jpn': instance.titleJpn,
33 | 'category': instance.category,
34 | 'thumb': instance.thumbnail,
35 | 'uploader': instance.uploader,
36 | 'filecount': instance.fileCount,
37 | 'filesize': instance.fileSize,
38 | 'expunged': instance.expunged,
39 | 'rating': instance.rating,
40 | 'tags': instance.tags,
41 | 'posted': instance.posted?.toIso8601String(),
42 | };
43 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/animated_navigation.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/image/store.dart';
2 | import 'package:flutter/widgets.dart';
3 | import 'package:mobx/mobx.dart';
4 | import 'package:provider/provider.dart';
5 |
6 | class AnimatedNavigation extends StatefulWidget {
7 | const AnimatedNavigation({
8 | Key key,
9 | @required this.child,
10 | }) : assert(child != null),
11 | super(key: key);
12 |
13 | final Widget child;
14 |
15 | @override
16 | _AnimatedNavigationState createState() => _AnimatedNavigationState();
17 | }
18 |
19 | class _AnimatedNavigationState extends State
20 | with SingleTickerProviderStateMixin {
21 | AnimationController _controller;
22 | Animation _animation;
23 | ReactionDisposer _dispose;
24 |
25 | @override
26 | void initState() {
27 | super.initState();
28 |
29 | final store = Provider.of(context, listen: false);
30 | _controller = AnimationController(
31 | vsync: this,
32 | duration: const Duration(milliseconds: 300),
33 | );
34 | _animation = Tween(begin: 0, end: 1)
35 | .animate(CurvedAnimation(parent: _controller, curve: Curves.ease));
36 | _dispose = reaction((_) => store.navVisible, (visible) {
37 | if (visible) {
38 | _controller.forward();
39 | } else {
40 | _controller.reverse();
41 | }
42 | });
43 | }
44 |
45 | @override
46 | void dispose() {
47 | _dispose();
48 | _controller.dispose();
49 | super.dispose();
50 | }
51 |
52 | @override
53 | Widget build(BuildContext context) {
54 | return FadeTransition(
55 | opacity: _animation,
56 | child: widget.child,
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/utils/notification.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_local_notifications/flutter_local_notifications.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 | import 'package:rxdart/rxdart.dart';
4 |
5 | part 'notification.freezed.dart';
6 |
7 | final notificationPlugin = FlutterLocalNotificationsPlugin();
8 | final didReceiveNotificationSubject = BehaviorSubject();
9 | final selectNotificationSubject = BehaviorSubject();
10 | NotificationAppLaunchDetails notificationAppLaunchDetails;
11 |
12 | @freezed
13 | abstract class ReceivedNotification with _$ReceivedNotification {
14 | const factory ReceivedNotification({
15 | @required int id,
16 | @required String title,
17 | @required String body,
18 | @required String payload,
19 | }) = _ReceivedNotification;
20 | }
21 |
22 | Future initializeNotificationPlugin() async {
23 | notificationAppLaunchDetails =
24 | await notificationPlugin.getNotificationAppLaunchDetails();
25 |
26 | const androidSettings = AndroidInitializationSettings('app_icon');
27 | final iosSettings = IOSInitializationSettings(
28 | requestAlertPermission: false,
29 | requestBadgePermission: false,
30 | requestSoundPermission: false,
31 | onDidReceiveLocalNotification: (id, title, body, payload) async {
32 | didReceiveNotificationSubject.add(ReceivedNotification(
33 | id: id,
34 | title: title,
35 | body: body,
36 | payload: payload,
37 | ));
38 | },
39 | );
40 | final initSettings = InitializationSettings(androidSettings, iosSettings);
41 |
42 | await notificationPlugin.initialize(
43 | initSettings,
44 | onSelectNotification: (payload) async {
45 | selectNotificationSubject.add(payload);
46 | },
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/key_event_detector.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/utils/key_event.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | class KeyEventDetector extends StatefulWidget {
5 | const KeyEventDetector({
6 | Key key,
7 | @required this.child,
8 | @required this.onPrevious,
9 | @required this.onNext,
10 | }) : assert(child != null),
11 | assert(onPrevious != null),
12 | assert(onNext != null),
13 | super(key: key);
14 |
15 | final Widget child;
16 | final void Function() onPrevious;
17 | final void Function() onNext;
18 |
19 | @override
20 | _KeyEventDetectorState createState() => _KeyEventDetectorState();
21 | }
22 |
23 | class _KeyEventDetectorState extends State {
24 | KeyEventListener _listener;
25 | Function _dispose;
26 |
27 | @override
28 | void initState() {
29 | super.initState();
30 | _listener = KeyEventListener();
31 | }
32 |
33 | @override
34 | void dispose() {
35 | _setupListener(false);
36 | super.dispose();
37 | }
38 |
39 | @override
40 | Widget build(BuildContext context) {
41 | return widget.child;
42 | }
43 |
44 | void _setupListener(bool enabled) {
45 | if (enabled) {
46 | if (_dispose != null) return;
47 |
48 | _dispose = _listener.listen([
49 | KeyCode.volumeDown,
50 | KeyCode.volumeUp,
51 | ], _handleKeyEvent);
52 | } else {
53 | if (_dispose != null) {
54 | _dispose();
55 | _dispose = null;
56 | }
57 | }
58 | }
59 |
60 | void _handleKeyEvent(KeyCode code) {
61 | switch (code) {
62 | case KeyCode.volumeDown:
63 | widget.onNext();
64 | break;
65 | case KeyCode.volumeUp:
66 | widget.onPrevious();
67 | break;
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/modules/favorite/widgets/bottom_sheet.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'bottom_sheet.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class _Content extends StatelessWidget {
10 | const _Content({Key key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext _context) => _content(_context);
14 | }
15 |
16 | class _LoadingPlaceholder extends StatelessWidget {
17 | const _LoadingPlaceholder({Key key}) : super(key: key);
18 |
19 | @override
20 | Widget build(BuildContext _context) => _loadingPlaceholder(_context);
21 | }
22 |
23 | class _FavoriteSelect extends StatelessWidget {
24 | const _FavoriteSelect({Key key}) : super(key: key);
25 |
26 | @override
27 | Widget build(BuildContext _context) => _favoriteSelect(_context);
28 | }
29 |
30 | class _NoteField extends StatelessWidget {
31 | const _NoteField({Key key}) : super(key: key);
32 |
33 | @override
34 | Widget build(BuildContext _context) => _noteField(_context);
35 | }
36 |
37 | class _AddButton extends StatelessWidget {
38 | const _AddButton({Key key}) : super(key: key);
39 |
40 | @override
41 | Widget build(BuildContext _context) => _addButton(_context);
42 | }
43 |
44 | class _DeleteButton extends StatelessWidget {
45 | const _DeleteButton({Key key}) : super(key: key);
46 |
47 | @override
48 | Widget build(BuildContext _context) => _deleteButton(_context);
49 | }
50 |
51 | class _DeleteConfirm extends StatelessWidget {
52 | const _DeleteConfirm({Key key, @required this.store}) : super(key: key);
53 |
54 | final FavoriteStore store;
55 |
56 | @override
57 | Widget build(BuildContext _context) => _deleteConfirm(_context, store: store);
58 | }
59 |
--------------------------------------------------------------------------------
/lib/utils/key_event.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:developer' as developer;
3 |
4 | import 'package:built_collection/built_collection.dart';
5 | import 'package:built_value/built_value.dart';
6 | import 'package:flutter/services.dart';
7 |
8 | part 'key_event.g.dart';
9 |
10 | class KeyCode extends EnumClass {
11 | const KeyCode._(String name) : super(name);
12 |
13 | static const KeyCode volumeDown = _$volumeDown;
14 | static const KeyCode volumeUp = _$volumeUp;
15 |
16 | static BuiltSet get values => _$keyCodeValues;
17 | static KeyCode valueOf(String name) => _$keyCodeValueOf(name);
18 | }
19 |
20 | typedef KeyEventCallback = Function(KeyCode);
21 |
22 | class KeyEventListener {
23 | static const _methodChannel = MethodChannel('app.ehredux/method');
24 | static const _keyDownEventChannel = EventChannel('app.ehredux/event/keyDown');
25 |
26 | Function listen(List keys, KeyEventCallback callback) {
27 | _interceptKeyDown(keys);
28 |
29 | final sub = _keyDownEventChannel.receiveBroadcastStream().listen((event) {
30 | final code = KeyCode.valueOf(event as String);
31 |
32 | if (code != null && keys.contains(code)) {
33 | callback(code);
34 | }
35 | });
36 |
37 | return () {
38 | _uninterceptKeyDown(keys);
39 | sub.cancel();
40 | };
41 | }
42 |
43 | Future _interceptKeyDown(List keys) async {
44 | for (final key in keys) {
45 | await _methodChannel.invokeMethod('interceptKeyDown', key.toString());
46 | developer.log('Intercept key down: $key');
47 | }
48 | }
49 |
50 | Future _uninterceptKeyDown(List keys) async {
51 | for (final key in keys) {
52 | await _methodChannel.invokeMethod('uninterceptKeyDown', key.toString());
53 | developer.log('Unintercept key down: $key');
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/modules/session/store.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'store.dart';
4 |
5 | // **************************************************************************
6 | // StoreGenerator
7 | // **************************************************************************
8 |
9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
10 |
11 | mixin _$SessionStore on _SessionStoreBase, Store {
12 | Computed _$loginStatusComputed;
13 |
14 | @override
15 | LoginStatus get loginStatus =>
16 | (_$loginStatusComputed ??= Computed(() => super.loginStatus,
17 | name: '_SessionStoreBase.loginStatus'))
18 | .value;
19 |
20 | final _$sessionAtom = Atom(name: '_SessionStoreBase.session');
21 |
22 | @override
23 | ObservableFuture get session {
24 | _$sessionAtom.reportRead();
25 | return super.session;
26 | }
27 |
28 | @override
29 | set session(ObservableFuture value) {
30 | _$sessionAtom.reportWrite(value, super.session, () {
31 | super.session = value;
32 | });
33 | }
34 |
35 | final _$setSessionAsyncAction = AsyncAction('_SessionStoreBase.setSession');
36 |
37 | @override
38 | Future setSession(String value) {
39 | return _$setSessionAsyncAction.run(() => super.setSession(value));
40 | }
41 |
42 | final _$deleteSessionAsyncAction =
43 | AsyncAction('_SessionStoreBase.deleteSession');
44 |
45 | @override
46 | Future deleteSession() {
47 | return _$deleteSessionAsyncAction.run(() => super.deleteSession());
48 | }
49 |
50 | @override
51 | String toString() {
52 | return '''
53 | session: ${session},
54 | loginStatus: ${loginStatus}
55 | ''';
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/modules/home/tabs.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:eh_redux/modules/download/widgets/tab.dart';
3 | import 'package:eh_redux/modules/favorite/widgets/tab.dart';
4 | import 'package:eh_redux/modules/gallery/widgets/tab.dart';
5 | import 'package:eh_redux/modules/history/widgets/tab.dart';
6 | import 'package:eh_redux/modules/setting/widgets/tab.dart';
7 | import 'package:flutter/material.dart';
8 | import 'package:freezed_annotation/freezed_annotation.dart';
9 |
10 | part 'tabs.freezed.dart';
11 |
12 | @freezed
13 | abstract class Tab with _$Tab {
14 | const factory Tab({
15 | @required String name,
16 | @required IconData icon,
17 | @required String Function(BuildContext) title,
18 | @required Widget Function(BuildContext) widget,
19 | }) = _Tab;
20 | }
21 |
22 | final tabs = [
23 | Tab(
24 | name: 'gallery',
25 | icon: Icons.photo_library,
26 | title: (context) => AppLocalizations.of(context).homeTabTitleGallery,
27 | widget: (context) => const GalleryTab(),
28 | ),
29 | Tab(
30 | name: 'favorite',
31 | icon: Icons.favorite,
32 | title: (context) => AppLocalizations.of(context).homeTabTitleFavorite,
33 | widget: (context) => const FavoriteTab(),
34 | ),
35 | Tab(
36 | name: 'download',
37 | icon: Icons.file_download,
38 | title: (context) => AppLocalizations.of(context).homeTabTitleDownload,
39 | widget: (context) => const DownloadTab(),
40 | ),
41 | Tab(
42 | name: 'history',
43 | icon: Icons.history,
44 | title: (context) => AppLocalizations.of(context).homeTabTitleHistory,
45 | widget: (context) => const HistoryTab(),
46 | ),
47 | Tab(
48 | name: 'settings',
49 | icon: Icons.settings,
50 | title: (context) => AppLocalizations.of(context).homeTabTitleSettings,
51 | widget: (context) => const SettingTab(),
52 | ),
53 | ];
54 |
--------------------------------------------------------------------------------
/lib/modules/check_update/types.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'types.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$_GitHubAsset _$_$_GitHubAssetFromJson(Map json) {
10 | return _$_GitHubAsset(
11 | name: json['name'] as String,
12 | contentType: json['content_type'] as String,
13 | state: json['state'] as String,
14 | size: json['size'] as int,
15 | browserDownloadUrl: json['browser_download_url'] as String,
16 | );
17 | }
18 |
19 | Map _$_$_GitHubAssetToJson(_$_GitHubAsset instance) =>
20 | {
21 | 'name': instance.name,
22 | 'content_type': instance.contentType,
23 | 'state': instance.state,
24 | 'size': instance.size,
25 | 'browser_download_url': instance.browserDownloadUrl,
26 | };
27 |
28 | _$_GitHubRelease _$_$_GitHubReleaseFromJson(Map json) {
29 | return _$_GitHubRelease(
30 | name: json['name'] as String,
31 | body: json['body'] as String,
32 | tagName: json['tag_name'] as String,
33 | htmlUrl: json['html_url'] as String,
34 | assets: json['assets'] != null
35 | ? (json['assets'] as List)
36 | .map((e) => e == null
37 | ? null
38 | : GitHubAsset.fromJson(e as Map))
39 | .toBuiltList()
40 | : null,
41 | );
42 | }
43 |
44 | Map _$_$_GitHubReleaseToJson(_$_GitHubRelease instance) =>
45 | {
46 | 'name': instance.name,
47 | 'body': instance.body,
48 | 'tag_name': instance.tagName,
49 | 'html_url': instance.htmlUrl,
50 | 'assets': instance.assets?.map((e) => e?.toJson())?.toList(),
51 | };
52 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/system_overlay_setter.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/image/store.dart';
2 | import 'package:eh_redux/utils/firebase.dart';
3 | import 'package:flutter/services.dart';
4 | import 'package:flutter/widgets.dart';
5 | import 'package:mobx/mobx.dart';
6 | import 'package:provider/provider.dart';
7 |
8 | class SystemOverlaySetter extends StatefulWidget {
9 | const SystemOverlaySetter({
10 | Key key,
11 | @required this.child,
12 | }) : assert(child != null),
13 | super(key: key);
14 |
15 | final Widget child;
16 |
17 | @override
18 | _SystemOverlaySetterState createState() => _SystemOverlaySetterState();
19 | }
20 |
21 | class _SystemOverlaySetterState extends State {
22 | ReactionDisposer _dispose;
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 |
28 | final store = Provider.of(context, listen: false);
29 |
30 | _hideOverlays(logEvent: false);
31 |
32 | _dispose = reaction((_) => store.navVisible, (visible) {
33 | if (visible) {
34 | _showOverlays();
35 | } else {
36 | _hideOverlays();
37 | }
38 | });
39 | }
40 |
41 | @override
42 | void dispose() {
43 | _dispose();
44 | _showOverlays(logEvent: false);
45 | super.dispose();
46 | }
47 |
48 | @override
49 | Widget build(BuildContext context) {
50 | return widget.child;
51 | }
52 |
53 | void _hideOverlays({bool logEvent = true}) {
54 | SystemChrome.setEnabledSystemUIOverlays([]);
55 |
56 | if (logEvent) {
57 | analytics.logEvent(name: 'hide_view_screen_ui');
58 | }
59 | }
60 |
61 | void _showOverlays({bool logEvent = true}) {
62 | SystemChrome.setEnabledSystemUIOverlays([
63 | SystemUiOverlay.top,
64 | SystemUiOverlay.bottom,
65 | ]);
66 |
67 | if (logEvent) {
68 | analytics.logEvent(name: 'show_view_screen_ui');
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/rating_bottom_sheet.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
4 | import 'package:eh_redux/modules/common/widgets/bottom_sheet_container.dart';
5 | import 'package:eh_redux/modules/search/store.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:flutter_mobx/flutter_mobx.dart';
8 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
9 | import 'package:provider/provider.dart';
10 |
11 | part 'rating_bottom_sheet.g.dart';
12 |
13 | @swidget
14 | Widget ratingBottomSheet(
15 | BuildContext context, {
16 | @required SearchStore store,
17 | }) {
18 | return Provider.value(
19 | value: store,
20 | child: const BottomSheetContainer(
21 | child: _Body(),
22 | ),
23 | );
24 | }
25 |
26 | @swidget
27 | Widget _body(BuildContext context) {
28 | final store = Provider.of(context);
29 |
30 | return Observer(
31 | builder: (context) {
32 | final label = store.minimumRating > 0
33 | ? AppLocalizations.of(context)
34 | .searchMinimumRatingLabel(store.minimumRating)
35 | : AppLocalizations.of(context).searchMinimumRatingDisabled;
36 |
37 | return Wrap(
38 | children: [
39 | ListTile(
40 | title: Text(AppLocalizations.of(context).searchMinimumRatingTitle),
41 | trailing: store.minimumRating > 0 ? Text(label) : null,
42 | ),
43 | Slider(
44 | value: max(store.minimumRating.toDouble(), 1),
45 | min: 1,
46 | max: 5,
47 | divisions: 4,
48 | label: label,
49 | onChanged: (value) {
50 | if (value < 2) {
51 | store.setMinimumRating(0);
52 | } else {
53 | store.setMinimumRating(value.floor());
54 | }
55 | },
56 | ),
57 | ],
58 | );
59 | },
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: eh_redux
2 | description: EH Redux
3 | publish_to: "none"
4 | version: 0.6.4
5 |
6 | environment:
7 | sdk: ">=2.7.0 <3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 | flutter_localizations:
13 | sdk: flutter
14 |
15 | http: ^0.12.1
16 | html: ^0.14.0+3
17 | cached_network_image: ^2.3.1
18 | built_collection: ^4.3.2
19 | provider: ^4.1.2
20 | meta: ^1.1.8
21 | mobx: ^1.2.1
22 | flutter_mobx: ^1.1.0
23 | built_value: ^7.1.0
24 | share: 0.6.5
25 | photo_view: ^0.10.2
26 | shared_preferences: ">=0.5.7+3 <2.0.0"
27 | preload_page_view: ^0.1.4
28 | collection: ^1.14.12
29 | flutter_secure_storage: ^3.3.3
30 | webview_flutter: ^1.0.0
31 | package_info: ">=0.4.1 <2.0.0"
32 | url_launcher: ^5.7.1
33 | filesize: ^1.0.4
34 | intl: ^0.16.1
35 | firebase_core: ^0.5.0
36 | firebase_analytics: ^6.0.0
37 | firebase_crashlytics: ^0.2.0
38 | flutter_markdown: ^0.4.2
39 | freezed_annotation: ^0.12.0
40 | json_annotation: ^3.0.1
41 | moor: ^3.3.1
42 | path: ^1.6.4
43 | path_provider: ^1.6.11
44 | flutter_typeahead: ^1.8.3
45 | device_info: ">=0.4.2+4 <2.0.0"
46 | workmanager: ^0.2.3
47 | functional_widget_annotation: ^0.8.0
48 | sqlite3_flutter_libs: ^0.2.0
49 | streaming_shared_preferences: ^1.0.1
50 | enum_to_string: ^1.0.9
51 | logging: ^0.11.4
52 | async: ^2.4.2
53 | rxdart: ^0.24.1
54 | octo_image: ^0.3.0
55 | flutter_cache_manager: ^1.4.2
56 | flutter_local_notifications: ^1.4.4+4
57 | html_unescape: ^1.0.2
58 |
59 | dev_dependencies:
60 | flutter_test:
61 | sdk: flutter
62 | build_runner: ^1.6.5
63 | mobx_codegen: ^1.1.0
64 | built_value_generator: ^7.1.0
65 | lint: ^1.0.0
66 | mockito: ^4.1.1
67 | matcher: ^0.12.6
68 | freezed: ^0.12.1
69 | json_serializable: ^3.4.0
70 | json_serializable_immutable_collections: ^0.4.0
71 | moor_generator: ^3.3.1
72 | functional_widget: ^0.8.0
73 |
74 | flutter:
75 | uses-material-design: true
76 | generate: true
77 |
--------------------------------------------------------------------------------
/lib/modules/download/widgets/list.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'list.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class DownloadList extends StatelessWidget {
10 | const DownloadList({Key key, @required this.data}) : super(key: key);
11 |
12 | final Iterable data;
13 |
14 | @override
15 | Widget build(BuildContext _context) => downloadList(_context, data: data);
16 | }
17 |
18 | class _DownloadCell extends StatelessWidget {
19 | const _DownloadCell({Key key, @required this.task}) : super(key: key);
20 |
21 | final DownloadTaskWithGallery task;
22 |
23 | @override
24 | Widget build(BuildContext _context) => _downloadCell(_context, task: task);
25 | }
26 |
27 | class _CellRight extends StatelessWidget {
28 | const _CellRight({Key key, @required this.task}) : super(key: key);
29 |
30 | final DownloadTaskWithGallery task;
31 |
32 | @override
33 | Widget build(BuildContext _context) => _cellRight(_context, task: task);
34 | }
35 |
36 | class _CellTitle extends StatelessWidget {
37 | const _CellTitle({Key key, @required this.title, this.titleJpn = ''})
38 | : super(key: key);
39 |
40 | final String title;
41 |
42 | final String titleJpn;
43 |
44 | @override
45 | Widget build(BuildContext _context) =>
46 | _cellTitle(_context, title: title, titleJpn: titleJpn);
47 | }
48 |
49 | class _ProgressBar extends StatelessWidget {
50 | const _ProgressBar({Key key, @required this.task}) : super(key: key);
51 |
52 | final DownloadTask task;
53 |
54 | @override
55 | Widget build(BuildContext _context) => _progressBar(_context, task: task);
56 | }
57 |
58 | class _MenuButton extends StatelessWidget {
59 | const _MenuButton({Key key, @required this.task}) : super(key: key);
60 |
61 | final DownloadTask task;
62 |
63 | @override
64 | Widget build(BuildContext _context) => _menuButton(_context, task: task);
65 | }
66 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/controlled_text_field.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class ControllerTextField extends StatefulWidget {
4 | const ControllerTextField({
5 | Key key,
6 | this.value,
7 | this.onChanged,
8 | this.decoration = const InputDecoration(),
9 | this.autofocus = false,
10 | this.obscuringCharacter = '•',
11 | this.obscureText = false,
12 | this.maxLines = 1,
13 | this.minLines,
14 | this.maxLength,
15 | this.maxLengthEnforced = true,
16 | }) : super(key: key);
17 |
18 | final String value;
19 | final void Function(String) onChanged;
20 | final InputDecoration decoration;
21 | final bool autofocus;
22 | final String obscuringCharacter;
23 | final bool obscureText;
24 | final int maxLines;
25 | final int minLines;
26 | final int maxLength;
27 | final bool maxLengthEnforced;
28 |
29 | @override
30 | _ControllerTextFieldState createState() => _ControllerTextFieldState();
31 | }
32 |
33 | class _ControllerTextFieldState extends State {
34 | TextEditingController _controller;
35 |
36 | @override
37 | void initState() {
38 | super.initState();
39 | _controller = TextEditingController(text: widget.value);
40 | }
41 |
42 | @override
43 | void dispose() {
44 | _controller.dispose();
45 | super.dispose();
46 | }
47 |
48 | @override
49 | void didUpdateWidget(ControllerTextField oldWidget) {
50 | super.didUpdateWidget(oldWidget);
51 |
52 | if (oldWidget.value != widget.value) {
53 | _controller.text = widget.value;
54 | }
55 | }
56 |
57 | @override
58 | Widget build(BuildContext context) {
59 | return TextField(
60 | controller: _controller,
61 | onChanged: widget.onChanged,
62 | decoration: widget.decoration,
63 | autocorrect: widget.autofocus,
64 | obscuringCharacter: widget.obscuringCharacter,
65 | obscureText: widget.obscureText,
66 | maxLines: widget.maxLines,
67 | minLines: widget.minLines,
68 | maxLength: widget.maxLength,
69 | maxLengthEnforced: widget.maxLengthEnforced,
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/list.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'list.dart';
4 |
5 | // **************************************************************************
6 | // FunctionalWidgetGenerator
7 | // **************************************************************************
8 |
9 | class GalleryList extends StatelessWidget {
10 | const GalleryList({Key key, @required this.data, this.footer})
11 | : super(key: key);
12 |
13 | final Iterable data;
14 |
15 | final Widget footer;
16 |
17 | @override
18 | Widget build(BuildContext _context) =>
19 | galleryList(_context, data: data, footer: footer);
20 | }
21 |
22 | class _GalleryCell extends StatelessWidget {
23 | const _GalleryCell({Key key, @required this.gallery}) : super(key: key);
24 |
25 | final Gallery gallery;
26 |
27 | @override
28 | Widget build(BuildContext _context) =>
29 | _galleryCell(_context, gallery: gallery);
30 | }
31 |
32 | class _CellRight extends StatelessWidget {
33 | const _CellRight({Key key, @required this.gallery}) : super(key: key);
34 |
35 | final Gallery gallery;
36 |
37 | @override
38 | Widget build(BuildContext _context) => _cellRight(_context, gallery: gallery);
39 | }
40 |
41 | class _CellTitle extends StatelessWidget {
42 | const _CellTitle({Key key, @required this.title, this.titleJpn = ''})
43 | : super(key: key);
44 |
45 | final String title;
46 |
47 | final String titleJpn;
48 |
49 | @override
50 | Widget build(BuildContext _context) =>
51 | _cellTitle(_context, title: title, titleJpn: titleJpn);
52 | }
53 |
54 | class _CellTags extends StatelessWidget {
55 | const _CellTags({Key key, @required this.tags}) : super(key: key);
56 |
57 | final Iterable tags;
58 |
59 | @override
60 | Widget build(BuildContext _context) => _cellTags(_context, tags: tags);
61 | }
62 |
63 | class _CellFooter extends StatelessWidget {
64 | const _CellFooter({Key key, @required this.gallery}) : super(key: key);
65 |
66 | final Gallery gallery;
67 |
68 | @override
69 | Widget build(BuildContext _context) =>
70 | _cellFooter(_context, gallery: gallery);
71 | }
72 |
--------------------------------------------------------------------------------
/lib/modules/image/widgets/bottom_nav.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/image/store.dart';
2 | import 'package:eh_redux/modules/image/widgets/animated_navigation.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:mobx/mobx.dart';
5 | import 'package:provider/provider.dart';
6 |
7 | class ImageBottomNav extends StatefulWidget {
8 | const ImageBottomNav({
9 | Key key,
10 | this.padding = EdgeInsets.zero,
11 | }) : super(key: key);
12 |
13 | final EdgeInsets padding;
14 |
15 | @override
16 | _ImageBottomNavState createState() => _ImageBottomNavState();
17 | }
18 |
19 | class _ImageBottomNavState extends State {
20 | double _value = 0;
21 | ReactionDisposer _dispose;
22 |
23 | @override
24 | void initState() {
25 | super.initState();
26 |
27 | final store = Provider.of(context, listen: false);
28 | _dispose = autorun((_) {
29 | final currentPage = store.currentPage;
30 |
31 | setState(() {
32 | _value = currentPage.toDouble();
33 | });
34 | });
35 | }
36 |
37 | @override
38 | void dispose() {
39 | _dispose();
40 | super.dispose();
41 | }
42 |
43 | @override
44 | Widget build(BuildContext context) {
45 | final store = Provider.of(context);
46 |
47 | return AnimatedNavigation(
48 | child: Container(
49 | color: Colors.black.withOpacity(0.4),
50 | padding: widget.padding,
51 | child: SizedBox(
52 | height: 60,
53 | child: SliderTheme(
54 | data: SliderTheme.of(context),
55 | child: Slider(
56 | max: store.totalPage.toDouble() - 1,
57 | value: _value,
58 | divisions: store.totalPage,
59 | label: '${_value.toInt() + 1}',
60 | onChanged: (double value) {
61 | setState(() {
62 | _value = value;
63 | });
64 | },
65 | onChangeEnd: (double value) {
66 | store.setCurrentPage(value.toInt());
67 | },
68 | ),
69 | ),
70 | ),
71 | ),
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/modules/common/widgets/orientation_setter.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:eh_redux/modules/setting/store.dart';
4 | import 'package:eh_redux/modules/setting/types.dart';
5 | import 'package:flutter/services.dart';
6 | import 'package:flutter/widgets.dart';
7 | import 'package:provider/provider.dart';
8 |
9 | class OrientationSetter extends StatefulWidget {
10 | const OrientationSetter({
11 | Key key,
12 | @required this.child,
13 | }) : assert(child != null),
14 | super(key: key);
15 |
16 | final Widget child;
17 |
18 | @override
19 | _OrientationSetterState createState() => _OrientationSetterState();
20 | }
21 |
22 | class _OrientationSetterState extends State {
23 | StreamSubscription _subscription;
24 |
25 | @override
26 | void initState() {
27 | super.initState();
28 |
29 | final store = Provider.of(context, listen: false);
30 | _subscription = store.orientation.listen((value) {
31 | _updateOrientation(value);
32 | });
33 | }
34 |
35 | @override
36 | void dispose() {
37 | _subscription.cancel();
38 | _updateOrientation(OrientationSetting.auto);
39 | super.dispose();
40 | }
41 |
42 | @override
43 | Widget build(BuildContext context) {
44 | return widget.child;
45 | }
46 |
47 | void _updateOrientation(OrientationSetting orientation) {
48 | switch (orientation) {
49 | case OrientationSetting.portrait:
50 | SystemChrome.setPreferredOrientations([
51 | DeviceOrientation.portraitUp,
52 | DeviceOrientation.portraitDown,
53 | ]);
54 | break;
55 |
56 | case OrientationSetting.landscape:
57 | SystemChrome.setPreferredOrientations([
58 | DeviceOrientation.landscapeRight,
59 | DeviceOrientation.landscapeLeft,
60 | ]);
61 | break;
62 |
63 | default:
64 | SystemChrome.setPreferredOrientations([
65 | DeviceOrientation.landscapeRight,
66 | DeviceOrientation.landscapeLeft,
67 | DeviceOrientation.portraitUp,
68 | DeviceOrientation.portraitDown,
69 | ]);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/tab.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:eh_redux/modules/gallery/stores/network_list.dart';
3 | import 'package:eh_redux/modules/gallery/widgets/network_list.dart';
4 | import 'package:eh_redux/modules/home/widgets/body.dart';
5 | import 'package:eh_redux/modules/search/types.dart';
6 | import 'package:eh_redux/modules/search/widgets/screen.dart';
7 | import 'package:eh_redux/services/ehentai.dart';
8 | import 'package:flutter/material.dart';
9 | import 'package:provider/provider.dart';
10 |
11 | class GalleryTab extends StatefulWidget {
12 | const GalleryTab({Key key}) : super(key: key);
13 |
14 | @override
15 | _GalleryTabState createState() => _GalleryTabState();
16 | }
17 |
18 | class _GalleryTabState extends State
19 | with AutomaticKeepAliveClientMixin {
20 | NetworkGalleryListStore _store;
21 |
22 | @override
23 | bool get wantKeepAlive => true;
24 |
25 | @override
26 | void initState() {
27 | super.initState();
28 | _store = NetworkGalleryListStore(
29 | client: Provider.of(context, listen: false),
30 | );
31 | }
32 |
33 | @override
34 | Widget build(BuildContext context) {
35 | super.build(context);
36 |
37 | return Provider.value(
38 | value: _store,
39 | child: NestedScrollView(
40 | headerSliverBuilder: (context, _) => [
41 | SliverAppBar(
42 | title: Text(AppLocalizations.of(context).homeTabTitleGallery),
43 | pinned: true,
44 | actions: [
45 | IconButton(
46 | icon: const Icon(Icons.search),
47 | tooltip: AppLocalizations.of(context).searchButtonTooltip,
48 | onPressed: () {
49 | Navigator.pushNamed(
50 | context,
51 | SearchScreen.route,
52 | arguments: const SearchArguments(),
53 | );
54 | },
55 | ),
56 | ],
57 | ),
58 | ],
59 | body: const HomeBody(
60 | child: NetworkGalleryList(),
61 | ),
62 | ),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/modules/download/types.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/database.dart';
2 | import 'package:eh_redux/modules/gallery/types.dart';
3 | import 'package:freezed_annotation/freezed_annotation.dart';
4 |
5 | part 'types.freezed.dart';
6 |
7 | enum DownloadTaskState {
8 | pending,
9 | downloading,
10 | paused,
11 | failed,
12 | succeeded,
13 | deleting,
14 | }
15 |
16 | @freezed
17 | abstract class DownloadTask implements _$DownloadTask {
18 | const factory DownloadTask({
19 | @required int galleryId,
20 | @required int totalCount,
21 | @Default(0) int downloadedCount,
22 | @required DateTime createdAt,
23 | @required DateTime queuedAt,
24 | @Default(DownloadTaskState.pending) DownloadTaskState state,
25 | String errorDetails,
26 | String thumbnail,
27 | }) = _DownloadTask;
28 |
29 | factory DownloadTask.fromEntry(DownloadTaskEntry entry) {
30 | return DownloadTask(
31 | galleryId: entry.galleryId,
32 | totalCount: entry.totalCount,
33 | downloadedCount: entry.downloadedCount,
34 | createdAt: entry.createdAt,
35 | queuedAt: entry.queuedAt,
36 | state: entry.state,
37 | errorDetails: entry.errorDetails,
38 | thumbnail: entry.thumbnail,
39 | );
40 | }
41 |
42 | const DownloadTask._();
43 |
44 | DownloadTaskEntry toEntry() {
45 | return DownloadTaskEntry(
46 | galleryId: galleryId,
47 | totalCount: totalCount,
48 | downloadedCount: downloadedCount,
49 | createdAt: createdAt,
50 | queuedAt: queuedAt,
51 | state: state,
52 | errorDetails: errorDetails,
53 | thumbnail: thumbnail,
54 | );
55 | }
56 | }
57 |
58 | @freezed
59 | abstract class DownloadTaskWithGallery with _$DownloadTaskWithGallery {
60 | const factory DownloadTaskWithGallery({
61 | @required DownloadTask task,
62 | @required Gallery gallery,
63 | }) = _DownloadTaskWithGallery;
64 |
65 | factory DownloadTaskWithGallery.fromEntry({
66 | @required DownloadTaskEntry task,
67 | @required GalleryEntry gallery,
68 | }) {
69 | return DownloadTaskWithGallery(
70 | task: DownloadTask.fromEntry(task),
71 | gallery: Gallery.fromEntry(gallery),
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/modules/login/widgets/screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 |
4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
5 | import 'package:eh_redux/modules/session/store.dart';
6 | import 'package:eh_redux/utils/cookie.dart';
7 | import 'package:eh_redux/utils/firebase.dart';
8 | import 'package:flutter/material.dart';
9 | import 'package:logging/logging.dart';
10 | import 'package:provider/provider.dart';
11 | import 'package:webview_flutter/webview_flutter.dart';
12 |
13 | final _log = Logger('LoginScreen');
14 |
15 | class LoginScreen extends StatefulWidget {
16 | const LoginScreen({Key key}) : super(key: key);
17 |
18 | static String route = '/login';
19 |
20 | @override
21 | _LoginScreenState createState() => _LoginScreenState();
22 | }
23 |
24 | class _LoginScreenState extends State {
25 | static const _loginUrl = 'https://e-hentai.org/bounce_login.php';
26 |
27 | final _webViewController = Completer();
28 | final _cookieManager = CookieManager();
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | final sessionStore = Provider.of(context);
33 |
34 | return Scaffold(
35 | appBar: AppBar(
36 | title: Text(AppLocalizations.of(context).logInScreenTitle),
37 | ),
38 | body: WebView(
39 | initialUrl: _loginUrl,
40 | javascriptMode: JavascriptMode.unrestricted,
41 | onWebViewCreated: (controller) {
42 | _webViewController.complete(controller);
43 | },
44 | onPageFinished: (url) {
45 | _checkCookie(sessionStore);
46 | },
47 | ),
48 | );
49 | }
50 |
51 | Future _checkCookie(SessionStore sessionStore) async {
52 | final controller = await _webViewController.future;
53 | final cookieString =
54 | jsonDecode(await controller.evaluateJavascript('document.cookie'))
55 | as String;
56 | final cookies = parseCookies(cookieString);
57 | _log.finer('Get cookies: $cookies');
58 |
59 | if (cookies.containsKey('ipb_member_id')) {
60 | await sessionStore.setSession(cookieString);
61 | await _cookieManager.clearCookies();
62 | analytics.logLogin(loginMethod: 'webview');
63 | Navigator.pop(context);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/test/repositories/fixtures/gallery_list.json:
--------------------------------------------------------------------------------
1 | {"gmetadata":[{"gid":1663099,"token":"d76bb5e89a","archiver_key":"442357--c8b30278e917a5fcd15c02f2848a8fac657aab7a","title":"[PigPanPan (Ikura Nagisa)] 12 Seiza yandere kokuhaku [Chinese] [\u7ec5\u58eb\u4ed3\u5e93\u6c49\u5316] [Digital]","title_jpn":"[PigPanPan (\u4f0a\u5009\u30ca\u30ae\u30b5)] 12\u661f\u5ea7\u30e4\u30f3\u30c7\u30ec \u30b3\u30af\u30cf\u30af [\u4e2d\u56fd\u7ffb\u8a33] [DL\u7248]","category":"Non-H","thumb":"https:\/\/ehgt.org\/f5\/b6\/f5b64c9c924fb032ce1e21c383c9db5edbaedc62-3865428-2508-3541-jpg_l.jpg","uploader":"BlossomPlus","posted":"1592401515","filecount":"26","filesize":79698072,"expunged":false,"rating":"4.65","torrentcount":"1","torrents":[{"hash":"8d6ef5a47bdd9469c8cb5ba6f5a79c535a7c3f68","added":"1592377189","name":"[PigPanPan (\u4f0a\u5009\u30ca\u30ae\u30b5)] 12\u661f\u5ea7\u30e4\u30f3\u30c7\u30ec \u30b3\u30af\u30cf\u30af [\u4e2d\u56fd\u7ffb\u8a33] [DL\u7248].zip","tsize":"6395","fsize":"78761783"}],"tags":["language:chinese","language:translated","group:pigpanpan","artist:ikura nagisa","full color"]},{"gid":1663615,"token":"acf3f209ac","archiver_key":"442357--809e7099d46eba2a3f9708136a2e1ded771edbca","title":"[Sagamani. (Sagami Inumaru)] MY TRUE FEELINGS ARE A SECRET (Kill Me Baby) [Chinese] [\u540e\u6094\u7684\u795e\u5b98\u4e2a\u4eba\u6c49\u5316] [Digital]","title_jpn":"[\u30b5\u30ac\u30de\u30cb\u3002 (\u4f50\u4e0a\u72ac\u4e38)] MY TRUE FEELINGS ARE A SECRET (\u30ad\u30eb\u30df\u30fc\u30d9\u30a4\u30d9\u30fc) [\u4e2d\u56fd\u7ffb\u8a33] [DL\u7248]","category":"Non-H","thumb":"https:\/\/ehgt.org\/b1\/9c\/b19cf368b5863145308c1c04bcb1d3e4829f1b70-243924-740-1035-jpg_l.jpg","uploader":"\u4e50\u00b7\u9ed1","posted":"1592469424","filecount":"16","filesize":5450803,"expunged":false,"rating":"4.50","torrentcount":"1","torrents":[{"hash":"435e802a2797821d15bc2d86abf0565939af83da","added":"1592474826","name":"[\u540e\u6094\u7684\u795e\u5b98\u4e2a\u4eba\u6c49\u5316][\u30b5\u30ac\u30de\u30cb\u3002 (\u4f50\u4e0a\u72ac\u4e38)] MY TRUE FEELINGS ARE A SECRET (\u30ad\u30eb\u30df\u30fc\u30d9\u30a4\u30d9\u30fc) [DL\u7248].7z","tsize":"3333","fsize":"4764787"}],"tags":["language:chinese","language:translated","parody:kill me baby","character:sonya","character:yasuna oribe","group:sagamani","artist:sagami inumaru","female:females only","female:schoolgirl uniform","female:twintails"]}]}
--------------------------------------------------------------------------------
/lib/modules/home/widgets/screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/home/store.dart';
2 | import 'package:eh_redux/modules/home/tabs.dart';
3 | import 'package:eh_redux/utils/firebase.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:mobx/mobx.dart';
6 | import 'package:provider/provider.dart';
7 |
8 | import 'bottom_nav.dart';
9 |
10 | class HomeScreen extends StatefulWidget {
11 | const HomeScreen({Key key}) : super(key: key);
12 |
13 | static String route = '/home';
14 |
15 | @override
16 | _HomeScreenState createState() => _HomeScreenState();
17 | }
18 |
19 | class _HomeScreenState extends State with RouteAware {
20 | PageController _pageController;
21 | HomeStore _homeStore;
22 | ReactionDisposer _dispose;
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 | _pageController = PageController();
28 | _homeStore = HomeStore();
29 |
30 | _dispose = reaction((_) => _homeStore.currentTab, (currentTab) {
31 | _pageController.jumpToPage(currentTab);
32 | });
33 | }
34 |
35 | @override
36 | void didChangeDependencies() {
37 | super.didChangeDependencies();
38 | firebaseAnalyticsObserver.subscribe(
39 | this, ModalRoute.of(context) as PageRoute);
40 | }
41 |
42 | @override
43 | void dispose() {
44 | firebaseAnalyticsObserver.unsubscribe(this);
45 | _dispose();
46 | super.dispose();
47 | }
48 |
49 | @override
50 | void didPush() {
51 | super.didPush();
52 | _homeStore.sendCurrentTabToAnalytics();
53 | }
54 |
55 | @override
56 | void didPopNext() {
57 | super.didPopNext();
58 | _homeStore.sendCurrentTabToAnalytics();
59 | }
60 |
61 | @override
62 | Widget build(BuildContext context) {
63 | final mediaQuery = MediaQuery.of(context);
64 |
65 | return Provider.value(
66 | value: _homeStore,
67 | child: MediaQuery(
68 | data: mediaQuery.copyWith(
69 | padding: mediaQuery.padding + mediaQuery.viewInsets,
70 | ),
71 | child: Scaffold(
72 | body: PageView(
73 | controller: _pageController,
74 | physics: const NeverScrollableScrollPhysics(),
75 | children: tabs.map((e) => e.widget(context)).toList(),
76 | ),
77 | bottomNavigationBar: const HomeBottomNav(),
78 | ),
79 | ),
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/text_field.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/database.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 | import 'package:eh_redux/modules/search/store.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_typeahead/flutter_typeahead.dart';
6 | import 'package:provider/provider.dart';
7 |
8 | class SearchTextField extends StatefulWidget {
9 | const SearchTextField({Key key}) : super(key: key);
10 |
11 | @override
12 | _SearchTextFieldState createState() => _SearchTextFieldState();
13 | }
14 |
15 | class _SearchTextFieldState extends State {
16 | TextEditingController _controller;
17 |
18 | @override
19 | void initState() {
20 | super.initState();
21 | final searchStore = Provider.of(context, listen: false);
22 | _controller = TextEditingController(text: searchStore.query);
23 | }
24 |
25 | @override
26 | void dispose() {
27 | _controller.dispose();
28 | super.dispose();
29 | }
30 |
31 | @override
32 | Widget build(BuildContext context) {
33 | final theme = Theme.of(context);
34 | final store = Provider.of(context);
35 |
36 | return TypeAheadField(
37 | hideOnEmpty: true,
38 | hideOnError: true,
39 | hideOnLoading: true,
40 | textFieldConfiguration: TextFieldConfiguration(
41 | autofocus: _controller.text.isEmpty,
42 | controller: _controller,
43 | onSubmitted: (value) {
44 | store.setQuery(value as String);
45 | },
46 | decoration: InputDecoration(
47 | hintText: AppLocalizations.of(context).searchTextFieldHint,
48 | border: InputBorder.none,
49 | ),
50 | style: theme.textTheme.headline6,
51 | textInputAction: TextInputAction.search,
52 | ),
53 | transitionBuilder: (context, suggestionsBox, animationController) {
54 | return suggestionsBox;
55 | },
56 | onSuggestionSelected: (value) {
57 | _controller.text = value.query;
58 | store.setQuery(value.query);
59 | },
60 | suggestionsCallback: (pattern) async {
61 | return store.searchHistoriesDao.listEntries(pattern);
62 | },
63 | itemBuilder: (context, value) {
64 | return ListTile(
65 | leading: const Icon(Icons.history),
66 | title: Text(value.query),
67 | );
68 | },
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/modules/download/daos/image.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/database.dart';
2 | import 'package:logging/logging.dart';
3 | import 'package:moor/moor.dart';
4 |
5 | part 'image.g.dart';
6 |
7 | @DataClassName('DownloadedImageEntry')
8 | class DownloadedImages extends Table {
9 | IntColumn get galleryId => integer()();
10 | IntColumn get page => integer()();
11 | TextColumn get key => text()();
12 | IntColumn get width => integer()();
13 | IntColumn get height => integer()();
14 | IntColumn get size => integer()();
15 | TextColumn get path => text()();
16 | DateTimeColumn get downloadedAt =>
17 | dateTime().withDefault(currentDateAndTime)();
18 |
19 | @override
20 | Set get primaryKey => {galleryId, page};
21 | }
22 |
23 | @UseDao(tables: [DownloadedImages])
24 | class DownloadedImagesDao extends DatabaseAccessor
25 | with _$DownloadedImagesDaoMixin {
26 | DownloadedImagesDao(Database db) : super(db);
27 |
28 | static final _log = Logger('DownloadedImagesDao');
29 |
30 | Future getEntry(int galleryId, int page) async {
31 | _log.fine('getEntry: galleryId=$galleryId, page=$page');
32 | final query = select(downloadedImages)
33 | ..where((t) => t.galleryId.equals(galleryId) & t.page.equals(page))
34 | ..limit(1);
35 |
36 | return query.getSingle();
37 | }
38 |
39 | Future upsertEntry(DownloadedImageEntry entry) async {
40 | _log.fine('upsertEntry: $entry');
41 | await into(downloadedImages).insertOnConflictUpdate(entry);
42 | }
43 |
44 | Future deleteEntry(int galleryId, int page) async {
45 | _log.fine('deleteEntry: galleryId=$galleryId, page=$page');
46 | final query = delete(downloadedImages)
47 | ..where((t) => t.galleryId.equals(galleryId) & t.page.equals(page));
48 |
49 | return query.go();
50 | }
51 |
52 | Future> listByGalleryId(int galleryId) async {
53 | _log.fine('listByGalleryId: galleryId=$galleryId');
54 | final query = select(downloadedImages)
55 | ..where((t) => t.galleryId.equals(galleryId))
56 | ..orderBy([
57 | (t) => OrderingTerm.asc(t.page),
58 | ]);
59 |
60 | return query.get();
61 | }
62 |
63 | Future deleteByGalleryId(int galleryId) async {
64 | _log.fine('deleteByGalleryId: galleryId=$galleryId');
65 | final query = delete(downloadedImages)
66 | ..where((t) => t.galleryId.equals(galleryId));
67 |
68 | return query.go();
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:developer' as developer;
3 |
4 | import 'package:eh_redux/database/database.dart';
5 | import 'package:eh_redux/modules/app/widgets/app.dart';
6 | import 'package:eh_redux/utils/firebase.dart';
7 | import 'package:eh_redux/utils/notification.dart';
8 | import 'package:firebase_crashlytics/firebase_crashlytics.dart';
9 | import 'package:flutter/foundation.dart';
10 | import 'package:flutter/widgets.dart';
11 | import 'package:logging/logging.dart';
12 | import 'package:mobx/mobx.dart';
13 | import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
14 | import 'package:workmanager/workmanager.dart';
15 |
16 | import 'tasks/handler.dart';
17 |
18 | Future callbackDispatcher() async {
19 | await _initializeMain();
20 |
21 | return runZonedGuarded(() async {
22 | final handler = BackgroundTaskHandler();
23 |
24 | Workmanager.executeTask(
25 | (taskName, inputData) => handler.handle(taskName, inputData));
26 | }, (error, stackTrace) {
27 | FirebaseCrashlytics.instance.recordError(error, stackTrace);
28 | });
29 | }
30 |
31 | Future main() async {
32 | await _initializeMain();
33 |
34 | Workmanager.initialize(callbackDispatcher, isInDebugMode: kDebugMode);
35 |
36 | runZonedGuarded(() async {
37 | final isolate = await Database.createIsolate();
38 | final database = Database.connect(await isolate.connect());
39 |
40 | Database.shareIsolate(isolate);
41 |
42 | runApp(App(
43 | database: database,
44 | preferences: await StreamingSharedPreferences.instance,
45 | ));
46 | }, (error, stackTrace) {
47 | FirebaseCrashlytics.instance.recordError(error, stackTrace);
48 | });
49 | }
50 |
51 | Future _initializeMain() async {
52 | _setupLogger();
53 | WidgetsFlutterBinding.ensureInitialized();
54 | _logMobxMainContext();
55 | await initializeFirebase();
56 | await initializeNotificationPlugin();
57 | }
58 |
59 | void _setupLogger() {
60 | if (kDebugMode) {
61 | Logger.root.level = Level.FINER;
62 | }
63 |
64 | Logger.root.onRecord.listen((event) {
65 | developer.log(
66 | event.message,
67 | time: event.time,
68 | name: event.loggerName,
69 | error: event.error,
70 | stackTrace: event.stackTrace,
71 | level: event.level.value,
72 | );
73 | });
74 | }
75 |
76 | void _logMobxMainContext() {
77 | final log = Logger('mobx');
78 |
79 | mainContext.spy((event) {
80 | log.finest('$event');
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/lib/modules/download/widgets/confirm_bottom_sheet.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:eh_redux/modules/common/widgets/bottom_sheet_container.dart';
3 | import 'package:eh_redux/modules/common/widgets/full_width.dart';
4 | import 'package:eh_redux/modules/common/widgets/loading_dialog.dart';
5 | import 'package:eh_redux/modules/download/controller.dart';
6 | import 'package:eh_redux/modules/gallery/types.dart';
7 | import 'package:filesize/filesize.dart';
8 | import 'package:flutter/material.dart';
9 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
10 | import 'package:provider/provider.dart';
11 |
12 | part 'confirm_bottom_sheet.g.dart';
13 |
14 | Future showDownloadConfirmBottomSheet({
15 | @required BuildContext context,
16 | @required Gallery gallery,
17 | }) async {
18 | final result = await showModalBottomSheet(
19 | context: context,
20 | isScrollControlled: true,
21 | builder: (context) {
22 | return DownloadConfirmBottomSheet(gallery: gallery);
23 | },
24 | );
25 |
26 | if (result is bool && result) {
27 | Scaffold.of(context).showSnackBar(SnackBar(
28 | content: Text(AppLocalizations.of(context).downloadStartedHint),
29 | ));
30 | }
31 | }
32 |
33 | @swidget
34 | Widget downloadConfirmBottomSheet(
35 | BuildContext context, {
36 | @required Gallery gallery,
37 | }) {
38 | final controller = Provider.of(context);
39 |
40 | return BottomSheetContainer(
41 | child: Wrap(
42 | runSpacing: 8,
43 | children: [
44 | ListTile(
45 | title: Text(
46 | AppLocalizations.of(context).downloadConfirmTitle(
47 | gallery.fileCount,
48 | filesize(gallery.fileSize),
49 | ),
50 | ),
51 | ),
52 | Padding(
53 | padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
54 | child: FullWidth(
55 | child: ElevatedButton.icon(
56 | onPressed: () async {
57 | await showLoadingDialog(
58 | context: context,
59 | future: controller.create(gallery),
60 | );
61 | Navigator.pop(context, true);
62 | },
63 | icon: const Icon(Icons.file_download),
64 | label: Text(AppLocalizations.of(context).downloadButtonLabel),
65 | ),
66 | ),
67 | ),
68 | ],
69 | ),
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 |
7 | jobs:
8 | android:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v1
12 | - uses: actions/setup-java@v1
13 | with:
14 | java-version: "12.x"
15 | - uses: subosito/flutter-action@v1
16 | with:
17 | flutter-version: "1.22.x"
18 | channel: stable
19 | - run: flutter pub get
20 | - run: echo $KEYSTORE_FILE | base64 -d > /tmp/keystore.jks
21 | env:
22 | KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
23 | - run: flutter build apk --split-per-abi
24 | env:
25 | KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
26 | KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
27 | KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
28 | KEYSTORE_PATH: /tmp/keystore.jks
29 | - uses: actions/upload-artifact@v2
30 | with:
31 | name: apk-release
32 | path: build/app/outputs/apk/release/
33 | - uses: actions/create-release@v1
34 | id: create_release
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 | with:
38 | tag_name: ${{ github.ref }}
39 | release_name: ${{ github.ref }}
40 | - uses: actions/upload-release-asset@v1
41 | env:
42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 | with:
44 | upload_url: ${{ steps.create_release.outputs.upload_url }}
45 | asset_path: build/app/outputs/apk/release/app-arm64-v8a-release.apk
46 | asset_name: app-arm64-v8a-release.apk
47 | asset_content_type: application/vnd.android.package-archive
48 | - uses: actions/upload-release-asset@v1
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 | with:
52 | upload_url: ${{ steps.create_release.outputs.upload_url }}
53 | asset_path: build/app/outputs/apk/release/app-armeabi-v7a-release.apk
54 | asset_name: app-armeabi-v7a-release.apk
55 | asset_content_type: application/vnd.android.package-archive
56 | - uses: actions/upload-release-asset@v1
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | with:
60 | upload_url: ${{ steps.create_release.outputs.upload_url }}
61 | asset_path: build/app/outputs/apk/release/app-x86_64-release.apk
62 | asset_name: app-x86_64-release.apk
63 | asset_content_type: application/vnd.android.package-archive
64 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/filter.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:eh_redux/modules/search/store.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter/rendering.dart';
5 | import 'package:provider/provider.dart';
6 |
7 | import 'category_bottom_sheet.dart';
8 | import 'filter_bottom_sheet.dart';
9 | import 'rating_bottom_sheet.dart';
10 |
11 | class SearchFilter extends StatelessWidget with PreferredSizeWidget {
12 | const SearchFilter({Key key}) : super(key: key);
13 |
14 | @override
15 | Size get preferredSize => const Size.fromHeight(48);
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | final store = Provider.of(context);
20 | final theme = Theme.of(context);
21 |
22 | return TextButtonTheme(
23 | data: TextButtonThemeData(
24 | style: TextButton.styleFrom(
25 | primary: theme.textTheme.bodyText1.color,
26 | ),
27 | ),
28 | child: Row(
29 | children: [
30 | const SizedBox(width: 8),
31 | TextButton.icon(
32 | onPressed: () {
33 | showModalBottomSheet(
34 | context: context,
35 | builder: (context) {
36 | return CategoryBottomSheet(store: store);
37 | },
38 | ).whenComplete(() => store.updateParams());
39 | },
40 | icon: const Icon(Icons.category),
41 | label: Text(AppLocalizations.of(context).searchCategoryButtonLabel),
42 | ),
43 | TextButton.icon(
44 | onPressed: () {
45 | showModalBottomSheet(
46 | context: context,
47 | isScrollControlled: true,
48 | builder: (context) {
49 | return RatingBottomSheet(store: store);
50 | },
51 | ).whenComplete(() => store.updateParams());
52 | },
53 | icon: const Icon(Icons.star),
54 | label: Text(AppLocalizations.of(context).searchRatingButtonLabel),
55 | ),
56 | const Spacer(),
57 | IconButton(
58 | onPressed: () {
59 | showModalBottomSheet(
60 | context: context,
61 | builder: (context) {
62 | return FilterBottomSheet(store: store);
63 | },
64 | ).whenComplete(() => store.updateParams());
65 | },
66 | icon: const Icon(Icons.more_vert),
67 | ),
68 | ],
69 | ),
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/filter_bottom_sheet.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:eh_redux/modules/common/widgets/bottom_sheet_container.dart';
3 | import 'package:eh_redux/modules/search/store.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_mobx/flutter_mobx.dart';
6 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
7 | import 'package:provider/provider.dart';
8 |
9 | part 'filter_bottom_sheet.g.dart';
10 |
11 | @swidget
12 | Widget filterBottomSheet(
13 | BuildContext context, {
14 | @required SearchStore store,
15 | }) {
16 | final options = {
17 | 'f_sname': AppLocalizations.of(context).searchFilterSearchGalleryName,
18 | 'f_stags': AppLocalizations.of(context).searchFilterSearchGalleryTags,
19 | 'f_sdesc':
20 | AppLocalizations.of(context).searchFilterSearchGalleryDescription,
21 | 'f_storr': AppLocalizations.of(context).searchFilterSearchTorrentFilenames,
22 | 'f_sto':
23 | AppLocalizations.of(context).searchFilterOnlyShowGalleriesWithTorrents,
24 | 'f_sdt1': AppLocalizations.of(context).searchFilterSearchLowPowerTags,
25 | 'f_sdt2': AppLocalizations.of(context).searchFilterSearchDownvotedTags,
26 | 'f_sh': AppLocalizations.of(context).searchFilterShowExpungedGalleries,
27 | };
28 |
29 | return Provider.value(
30 | value: store,
31 | child: DraggableScrollableSheet(
32 | expand: false,
33 | initialChildSize: 1,
34 | builder: (context, controller) {
35 | return BottomSheetContainer(
36 | child: ListView.builder(
37 | controller: controller,
38 | itemCount: options.length,
39 | itemBuilder: (context, index) {
40 | final entry = options.entries.elementAt(index);
41 |
42 | return _OptionTile(
43 | name: entry.key,
44 | label: entry.value,
45 | );
46 | },
47 | ),
48 | );
49 | },
50 | ),
51 | );
52 | }
53 |
54 | @swidget
55 | Widget _optionTile(
56 | BuildContext context, {
57 | @required String name,
58 | @required String label,
59 | }) {
60 | final store = Provider.of(context);
61 |
62 | return Observer(
63 | builder: (context) {
64 | return SwitchListTile.adaptive(
65 | title: Text(label),
66 | value: store.advancedOptions[name] ?? false,
67 | onChanged: (checked) {
68 | store.setAdvancedOption(key: name, value: checked);
69 | },
70 | );
71 | },
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/lib/modules/gallery/dao.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/converter.dart';
2 | import 'package:eh_redux/database/database.dart';
3 | import 'package:eh_redux/modules/gallery/types.dart';
4 | import 'package:logging/logging.dart';
5 | import 'package:moor/moor.dart';
6 |
7 | part 'dao.g.dart';
8 |
9 | @DataClassName('GalleryEntry')
10 | class Galleries extends Table {
11 | IntColumn get id => integer()();
12 | TextColumn get token => text()();
13 | TextColumn get title => text()();
14 | TextColumn get titleJpn => text()();
15 | TextColumn get category => text()();
16 | TextColumn get thumbnail => text()();
17 | TextColumn get uploader => text()();
18 | IntColumn get fileCount => integer()();
19 | IntColumn get fileSize => integer()();
20 | BoolColumn get expunged => boolean()();
21 | RealColumn get rating => real()();
22 | DateTimeColumn get posted => dateTime()();
23 | TextColumn get tags => text().map(ListConverter())();
24 | DateTimeColumn get lastReadAt => dateTime().nullable()();
25 | IntColumn get lastReadPage => integer().nullable()();
26 |
27 | @override
28 | Set get primaryKey => {id};
29 | }
30 |
31 | @UseDao(tables: [Galleries])
32 | class GalleriesDao extends DatabaseAccessor with _$GalleriesDaoMixin {
33 | GalleriesDao(Database db) : super(db);
34 |
35 | static final _log = Logger('GalleriesDao');
36 |
37 | Future getEntry(int id) async {
38 | _log.fine('getEntry: $id');
39 | final query = select(galleries)..where((t) => t.id.equals(id));
40 |
41 | return query.getSingle();
42 | }
43 |
44 | Future upsertEntry(GalleryEntry entry) async {
45 | _log.fine('upsertEntry: $entry');
46 | await into(galleries).insertOnConflictUpdate(entry);
47 | }
48 |
49 | Future getReadPosition(int id) async {
50 | _log.fine('getReadPosition: $id');
51 | final query = select(galleries)..where((t) => t.id.equals(id));
52 |
53 | return query.map((e) {
54 | if (e != null && e.lastReadPage != null && e.lastReadAt != null) {
55 | return GalleryReadPosition(
56 | page: e.lastReadPage,
57 | time: e.lastReadAt,
58 | );
59 | }
60 |
61 | return null;
62 | }).getSingle();
63 | }
64 |
65 | Future updateReadPosition(int id, int page) async {
66 | _log.fine('updateReadPosition: id=$id, page=$page');
67 | final query = update(galleries)..where((t) => t.id.equals(id));
68 |
69 | await query.write(GalleriesCompanion(
70 | lastReadPage: Value(page),
71 | lastReadAt: Value(DateTime.now()),
72 | ));
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/modules/check_update/store.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:device_info/device_info.dart';
4 | import 'package:eh_redux/utils/string.dart';
5 | import 'package:mobx/mobx.dart';
6 | import 'package:package_info/package_info.dart';
7 | import 'package:http/http.dart' as http;
8 |
9 | import 'types.dart';
10 |
11 | part 'store.g.dart';
12 |
13 | class CheckUpdateStore = _CheckUpdateStoreBase with _$CheckUpdateStore;
14 |
15 | abstract class _CheckUpdateStoreBase with Store {
16 | final _deviceInfo = DeviceInfoPlugin();
17 |
18 | @observable
19 | ObservableFuture releaseFuture;
20 |
21 | @observable
22 | ObservableFuture packageInfoFuture;
23 |
24 | @observable
25 | ObservableFuture androidDeviceInfoFuture;
26 |
27 | @computed
28 | UpdateStatus get status {
29 | final futures = [
30 | releaseFuture,
31 | packageInfoFuture,
32 | androidDeviceInfoFuture,
33 | ];
34 |
35 | if (futures.any((x) => x.status == FutureStatus.rejected)) {
36 | return UpdateStatus.failed;
37 | }
38 |
39 | if (futures.any((x) => x.status == FutureStatus.pending)) {
40 | return UpdateStatus.pending;
41 | }
42 |
43 | if (trimPrefix(releaseFuture.value.tagName, 'v') !=
44 | packageInfoFuture.value.version) {
45 | return UpdateStatus.canUpdate;
46 | }
47 |
48 | return UpdateStatus.noUpdate;
49 | }
50 |
51 | @computed
52 | GitHubAsset get asset {
53 | final release = releaseFuture.value;
54 | final info = androidDeviceInfoFuture.value;
55 |
56 | if (release == null || info == null) return null;
57 |
58 | final assets = release.assets.where((asset) =>
59 | asset.contentType == 'application/vnd.android.package-archive' &&
60 | asset.state == 'uploaded');
61 |
62 | for (final abi in info.supportedAbis) {
63 | final asset = assets.firstWhere((asset) => asset.name.contains(abi),
64 | orElse: () => null);
65 | if (asset != null) return asset;
66 | }
67 |
68 | return null;
69 | }
70 |
71 | @action
72 | void check() {
73 | releaseFuture = ObservableFuture(_fetchLatestRelease());
74 | packageInfoFuture = ObservableFuture(PackageInfo.fromPlatform());
75 | androidDeviceInfoFuture = ObservableFuture(_deviceInfo.androidInfo);
76 | }
77 |
78 | Future _fetchLatestRelease() async {
79 | final res = await http
80 | .get('https://api.github.com/repos/tommy351/eh-redux/releases/latest');
81 | return GitHubRelease.fromJson(jsonDecode(res.body) as Map);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/lib/modules/app/widgets/theme_data_builder.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/common/widgets/brightness_observer.dart';
2 | import 'package:eh_redux/modules/setting/store.dart';
3 | import 'package:eh_redux/modules/setting/types.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
6 | import 'package:provider/provider.dart';
7 | import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
8 |
9 | part 'theme_data_builder.g.dart';
10 |
11 | const _accentColor = Colors.deepOrangeAccent;
12 | final _sliderTheme = SliderThemeData.fromPrimaryColors(
13 | primaryColor: _accentColor,
14 | primaryColorDark: _accentColor,
15 | primaryColorLight: _accentColor,
16 | valueIndicatorTextStyle: const TextStyle(),
17 | );
18 | final _elevatedButtonTheme = ElevatedButtonThemeData(
19 | style: ElevatedButton.styleFrom(primary: _accentColor),
20 | );
21 | final _themeData = {
22 | ThemeSetting.light: ThemeData(
23 | primarySwatch: Colors.brown,
24 | accentColor: _accentColor,
25 | brightness: Brightness.light,
26 | sliderTheme: _sliderTheme,
27 | elevatedButtonTheme: _elevatedButtonTheme,
28 | ),
29 | ThemeSetting.dark: ThemeData(
30 | accentColor: _accentColor,
31 | toggleableActiveColor: _accentColor,
32 | brightness: Brightness.dark,
33 | sliderTheme: _sliderTheme,
34 | elevatedButtonTheme: _elevatedButtonTheme,
35 | ),
36 | ThemeSetting.black: ThemeData(
37 | accentColor: _accentColor,
38 | toggleableActiveColor: _accentColor,
39 | brightness: Brightness.dark,
40 | scaffoldBackgroundColor: Colors.black,
41 | dividerColor: Colors.grey[800],
42 | sliderTheme: _sliderTheme,
43 | elevatedButtonTheme: _elevatedButtonTheme,
44 | ),
45 | };
46 |
47 | @swidget
48 | Widget themeDataBuilder(
49 | BuildContext context, {
50 | @required Widget Function(BuildContext, ThemeData) builder,
51 | }) {
52 | final settings = Provider.of(context);
53 |
54 | return BrightnessObserver(
55 | builder: (context, brightness) {
56 | return PreferenceBuilder(
57 | preference: settings.theme,
58 | builder: (context, theme) {
59 | ThemeSetting key = theme;
60 |
61 | if (theme == ThemeSetting.system) {
62 | if (brightness == Brightness.dark) {
63 | key = ThemeSetting.dark;
64 | } else {
65 | key = ThemeSetting.light;
66 | }
67 | }
68 |
69 | return builder(context, _themeData[key]);
70 | },
71 | );
72 | },
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/category_bottom_sheet.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/modules/common/widgets/bottom_sheet_container.dart';
2 | import 'package:eh_redux/modules/gallery/widgets/category_icon.dart';
3 | import 'package:eh_redux/modules/gallery/widgets/category_label.dart';
4 | import 'package:eh_redux/modules/search/store.dart';
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter_mobx/flutter_mobx.dart';
7 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
8 | import 'package:provider/provider.dart';
9 |
10 | part 'category_bottom_sheet.g.dart';
11 |
12 | @swidget
13 | Widget categoryBottomSheet(
14 | BuildContext context, {
15 | @required SearchStore store,
16 | }) {
17 | const categories = {
18 | 'Doujinshi': 1 << 1,
19 | 'Manga': 1 << 2,
20 | 'Artist CG': 1 << 3,
21 | 'Game CG': 1 << 4,
22 | 'Western': 1 << 9,
23 | 'Non-H': 1 << 8,
24 | 'Image Set': 1 << 5,
25 | 'Cosplay': 1 << 6,
26 | 'Asian Porn': 1 << 7,
27 | 'Misc': 1 << 0,
28 | };
29 |
30 | return Provider.value(
31 | value: store,
32 | child: DraggableScrollableSheet(
33 | expand: false,
34 | initialChildSize: 1,
35 | builder: (context, controller) {
36 | return BottomSheetContainer(
37 | child: ListView.builder(
38 | controller: controller,
39 | itemCount: categories.length,
40 | itemBuilder: (context, index) {
41 | final entry = categories.entries.elementAt(index);
42 |
43 | return _CategoryTile(
44 | category: entry.key,
45 | value: entry.value,
46 | );
47 | },
48 | ),
49 | );
50 | },
51 | ),
52 | );
53 | }
54 |
55 | @swidget
56 | Widget _categoryTile(
57 | BuildContext context, {
58 | @required String category,
59 | @required int value,
60 | }) {
61 | final store = Provider.of(context);
62 |
63 | return Observer(
64 | builder: (context) {
65 | return CheckboxListTile(
66 | title: Row(
67 | children: [
68 | IconTheme(
69 | data: const IconThemeData(size: 16),
70 | child: CategoryIcon(category: category),
71 | ),
72 | const SizedBox(width: 8),
73 | CategoryLabel(category: category),
74 | ],
75 | ),
76 | value: store.categoryFilter & value == 0,
77 | onChanged: (checked) {
78 | if (checked) {
79 | store.setCategoryFilter(store.categoryFilter - value);
80 | } else {
81 | store.setCategoryFilter(store.categoryFilter + value);
82 | }
83 | },
84 | );
85 | },
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/lib/modules/favorite/widgets/tab.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:eh_redux/modules/gallery/stores/network_list.dart';
3 | import 'package:eh_redux/modules/gallery/widgets/network_list.dart';
4 | import 'package:eh_redux/modules/home/widgets/body.dart';
5 | import 'package:eh_redux/modules/session/store.dart';
6 | import 'package:eh_redux/services/ehentai.dart';
7 | import 'package:flutter/material.dart';
8 | import 'package:flutter_mobx/flutter_mobx.dart';
9 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
10 | import 'package:provider/provider.dart';
11 |
12 | part 'tab.g.dart';
13 |
14 | class FavoriteTab extends StatefulWidget {
15 | const FavoriteTab({Key key}) : super(key: key);
16 |
17 | @override
18 | _FavoriteTabState createState() => _FavoriteTabState();
19 | }
20 |
21 | class _FavoriteTabState extends State
22 | with AutomaticKeepAliveClientMixin {
23 | NetworkGalleryListStore _store;
24 |
25 | @override
26 | bool get wantKeepAlive => true;
27 |
28 | @override
29 | void initState() {
30 | super.initState();
31 | _store = NetworkGalleryListStore(
32 | client: Provider.of(context, listen: false),
33 | path: '/favorites.php',
34 | );
35 | }
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | super.build(context);
40 |
41 | final sessionStore = Provider.of(context);
42 |
43 | return Provider.value(
44 | value: _store,
45 | child: Observer(
46 | builder: (context) {
47 | if (sessionStore.loginStatus != LoginStatus.loggedIn) {
48 | return const _LoginHint();
49 | }
50 |
51 | return const _Content();
52 | },
53 | ),
54 | );
55 | }
56 | }
57 |
58 | @swidget
59 | Widget _appBar(BuildContext context) {
60 | return SliverAppBar(
61 | title: Text(AppLocalizations.of(context).homeTabTitleFavorite),
62 | pinned: true,
63 | );
64 | }
65 |
66 | @swidget
67 | Widget _loginHint(BuildContext context) {
68 | final theme = Theme.of(context);
69 |
70 | return CustomScrollView(
71 | slivers: [
72 | const _AppBar(),
73 | SliverFillRemaining(
74 | child: Center(
75 | child: Text(
76 | AppLocalizations.of(context).logInRequiredHint,
77 | style: theme.textTheme.headline6,
78 | ),
79 | ),
80 | ),
81 | ],
82 | );
83 | }
84 |
85 | @swidget
86 | Widget _content(BuildContext context) {
87 | return NestedScrollView(
88 | headerSliverBuilder: (context, _) => const [
89 | _AppBar(),
90 | ],
91 | body: const HomeBody(
92 | child: NetworkGalleryList(),
93 | ),
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/android/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "728176174122",
4 | "firebase_url": "https://eh-redux.firebaseio.com",
5 | "project_id": "eh-redux",
6 | "storage_bucket": "eh-redux.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:728176174122:android:b88178eaec4574bdbe0f53",
12 | "android_client_info": {
13 | "package_name": "app.ehredux"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "728176174122-7dhf70r8b2njf6b7fr54497vfp8r4b30.apps.googleusercontent.com",
19 | "client_type": 1,
20 | "android_info": {
21 | "package_name": "app.ehredux",
22 | "certificate_hash": "70fe2cbc3f752e4cf1928d3c57d4e66c573b7a57"
23 | }
24 | },
25 | {
26 | "client_id": "728176174122-rau8ssjmm3vk077ntftp6a91rcqpe8ma.apps.googleusercontent.com",
27 | "client_type": 3
28 | }
29 | ],
30 | "api_key": [
31 | {
32 | "current_key": "AIzaSyBoCx7GLY9bxuw2rfTMEk6e45nqGF_WQ1A"
33 | }
34 | ],
35 | "services": {
36 | "appinvite_service": {
37 | "other_platform_oauth_client": [
38 | {
39 | "client_id": "728176174122-rau8ssjmm3vk077ntftp6a91rcqpe8ma.apps.googleusercontent.com",
40 | "client_type": 3
41 | }
42 | ]
43 | }
44 | }
45 | },
46 | {
47 | "client_info": {
48 | "mobilesdk_app_id": "1:728176174122:android:a40bcd238d2ac2efbe0f53",
49 | "android_client_info": {
50 | "package_name": "app.ehredux.debug"
51 | }
52 | },
53 | "oauth_client": [
54 | {
55 | "client_id": "728176174122-5n8vcr5it48o0a5qretbir7ekg77c000.apps.googleusercontent.com",
56 | "client_type": 1,
57 | "android_info": {
58 | "package_name": "app.ehredux.debug",
59 | "certificate_hash": "70fe2cbc3f752e4cf1928d3c57d4e66c573b7a57"
60 | }
61 | },
62 | {
63 | "client_id": "728176174122-rau8ssjmm3vk077ntftp6a91rcqpe8ma.apps.googleusercontent.com",
64 | "client_type": 3
65 | }
66 | ],
67 | "api_key": [
68 | {
69 | "current_key": "AIzaSyBoCx7GLY9bxuw2rfTMEk6e45nqGF_WQ1A"
70 | }
71 | ],
72 | "services": {
73 | "appinvite_service": {
74 | "other_platform_oauth_client": [
75 | {
76 | "client_id": "728176174122-rau8ssjmm3vk077ntftp6a91rcqpe8ma.apps.googleusercontent.com",
77 | "client_type": 3
78 | }
79 | ]
80 | }
81 | }
82 | }
83 | ],
84 | "configuration_version": "1"
85 | }
--------------------------------------------------------------------------------
/lib/modules/download/widgets/tab.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/database.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 | import 'package:eh_redux/modules/common/widgets/loading_dialog.dart';
4 | import 'package:eh_redux/modules/download/controller.dart';
5 | import 'package:eh_redux/modules/download/types.dart';
6 | import 'package:eh_redux/modules/home/widgets/body.dart';
7 | import 'package:flutter/material.dart';
8 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
9 | import 'package:provider/provider.dart';
10 |
11 | import 'list.dart';
12 |
13 | part 'tab.g.dart';
14 |
15 | @swidget
16 | Widget downloadTab(BuildContext context) {
17 | final database = Provider.of(context);
18 |
19 | return StreamProvider>(
20 | create: (_) => database.downloadTasksDao.watchAllWithGallery(),
21 | child: NestedScrollView(
22 | headerSliverBuilder: (context, _) => const [_AppBar()],
23 | body: const _Content(),
24 | ),
25 | );
26 | }
27 |
28 | @swidget
29 | Widget _appBar(BuildContext context) {
30 | return SliverAppBar(
31 | title: Text(AppLocalizations.of(context).homeTabTitleDownload),
32 | pinned: true,
33 | actions: const [
34 | _ResumeAllButton(),
35 | _PauseAllButton(),
36 | ],
37 | );
38 | }
39 |
40 | @swidget
41 | Widget _resumeAllButton(BuildContext context) {
42 | final controller = Provider.of(context);
43 |
44 | return IconButton(
45 | icon: const Icon(Icons.play_arrow),
46 | tooltip: AppLocalizations.of(context).downloadResumeAllButtonTooltip,
47 | onPressed: () async {
48 | final count = await showLoadingDialog(
49 | context: context, future: controller.resumeAll());
50 |
51 | if (count > 0) {
52 | Scaffold.of(context).showSnackBar(SnackBar(
53 | content: Text(AppLocalizations.of(context).downloadResumedHint),
54 | ));
55 | }
56 | },
57 | );
58 | }
59 |
60 | @swidget
61 | Widget _pauseAllButton(BuildContext context) {
62 | final controller = Provider.of(context);
63 |
64 | return IconButton(
65 | icon: const Icon(Icons.pause),
66 | tooltip: AppLocalizations.of(context).downloadPauseAllButtonTooltip,
67 | onPressed: () async {
68 | final count = await showLoadingDialog(
69 | context: context, future: controller.pauseAll());
70 |
71 | if (count > 0) {
72 | Scaffold.of(context).showSnackBar(SnackBar(
73 | content: Text(AppLocalizations.of(context).downloadPausedHint),
74 | ));
75 | }
76 | },
77 | );
78 | }
79 |
80 | @swidget
81 | Widget _content(BuildContext context) {
82 | final data = Provider.of>(context);
83 |
84 | return HomeBody(
85 | child: DownloadList(data: data ?? []),
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
8 |
9 |
10 |
16 |
23 |
27 |
31 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
47 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/lib/modules/gallery/widgets/network_list.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2 | import 'package:eh_redux/modules/common/widgets/stateful_wrapper.dart';
3 | import 'package:eh_redux/modules/gallery/stores/network_list.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_mobx/flutter_mobx.dart';
6 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
7 | import 'package:provider/provider.dart';
8 |
9 | import 'list.dart';
10 |
11 | part 'network_list.g.dart';
12 |
13 | class NetworkGalleryList extends StatefulWidget {
14 | const NetworkGalleryList({Key key}) : super(key: key);
15 |
16 | @override
17 | _NetworkGalleryListState createState() => _NetworkGalleryListState();
18 | }
19 |
20 | class _NetworkGalleryListState extends State {
21 | @override
22 | void initState() {
23 | super.initState();
24 |
25 | Provider.of(context, listen: false)
26 | .loadInitialPage();
27 | }
28 |
29 | @override
30 | void didChangeDependencies() {
31 | super.didChangeDependencies();
32 |
33 | Provider.of(context, listen: false)
34 | .loadInitialPage();
35 | }
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | final listStore = Provider.of(context);
40 |
41 | return Observer(
42 | builder: (context) {
43 | if (listStore.currentPage < 0) {
44 | return const Center(child: CircularProgressIndicator());
45 | }
46 |
47 | return RefreshIndicator(
48 | onRefresh: () => listStore.refreshPage(),
49 | child: listStore.data.isEmpty
50 | ? const _Placeholder()
51 | : GalleryList(
52 | data: listStore.data.values,
53 | footer: listStore.hasMore ? const _Footer() : null,
54 | ),
55 | );
56 | },
57 | );
58 | }
59 | }
60 |
61 | @swidget
62 | Widget _placeholder(BuildContext context) {
63 | final theme = Theme.of(context);
64 |
65 | return LayoutBuilder(
66 | builder: (context, constraints) {
67 | return SingleChildScrollView(
68 | child: ConstrainedBox(
69 | constraints: BoxConstraints(
70 | minHeight: constraints.maxHeight,
71 | ),
72 | child: Center(
73 | child: Text(
74 | AppLocalizations.of(context).galleryListNoDataPlaceholderTitle,
75 | style: theme.textTheme.headline6,
76 | ),
77 | ),
78 | ),
79 | );
80 | },
81 | );
82 | }
83 |
84 | @swidget
85 | Widget _footer(BuildContext context) {
86 | final listStore = Provider.of(context);
87 |
88 | return StatefulWrapper(
89 | onInit: (context) {
90 | listStore.loadNextPage();
91 | return () {};
92 | },
93 | builder: (context) {
94 | return const Center(
95 | child: Padding(
96 | padding: EdgeInsets.all(16),
97 | child: CircularProgressIndicator(),
98 | ),
99 | );
100 | },
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/lib/modules/gallery/stores/network_list.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'network_list.dart';
4 |
5 | // **************************************************************************
6 | // StoreGenerator
7 | // **************************************************************************
8 |
9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
10 |
11 | mixin _$NetworkGalleryListStore on _NetworkGalleryListStoreBase, Store {
12 | final _$dataAtom = Atom(name: '_NetworkGalleryListStoreBase.data');
13 |
14 | @override
15 | ObservableMap get data {
16 | _$dataAtom.reportRead();
17 | return super.data;
18 | }
19 |
20 | @override
21 | set data(ObservableMap value) {
22 | _$dataAtom.reportWrite(value, super.data, () {
23 | super.data = value;
24 | });
25 | }
26 |
27 | final _$currentPageAtom =
28 | Atom(name: '_NetworkGalleryListStoreBase.currentPage');
29 |
30 | @override
31 | int get currentPage {
32 | _$currentPageAtom.reportRead();
33 | return super.currentPage;
34 | }
35 |
36 | @override
37 | set currentPage(int value) {
38 | _$currentPageAtom.reportWrite(value, super.currentPage, () {
39 | super.currentPage = value;
40 | });
41 | }
42 |
43 | final _$loadingAtom = Atom(name: '_NetworkGalleryListStoreBase.loading');
44 |
45 | @override
46 | bool get loading {
47 | _$loadingAtom.reportRead();
48 | return super.loading;
49 | }
50 |
51 | @override
52 | set loading(bool value) {
53 | _$loadingAtom.reportWrite(value, super.loading, () {
54 | super.loading = value;
55 | });
56 | }
57 |
58 | final _$hasMoreAtom = Atom(name: '_NetworkGalleryListStoreBase.hasMore');
59 |
60 | @override
61 | bool get hasMore {
62 | _$hasMoreAtom.reportRead();
63 | return super.hasMore;
64 | }
65 |
66 | @override
67 | set hasMore(bool value) {
68 | _$hasMoreAtom.reportWrite(value, super.hasMore, () {
69 | super.hasMore = value;
70 | });
71 | }
72 |
73 | final _$loadInitialPageAsyncAction =
74 | AsyncAction('_NetworkGalleryListStoreBase.loadInitialPage');
75 |
76 | @override
77 | Future loadInitialPage() {
78 | return _$loadInitialPageAsyncAction.run(() => super.loadInitialPage());
79 | }
80 |
81 | final _$loadNextPageAsyncAction =
82 | AsyncAction('_NetworkGalleryListStoreBase.loadNextPage');
83 |
84 | @override
85 | Future loadNextPage() {
86 | return _$loadNextPageAsyncAction.run(() => super.loadNextPage());
87 | }
88 |
89 | final _$refreshPageAsyncAction =
90 | AsyncAction('_NetworkGalleryListStoreBase.refreshPage');
91 |
92 | @override
93 | Future refreshPage() {
94 | return _$refreshPageAsyncAction.run(() => super.refreshPage());
95 | }
96 |
97 | @override
98 | String toString() {
99 | return '''
100 | data: ${data},
101 | currentPage: ${currentPage},
102 | loading: ${loading},
103 | hasMore: ${hasMore}
104 | ''';
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def keystoreProperties = new Properties()
10 | def keystorePropertiesFile = rootProject.file('key.properties')
11 | if (keystorePropertiesFile.exists()) {
12 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
13 | } else {
14 | keystoreProperties.setProperty('storePassword', String.valueOf(System.getenv('KEYSTORE_STORE_PASSWORD')));
15 | keystoreProperties.setProperty('keyPassword', String.valueOf(System.getenv('KEYSTORE_KEY_PASSWORD')));
16 | keystoreProperties.setProperty('keyAlias', String.valueOf(System.getenv('KEYSTORE_ALIAS')));
17 | keystoreProperties.setProperty('storeFile', String.valueOf(System.getenv('KEYSTORE_PATH')));
18 | }
19 |
20 | def flutterRoot = localProperties.getProperty('flutter.sdk')
21 | if (flutterRoot == null) {
22 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
23 | }
24 |
25 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
26 | if (flutterVersionCode == null) {
27 | flutterVersionCode = '1'
28 | }
29 |
30 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
31 | if (flutterVersionName == null) {
32 | flutterVersionName = '1.0'
33 | }
34 |
35 | apply plugin: 'com.android.application'
36 | apply plugin: 'kotlin-android'
37 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
38 |
39 | android {
40 | compileSdkVersion 28
41 |
42 | sourceSets {
43 | main.java.srcDirs += 'src/main/kotlin'
44 | }
45 |
46 | lintOptions {
47 | disable 'InvalidPackage'
48 | }
49 |
50 | defaultConfig {
51 | applicationId "app.ehredux"
52 | minSdkVersion 21
53 | targetSdkVersion 28
54 | versionCode flutterVersionCode.toInteger()
55 | versionName flutterVersionName
56 | }
57 |
58 | signingConfigs {
59 | release {
60 | keyAlias keystoreProperties['keyAlias']
61 | keyPassword keystoreProperties['keyPassword']
62 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
63 | storePassword keystoreProperties['storePassword']
64 | }
65 | }
66 |
67 | buildTypes {
68 | debug {
69 | debuggable true
70 | applicationIdSuffix ".debug"
71 | versionNameSuffix "-debug"
72 | }
73 |
74 | release {
75 | signingConfig signingConfigs.release
76 | }
77 | }
78 | }
79 |
80 | flutter {
81 | source '../..'
82 | }
83 |
84 | dependencies {
85 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
86 | implementation "io.reactivex.rxjava3:rxkotlin:3.0.0"
87 | implementation 'com.uchuhimo:kotlinx-bimap:1.2'
88 | implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
89 | }
90 |
91 | apply plugin: 'com.google.gms.google-services'
92 | apply plugin: 'com.google.firebase.crashlytics'
93 |
--------------------------------------------------------------------------------
/lib/modules/search/widgets/screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:eh_redux/database/database.dart';
2 | import 'package:eh_redux/modules/gallery/stores/network_list.dart';
3 | import 'package:eh_redux/modules/gallery/widgets/network_list.dart';
4 | import 'package:eh_redux/modules/home/widgets/body.dart';
5 | import 'package:eh_redux/modules/search/store.dart';
6 | import 'package:eh_redux/modules/search/types.dart';
7 | import 'package:eh_redux/services/ehentai.dart';
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_mobx/flutter_mobx.dart';
10 | import 'package:functional_widget_annotation/functional_widget_annotation.dart';
11 | import 'package:provider/provider.dart';
12 |
13 | import 'filter.dart';
14 | import 'text_field.dart';
15 |
16 | part 'screen.g.dart';
17 |
18 | class SearchScreen extends StatefulWidget {
19 | const SearchScreen({
20 | Key key,
21 | this.arguments = const SearchArguments(),
22 | }) : assert(arguments != null),
23 | super(key: key);
24 |
25 | static const route = '/search';
26 |
27 | final SearchArguments arguments;
28 |
29 | @override
30 | _SearchScreenState createState() => _SearchScreenState();
31 | }
32 |
33 | class _SearchScreenState extends State {
34 | SearchStore _store;
35 |
36 | @override
37 | void initState() {
38 | super.initState();
39 |
40 | final database = Provider.of(context, listen: false);
41 | _store = SearchStore(
42 | searchHistoriesDao: database.searchHistoriesDao,
43 | )..setQuery(widget.arguments.query);
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return Provider.value(
49 | value: _store,
50 | child: GestureDetector(
51 | onTap: () {
52 | FocusScope.of(context).unfocus();
53 | },
54 | child: Scaffold(
55 | body: NestedScrollView(
56 | headerSliverBuilder: (context, _) => const [_AppBar()],
57 | body: const _Body(),
58 | ),
59 | ),
60 | ),
61 | );
62 | }
63 | }
64 |
65 | @swidget
66 | Widget _appBar(BuildContext context) {
67 | final theme = Theme.of(context);
68 | final isDark = theme.brightness == Brightness.dark;
69 | final iconTheme =
70 | isDark ? theme.iconTheme : const IconThemeData(color: Colors.black);
71 |
72 | return SliverAppBar(
73 | backgroundColor: isDark ? theme.appBarTheme.color : Colors.white,
74 | iconTheme: iconTheme,
75 | pinned: true,
76 | title: const SearchTextField(),
77 | bottom: const SearchFilter(),
78 | );
79 | }
80 |
81 | @swidget
82 | Widget _body(BuildContext context) {
83 | final client = Provider.of(context);
84 | final store = Provider.of(context);
85 |
86 | return Observer(
87 | builder: (context) {
88 | if (store.query.isEmpty) {
89 | return Container();
90 | }
91 |
92 | return ProxyProvider0(
93 | update: (_, __) => NetworkGalleryListStore(
94 | client: client,
95 | params: store.params,
96 | ),
97 | child: const HomeBody(
98 | child: NetworkGalleryList(),
99 | ),
100 | );
101 | },
102 | );
103 | }
104 |
--------------------------------------------------------------------------------