├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── build.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .metadata ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── google-services.json │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── app │ │ │ │ └── ehredux │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── app_icon.png │ │ │ └── download.png │ │ │ ├── drawable-mdpi │ │ │ ├── app_icon.png │ │ │ └── download.png │ │ │ ├── drawable-xhdpi │ │ │ ├── app_icon.png │ │ │ └── download.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── app_icon.png │ │ │ └── download.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── app_icon.png │ │ │ └── download.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── raw │ │ │ └── keep.xml │ │ │ ├── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── backup_rules.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── build.yaml ├── docs ├── download-qrcode.png ├── screenshot-download.png ├── screenshot-info.png ├── screenshot-list.png ├── screenshot-search.png └── screenshot-view.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 172.png │ │ ├── 180.png │ │ ├── 196.png │ │ ├── 20.png │ │ ├── 216.png │ │ ├── 256.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 48.png │ │ ├── 50.png │ │ ├── 512.png │ │ ├── 55.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 88.png │ │ └── Contents.json │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── l10n.yaml ├── lib ├── database │ ├── converter.dart │ ├── database.dart │ └── database.g.dart ├── l10n │ ├── app_en.arb │ ├── app_zh.arb │ └── app_zh_Hant.arb ├── main.dart ├── modules │ ├── app │ │ └── widgets │ │ │ ├── app.dart │ │ │ ├── theme_data_builder.dart │ │ │ └── theme_data_builder.g.dart │ ├── check_update │ │ ├── store.dart │ │ ├── store.g.dart │ │ ├── types.dart │ │ ├── types.freezed.dart │ │ ├── types.g.dart │ │ └── widgets │ │ │ ├── screen.dart │ │ │ └── screen.g.dart │ ├── common │ │ └── widgets │ │ │ ├── app_lifecycle_observer.dart │ │ │ ├── bottom_sheet_container.dart │ │ │ ├── bottom_sheet_container.g.dart │ │ │ ├── brightness_observer.dart │ │ │ ├── controlled_text_field.dart │ │ │ ├── full_width.dart │ │ │ ├── full_width.g.dart │ │ │ ├── loading_dialog.dart │ │ │ ├── orientation_setter.dart │ │ │ ├── rating_bar.dart │ │ │ ├── rating_bar.g.dart │ │ │ └── stateful_wrapper.dart │ ├── download │ │ ├── controller.dart │ │ ├── daos │ │ │ ├── image.dart │ │ │ ├── image.g.dart │ │ │ ├── task.dart │ │ │ └── task.g.dart │ │ ├── interrupter.dart │ │ ├── tasks │ │ │ └── download.dart │ │ ├── types.dart │ │ ├── types.freezed.dart │ │ ├── utils.dart │ │ └── widgets │ │ │ ├── confirm_bottom_sheet.dart │ │ │ ├── confirm_bottom_sheet.g.dart │ │ │ ├── list.dart │ │ │ ├── list.g.dart │ │ │ ├── menu_bottom_sheet.dart │ │ │ ├── menu_bottom_sheet.g.dart │ │ │ ├── tab.dart │ │ │ └── tab.g.dart │ ├── favorite │ │ ├── store.dart │ │ ├── store.g.dart │ │ └── widgets │ │ │ ├── bottom_sheet.dart │ │ │ ├── bottom_sheet.g.dart │ │ │ ├── icon.dart │ │ │ ├── icon.g.dart │ │ │ ├── tab.dart │ │ │ └── tab.g.dart │ ├── gallery │ │ ├── dao.dart │ │ ├── dao.g.dart │ │ ├── stores │ │ │ ├── network_list.dart │ │ │ ├── network_list.g.dart │ │ │ ├── screen.dart │ │ │ ├── screen.freezed.dart │ │ │ └── screen.g.dart │ │ ├── types.dart │ │ ├── types.freezed.dart │ │ ├── types.g.dart │ │ └── widgets │ │ │ ├── category_icon.dart │ │ │ ├── category_icon.g.dart │ │ │ ├── category_label.dart │ │ │ ├── category_label.g.dart │ │ │ ├── list.dart │ │ │ ├── list.g.dart │ │ │ ├── network_list.dart │ │ │ ├── network_list.g.dart │ │ │ ├── screen.dart │ │ │ ├── screen.g.dart │ │ │ ├── square_thumbnail.dart │ │ │ ├── square_thumbnail.g.dart │ │ │ ├── tab.dart │ │ │ ├── thumbnail.dart │ │ │ ├── thumbnail.g.dart │ │ │ ├── title.dart │ │ │ └── title.g.dart │ ├── history │ │ ├── dao.dart │ │ ├── dao.g.dart │ │ └── widgets │ │ │ ├── tab.dart │ │ │ └── tab.g.dart │ ├── home │ │ ├── store.dart │ │ ├── store.g.dart │ │ ├── tabs.dart │ │ ├── tabs.freezed.dart │ │ └── widgets │ │ │ ├── body.dart │ │ │ ├── body.g.dart │ │ │ ├── bottom_nav.dart │ │ │ ├── bottom_nav.g.dart │ │ │ └── screen.dart │ ├── image │ │ ├── file_fallback_image.dart │ │ ├── store.dart │ │ ├── store.freezed.dart │ │ ├── store.g.dart │ │ ├── types.dart │ │ ├── types.freezed.dart │ │ └── widgets │ │ │ ├── animated_navigation.dart │ │ │ ├── app_bar.dart │ │ │ ├── app_bar.g.dart │ │ │ ├── body.dart │ │ │ ├── body.g.dart │ │ │ ├── bottom_nav.dart │ │ │ ├── key_event_detector.dart │ │ │ ├── screen.dart │ │ │ ├── screen.g.dart │ │ │ ├── system_overlay_setter.dart │ │ │ ├── tap_event_detector.dart │ │ │ └── tap_event_detector.g.dart │ ├── login │ │ └── widgets │ │ │ └── screen.dart │ ├── search │ │ ├── dao.dart │ │ ├── dao.g.dart │ │ ├── store.dart │ │ ├── store.g.dart │ │ ├── types.dart │ │ ├── types.freezed.dart │ │ └── widgets │ │ │ ├── category_bottom_sheet.dart │ │ │ ├── category_bottom_sheet.g.dart │ │ │ ├── filter.dart │ │ │ ├── filter_bottom_sheet.dart │ │ │ ├── filter_bottom_sheet.g.dart │ │ │ ├── rating_bottom_sheet.dart │ │ │ ├── rating_bottom_sheet.g.dart │ │ │ ├── screen.dart │ │ │ ├── screen.g.dart │ │ │ └── text_field.dart │ ├── session │ │ ├── store.dart │ │ └── store.g.dart │ └── setting │ │ ├── enum_adapter.dart │ │ ├── store.dart │ │ ├── types.dart │ │ └── widgets │ │ ├── confirm_list_tile.dart │ │ ├── confirm_list_tile.g.dart │ │ ├── log_out_confirm.dart │ │ ├── log_out_confirm.g.dart │ │ ├── screen.dart │ │ ├── screen.g.dart │ │ ├── select_list_tile.dart │ │ ├── select_list_tile.g.dart │ │ ├── tab.dart │ │ └── tab.g.dart ├── services │ ├── ehentai.dart │ └── ehentai.freezed.dart ├── tasks │ └── handler.dart └── utils │ ├── cookie.dart │ ├── css.dart │ ├── datetime.dart │ ├── firebase.dart │ ├── http.dart │ ├── key_event.dart │ ├── key_event.g.dart │ ├── launch.dart │ ├── notification.dart │ ├── notification.freezed.dart │ └── string.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── repositories └── fixtures │ ├── gallery.html │ ├── gallery_content_warning.html │ ├── gallery_image_list.html │ ├── gallery_list.json │ ├── gallery_list_incorrect_token.json │ ├── gallery_list_not_found.json │ ├── image.html │ ├── image_without_onerror.html │ ├── index.html │ └── search_no_hit.html └── utils ├── cookie_test.dart ├── css_test.dart ├── datetime_test.dart └── string_test.dart /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EH Redux 2 | 3 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/tommy351/eh-redux)](https://github.com/tommy351/eh-redux/releases) ![Test](https://github.com/tommy351/eh-redux/workflows/Test/badge.svg) ![Build](https://github.com/tommy351/eh-redux/workflows/Build/badge.svg) ![Release](https://github.com/tommy351/eh-redux/workflows/Release/badge.svg) [![codecov](https://codecov.io/gh/tommy351/eh-redux/branch/master/graph/badge.svg)](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 | [![Download link](docs/download-qrcode.png)](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 | ![Gallery list](docs/screenshot-list.png) ![Gallery info](docs/screenshot-info.png) ![Image view](docs/screenshot-view.png) ![Search](docs/screenshot-search.png) ![Download](docs/screenshot-download.png) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | EH Redux (Debug) 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/android/app/src/main/res/drawable-xhdpi/download.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/android/app/src/main/res/drawable-xxxhdpi/download.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/raw/keep.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | EH Redux 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | json_serializable_immutable_collections: 5 | options: 6 | explicit_to_json: true 7 | generate_for: 8 | include: 9 | - lib/** 10 | - test/** 11 | - example/** 12 | json_serializable:json_serializable: 13 | options: 14 | explicit_to_json: true 15 | generate_for: 16 | include: 17 | exclude: 18 | - test/** 19 | - lib/** 20 | - example/** 21 | moor_generator: 22 | options: 23 | generate_connect_constructor: true 24 | -------------------------------------------------------------------------------- /docs/download-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/docs/download-qrcode.png -------------------------------------------------------------------------------- /docs/screenshot-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/docs/screenshot-download.png -------------------------------------------------------------------------------- /docs/screenshot-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/docs/screenshot-info.png -------------------------------------------------------------------------------- /docs/screenshot-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/docs/screenshot-list.png -------------------------------------------------------------------------------- /docs/screenshot-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/docs/screenshot-search.png -------------------------------------------------------------------------------- /docs/screenshot-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/docs/screenshot-view.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommy351/eh-redux/c4bfaf4abee5795da7f649fe2b63597c288c71a1/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/c4bfaf4abee5795da7f649fe2b63597c288c71a1/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | -------------------------------------------------------------------------------- /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/l10n/app_zh.arb: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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.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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/utils/http.dart: -------------------------------------------------------------------------------- 1 | bool isStatusCodeOk(int statusCode) { 2 | return statusCode >= 200 && statusCode < 300; 3 | } 4 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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"]}]} -------------------------------------------------------------------------------- /test/repositories/fixtures/gallery_list_incorrect_token.json: -------------------------------------------------------------------------------- 1 | {"gmetadata":[{"gid":1663615,"error":"Key missing, or incorrect key provided."}]} -------------------------------------------------------------------------------- /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."}]} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------