├── .github └── workflows │ └── deploy.yaml ├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.sample.xml │ │ ├── ic_launcher-playstore.png │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── aninforme │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-anydpi-v24 │ │ │ └── notification_icon.xml │ │ │ ├── drawable-anydpi │ │ │ └── app_icon.xml │ │ │ ├── drawable-hdpi │ │ │ ├── app_icon.png │ │ │ └── notification_icon.png │ │ │ ├── drawable-mdpi │ │ │ ├── app_icon.png │ │ │ └── notification_icon.png │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ ├── app_icon.png │ │ │ └── notification_icon.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── app_icon.png │ │ │ └── notification_icon.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── app_icon.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-mdpi │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xhdpi │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── launcher_icon.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── ic_launcher_background.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── settings_aar.gradle ├── assets ├── google_sign_in.png ├── q.png └── sources-icons │ ├── ann.png │ ├── livechart.png │ ├── mal.png │ └── reddit.png ├── build_script.dart ├── 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 │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── main.dart └── src │ ├── components │ ├── admob │ │ ├── admob.controller.dart │ │ ├── admob.model.dart │ │ └── index.dart │ ├── animelist │ │ ├── animelist.controller.dart │ │ ├── animelist.event.dart │ │ ├── animelist.model.dart │ │ └── index.dart │ ├── cloud-backup │ │ ├── cloud-backup.controller.dart │ │ ├── cloud-backup.events.dart │ │ ├── cloud-backup.model.dart │ │ └── index.dart │ ├── feed │ │ ├── feed.controller.dart │ │ ├── feed.model.dart │ │ └── index.dart │ ├── filter │ │ ├── filter.controller.dart │ │ ├── filter.model.dart │ │ ├── filter.types.dart │ │ └── index.dart │ ├── google-flow │ │ ├── google-flow.controller.dart │ │ ├── google-flow.model.dart │ │ └── index.dart │ ├── index.dart │ ├── integration │ │ ├── index.dart │ │ ├── integration.controller.dart │ │ ├── integration.events.dart │ │ └── integration.model.dart │ ├── notification │ │ ├── index.dart │ │ ├── notification.controller.dart │ │ └── notification.model.dart │ ├── sources │ │ ├── index.dart │ │ ├── source.news.dart │ │ ├── sources.controller.dart │ │ └── sources.model.dart │ ├── supporter-subscription │ │ ├── index.dart │ │ ├── supporter-subscription.controller.dart │ │ └── supporter-subscription.model.dart │ └── topic │ │ ├── index.dart │ │ ├── topic.controller.dart │ │ └── topic.model.dart │ ├── core │ ├── admob_units.dart │ ├── api_key.sample.dart │ ├── config.dart │ ├── controllers.dart │ ├── in-app-purchase.sample.dart │ ├── index.dart │ ├── initializer.dart │ ├── mal.client.sample.dart │ ├── persistence.dart │ ├── services.dart │ └── version.dart │ ├── data │ ├── backup.dart │ ├── feed.response.dart │ ├── firebase.topics.dart │ ├── index.dart │ ├── jwt-token.profile.dart │ ├── mal-token.dart │ ├── mal-user.animelist.dart │ ├── mal-user.animeupdate.dart │ ├── mal-user.profile.dart │ └── response.all_anime.dart │ ├── misc │ ├── date.dart │ ├── feed-icons.dart │ ├── index.dart │ └── keys.dart │ ├── notification │ ├── app_lifecycle_handler.dart │ ├── fcm_handler.dart │ ├── index.dart │ ├── notification.firebase.dart │ └── notification.local.dart │ ├── services │ ├── concretes │ │ ├── api.service.dart │ │ ├── google-api.service.dart │ │ ├── index.dart │ │ └── mal.service.dart │ ├── fcm.service.dart │ ├── index.dart │ ├── interface │ │ ├── api.interface.dart │ │ ├── google-api.interface.dart │ │ ├── index.dart │ │ └── mal.interface.dart │ └── mocks │ │ ├── api.mock-service.dart │ │ ├── google-api.mock-service.dart │ │ ├── index.dart │ │ └── mal.mock-service.dart │ └── widgets │ ├── app.dart │ ├── async-loading.dart │ ├── badge.dart │ ├── button.dart │ ├── colors.dart │ ├── empty.dart │ ├── icon_button.dart │ ├── index.dart │ ├── inputs │ ├── checkbox.dart │ ├── custom.switch.dart │ └── index.dart │ ├── listing │ ├── ad.feed_tab.dart │ ├── ad.library_tab.dart │ ├── ad.sources_tab.dart │ ├── anime.item-integration.dart │ ├── anime.item.dart │ ├── anime.list.dart │ ├── feed.item.dart │ ├── index.dart │ ├── menu-item.dart │ └── news-source.item.dart │ ├── loader.dart │ ├── menu_actions │ ├── filter_bottom_sheet │ │ ├── bottom_menu.display.dart │ │ ├── bottom_menu.order.dart │ │ ├── bottom_menu.sort.dart │ │ ├── bottom_menu.status.dart │ │ ├── bottom_sheet.dart │ │ └── index.dart │ ├── index.dart │ ├── more_page_menu │ │ ├── index.dart │ │ ├── menu.backup-restore.dart │ │ ├── menu.github.dart │ │ ├── menu.licenses.dart │ │ ├── menu.mal-integration.dart │ │ └── menu.support-dev.dart │ ├── news_guide │ │ ├── guide.item.dart │ │ ├── index.dart │ │ └── sources_guide_prompt.dart │ └── refresh_anime_list.dart │ ├── pages │ ├── core.dashboard.dart │ ├── index.dart │ ├── page.feed.dart │ ├── page.library.dart │ ├── page.more.dart │ └── page.sources.dart │ ├── refreshing.dart │ ├── search.dart │ ├── shadow.dart │ ├── syncing │ ├── cloud-backup-restore.dart │ ├── index.dart │ ├── integration.mal.dart │ └── mal-updater.dart │ ├── tabview.dart │ ├── thumbnail.dart │ └── toast.dart ├── pubspec.lock ├── pubspec.yaml └── test └── interface_test.dart /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | if: github.event.base_ref == 'refs/heads/release' # if tag is created in release branch 11 | name: Build APK 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-java@v1 16 | with: 17 | java-version: '12.x' 18 | - run: echo $ANDROID_MANIFEST | base64 -d > android/app/src/main/AndroidManifest.xml 19 | env: 20 | ANDROID_MANIFEST: ${{ secrets.ANDROID_MANIFEST }} 21 | - run: echo $GOOGLE_SERVICES | base64 -d > android/app/google-services.json 22 | env: 23 | GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES }} 24 | - run: echo $SIGNING_KEY | base64 -d > android/app/key.jks 25 | env: 26 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 27 | - run: echo $SIGNING_PROPERTIES | base64 -d > android/key.properties 28 | env: 29 | SIGNING_PROPERTIES: ${{ secrets.SIGNING_PROPERTIES }} 30 | - run: echo $ADMOB_UNITS_CONFIG | base64 -d > lib/src/core/admob_units.dart 31 | env: 32 | ADMOB_UNITS_CONFIG: ${{ secrets.ADMOB_UNITS_CONFIG }} 33 | - run: echo $GOOGLE_PLAY_SERVICE_ACCOUNT | base64 -d > service_account.json 34 | env: 35 | GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }} 36 | - run: echo $IN_APP_PURCHASE_CONFIG | base64 -d > lib/src/core/in-app-purchase.dart 37 | env: 38 | IN_APP_PURCHASE_CONFIG: ${{ secrets.IN_APP_PURCHASE_CONFIG }} 39 | - run: echo $MAL_CLIENT_CONFIG | base64 -d > lib/src/core/mal.client.dart 40 | env: 41 | MAL_CLIENT_CONFIG: ${{ secrets.MAL_CLIENT_CONFIG }} 42 | - run: echo $SERVICES_DART_CODE | base64 -d > lib/src/core/services.dart 43 | env: 44 | SERVICES_DART_CODE: ${{ secrets.SERVICES_DART_CODE }} 45 | - run: echo $API_KEY | base64 -d > lib/src/core/api_key.dart 46 | env: 47 | API_KEY: ${{ secrets.API_KEY }} 48 | - uses: subosito/flutter-action@v1 49 | with: 50 | flutter-version: '2.10.4' 51 | - run: flutter pub get 52 | - run: flutter test 53 | - run: flutter build apk --release --build-name=${GITHUB_REF#refs/*/} --build-number=$GITHUB_RUN_NUMBER --obfuscate --split-debug-info=build/app/outputs/mapping/ 54 | - run: flutter build appbundle --release --build-name=${GITHUB_REF#refs/*/} --build-number=$GITHUB_RUN_NUMBER --obfuscate --split-debug-info=build/app/outputs/mapping/ 55 | - run: dart build_script.dart 56 | - run: mv "build/app/outputs/apk/release/app-release.apk" "build/app/outputs/apk/release/quantz.${GITHUB_REF#refs/*/}.apk" 57 | - name: Upload APK/s 58 | uses: ncipollo/release-action@v1.8.3 59 | with: 60 | artifacts: "build/app/outputs/apk/release/*.apk" 61 | bodyFile: RELEASE_NOTES.md 62 | token: ${{ secrets.GITHUB_TOKEN }} 63 | allowUpdates: true 64 | prerelease: ${{ secrets.IS_PRE_RELEASE == 'true' }} 65 | - name: Upload .aab to Google Play 66 | uses: r0adkll/upload-google-play@v1.0.15 67 | with: 68 | serviceAccountJson: service_account.json 69 | packageName: dev.xamantra.quantz 70 | releaseFiles: build/app/outputs/bundle/release/app-release.aab 71 | track: production 72 | whatsNewDirectory: release_notes/ 73 | mappingFile: build/app/outputs/mapping/release/mapping.txt 74 | -------------------------------------------------------------------------------- /.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 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | 48 | # credentials 49 | google-services.json 50 | key.properties 51 | key.jks 52 | .secrets 53 | *.ignore.* 54 | android/app/src/main/AndroidManifest.xml 55 | ./personal/ 56 | pepk.jar 57 | private_key.pepk 58 | quantz_key.zip 59 | lib/src/core/admob_units.prod.dart 60 | lib/src/core/in-app-purchase.dart 61 | android/app/google-services.prod.json 62 | android/app/google-services.dev.json 63 | lib/src/core/mal.client.dart 64 | android/app/google-services.prev.json 65 | lib/src/core/api_key.dart 66 | -------------------------------------------------------------------------------- /.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: c5a4b4029c0798f37c4a39b479d7cb75daa7b05c 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run - Debug", 9 | "request": "launch", 10 | "type": "dart", 11 | "flutterMode": "debug", 12 | "args": [ 13 | "-v" 14 | ] 15 | }, 16 | { 17 | "name": "Run - Release", 18 | "request": "launch", 19 | "type": "dart", 20 | "flutterMode": "release", 21 | "args": [ 22 | "-v" 23 | ] 24 | }, 25 | { 26 | "name": "Run - Profile", 27 | "request": "launch", 28 | "type": "dart", 29 | "flutterMode": "profile", 30 | "args": [ 31 | "-v" 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | A lot of files in this project are not commited into the repo and you have to set them up properly first. 2 | 3 | - `android/app/src/main/AndroidManifest.xml` - rename the sample file provided in the same path. 4 | - `android/app/google-services.json` - create this from Firebase console. This is for push notification and google sign-in. 5 | - `android/app/key.jks` - apk signing key, [you can create this by yourself](https://developer.android.com/studio/publish/app-signing). 6 | - `android/key.properties` - credentials for the key above. [guide here](https://developer.android.com/studio/publish/app-signing#secure-shared-keystore). 7 | - `lib/src/core/in-app-purchase.dart` - rename the sample file provided in the same path. 8 | - `lib/src/core/mal.client.dart` - rename the sample file provided in the same path. 9 | - `lib/src/core/api_key.dart` - rename the sample file provided in the same path. 10 | 11 | Once you have all the files set up. You can try to run `flutter run` or `flutter build` 12 | 13 | **NOTE**: This flutter app has not been set up for iOS yet. 14 | 15 | # Backend API 16 | The backend for this app is currently closed-source. I provided mock data using mock services in the code. You can test the functions through them instead. 17 | 18 | I will also try to update the mock data from time to time. 19 | 20 | # Push Notifications 21 | You can send push notification through FCM api. 22 | 23 | # Contributions 24 | Since the backend is closed-source, the type of contribution you'll probably be able to do are mostly frontend stuffs. Like design improvements or user-experience. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Quantz

5 |

Push Notification service for anime episodes and news.

6 |

7 | 8 |

9 | 10 | 11 |

12 | GitHub all releases 13 | GitHub all releases 14 | deploy_badge 15 |

16 | 17 | 18 | # Features 19 | - **Sub and dub** - get notified with latest anime episodes on the internet. 20 | - **Ongoing and upcoming** - get notified with latest episode for ongoing series or first episodes of upcoming shows. 21 | - **Chinese series** - this app also gets you notified with them too. 22 | - **News** - anime and manga news updates. 23 | - **Follow/unfollow** - follow or unfollow specific series or news site to get notified. 24 | - **MyAnimeList integration** - if you are a MAL user, this app will make it convenient for you to track episodes. 25 | 26 | # Why this app? 27 | There are two types of episode updates. **Japan TV schedule** and **actual episode uploads** on the internet. This app do the *latter*. 28 | - **Japan TV schedule** - most apps out there do this one. I've been there. I used them and they're not really helpful. I get an episode notification but it's Japan TV schedule and not actual availability on the internet. 29 | - **Actual episodes uploads** - with this app, you get updates based on actual episodes being uploaded on the internet. 30 | 31 | The target users of this app are those anime fans not living in Japan. And that is most of us, I'm pretty sure :) 32 | 33 | This app is inspired by [r/anime Subreddit's Episode BOT](https://www.reddit.com/user/AutoLovepon/) - this bot gets update from legal sites and anime torrent sites. 34 | 35 | # Disclaimer 36 | - This app is not a streaming service. You can't watch anime in here. This is just for quick updates. 37 | - The app does not tell you what website you can find an episode. But instead you can browse list of legal streaming sites from the app. 38 | - This app's server gets an update from a certain pirate site for episode updates **but the app itself does not pirate any content**. 39 | - Only the mobile app is open-source (for now). The backend server is currently closed-source. 40 | 41 | # Develop 42 | Visit the [contributing guide](https://github.com/xamantra/quantz-app/blob/release/CONTRIBUTING.md) to set up the project. 43 | 44 | This app is made with [Flutter](https://flutter.dev/). 45 | 46 | # Support 47 | If you're using the app and liking it so far. Consider supporting the developer by donating from the mobile app through google play in-app purchase. It's a $1 per month subscription. 48 | 49 | By donating, you'll receive push notifications 15 minutes earlier. Non-supporter have 15 minutes delay for push notifications. 50 | 51 | There's also a posibility for me to build and publish an **iOS** version if donations go well. This app's source code is cross-platform. 52 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | - Fixed infinite loading issue when the app is opened for the first time. 2 | - Fixed text overflow with anime title on MAL updater dialog. 3 | - Fixed issue with MAL updater for dub entries. 4 | - Added sub or dub indicator in MAL updater dialog. 5 | - Fixed MAL syncing bug. 6 | - Improve loading speed in splash screen. 7 | - Fixed issue with news feed not properly caching. 8 | - Improved user experience. 9 | - Fixed an issue with ads. 10 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | # key.properties 12 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | ## Gson rules 2 | # Gson uses generic type information stored in a class file when working with fields. Proguard 3 | # removes such information by default, so configure it to keep all of it. 4 | -keepattributes Signature 5 | 6 | # For using GSON @Expose annotation 7 | -keepattributes *Annotation* 8 | 9 | # Gson specific classes 10 | -dontwarn sun.misc.** 11 | #-keep class com.google.gson.stream.** { *; } 12 | 13 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, 14 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) 15 | -keep class * extends com.google.gson.TypeAdapter 16 | -keep class * implements com.google.gson.TypeAdapterFactory 17 | -keep class * implements com.google.gson.JsonSerializer 18 | -keep class * implements com.google.gson.JsonDeserializer 19 | 20 | # Prevent R8 from leaving Data object members always null 21 | -keepclassmembers,allowobfuscation class * { 22 | @com.google.gson.annotations.SerializedName ; 23 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.sample.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 9 | 16 | 20 | 24 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 50 | 51 | 52 | 54 | 57 | 60 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/aninforme/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.xamantra.quantz 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-anydpi-v24/notification_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-anydpi/app_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-hdpi/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-hdpi/notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-mdpi/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-mdpi/notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-xhdpi/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-xhdpi/notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-xxhdpi/app_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-xxhdpi/notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/drawable-xxxhdpi/app_icon.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-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4D79CC 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.3' 10 | classpath 'com.google.gms:google-services:4.3.8' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /assets/google_sign_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/assets/google_sign_in.png -------------------------------------------------------------------------------- /assets/q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/assets/q.png -------------------------------------------------------------------------------- /assets/sources-icons/ann.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/assets/sources-icons/ann.png -------------------------------------------------------------------------------- /assets/sources-icons/livechart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/assets/sources-icons/livechart.png -------------------------------------------------------------------------------- /assets/sources-icons/mal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/assets/sources-icons/mal.png -------------------------------------------------------------------------------- /assets/sources-icons/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/assets/sources-icons/reddit.png -------------------------------------------------------------------------------- /build_script.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | void main() async { 4 | await copyReleaseNotesForGooglePlay(); 5 | } 6 | 7 | const googlePlayReleaseNotesFolder = './release_notes/'; 8 | const googlePlayReleaseNotesFileName = 'whatsnew-en-US'; 9 | const githubReleaseNotesFileName = './RELEASE_NOTES.md'; 10 | 11 | Future copyReleaseNotesForGooglePlay() async { 12 | final contents = await File(githubReleaseNotesFileName).readAsString(); 13 | await Directory(googlePlayReleaseNotesFolder).create(recursive: true); 14 | await File(googlePlayReleaseNotesFolder + googlePlayReleaseNotesFileName).writeAsString(contents); 15 | } 16 | -------------------------------------------------------------------------------- /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 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 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.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /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/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/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 | quantz 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 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import 'src/core/index.dart'; 5 | import 'src/widgets/index.dart'; 6 | 7 | void main() { 8 | WidgetsFlutterBinding.ensureInitialized(); 9 | runApp(momentum()); 10 | } 11 | 12 | Momentum momentum() { 13 | return Momentum( 14 | child: MyApp(), 15 | key: UniqueKey(), 16 | appLoader: Loader(), 17 | controllers: controllers(), 18 | services: services(), 19 | initializer: initializer, 20 | persistSave: persistSave, 21 | persistGet: persistGet, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/components/admob/admob.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import 'index.dart'; 5 | 6 | class AdmobModel extends MomentumModel { 7 | AdmobModel( 8 | AdmobController controller, { 9 | required this.libraryTabAd, 10 | required this.showLibraryTabAd, 11 | required this.sourcesTabAd, 12 | required this.showSourcesTabAd, 13 | required this.feedTabAd, 14 | required this.showFeedTabAd, 15 | required this.lastTimeUserClickedAnAd, 16 | }) : super(controller); 17 | 18 | final BannerAd libraryTabAd; 19 | final bool showLibraryTabAd; 20 | 21 | final BannerAd sourcesTabAd; 22 | final bool showSourcesTabAd; 23 | 24 | final BannerAd feedTabAd; 25 | final bool showFeedTabAd; 26 | 27 | final int lastTimeUserClickedAnAd; 28 | 29 | @override 30 | void update({ 31 | BannerAd? libraryTabAd, 32 | bool? showLibraryTabAd, 33 | BannerAd? sourcesTabAd, 34 | bool? showSourcesTabAd, 35 | BannerAd? feedTabAd, 36 | bool? showFeedTabAd, 37 | int? lastTimeUserClickedAnAd, 38 | }) { 39 | AdmobModel( 40 | controller, 41 | libraryTabAd: libraryTabAd ?? this.libraryTabAd, 42 | showLibraryTabAd: showLibraryTabAd ?? this.showLibraryTabAd, 43 | sourcesTabAd: sourcesTabAd ?? this.sourcesTabAd, 44 | showSourcesTabAd: showSourcesTabAd ?? this.showSourcesTabAd, 45 | feedTabAd: feedTabAd ?? this.feedTabAd, 46 | showFeedTabAd: showFeedTabAd ?? this.showFeedTabAd, 47 | lastTimeUserClickedAnAd: lastTimeUserClickedAnAd ?? this.lastTimeUserClickedAnAd, 48 | ).updateMomentum(); 49 | } 50 | 51 | Map toJson() { 52 | return { 53 | "lastTimeUserClickedAnAd": lastTimeUserClickedAnAd, 54 | }; 55 | } 56 | 57 | AdmobModel? fromJson(Map? json) { 58 | if (json == null) return null; 59 | return AdmobModel( 60 | controller, 61 | libraryTabAd: libraryTabAd, 62 | showLibraryTabAd: showLibraryTabAd, 63 | sourcesTabAd: sourcesTabAd, 64 | showSourcesTabAd: showSourcesTabAd, 65 | feedTabAd: feedTabAd, 66 | showFeedTabAd: showFeedTabAd, 67 | lastTimeUserClickedAnAd: json['lastTimeUserClickedAnAd'] ?? 0, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/components/admob/index.dart: -------------------------------------------------------------------------------- 1 | export 'admob.controller.dart'; 2 | export 'admob.model.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/components/animelist/animelist.event.dart: -------------------------------------------------------------------------------- 1 | class AnimelistEvent { 2 | AnimelistEvent(this.followingCount); 3 | 4 | final int followingCount; 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/components/animelist/index.dart: -------------------------------------------------------------------------------- 1 | export 'animelist.controller.dart'; 2 | export 'animelist.event.dart'; 3 | export 'animelist.model.dart'; 4 | export 'index.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/components/cloud-backup/cloud-backup.controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:momentum/momentum.dart'; 4 | 5 | import '../../data/index.dart'; 6 | import '../../misc/index.dart'; 7 | import '../../services/interface/api.interface.dart'; 8 | import '../../services/interface/google-api.interface.dart'; 9 | import '../../widgets/index.dart'; 10 | import '../animelist/index.dart'; 11 | import '../sources/index.dart'; 12 | import 'index.dart'; 13 | 14 | class CloudBackupController extends MomentumController { 15 | @override 16 | CloudBackupModel init() { 17 | return CloudBackupModel( 18 | this, 19 | latestBackupInfo: CloudBackup(), 20 | loading: false, 21 | ); 22 | } 23 | 24 | ApiInterface get api => service(runtimeType: false); 25 | GoogleApiInterface get google => service(runtimeType: false); 26 | 27 | Future initialize() async { 28 | if (model.signedIn) { 29 | model.update(loading: true); 30 | final latestBackupInfo = await api.fetchBackup(token: model.token, includeData: false); 31 | model.update(latestBackupInfo: latestBackupInfo, loading: false); 32 | } 33 | } 34 | 35 | Future triggerCloudBackupPrompt() async { 36 | model.update(loading: true); 37 | final signedIn = await google.isSignedIn(); 38 | if (signedIn) { 39 | sendEvent(CloudbackupEvents.alreadySignedIn); 40 | } 41 | model.update(loading: false); 42 | return signedIn; 43 | } 44 | 45 | Future startNewBackup() async { 46 | if (model.signedIn) { 47 | final animeListState = controller().model.toBackup(); 48 | final sourcesState = controller().model.toJson(); 49 | 50 | final data = BackupData( 51 | animeListState: animeListState, 52 | sourcesState: sourcesState, 53 | ).toRawJson(); 54 | 55 | final result = await api.newBackup(token: model.token, data: data); 56 | model.update(latestBackupInfo: result); 57 | } else { 58 | showToast('Please sign in with google first', error: true); 59 | } 60 | } 61 | 62 | Future restoreFromLatest() async { 63 | if (model.signedIn) { 64 | try { 65 | final backupData = await api.fetchBackup(token: model.token); 66 | 67 | if (backupData.data.isNotEmpty) { 68 | /* Parsing */ 69 | final json = jsonDecode(backupData.data); 70 | final animeListState = jsonEncode(json[ANIMELIST_STATE_KEY]); 71 | final sourcesState = jsonEncode(json[SOURCES_STATE_KEY]); 72 | /* Parsing */ 73 | 74 | /* Restoration */ 75 | await controller().restoreFromBackup(sourcesState); 76 | await controller().restoreFromBackup(animeListState); 77 | model.update(lastRestore: DateTime.now()); 78 | /* Restoration */ 79 | } 80 | } catch (e) { 81 | showToast(e.toString(), error: true); 82 | } 83 | } else { 84 | showToast('Please sign in with google first', error: true); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/components/cloud-backup/cloud-backup.events.dart: -------------------------------------------------------------------------------- 1 | enum CloudbackupEvents { 2 | alreadySignedIn, 3 | startNewBackup, 4 | restoreFromLatest, 5 | logoutGoogle, 6 | none, 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/components/cloud-backup/cloud-backup.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | import '../google-flow/google-flow.controller.dart'; 3 | 4 | import '../../data/index.dart'; 5 | import 'index.dart'; 6 | 7 | class CloudBackupModel extends MomentumModel { 8 | CloudBackupModel( 9 | CloudBackupController controller, { 10 | required this.latestBackupInfo, 11 | required this.loading, 12 | this.lastRestore, 13 | }) : super(controller); 14 | 15 | final CloudBackup latestBackupInfo; 16 | final DateTime? lastRestore; 17 | final bool loading; 18 | 19 | bool get hasLatestBackup => latestBackupInfo.updatedAt != null; 20 | bool get hasLastRestored => lastRestore != null; 21 | 22 | bool get signedIn => controller.controller().model.signedIn; 23 | String get token => controller.controller().model.token; 24 | 25 | @override 26 | void update({ 27 | CloudBackup? latestBackupInfo, 28 | DateTime? lastRestore, 29 | bool? loading, 30 | }) { 31 | CloudBackupModel( 32 | controller, 33 | latestBackupInfo: latestBackupInfo ?? this.latestBackupInfo, 34 | lastRestore: lastRestore ?? this.lastRestore, 35 | loading: loading ?? this.loading, 36 | ).updateMomentum(); 37 | } 38 | 39 | void modifyLastRestore({ 40 | DateTime? lastRestore, 41 | }) { 42 | CloudBackupModel( 43 | controller, 44 | latestBackupInfo: this.latestBackupInfo, 45 | lastRestore: lastRestore, 46 | loading: false, 47 | ).updateMomentum(); 48 | } 49 | 50 | Map toJson() { 51 | return { 52 | 'lastRestore': lastRestore?.millisecondsSinceEpoch, 53 | }; 54 | } 55 | 56 | CloudBackupModel? fromJson(Map? map) { 57 | if (map == null) return null; 58 | return CloudBackupModel( 59 | controller, 60 | latestBackupInfo: CloudBackup(), 61 | lastRestore: map['lastRestore'] == null ? null : DateTime.fromMillisecondsSinceEpoch(map['lastRestore']), 62 | loading: false, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/components/cloud-backup/index.dart: -------------------------------------------------------------------------------- 1 | export 'cloud-backup.controller.dart'; 2 | export 'cloud-backup.events.dart'; 3 | export 'cloud-backup.model.dart'; 4 | export 'index.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/components/feed/feed.controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | import '../../services/interface/api.interface.dart'; 3 | 4 | import '../../data/feed.response.dart'; 5 | import 'index.dart'; 6 | 7 | class FeedController extends MomentumController { 8 | @override 9 | FeedModel init() { 10 | return FeedModel( 11 | this, 12 | feed: QuantzFeed(), 13 | loading: false, 14 | ); 15 | } 16 | 17 | ApiInterface get api => service(runtimeType: false); 18 | 19 | void bootstrap() async { 20 | loadInitial(); 21 | } 22 | 23 | Future loadInitial({bool refresh = false}) async { 24 | if (!refresh) model.update(loading: true); 25 | 26 | final feed = await api.getLatestFeed(); 27 | var feedItems = feed.items; 28 | if (feedItems.isNotEmpty) { 29 | feedItems.sort((a, b) => b.utcTimestampSeconds.compareTo(a.utcTimestampSeconds)); 30 | model.update(feed: feed.copyWith(items: feedItems), loading: false); 31 | } else { 32 | model.update(loading: false); 33 | } 34 | } 35 | 36 | Future loadMore() async { 37 | final feed = await api.getLatestFeed(page: model.feed.page + 1); 38 | var feedItems = List.from(model.feed.items)..addAll(feed.items); 39 | if (feedItems.isNotEmpty) { 40 | feedItems = feedItems.toSet().toList(); 41 | feedItems.sort((a, b) => b.utcTimestampSeconds.compareTo(a.utcTimestampSeconds)); 42 | model.update(feed: feed.copyWith(items: feedItems)); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/components/feed/feed.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../../data/feed.response.dart'; 4 | import 'index.dart'; 5 | 6 | class FeedModel extends MomentumModel { 7 | FeedModel( 8 | FeedController controller, { 9 | required this.feed, 10 | required this.loading, 11 | }) : super(controller); 12 | 13 | final QuantzFeed feed; 14 | final bool loading; 15 | 16 | @override 17 | void update({ 18 | QuantzFeed? feed, 19 | bool? loading, 20 | }) { 21 | FeedModel( 22 | controller, 23 | feed: feed ?? this.feed, 24 | loading: loading ?? this.loading, 25 | ).updateMomentum(); 26 | } 27 | 28 | Map toJson() { 29 | return { 30 | "feed": feed.toJson(), 31 | "loading": false, 32 | }; 33 | } 34 | 35 | FeedModel? fromJson(Map? map) { 36 | if (map == null) return null; 37 | 38 | return FeedModel( 39 | controller, 40 | feed: QuantzFeed.fromJson(map['feed']), 41 | loading: false, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/components/feed/index.dart: -------------------------------------------------------------------------------- 1 | export 'feed.controller.dart'; 2 | export 'feed.model.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/components/filter/filter.controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../../data/index.dart'; 4 | import '../animelist/index.dart'; 5 | import 'index.dart'; 6 | 7 | class FilterController extends MomentumController { 8 | @override 9 | FilterModel init() { 10 | return FilterModel( 11 | this, 12 | displayTitle: DisplayTitle.defaultTitle, 13 | sortBy: SortBy.desc, 14 | orderBy: OrderBy.episodeRelease, 15 | showOngoing: true, 16 | showUpcoming: true, 17 | ); 18 | } 19 | 20 | AnimelistController get animeListCtrl => controller(); 21 | 22 | void setDisplayTitle(DisplayTitle displayTitle) { 23 | model.update(displayTitle: displayTitle); 24 | animeListCtrl.flagEntries(); 25 | animeListCtrl.arrangeList(); 26 | animeListCtrl.separateList(); 27 | } 28 | 29 | void setOrderBy(OrderBy orderBy) { 30 | model.update(orderBy: orderBy); 31 | animeListCtrl.flagEntries(); 32 | animeListCtrl.arrangeList(); 33 | animeListCtrl.separateList(); 34 | } 35 | 36 | void setSortBy(SortBy sortBy) { 37 | model.update(sortBy: sortBy); 38 | animeListCtrl.arrangeList(); 39 | animeListCtrl.separateList(); 40 | } 41 | 42 | int compare(AnimeEntry a, AnimeEntry b) { 43 | switch (model.sortBy) { 44 | case SortBy.asc: 45 | return _compareOrder(a, b); 46 | case SortBy.desc: 47 | return _compareOrder(b, a); 48 | } 49 | } 50 | 51 | int _compareOrder(AnimeEntry x, AnimeEntry y) { 52 | switch (model.orderBy) { 53 | case OrderBy.title: 54 | return _compareTitle(x, y); 55 | case OrderBy.episodeCount: 56 | return x.latestEpisode.compareTo(y.latestEpisode); 57 | case OrderBy.episodeRelease: 58 | return x.episodeTimestamp.compareTo(y.episodeTimestamp); 59 | case OrderBy.popularity: 60 | return x.malTotalUsers.compareTo(y.malTotalUsers); 61 | case OrderBy.score: 62 | return x.malScore.compareTo(y.malScore); 63 | } 64 | } 65 | 66 | int _compareTitle(AnimeEntry x, AnimeEntry y) { 67 | switch (model.displayTitle) { 68 | case DisplayTitle.defaultTitle: 69 | return x.title.compareTo(y.title); 70 | case DisplayTitle.english: 71 | return x.malTitleEnglish.compareTo(y.malTitleEnglish); 72 | case DisplayTitle.japanese: 73 | return x.malTitleJapanese.compareTo(y.malTitleJapanese); 74 | } 75 | } 76 | 77 | void toggleOngoing() { 78 | model.update(showOngoing: !model.showOngoing); 79 | animeListCtrl.arrangeList(); 80 | animeListCtrl.separateList(); 81 | } 82 | 83 | void toggleUpcoming() { 84 | model.update(showUpcoming: !model.showUpcoming); 85 | animeListCtrl.arrangeList(); 86 | animeListCtrl.separateList(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/components/filter/filter.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class FilterModel extends MomentumModel { 6 | FilterModel( 7 | FilterController controller, { 8 | required this.displayTitle, 9 | required this.sortBy, 10 | required this.orderBy, 11 | required this.showOngoing, 12 | required this.showUpcoming, 13 | }) : super(controller); 14 | 15 | final DisplayTitle displayTitle; 16 | final SortBy sortBy; 17 | final OrderBy orderBy; 18 | 19 | final bool showOngoing; 20 | final bool showUpcoming; 21 | 22 | @override 23 | void update({ 24 | DisplayTitle? displayTitle, 25 | SortBy? sortBy, 26 | OrderBy? orderBy, 27 | bool? showOngoing, 28 | bool? showUpcoming, 29 | }) { 30 | FilterModel( 31 | controller, 32 | displayTitle: displayTitle ?? this.displayTitle, 33 | sortBy: sortBy ?? this.sortBy, 34 | orderBy: orderBy ?? this.orderBy, 35 | showOngoing: showOngoing ?? this.showOngoing, 36 | showUpcoming: showUpcoming ?? this.showUpcoming, 37 | ).updateMomentum(); 38 | } 39 | 40 | Map toJson() { 41 | return { 42 | 'displayTitle': DisplayTitle.values.indexOf(displayTitle), 43 | 'sortBy': SortBy.values.indexOf(SortBy.desc), 44 | 'orderBy': OrderBy.values.indexOf(OrderBy.episodeRelease), 45 | 'showOngoing': true, 46 | 'showUpcoming': true, 47 | }; 48 | } 49 | 50 | FilterModel? fromJson(Map? map) { 51 | if (map == null) { 52 | return null; 53 | } 54 | return FilterModel( 55 | controller, 56 | displayTitle: DisplayTitle.values[map['displayTitle']], 57 | sortBy: SortBy.values[map['sortBy']], 58 | orderBy: OrderBy.values[map['orderBy']], 59 | showOngoing: true, 60 | showUpcoming: true, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/components/filter/filter.types.dart: -------------------------------------------------------------------------------- 1 | enum DisplayTitle { 2 | defaultTitle, 3 | english, 4 | japanese, 5 | } 6 | 7 | String displayTitleLabel(DisplayTitle displayTitle) { 8 | switch (displayTitle) { 9 | case DisplayTitle.defaultTitle: 10 | return 'Default'; 11 | case DisplayTitle.english: 12 | return 'English'; 13 | case DisplayTitle.japanese: 14 | return 'Japanese'; 15 | } 16 | } 17 | 18 | enum OrderBy { 19 | title, 20 | episodeCount, 21 | episodeRelease, 22 | popularity, 23 | score, 24 | } 25 | 26 | String orderByLabel(OrderBy orderBy) { 27 | switch (orderBy) { 28 | case OrderBy.title: 29 | return 'Title'; 30 | case OrderBy.episodeCount: 31 | return 'Episode Number'; 32 | case OrderBy.episodeRelease: 33 | return 'Episode Release'; 34 | case OrderBy.popularity: 35 | return 'Popularity'; 36 | case OrderBy.score: 37 | return 'Score'; 38 | } 39 | } 40 | 41 | enum SortBy { 42 | asc, 43 | desc, 44 | } 45 | 46 | String sortByLabel(SortBy sortBy) { 47 | switch (sortBy) { 48 | case SortBy.asc: 49 | return 'Ascending'; 50 | case SortBy.desc: 51 | return 'Descending'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/components/filter/index.dart: -------------------------------------------------------------------------------- 1 | export 'filter.controller.dart'; 2 | export 'filter.model.dart'; 3 | export 'filter.types.dart'; 4 | export 'index.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/components/google-flow/google-flow.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../../data/index.dart'; 4 | import 'index.dart'; 5 | 6 | class GoogleFlowModel extends MomentumModel { 7 | GoogleFlowModel( 8 | GoogleFlowController controller, { 9 | required this.token, 10 | required this.profile, 11 | }) : super(controller); 12 | 13 | final String token; 14 | final JwtTokenProfile profile; 15 | 16 | bool get signedIn => token.isNotEmpty; 17 | String get emailObscure { 18 | final e = profile.email; 19 | if (e.isNotEmpty) { 20 | final username = e.substring(0, e.indexOf('@')); 21 | final domain = e.replaceAll('$username@', ''); 22 | final firstLetter = username[0]; 23 | final lastLetter = username[username.length - 1]; 24 | return '$firstLetter********$lastLetter@$domain'; 25 | } 26 | return ''; 27 | } 28 | 29 | @override 30 | void update({ 31 | String? token, 32 | JwtTokenProfile? profile, 33 | }) { 34 | GoogleFlowModel( 35 | controller, 36 | token: token ?? this.token, 37 | profile: profile ?? this.profile, 38 | ).updateMomentum(); 39 | } 40 | 41 | Map toJson() { 42 | return { 43 | 'token': token, 44 | }; 45 | } 46 | 47 | GoogleFlowModel? fromJson(Map? map) { 48 | if (map == null) return null; 49 | return GoogleFlowModel( 50 | controller, 51 | token: map['token'], 52 | profile: JwtTokenProfile(), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/components/google-flow/index.dart: -------------------------------------------------------------------------------- 1 | export 'google-flow.controller.dart'; 2 | export 'google-flow.model.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/components/index.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantz-dev/quantz-app/690abc2d76edebd9e2cb3792707e27a8e5ebac87/lib/src/components/index.dart -------------------------------------------------------------------------------- /lib/src/components/integration/index.dart: -------------------------------------------------------------------------------- 1 | export 'integration.controller.dart'; 2 | export 'integration.events.dart'; 3 | export 'integration.model.dart'; 4 | export 'index.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/components/integration/integration.events.dart: -------------------------------------------------------------------------------- 1 | enum IntegrationEvents { 2 | done, 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/components/integration/integration.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../../data/index.dart'; 4 | import '../../data/mal-user.animelist.dart'; 5 | import 'index.dart'; 6 | 7 | class IntegrationModel extends MomentumModel { 8 | IntegrationModel( 9 | IntegrationController controller, { 10 | required this.loading, 11 | required this.malList, 12 | required this.toFollow, 13 | required this.toUnfollow, 14 | required this.malUsername, 15 | required this.syncSub, 16 | required this.syncDub, 17 | required this.malUserAnimeListCache, 18 | required this.statProgress, 19 | required this.statToImport, 20 | }) : super(controller); 21 | 22 | final bool loading; 23 | final List malList; 24 | final List toFollow; 25 | final List toUnfollow; 26 | final String malUsername; 27 | final bool syncSub; 28 | final bool syncDub; 29 | 30 | final List malUserAnimeListCache; 31 | 32 | final int statProgress; 33 | final int statToImport; 34 | 35 | bool get loggedIn => controller.mal.loggedIn; 36 | 37 | @override 38 | void update({ 39 | bool? loading, 40 | List? malList, 41 | List? toFollow, 42 | List? toUnfollow, 43 | String? malUsername, 44 | bool? syncSub, 45 | bool? syncDub, 46 | int? statProgress, 47 | int? statToImport, 48 | List? malUserAnimeListCache, 49 | }) { 50 | IntegrationModel( 51 | controller, 52 | loading: loading ?? this.loading, 53 | malList: malList ?? this.malList, 54 | toFollow: toFollow ?? this.toFollow, 55 | toUnfollow: toUnfollow ?? this.toUnfollow, 56 | malUsername: malUsername ?? this.malUsername, 57 | syncSub: syncSub ?? this.syncSub, 58 | syncDub: syncDub ?? this.syncDub, 59 | malUserAnimeListCache: malUserAnimeListCache ?? this.malUserAnimeListCache, 60 | statProgress: statProgress ?? this.statProgress, 61 | statToImport: statToImport ?? this.statToImport, 62 | ).updateMomentum(); 63 | } 64 | 65 | Map toJson() { 66 | return { 67 | 'syncSub': syncSub, 68 | 'syncDub': syncDub, 69 | 'malUsername': malUsername, 70 | 'malUserAnimeListCache': malUserAnimeListCache.map((e) => e.toJson()).toList(), 71 | }; 72 | } 73 | 74 | IntegrationModel? fromJson(Map? map) { 75 | if (map == null) return null; 76 | return IntegrationModel( 77 | controller, 78 | loading: false, 79 | malList: [], 80 | toFollow: [], 81 | toUnfollow: [], 82 | malUsername: map['malUsername'] ?? '', 83 | syncSub: map['syncSub'], 84 | syncDub: map['syncDub'], 85 | malUserAnimeListCache: map['malUserAnimeListCache'] == null ? [] : List.from((map['malUserAnimeListCache'] as List).map((e) => MalUserAnimeItem.fromJson(e))), 86 | statProgress: statProgress, 87 | statToImport: statToImport, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/components/notification/index.dart: -------------------------------------------------------------------------------- 1 | export 'notification.controller.dart'; 2 | export 'notification.model.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/components/notification/notification.controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_messaging/firebase_messaging.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:momentum/momentum.dart'; 4 | import 'package:url_launcher/url_launcher.dart'; 5 | 6 | import '../../notification/index.dart'; 7 | import '../../services/index.dart'; 8 | import '../../widgets/index.dart'; 9 | import 'index.dart'; 10 | 11 | class NotificationController extends MomentumController { 12 | @override 13 | NotificationModel init() { 14 | return NotificationModel( 15 | this, 16 | loading: false, 17 | permalinks: [], 18 | ); 19 | } 20 | 21 | FirebaseMessaging? _messaging; 22 | FirebaseMessaging get messaging => _messaging!; 23 | 24 | @override 25 | void onReady() async { 26 | await waitForFirebaseInit(); 27 | _messaging = FirebaseMessaging.instance; 28 | var message = await _messaging!.getInitialMessage(); 29 | if (message != null) { 30 | service().setMessage(message); 31 | } 32 | openNotification(); 33 | } 34 | 35 | void markPermalinkAsVisited(String permalink) { 36 | var links = List.from(model.permalinks); 37 | links.add(permalink); // mark the permalink as visited. 38 | links = links.toSet().toList(); 39 | model.update(permalinks: links); 40 | } 41 | 42 | bool isVisited(String permalink) { 43 | return model.permalinks.any((x) => x == permalink); 44 | } 45 | 46 | /// Try to open permalink if there's a pending notification click result 47 | Future openNotification() async { 48 | model.update(loading: true); 49 | try { 50 | var message = service().message; 51 | try { 52 | if (message != null) { 53 | try { 54 | var permalink = message.data['permalink'] as String; 55 | await _openPermalink(permalink); 56 | } catch (e) { 57 | showToast(e.toString(), error: true); 58 | } 59 | } 60 | } catch (e) { 61 | showToast(e.toString(), error: true); 62 | } 63 | } catch (e, trace) { 64 | print(trace); 65 | showToast(e.toString(), error: true); 66 | } 67 | model.update(loading: false); 68 | } 69 | 70 | Future _openPermalink(String permalink) async { 71 | try { 72 | final visited = isVisited(permalink); 73 | if (visited) { 74 | service().clear(); 75 | return; 76 | } 77 | await launch(permalink); 78 | markPermalinkAsVisited(permalink); 79 | SystemChannels.platform.invokeMethod('SystemNavigator.pop'); 80 | } catch (e) { 81 | showToast('Unable to open notification.', error: true); 82 | } 83 | service().clear(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/components/notification/notification.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class NotificationModel extends MomentumModel { 6 | NotificationModel( 7 | NotificationController controller, { 8 | required this.loading, 9 | required this.permalinks, 10 | }) : super(controller); 11 | 12 | final bool loading; 13 | final List permalinks; 14 | 15 | @override 16 | void update({ 17 | bool? loading, 18 | List? permalinks, 19 | }) { 20 | NotificationModel( 21 | controller, 22 | loading: loading ?? this.loading, 23 | permalinks: permalinks ?? this.permalinks, 24 | ).updateMomentum(); 25 | } 26 | 27 | Map toJson() { 28 | return { 29 | 'permalinks': permalinks, 30 | }; 31 | } 32 | 33 | NotificationModel? fromJson(Map? json) { 34 | if (json == null) return null; 35 | 36 | return NotificationModel( 37 | controller, 38 | loading: false, 39 | permalinks: List.from(json['permalinks']), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/components/sources/index.dart: -------------------------------------------------------------------------------- 1 | export 'index.dart'; 2 | export 'sources.controller.dart'; 3 | export 'sources.model.dart'; 4 | export 'source.news.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/components/sources/source.news.dart: -------------------------------------------------------------------------------- 1 | class NewsSource { 2 | const NewsSource({ 3 | required this.name, 4 | required this.iconAssetPath, 5 | required this.firebaseTopic, 6 | required this.following, 7 | required this.links, 8 | }); 9 | 10 | final String name; 11 | final String iconAssetPath; 12 | final String firebaseTopic; 13 | final bool following; 14 | final List links; 15 | 16 | Map toMap() { 17 | return { 18 | 'name': name, 19 | 'iconAssetPath': iconAssetPath, 20 | 'firebaseTopic': firebaseTopic, 21 | 'following': following, 22 | 'links': links.map((x) => x.toMap()).toList(), 23 | }; 24 | } 25 | 26 | factory NewsSource.fromMap(Map map) { 27 | return NewsSource( 28 | name: map['name'], 29 | iconAssetPath: map['iconAssetPath'], 30 | firebaseTopic: map['firebaseTopic'], 31 | following: map['following'], 32 | links: List.from(map['links']?.map((x) => NewsSourceLink.fromMap(x))), 33 | ); 34 | } 35 | 36 | NewsSource copyWith({ 37 | String? name, 38 | String? iconAssetPath, 39 | String? firebaseTopic, 40 | bool? following, 41 | List? links, 42 | }) { 43 | return NewsSource( 44 | name: name ?? this.name, 45 | iconAssetPath: iconAssetPath ?? this.iconAssetPath, 46 | firebaseTopic: firebaseTopic ?? this.firebaseTopic, 47 | following: following ?? this.following, 48 | links: links ?? this.links, 49 | ); 50 | } 51 | } 52 | 53 | class NewsSourceLink { 54 | const NewsSourceLink(this.name, this.url); 55 | 56 | final String name; 57 | final String url; 58 | 59 | Map toMap() { 60 | return { 61 | 'name': name, 62 | 'url': url, 63 | }; 64 | } 65 | 66 | factory NewsSourceLink.fromMap(Map map) { 67 | return NewsSourceLink( 68 | map['name'], 69 | map['url'], 70 | ); 71 | } 72 | } 73 | 74 | const sourcesList = [ 75 | const NewsSource( 76 | name: 'AnimeNewsNetwork', 77 | iconAssetPath: 'assets/sources-icons/ann.png', 78 | firebaseTopic: 'anime_news_network', 79 | following: false, 80 | links: [ 81 | const NewsSourceLink('Homepage', 'https://www.animenewsnetwork.com/'), 82 | ], 83 | ), 84 | const NewsSource( 85 | name: 'Livechart Headlines', 86 | iconAssetPath: 'assets/sources-icons/livechart.png', 87 | firebaseTopic: 'livechart_headlines', 88 | following: false, 89 | links: [ 90 | const NewsSourceLink('Recent Anime Headlines', 'https://www.livechart.me/headlines'), 91 | ], 92 | ), 93 | const NewsSource( 94 | name: 'MyAnimeList', 95 | iconAssetPath: 'assets/sources-icons/mal.png', 96 | firebaseTopic: 'my_anime_list', 97 | following: false, 98 | links: [ 99 | const NewsSourceLink('All News', 'https://myanimelist.net/news'), 100 | const NewsSourceLink('New Anime', 'https://myanimelist.net/news/tag/new_anime'), 101 | ], 102 | ), 103 | const NewsSource( 104 | name: 'r/anime - Subreddit', 105 | iconAssetPath: 'assets/sources-icons/reddit.png', 106 | firebaseTopic: 'r_anime_feed', 107 | following: false, 108 | links: [ 109 | const NewsSourceLink('News', 'https://www.reddit.com/r/anime?f=flair_name:"News"'), 110 | const NewsSourceLink('Official Media', 'https://www.reddit.com/r/anime?f=flair_name:"Official Media"'), 111 | ], 112 | ), 113 | ]; 114 | -------------------------------------------------------------------------------- /lib/src/components/sources/sources.controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:firebase_messaging/firebase_messaging.dart'; 4 | import 'package:momentum/momentum.dart'; 5 | 6 | import '../../misc/index.dart'; 7 | import '../../notification/index.dart'; 8 | import 'index.dart'; 9 | 10 | class SourcesController extends MomentumController { 11 | @override 12 | SourcesModel init() { 13 | return SourcesModel( 14 | this, 15 | initialized: false, 16 | loading: false, 17 | sourcesSubscriptionList: sourcesList, 18 | ); 19 | } 20 | 21 | String get persistenceKey => SOURCES_STATE_KEY; 22 | 23 | FirebaseMessaging? _messaging; 24 | FirebaseMessaging get messaging => _messaging!; 25 | 26 | @override 27 | void onReady() async { 28 | await waitForFirebaseInit(); 29 | _messaging = FirebaseMessaging.instance; 30 | if (!model.initialized) { 31 | model.update(loading: true); 32 | await toggleNews(model.sourcesSubscriptionList[0], true); 33 | model.update(loading: false, initialized: true); 34 | } 35 | syncNewsSources(); 36 | } 37 | 38 | void syncNewsSources() { 39 | var list = List.from(model.sourcesSubscriptionList); 40 | for (var i = 0; i < sourcesList.length; i++) { 41 | var exist = list.any((x) => x.name == sourcesList[i].name); 42 | if (!exist) { 43 | list.insert(i, sourcesList[i]); 44 | } else { 45 | var current = list.firstWhere((x) => x.name == sourcesList[i].name); 46 | list.replaceRange(i, i + 1, [sourcesList[i].copyWith(following: current.following)]); 47 | } 48 | } 49 | model.update(sourcesSubscriptionList: list); 50 | } 51 | 52 | Future toggleNews(NewsSource source, bool state) async { 53 | model.update(loading: true); 54 | try { 55 | var list = List.from(model.sourcesSubscriptionList); 56 | var updated = source.copyWith(following: state); 57 | var index = list.indexWhere((x) => x.firebaseTopic == source.firebaseTopic); 58 | if (state) { 59 | await messaging.subscribeToTopic(source.firebaseTopic); 60 | } else { 61 | await messaging.unsubscribeFromTopic(source.firebaseTopic); 62 | } 63 | list.removeAt(index); 64 | list.insert(index, updated); 65 | model.update(sourcesSubscriptionList: list); 66 | } catch (e) { 67 | print(e); 68 | } 69 | model.update(loading: false); 70 | } 71 | 72 | Future restoreFromBackup(String source) async { 73 | var json = jsonDecode(source); 74 | var backup = model.fromJson(json); 75 | final sources = backup?.sourcesSubscriptionList ?? []; 76 | for (var item in sources) { 77 | await toggleNews(item, item.following); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/components/sources/sources.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class SourcesModel extends MomentumModel { 6 | SourcesModel( 7 | SourcesController controller, { 8 | required this.initialized, 9 | required this.loading, 10 | required this.sourcesSubscriptionList, 11 | }) : super(controller); 12 | 13 | final bool initialized; 14 | final bool loading; 15 | final List sourcesSubscriptionList; 16 | 17 | @override 18 | void update({ 19 | bool? initialized, 20 | bool? loading, 21 | List? sourcesSubscriptionList, 22 | }) { 23 | SourcesModel( 24 | controller, 25 | initialized: initialized ?? this.initialized, 26 | loading: loading ?? this.loading, 27 | sourcesSubscriptionList: sourcesSubscriptionList ?? this.sourcesSubscriptionList, 28 | ).updateMomentum(); 29 | } 30 | 31 | Map toJson() { 32 | return { 33 | 'initialized': initialized, 34 | 'sourcesSubscriptionList': sourcesSubscriptionList.map((x) => x.toMap()).toList(), 35 | }; 36 | } 37 | 38 | SourcesModel? fromJson(Map? json) { 39 | if (json == null) return null; 40 | return SourcesModel( 41 | controller, 42 | initialized: json['initialized'], 43 | loading: false, 44 | sourcesSubscriptionList: List.from(json['sourcesSubscriptionList']?.map((x) => NewsSource.fromMap(x))), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/components/supporter-subscription/index.dart: -------------------------------------------------------------------------------- 1 | export 'supporter-subscription.controller.dart'; 2 | export 'supporter-subscription.model.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/components/supporter-subscription/supporter-subscription.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class SupporterSubscriptionModel extends MomentumModel { 6 | SupporterSubscriptionModel( 7 | SupporterSubscriptionController controller, { 8 | required this.loading, 9 | required this.purchaseIsPending, 10 | required this.purchaseError, 11 | required this.subscriptionActive, 12 | required this.storeIsAvailable, 13 | }) : super(controller); 14 | 15 | final bool loading; 16 | final bool purchaseIsPending; 17 | final String purchaseError; 18 | final bool subscriptionActive; 19 | final bool storeIsAvailable; 20 | 21 | @override 22 | void update({ 23 | bool? loading, 24 | bool? purchaseIsPending, 25 | String? purchaseError, 26 | bool? subscriptionActive, 27 | bool? storeIsAvailable, 28 | }) { 29 | SupporterSubscriptionModel( 30 | controller, 31 | loading: loading ?? this.loading, 32 | purchaseIsPending: purchaseIsPending ?? this.purchaseIsPending, 33 | purchaseError: purchaseError ?? this.purchaseError, 34 | subscriptionActive: subscriptionActive ?? this.subscriptionActive, 35 | storeIsAvailable: storeIsAvailable ?? this.storeIsAvailable, 36 | ).updateMomentum(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/components/topic/index.dart: -------------------------------------------------------------------------------- 1 | export 'topic.controller.dart'; 2 | export 'topic.model.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/components/topic/topic.controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../../data/index.dart'; 4 | import '../../services/interface/google-api.interface.dart'; 5 | import 'index.dart'; 6 | 7 | class TopicController extends MomentumController { 8 | @override 9 | TopicModel init() { 10 | return TopicModel( 11 | this, 12 | loading: false, 13 | firebaseSubscription: FirebaseSubscription(), 14 | ); 15 | } 16 | 17 | Future loadSubscription() async { 18 | final api = service(runtimeType: false); 19 | model.update(loading: true); 20 | var firebaseSubscription = await api.getFirebaseSubscription(); 21 | model.update(loading: false, firebaseSubscription: firebaseSubscription); 22 | return firebaseSubscription; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/components/topic/topic.model.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../../data/index.dart'; 4 | import 'index.dart'; 5 | 6 | class TopicModel extends MomentumModel { 7 | TopicModel( 8 | TopicController controller, { 9 | required this.loading, 10 | required this.firebaseSubscription, 11 | }) : super(controller); 12 | 13 | final bool loading; 14 | final FirebaseSubscription firebaseSubscription; 15 | 16 | @override 17 | void update({ 18 | bool? loading, 19 | FirebaseSubscription? firebaseSubscription, 20 | }) { 21 | TopicModel( 22 | controller, 23 | loading: loading ?? this.loading, 24 | firebaseSubscription: firebaseSubscription ?? this.firebaseSubscription, 25 | ).updateMomentum(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/core/admob_units.dart: -------------------------------------------------------------------------------- 1 | const AD_UNIT_LIBRARY = 'ca-app-pub-3940256099942544/6300978111'; 2 | const AD_UNIT_SOURCES = 'ca-app-pub-3940256099942544/6300978111'; 3 | const AD_UNIT_FEED = 'ca-app-pub-3940256099942544/6300978111'; 4 | -------------------------------------------------------------------------------- /lib/src/core/api_key.sample.dart: -------------------------------------------------------------------------------- 1 | /// This is to close the quantz's api from public access. 2 | /// 3 | /// I'm planning to open it to public when considerable amount of support or donations is reached. 4 | const api_key = '________________'; -------------------------------------------------------------------------------- /lib/src/core/config.dart: -------------------------------------------------------------------------------- 1 | const api = 'https://api.quantz.app'; 2 | -------------------------------------------------------------------------------- /lib/src/core/controllers.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../components/admob/index.dart'; 4 | import '../components/animelist/index.dart'; 5 | import '../components/cloud-backup/index.dart'; 6 | import '../components/feed/index.dart'; 7 | import '../components/filter/index.dart'; 8 | import '../components/google-flow/index.dart'; 9 | import '../components/integration/index.dart'; 10 | import '../components/sources/index.dart'; 11 | import '../components/notification/index.dart'; 12 | import '../components/supporter-subscription/index.dart'; 13 | import '../components/topic/index.dart'; 14 | 15 | final notificationController = NotificationController(); 16 | 17 | List controllers() { 18 | return [ 19 | AnimelistController(), 20 | notificationController, 21 | IntegrationController(), 22 | SourcesController(), 23 | TopicController(), 24 | FilterController()..config(maxTimeTravelSteps: 2), 25 | FeedController(), 26 | CloudBackupController(), 27 | AdmobController(), 28 | SupporterSubscriptionController(), 29 | GoogleFlowController()..config(lazy: false), 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/core/in-app-purchase.sample.dart: -------------------------------------------------------------------------------- 1 | const SUPPORTER_PRODUCT_ID = 'GOOGLE_PLAY_PRODUCT_ID_HERE'; 2 | -------------------------------------------------------------------------------- /lib/src/core/index.dart: -------------------------------------------------------------------------------- 1 | export 'config.dart'; 2 | export 'controllers.dart'; 3 | export 'index.dart'; 4 | export 'initializer.dart'; 5 | export 'persistence.dart'; 6 | export 'admob_units.dart'; 7 | export 'services.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/core/initializer.dart: -------------------------------------------------------------------------------- 1 | // import 'package:flutter/foundation.dart'; 2 | // import 'package:in_app_purchase_android/in_app_purchase_android.dart'; 3 | 4 | import '../notification/index.dart'; 5 | import 'index.dart'; 6 | import 'version.dart'; 7 | 8 | Future initializer() async { 9 | initFirebaseNotification(); 10 | 11 | await Future.wait([ 12 | initSharedPreferences(), 13 | initLocalNotification(), 14 | checkAppVersion(), 15 | ]); 16 | 17 | // if (defaultTargetPlatform == TargetPlatform.android) { 18 | // InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); 19 | // } 20 | 21 | listenToAppLifecycle(); 22 | listenToFCM(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/core/mal.client.sample.dart: -------------------------------------------------------------------------------- 1 | const MAL_CLIENT_ID = 'PASTE_YOUR_MAL_CLIENT_ID_HERE'; 2 | const MAL_REDIRECT_URI = 'PASTE_YOUR_REDIRECT_URI_HERE'; 3 | const MAL_AUTH = 'PASTE_YOUR_POSTMAN_GENERATED_BASIC_AUTH_HERE'; 4 | -------------------------------------------------------------------------------- /lib/src/core/persistence.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | SharedPreferences? _sharedPreferences; 5 | SharedPreferences get sharedPreferences => _sharedPreferences!; 6 | 7 | Future initSharedPreferences() async { 8 | if (_sharedPreferences == null) _sharedPreferences = await SharedPreferences.getInstance(); 9 | } 10 | 11 | Future persistSave(BuildContext? context, String key, String? value) async { 12 | var result = await sharedPreferences.setString(key, value ?? ''); 13 | return result; 14 | } 15 | 16 | Future persistGet( 17 | BuildContext? context, 18 | String key, 19 | ) async { 20 | var result = sharedPreferences.getString(key); 21 | return result; 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/core/services.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../services/index.dart'; 4 | 5 | var fcmService = FcmService(); 6 | 7 | List services() { 8 | return [ 9 | fcmService, 10 | // ApiService(), 11 | // GoogleApiService(), 12 | // MalService(), 13 | ApiMockService(), 14 | GoogleApiMockService(), 15 | MalMockService(), 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/core/version.dart: -------------------------------------------------------------------------------- 1 | import 'package:package_info_plus/package_info_plus.dart'; 2 | 3 | String _version = ''; 4 | String get appVersion => _version; 5 | 6 | Future checkAppVersion() async { 7 | PackageInfo packageInfo = await PackageInfo.fromPlatform(); 8 | _version = packageInfo.version; 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/data/backup.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../misc/index.dart'; 4 | 5 | class BackupData { 6 | final Map animeListState; 7 | final Map sourcesState; 8 | 9 | BackupData({ 10 | required this.animeListState, 11 | required this.sourcesState, 12 | }); 13 | 14 | Map toJson() { 15 | return { 16 | ANIMELIST_STATE_KEY: animeListState, 17 | SOURCES_STATE_KEY: sourcesState, 18 | }; 19 | } 20 | 21 | factory BackupData.fromJson(Map map) { 22 | return BackupData( 23 | animeListState: Map.from(map[ANIMELIST_STATE_KEY]), 24 | sourcesState: Map.from(map[SOURCES_STATE_KEY]), 25 | ); 26 | } 27 | 28 | String toRawJson() => json.encode(toJson()); 29 | 30 | factory BackupData.fromRawJson(String source) => BackupData.fromJson(json.decode(source)); 31 | } 32 | 33 | class CloudBackup { 34 | CloudBackup({ 35 | this.id = '', 36 | this.userId = '', 37 | this.data = '', 38 | this.createdAt, 39 | this.updatedAt, 40 | this.v = -1, 41 | }); 42 | 43 | final String id; 44 | final String userId; 45 | final String data; 46 | final DateTime? createdAt; 47 | final DateTime? updatedAt; 48 | final int v; 49 | 50 | CloudBackup copyWith({ 51 | String? id, 52 | String? userId, 53 | String? data, 54 | DateTime? createdAt, 55 | DateTime? updatedAt, 56 | int? v, 57 | }) => 58 | CloudBackup( 59 | id: id ?? this.id, 60 | userId: userId ?? this.userId, 61 | data: data ?? this.data, 62 | createdAt: createdAt ?? this.createdAt, 63 | updatedAt: updatedAt ?? this.updatedAt, 64 | v: v ?? this.v, 65 | ); 66 | 67 | factory CloudBackup.fromRawJson(String str) => CloudBackup.fromJson(json.decode(str)); 68 | 69 | String toRawJson() => json.encode(toJson()); 70 | 71 | factory CloudBackup.fromJson(Map json) => CloudBackup( 72 | id: json["_id"] == null ? '' : json["_id"], 73 | userId: json["user_id"] == '' ? null : json["user_id"], 74 | data: json["data"] == null ? '' : json["data"], 75 | createdAt: json["createdAt"] == null ? null : DateTime.parse(json["createdAt"]), 76 | updatedAt: json["updatedAt"] == null ? null : DateTime.parse(json["updatedAt"]), 77 | v: json["__v"] == null ? -1 : json["__v"], 78 | ); 79 | 80 | Map toJson() => { 81 | "_id": id, 82 | "user_id": userId, 83 | "data": data, 84 | "createdAt": createdAt?.toIso8601String(), 85 | "updatedAt": updatedAt?.toIso8601String(), 86 | "__v": v, 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/data/feed.response.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | import 'package:timeago/timeago.dart' as timeago; 5 | 6 | import '../misc/feed-icons.dart'; 7 | 8 | class QuantzFeed { 9 | QuantzFeed({ 10 | this.page = 0, 11 | this.count = 0, 12 | this.items = const [], 13 | }); 14 | 15 | final int page; 16 | final int count; 17 | final List items; 18 | 19 | QuantzFeed copyWith({ 20 | int? page, 21 | int? count, 22 | List? items, 23 | }) => 24 | QuantzFeed( 25 | page: page ?? this.page, 26 | count: count ?? this.count, 27 | items: items ?? this.items, 28 | ); 29 | 30 | factory QuantzFeed.fromRawJson(String str) => QuantzFeed.fromJson(json.decode(str)); 31 | 32 | String toRawJson() => json.encode(toJson()); 33 | 34 | factory QuantzFeed.fromJson(Map json) => QuantzFeed( 35 | page: json["page"] == null ? 0 : json["page"], 36 | count: json["count"] == null ? 0 : json["count"], 37 | items: json["feed"] == null ? [] : List.from(json["feed"].map((x) => QuantzFeedItem.fromJson(x))), 38 | ); 39 | 40 | Map toJson() => { 41 | "page": page, 42 | "count": count, 43 | "feed": items.map((x) => x.toJson()).toList(), 44 | }; 45 | } 46 | 47 | class QuantzFeedItem extends Equatable { 48 | QuantzFeedItem({ 49 | this.title = '', 50 | this.permalink = '', 51 | this.utcTimestampSeconds = 0, 52 | this.provider = '', 53 | this.sourceDomain = '', 54 | }); 55 | 56 | final String title; 57 | final String permalink; 58 | final int utcTimestampSeconds; 59 | final String provider; 60 | final String sourceDomain; 61 | 62 | /// For UI to indicate when did the news got detected by Quantz. 63 | String get ago => timeago.format(DateTime.fromMillisecondsSinceEpoch(utcTimestampSeconds * 1000)); 64 | String get sourceImage => feedIconsMap[provider] ?? ''; 65 | int get tooltipDuration => title.length * 70; // 100ms per character 66 | 67 | QuantzFeedItem copyWith({ 68 | String? title, 69 | String? permalink, 70 | int? utcTimestampSeconds, 71 | String? provider, 72 | String? sourceDomain, 73 | }) => 74 | QuantzFeedItem( 75 | title: title ?? this.title, 76 | permalink: permalink ?? this.permalink, 77 | utcTimestampSeconds: utcTimestampSeconds ?? this.utcTimestampSeconds, 78 | provider: provider ?? this.provider, 79 | sourceDomain: sourceDomain ?? this.sourceDomain, 80 | ); 81 | 82 | factory QuantzFeedItem.fromRawJson(String str) => QuantzFeedItem.fromJson(json.decode(str)); 83 | 84 | String toRawJson() => json.encode(toJson()); 85 | 86 | factory QuantzFeedItem.fromJson(Map json) => QuantzFeedItem( 87 | title: json["title"] == null ? '' : json["title"], 88 | permalink: json["permalink"] == null ? '' : json["permalink"], 89 | utcTimestampSeconds: json["utcTimestampSeconds"] == null ? 0 : json["utcTimestampSeconds"], 90 | provider: json["provider"] == null ? '' : json["provider"], 91 | sourceDomain: json["sourceDomain"] == null ? '' : json["sourceDomain"], 92 | ); 93 | 94 | Map toJson() => { 95 | "title": title, 96 | "permalink": permalink, 97 | "utcTimestampSeconds": utcTimestampSeconds, 98 | "provider": provider, 99 | "sourceDomain": sourceDomain, 100 | }; 101 | 102 | List get props => [ 103 | title, 104 | permalink, 105 | utcTimestampSeconds, 106 | provider, 107 | sourceDomain, 108 | ]; 109 | } 110 | -------------------------------------------------------------------------------- /lib/src/data/firebase.topics.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class FirebaseSubscription { 4 | FirebaseSubscription({ 5 | this.topics = const {}, 6 | }); 7 | 8 | final Map topics; 9 | 10 | FirebaseSubscription copyWith({ 11 | Map? topics, 12 | }) => 13 | FirebaseSubscription( 14 | topics: topics ?? this.topics, 15 | ); 16 | 17 | factory FirebaseSubscription.fromRawJson(String str) => FirebaseSubscription.fromJson(json.decode(str)); 18 | 19 | String toRawJson() => json.encode(toJson()); 20 | 21 | factory FirebaseSubscription.fromJson(Map json) => FirebaseSubscription( 22 | topics: json["topics"] == null ? {} : Map.from(json["topics"]).map((k, v) => MapEntry(k, _Info.fromJson(v))), 23 | ); 24 | 25 | Map toJson() => { 26 | "topics": Map.from(topics).map((k, v) => MapEntry(k, v.toJson())), 27 | }; 28 | } 29 | 30 | class _Info { 31 | _Info({ 32 | this.addDate, 33 | }); 34 | 35 | final DateTime? addDate; 36 | 37 | _Info copyWith({ 38 | DateTime? addDate, 39 | }) => 40 | _Info( 41 | addDate: addDate ?? this.addDate, 42 | ); 43 | 44 | String toRawJson() => json.encode(toJson()); 45 | 46 | factory _Info.fromJson(Map json) => _Info( 47 | addDate: json["addDate"] == null ? null : DateTime.parse(json["addDate"]), 48 | ); 49 | 50 | Map toJson() => { 51 | "addDate": "${addDate?.year.toString().padLeft(4, '0')}-${addDate?.month.toString().padLeft(2, '0')}-${addDate?.day.toString().padLeft(2, '0')}", 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/data/index.dart: -------------------------------------------------------------------------------- 1 | export 'backup.dart'; 2 | export 'firebase.topics.dart'; 3 | export 'index.dart'; 4 | export 'jwt-token.profile.dart'; 5 | export 'response.all_anime.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/data/jwt-token.profile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class JwtTokenProfile { 4 | JwtTokenProfile({ 5 | this.iss = '', 6 | this.azp = '', 7 | this.aud = '', 8 | this.sub = '', 9 | this.email = '', 10 | this.emailVerified = false, 11 | this.name = '', 12 | this.picture = '', 13 | this.givenName = '', 14 | this.familyName = '', 15 | this.locale = '', 16 | this.iat = -1, 17 | this.exp = -1, 18 | }); 19 | 20 | final String iss; 21 | final String azp; 22 | final String aud; 23 | final String sub; 24 | final String email; 25 | final bool emailVerified; 26 | final String name; 27 | final String picture; 28 | final String givenName; 29 | final String familyName; 30 | final String locale; 31 | final int iat; 32 | final int exp; 33 | 34 | JwtTokenProfile copyWith({ 35 | String? iss, 36 | String? azp, 37 | String? aud, 38 | String? sub, 39 | String? email, 40 | bool? emailVerified, 41 | String? name, 42 | String? picture, 43 | String? givenName, 44 | String? familyName, 45 | String? locale, 46 | int? iat, 47 | int? exp, 48 | }) => 49 | JwtTokenProfile( 50 | iss: iss ?? this.iss, 51 | azp: azp ?? this.azp, 52 | aud: aud ?? this.aud, 53 | sub: sub ?? this.sub, 54 | email: email ?? this.email, 55 | emailVerified: emailVerified ?? this.emailVerified, 56 | name: name ?? this.name, 57 | picture: picture ?? this.picture, 58 | givenName: givenName ?? this.givenName, 59 | familyName: familyName ?? this.familyName, 60 | locale: locale ?? this.locale, 61 | iat: iat ?? this.iat, 62 | exp: exp ?? this.exp, 63 | ); 64 | 65 | factory JwtTokenProfile.fromRawJson(String str) => JwtTokenProfile.fromJson(json.decode(str)); 66 | 67 | String toRawJson() => json.encode(toJson()); 68 | 69 | factory JwtTokenProfile.fromJson(Map json) => JwtTokenProfile( 70 | iss: json["iss"] == null ? '' : json["iss"], 71 | azp: json["azp"] == null ? '' : json["azp"], 72 | aud: json["aud"] == null ? '' : json["aud"], 73 | sub: json["sub"] == null ? '' : json["sub"], 74 | email: json["email"] == null ? '' : json["email"], 75 | emailVerified: json["email_verified"] == null ? false : json["email_verified"], 76 | name: json["name"] == null ? '' : json["name"], 77 | picture: json["picture"] == null ? '' : json["picture"], 78 | givenName: json["given_name"] == null ? '' : json["given_name"], 79 | familyName: json["family_name"] == null ? '' : json["family_name"], 80 | locale: json["locale"] == null ? '' : json["locale"], 81 | iat: json["iat"] == null ? -1 : json["iat"], 82 | exp: json["exp"] == null ? -1 : json["exp"], 83 | ); 84 | 85 | Map toJson() => { 86 | "iss": iss, 87 | "azp": azp, 88 | "aud": aud, 89 | "sub": sub, 90 | "email": email, 91 | "email_verified": emailVerified, 92 | "name": name, 93 | "picture": picture, 94 | "given_name": givenName, 95 | "family_name": familyName, 96 | "locale": locale, 97 | "iat": iat, 98 | "exp": exp, 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/data/mal-token.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class MalTokenResponse { 4 | MalTokenResponse({ 5 | this.tokenType = '', 6 | this.expiresIn = -1, 7 | this.accessToken = '', 8 | this.refreshToken = '', 9 | }); 10 | 11 | final String tokenType; 12 | final int expiresIn; 13 | final String accessToken; 14 | final String refreshToken; 15 | 16 | MalTokenResponse copyWith({ 17 | String? tokenType, 18 | int? expiresIn, 19 | String? accessToken, 20 | String? refreshToken, 21 | }) => 22 | MalTokenResponse( 23 | tokenType: tokenType ?? this.tokenType, 24 | expiresIn: expiresIn ?? this.expiresIn, 25 | accessToken: accessToken ?? this.accessToken, 26 | refreshToken: refreshToken ?? this.refreshToken, 27 | ); 28 | 29 | factory MalTokenResponse.fromRawJson(String str) => MalTokenResponse.fromJson(json.decode(str)); 30 | 31 | String toRawJson() => json.encode(toJson()); 32 | 33 | factory MalTokenResponse.fromJson(Map json) => MalTokenResponse( 34 | tokenType: json["token_type"], 35 | expiresIn: json["expires_in"], 36 | accessToken: json["access_token"], 37 | refreshToken: json["refresh_token"], 38 | ); 39 | 40 | Map toJson() => { 41 | "token_type": tokenType, 42 | "expires_in": expiresIn, 43 | "access_token": accessToken, 44 | "refresh_token": refreshToken, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/data/mal-user.animeupdate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class MalUserAnimeUpdate { 4 | MalUserAnimeUpdate({ 5 | this.status = '', 6 | this.score = 0, 7 | this.numEpisodesWatched = 0, 8 | this.isRewatching = false, 9 | this.updatedAt, 10 | this.startDate = '', 11 | this.finishDate = '', 12 | this.priority = 0, 13 | this.numTimesRewatched = 0, 14 | this.rewatchValue = 0, 15 | this.tags = const [], 16 | this.comments = '', 17 | }); 18 | 19 | final String status; 20 | final int score; 21 | final int numEpisodesWatched; 22 | final bool isRewatching; 23 | final DateTime? updatedAt; 24 | final String startDate; 25 | final String finishDate; 26 | final int priority; 27 | final int numTimesRewatched; 28 | final int rewatchValue; 29 | final List tags; 30 | final String comments; 31 | 32 | MalUserAnimeUpdate copyWith({ 33 | String? status, 34 | int? score, 35 | int? numEpisodesWatched, 36 | bool? isRewatching, 37 | DateTime? updatedAt, 38 | String? startDate, 39 | String? finishDate, 40 | int? priority, 41 | int? numTimesRewatched, 42 | int? rewatchValue, 43 | List? tags, 44 | String? comments, 45 | }) => 46 | MalUserAnimeUpdate( 47 | status: status ?? this.status, 48 | score: score ?? this.score, 49 | numEpisodesWatched: numEpisodesWatched ?? this.numEpisodesWatched, 50 | isRewatching: isRewatching ?? this.isRewatching, 51 | updatedAt: updatedAt ?? this.updatedAt, 52 | startDate: startDate ?? this.startDate, 53 | finishDate: finishDate ?? this.finishDate, 54 | priority: priority ?? this.priority, 55 | numTimesRewatched: numTimesRewatched ?? this.numTimesRewatched, 56 | rewatchValue: rewatchValue ?? this.rewatchValue, 57 | tags: tags ?? this.tags, 58 | comments: comments ?? this.comments, 59 | ); 60 | 61 | factory MalUserAnimeUpdate.fromRawJson(String str) => MalUserAnimeUpdate.fromJson(json.decode(str)); 62 | 63 | String toRawJson() => json.encode(toJson()); 64 | 65 | factory MalUserAnimeUpdate.fromJson(Map json) => MalUserAnimeUpdate( 66 | status: json["status"] == null ? '' : json["status"], 67 | score: json["score"] == null ? 0 : json["score"], 68 | numEpisodesWatched: json["num_episodes_watched"] == null ? 0 : json["num_episodes_watched"], 69 | isRewatching: json["is_rewatching"] == null ? false : json["is_rewatching"], 70 | updatedAt: json["updated_at"] == null ? null : DateTime.parse(json["updated_at"]), 71 | startDate: json["start_date"] == null ? '' : json["start_date"], 72 | finishDate: json["finish_date"] == null ? '' : json["finish_date"], 73 | priority: json["priority"] == null ? 0 : json["priority"], 74 | numTimesRewatched: json["num_times_rewatched"] == null ? 0 : json["num_times_rewatched"], 75 | rewatchValue: json["rewatch_value"] == null ? 0 : json["rewatch_value"], 76 | tags: json["tags"] == null ? [] : List.from(json["tags"].map((x) => x)), 77 | comments: json["comments"] == null ? '' : json["comments"], 78 | ); 79 | 80 | Map toJson() => { 81 | "status": status, 82 | "score": score, 83 | "num_episodes_watched": numEpisodesWatched, 84 | "is_rewatching": isRewatching, 85 | "updated_at": updatedAt == null ? null : updatedAt?.toIso8601String(), 86 | "start_date": startDate, 87 | "finish_date": finishDate, 88 | "priority": priority, 89 | "num_times_rewatched": numTimesRewatched, 90 | "rewatch_value": rewatchValue, 91 | "tags": List.from(tags.map((x) => x)), 92 | "comments": comments, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/data/mal-user.profile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class MalUserProfile { 4 | MalUserProfile({ 5 | this.id = -1, 6 | this.name = '', 7 | this.location = '', 8 | this.picture = '', 9 | }); 10 | 11 | final int id; 12 | final String name; 13 | final String location; 14 | final String picture; 15 | 16 | MalUserProfile copyWith({ 17 | int? id, 18 | String? name, 19 | String? location, 20 | String? picture, 21 | }) => 22 | MalUserProfile( 23 | id: id ?? this.id, 24 | name: name ?? this.name, 25 | location: location ?? this.location, 26 | picture: picture ?? this.picture, 27 | ); 28 | 29 | factory MalUserProfile.fromRawJson(String str) => MalUserProfile.fromJson(json.decode(str)); 30 | 31 | String toRawJson() => json.encode(toJson()); 32 | 33 | factory MalUserProfile.fromJson(Map json) => MalUserProfile( 34 | id: json["id"], 35 | name: json["name"], 36 | location: json["location"], 37 | picture: json["picture"], 38 | ); 39 | 40 | Map toJson() => { 41 | "id": id, 42 | "name": name, 43 | "location": location, 44 | "picture": picture, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/misc/date.dart: -------------------------------------------------------------------------------- 1 | DateTime _now = DateTime.now().toUtc(); 2 | String getFuzzyDay(int timestamp) { 3 | if (timestamp == -1) { 4 | return 'is available now'; 5 | } 6 | var nowMillis = _now.millisecondsSinceEpoch ~/ 1000; 7 | var diff = timestamp - nowMillis; 8 | var diffDuration = Duration(milliseconds: diff * 1000); 9 | var days = diffDuration.inDays; 10 | if (days == 0) { 11 | var inHours = diffDuration.inHours; 12 | if (inHours > 0) { 13 | return 'in ${inHours == 1 ? "an" : inHours} hour${inHours == 1 ? "" : "s"}'; 14 | } 15 | var inMinutes = diffDuration.inMinutes; 16 | if (inMinutes > 40) { 17 | return 'in an hour'; 18 | } 19 | return 'in a few minutes'; 20 | } else { 21 | return 'in ${days == 1 ? "a" : days} day${days == 1 ? "" : "s"}'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/misc/feed-icons.dart: -------------------------------------------------------------------------------- 1 | const feedIconsMap = { 2 | "ANIME_NEWS_NETWORK": "assets/sources-icons/ann.png", 3 | "LIVECHART_HEADLINES": "assets/sources-icons/livechart.png", 4 | "MYANIMELIST_NEWS": "assets/sources-icons/mal.png", 5 | "R_ANIME_REDDIT": "assets/sources-icons/reddit.png", 6 | }; 7 | -------------------------------------------------------------------------------- /lib/src/misc/index.dart: -------------------------------------------------------------------------------- 1 | export 'date.dart'; 2 | export 'index.dart'; 3 | export 'keys.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/misc/keys.dart: -------------------------------------------------------------------------------- 1 | const IMPORT_STATE_KEY = 'IMPORT_STATE_KEY'; 2 | const ANIMELIST_STATE_KEY = 'ANIMELIST_STATE_KEY'; 3 | const SOURCES_STATE_KEY = 'SOURCES_STATE_KEY'; 4 | -------------------------------------------------------------------------------- /lib/src/notification/app_lifecycle_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_fgbg/flutter_fgbg.dart'; 2 | 3 | import '../core/index.dart'; 4 | 5 | /// This is for when the app switches between foreground and background states. 6 | void listenToAppLifecycle() { 7 | FGBGEvents.stream.listen((event) { 8 | switch (event) { 9 | case FGBGType.foreground: 10 | // This case is for when: 11 | // - The app is in the background. 12 | // - Receives a notification. 13 | // - User clicks the notification (the app goes to foreground as a result). 14 | notificationController.model.update(loading: false); 15 | // - The app will then try to open the url that came with the notification. 16 | notificationController.openNotification(); 17 | break; 18 | default: 19 | break; 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/notification/fcm_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_messaging/firebase_messaging.dart'; 2 | 3 | import '../core/index.dart'; 4 | import 'index.dart'; 5 | 6 | void listenToFCM() { 7 | FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp); 8 | FirebaseMessaging.onMessage.listen(_onMessage); 9 | } 10 | 11 | /// When the app is in the background 12 | /// *(not visible on screen but not terminated either)* 13 | void _onMessageOpenedApp(RemoteMessage event) { 14 | try { 15 | fcmService.setMessage(event); 16 | } catch (e, trace) { 17 | fcmService.clear(); 18 | print(e.toString()); 19 | print(trace.toString()); 20 | } 21 | } 22 | 23 | /// When the app is in the foreground *(visible on screen).* 24 | void _onMessage(RemoteMessage event) { 25 | try { 26 | fcmService.setMessage(event); 27 | // When the app is in the foreground, FCM will never show a notification. 28 | // The package called `flutter_local_notification` is needed for this. 29 | showLocalNotification(event); 30 | } catch (e, trace) { 31 | fcmService.clear(); 32 | print(e.toString()); 33 | print(trace.toString()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/notification/index.dart: -------------------------------------------------------------------------------- 1 | export 'app_lifecycle_handler.dart'; 2 | export 'fcm_handler.dart'; 3 | export 'index.dart'; 4 | export 'notification.firebase.dart'; 5 | export 'notification.local.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/notification/notification.firebase.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:firebase_messaging/firebase_messaging.dart'; 5 | 6 | Completer firebaseInitializer = Completer(); 7 | Future waitForFirebaseInit() async { 8 | await firebaseInitializer.future; 9 | } 10 | 11 | Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { 12 | // If you're going to use other Firebase services in the background, such as Firestore, 13 | // make sure you call `initializeApp` before using other Firebase services. 14 | await Firebase.initializeApp(); 15 | print('Handling a background message ${message.messageId}'); 16 | } 17 | 18 | Future initFirebaseNotification() async { 19 | try { 20 | await Firebase.initializeApp(); 21 | 22 | // Set the background messaging handler early on, as a named top-level function 23 | FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); 24 | 25 | FirebaseMessaging messaging = FirebaseMessaging.instance; 26 | 27 | final token = await messaging.getToken(); 28 | print(['firebase_token', token]); 29 | 30 | NotificationSettings settings = await messaging.requestPermission( 31 | alert: true, 32 | announcement: false, 33 | badge: true, 34 | carPlay: false, 35 | criticalAlert: false, 36 | provisional: false, 37 | sound: true, 38 | ); 39 | 40 | if (settings.authorizationStatus == AuthorizationStatus.authorized) { 41 | print('User granted permission'); 42 | } else if (settings.authorizationStatus == AuthorizationStatus.provisional) { 43 | print('User granted provisional permission'); 44 | } else { 45 | print('User declined or has not accepted permission'); 46 | } 47 | 48 | await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( 49 | alert: true, 50 | badge: true, 51 | sound: true, 52 | ); 53 | 54 | if (!firebaseInitializer.isCompleted) firebaseInitializer.complete(); 55 | } catch (e) { 56 | print(e); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/notification/notification.local.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:firebase_messaging/firebase_messaging.dart'; 4 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 5 | 6 | import '../core/index.dart'; 7 | 8 | FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); 9 | const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); 10 | final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(); 11 | final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid, macOS: initializationSettingsMacOS); 12 | 13 | Future initLocalNotification() async { 14 | await flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: onSelectNotification); 15 | } 16 | 17 | Future showLocalNotification(RemoteMessage? message) async { 18 | if (message != null) { 19 | final _random = new Random(); 20 | int next(int min, int max) => min + _random.nextInt(max - min); 21 | 22 | const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails( 23 | 'high_importance_channel', 24 | 'Quantz Notifications', 25 | channelDescription: 'Notifications for episode releases and news updates.', 26 | importance: Importance.max, 27 | priority: Priority.high, 28 | showWhen: true, 29 | ); 30 | const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); 31 | 32 | await flutterLocalNotificationsPlugin.show( 33 | next(10000000, 99999999), 34 | message.notification?.title, 35 | message.notification?.body, 36 | platformChannelSpecifics, 37 | payload: '{}', 38 | ); 39 | } 40 | } 41 | 42 | Future onSelectNotification(String? event) async { 43 | try { 44 | print('Local notification clicked, opening permalink...'); 45 | notificationController.openNotification(); 46 | } catch (e, trace) { 47 | print(e); 48 | print(trace); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/services/concretes/api.service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | import '../../core/api_key.dart'; 4 | import '../../core/config.dart'; 5 | import '../../data/backup.dart'; 6 | import '../../data/feed.response.dart'; 7 | import '../../data/response.all_anime.dart'; 8 | import '../../widgets/toast.dart'; 9 | import '../interface/api.interface.dart'; 10 | 11 | class ApiService extends ApiInterface { 12 | final _dio = Dio(); 13 | 14 | ApiService() { 15 | _dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) { 16 | options.headers['api-key'] = api_key; 17 | return handler.next(options); //continue 18 | }, onResponse: (response, handler) { 19 | return handler.next(response); // continue 20 | }, onError: (DioError e, handler) { 21 | return handler.next(e); //continue 22 | })); 23 | } 24 | 25 | Future getAnimeList() async { 26 | const path = '$api/anime/list'; 27 | try { 28 | var response = await _dio.get(path); 29 | return AnimeListResponse.fromJson(response.data); 30 | } catch (e) { 31 | showToast('$e', error: true); 32 | print(e); 33 | print(['ERROR', path]); 34 | return AnimeListResponse(count: 0, entries: []); 35 | } 36 | } 37 | 38 | Future newBackup({required String token, required String data}) async { 39 | try { 40 | final param = { 41 | 'jwt_token': token, 42 | 'data': data, 43 | }; 44 | var response = await _dio.post( 45 | '$api/backup/new', 46 | data: param, 47 | ); 48 | return CloudBackup.fromJson(response.data); 49 | } catch (e) { 50 | showToast('Error creating backup', error: true); 51 | print(e); 52 | return CloudBackup(); 53 | } 54 | } 55 | 56 | Future fetchBackup({ 57 | required String token, 58 | bool includeData = true, 59 | }) async { 60 | const path = '$api/backup/fetch'; 61 | try { 62 | final param = { 63 | 'jwt_token': token, 64 | 'include_data': includeData, 65 | }; 66 | var response = await _dio.post(path, data: param); 67 | return CloudBackup.fromJson(response.data); 68 | } catch (e) { 69 | if (e is DioError) { 70 | final notFound = e.response?.statusCode == 404; 71 | if (notFound) { 72 | return CloudBackup(); 73 | } 74 | } else { 75 | showToast('Error fetching backup.', error: true); 76 | } 77 | print(e); 78 | print(['ERROR', path]); 79 | return CloudBackup(); 80 | } 81 | } 82 | 83 | Future getLatestFeed({int page = 1, int limit = 10}) async { 84 | try { 85 | final response = await _dio.get( 86 | '$api/feed/latest', 87 | queryParameters: { 88 | 'page': page, 89 | 'limit': limit, 90 | }, 91 | ); 92 | return QuantzFeed.fromJson(response.data); 93 | } catch (e) { 94 | return QuantzFeed(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/src/services/concretes/index.dart: -------------------------------------------------------------------------------- 1 | export 'api.service.dart'; 2 | export 'google-api.service.dart'; 3 | export 'mal.service.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/services/fcm.service.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_messaging/firebase_messaging.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | class FcmService extends MomentumService { 5 | RemoteMessage? _message; 6 | RemoteMessage? get message => _message; 7 | 8 | void setMessage(RemoteMessage? message) { 9 | _message = message; 10 | } 11 | 12 | void clear() { 13 | _message = null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/services/index.dart: -------------------------------------------------------------------------------- 1 | export 'fcm.service.dart'; 2 | export 'index.dart'; 3 | export 'concretes/index.dart'; 4 | export 'interface/index.dart'; 5 | export 'mocks/index.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/services/interface/api.interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../../data/backup.dart'; 4 | import '../../data/feed.response.dart'; 5 | import '../../data/response.all_anime.dart'; 6 | 7 | abstract class ApiInterface extends MomentumService { 8 | Future getAnimeList(); 9 | 10 | Future newBackup({required String token, required String data}); 11 | 12 | Future fetchBackup({required String token, bool includeData = true}); 13 | 14 | Future getLatestFeed({int page = 1, int limit = 10}); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/services/interface/google-api.interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:in_app_purchase/in_app_purchase.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../../data/firebase.topics.dart'; 5 | 6 | abstract class GoogleApiInterface extends MomentumService { 7 | /// In case the consumer of this service have a client-stored (persisted) token. 8 | /// This lets the consumer set an initial token. 9 | void setAuthToken(String authToken); 10 | 11 | Future isSignedIn(); 12 | 13 | Future getFirebaseSubscription(); 14 | 15 | Future signInWithGoogle(); 16 | 17 | Future signOut(); 18 | 19 | Future refreshToken(); 20 | 21 | /* IN-APP PURCHASE INTERFACE */ 22 | PurchaseDetails? get pendingPurchase; 23 | 24 | Future restorePurchases({String? applicationUserName}); 25 | 26 | Future processPurchaseUpdated(PurchaseDetails purchaseDetails); 27 | 28 | Future isPurchaseValid(PurchaseDetails purchaseDetails); 29 | 30 | Future getSupporterSubscription(); 31 | 32 | Future checkStore(); 33 | /* IN-APP PURCHASE INTERFACE */ 34 | } 35 | 36 | enum UpdatedPurchaseStatus { 37 | none, 38 | purchaseIsPending, 39 | subscriptionValid, 40 | subscriptionInvalid, 41 | purchaseDetailsError, 42 | completingPurchaseError, 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/services/interface/index.dart: -------------------------------------------------------------------------------- 1 | export 'api.interface.dart'; 2 | export 'google-api.interface.dart'; 3 | export 'mal.interface.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/services/interface/mal.interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:momentum/momentum.dart'; 2 | 3 | import '../../data/mal-user.animelist.dart'; 4 | import '../../data/mal-user.animeupdate.dart'; 5 | import '../../data/mal-user.profile.dart'; 6 | 7 | abstract class MalInterface extends MomentumService { 8 | bool get loggedIn; 9 | 10 | Future getLoginUrl(); 11 | 12 | void onLoggedIn(void Function(Future Function() getToken) callback); 13 | 14 | Future getUserAnimeList({required String status, required int offset}); 15 | 16 | Future getUserProfile(); 17 | 18 | Future updateUserAnime({ 19 | required int malId, 20 | String? status, 21 | int? numWatchedEpisodes, 22 | String? startDate, 23 | String? finishDate, 24 | }); 25 | 26 | Future getUserAnime(int malId); 27 | 28 | Future logout(); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/services/mocks/api.mock-service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | import '../../data/backup.dart'; 6 | import '../../data/feed.response.dart'; 7 | import '../../data/response.all_anime.dart'; 8 | import '../../widgets/toast.dart'; 9 | import '../interface/api.interface.dart'; 10 | 11 | class ApiMockService extends ApiInterface { 12 | final _dio = Dio(); 13 | 14 | CloudBackup _backupData = CloudBackup(); 15 | 16 | @override 17 | Future getAnimeList() async { 18 | const path = 'https://gist.githubusercontent.com/xamantra/6607da2e19b3b4a6749aa4e25df15281/raw/ecc9380cc6e03b53aa97b90b4f6477709c0a9ddd/mock_anime_list.json'; 19 | try { 20 | var response = await _dio.get(path); 21 | final json = jsonDecode(response.data); 22 | return AnimeListResponse.fromJson(json); 23 | } catch (e) { 24 | showToast('$e', error: true); 25 | print(e); 26 | print(['ERROR', path]); 27 | return AnimeListResponse(count: 0, entries: []); 28 | } 29 | } 30 | 31 | @override 32 | Future newBackup({required String token, required String data}) async { 33 | try { 34 | await Future.delayed(Duration(seconds: 2)); 35 | _backupData = CloudBackup( 36 | id: '60f3aa8051a6980015c9904e', 37 | userId: '106243734760415480007', 38 | data: data, 39 | createdAt: DateTime.now(), 40 | updatedAt: DateTime.now(), 41 | ); 42 | return _backupData; 43 | } catch (e) { 44 | showToast('Error creating backup', error: true); 45 | print(e); 46 | return CloudBackup(); 47 | } 48 | } 49 | 50 | @override 51 | Future fetchBackup({required String token, bool includeData = true}) async { 52 | await Future.delayed(Duration(seconds: 2)); 53 | if (includeData) { 54 | return _backupData; 55 | } 56 | return _backupData.copyWith(data: ''); 57 | } 58 | 59 | @override 60 | Future getLatestFeed({int page = 1, int limit = 10}) async { 61 | try { 62 | if (page > 1) { 63 | return QuantzFeed(); 64 | } 65 | final response = await _dio.get('https://gist.githubusercontent.com/xamantra/c005d735474c0dea2f76738be15925dc/raw/c91b3c1bb3e27ffd0e2ee17560146ddd1eb9a06a/mock_news_feed.json'); 66 | final json = jsonDecode(response.data); 67 | return QuantzFeed.fromJson(json); 68 | } catch (e) { 69 | return QuantzFeed(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/services/mocks/index.dart: -------------------------------------------------------------------------------- 1 | export 'api.mock-service.dart'; 2 | export 'google-api.mock-service.dart'; 3 | export 'mal.mock-service.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/services/mocks/mal.mock-service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | import '../../data/mal-user.animelist.dart'; 6 | import '../../data/mal-user.animeupdate.dart'; 7 | import '../../data/mal-user.profile.dart'; 8 | import '../interface/mal.interface.dart'; 9 | 10 | class MalMockService extends MalInterface { 11 | final _dio = Dio(); 12 | 13 | @override 14 | bool get loggedIn => _loggedIn; 15 | bool _loggedIn = false; 16 | 17 | @override 18 | Future getLoginUrl() async { 19 | await Future.delayed(Duration(seconds: 1)); 20 | _loggedIn = true; 21 | return ''; 22 | } 23 | 24 | @override 25 | void onLoggedIn(void Function(Future Function() getToken) callback) { 26 | return; 27 | } 28 | 29 | @override 30 | Future getUserAnimeList({required String status, required int offset}) async { 31 | const path = 'https://gist.githubusercontent.com/xamantra/2c6b8d8cec2004c5030753b08e65a981/raw/3d39b6e11cc4ce368e8f8ae8166c15207879b33a/mock_mal_user_animelist.json'; 32 | try { 33 | final response = await _dio.get(path); 34 | final json = jsonDecode(response.data); 35 | return MalUserAnimeListResponse.fromJson(json); 36 | } catch (e) { 37 | return MalUserAnimeListResponse(); 38 | } 39 | } 40 | 41 | @override 42 | Future getUserProfile() async { 43 | await Future.delayed(Duration(seconds: 1)); 44 | return MalUserProfile.fromJson({ 45 | "id": 91292189, // random number only 46 | "name": "mockdata", 47 | "location": "", 48 | "joined_at": "2020-12-20T21:18:18+00:00", 49 | "picture": "https://cdn.myanimelist.net/images/anime/1527/113656.jpg", 50 | }); 51 | } 52 | 53 | @override 54 | Future logout() async { 55 | await Future.delayed(Duration(seconds: 1)); 56 | _loggedIn = false; 57 | } 58 | 59 | @override 60 | Future updateUserAnime({ 61 | required int malId, 62 | String? status, 63 | int? numWatchedEpisodes, 64 | String? startDate, 65 | String? finishDate, 66 | }) async { 67 | await Future.delayed(Duration(seconds: 2)); 68 | return MalUserAnimeUpdate( 69 | status: status ?? 'watching', 70 | numEpisodesWatched: numWatchedEpisodes ?? 0, 71 | startDate: startDate ?? '', 72 | finishDate: finishDate ?? '', 73 | ); 74 | } 75 | 76 | @override 77 | Future getUserAnime(int malId) async { 78 | await Future.delayed(Duration(seconds: 2)); 79 | return MalUserAnimeDetails(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/src/widgets/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'index.dart'; 4 | import 'pages/core.dashboard.dart'; 5 | 6 | class MyApp extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | final theme = ThemeData.dark(); 10 | return MaterialApp( 11 | title: 'Quantz', 12 | theme: theme.copyWith( 13 | primaryColor: primary, 14 | colorScheme: theme.colorScheme.copyWith( 15 | secondary: primary, 16 | ), 17 | backgroundColor: background, 18 | scaffoldBackgroundColor: background, 19 | dialogBackgroundColor: background, 20 | ), 21 | themeMode: ThemeMode.dark, 22 | debugShowCheckedModeBanner: false, 23 | home: DashboardPage(), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/widgets/async-loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void showLoading(BuildContext context, Future callback) { 4 | showDialog( 5 | context: context, 6 | barrierDismissible: false, 7 | builder: (_) => _AsyncLoading( 8 | callback: callback, 9 | ), 10 | ); 11 | } 12 | 13 | class _AsyncLoading extends StatefulWidget { 14 | const _AsyncLoading({ 15 | Key? key, 16 | required this.callback, 17 | }) : super(key: key); 18 | 19 | final Future callback; 20 | 21 | @override 22 | __AsyncLoadingState createState() => __AsyncLoadingState(); 23 | } 24 | 25 | class __AsyncLoadingState extends State<_AsyncLoading> { 26 | @override 27 | void didChangeDependencies() { 28 | super.didChangeDependencies(); 29 | Future.microtask(() async { 30 | await widget.callback; 31 | pop(); 32 | }); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return WillPopScope( 38 | onWillPop: () async => false, 39 | child: Dialog( 40 | child: Container( 41 | height: 48, 42 | width: 48, 43 | child: Center( 44 | child: SizedBox( 45 | height: 32, 46 | width: 32, 47 | child: CircularProgressIndicator(), 48 | ), 49 | ), 50 | ), 51 | ), 52 | ); 53 | } 54 | 55 | void pop() { 56 | Navigator.pop(context); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/widgets/badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class TextBadge extends StatelessWidget { 6 | const TextBadge( 7 | this.text, { 8 | Key? key, 9 | this.color, 10 | this.size, 11 | }) : super(key: key); 12 | 13 | final String text; 14 | final double? size; 15 | final Color? color; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | decoration: BoxDecoration( 21 | color: color ?? primary, 22 | borderRadius: BorderRadius.circular(5), 23 | ), 24 | padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), 25 | child: Text( 26 | text.toUpperCase(), 27 | style: TextStyle( 28 | fontSize: size ?? 9, 29 | color: Colors.white, 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/widgets/button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomButton extends StatelessWidget { 4 | const CustomButton({ 5 | Key? key, 6 | this.onPressed, 7 | required this.color, 8 | required this.text, 9 | required this.textColor, 10 | this.textSize, 11 | }) : super(key: key); 12 | 13 | final void Function()? onPressed; 14 | final Color color; 15 | final String text; 16 | final Color textColor; 17 | final double? textSize; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Container( 22 | width: double.infinity, 23 | child: TextButton( 24 | onPressed: onPressed, 25 | style: ButtonStyle( 26 | tapTargetSize: MaterialTapTargetSize.shrinkWrap, 27 | backgroundColor: MaterialStateProperty.all(color), 28 | ), 29 | child: Padding( 30 | padding: const EdgeInsets.symmetric(vertical: 4), 31 | child: Text( 32 | text, 33 | style: TextStyle( 34 | fontSize: textSize ?? 16, 35 | color: textColor, 36 | ), 37 | ), 38 | ), 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/widgets/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | const primary = Color(0xff3399FF); 4 | const background = Color(0xff202125); 5 | const secondaryBackground = Color(0xff242529); 6 | -------------------------------------------------------------------------------- /lib/src/widgets/empty.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyList extends StatelessWidget { 4 | const EmptyList({Key? key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Center( 9 | child: Text( 10 | 'Wow, such empty.', 11 | style: TextStyle( 12 | fontSize: 16, 13 | fontWeight: FontWeight.w600, 14 | color: Colors.white.withOpacity(0.3), 15 | ), 16 | ), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/widgets/icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomIconButton extends StatelessWidget { 4 | const CustomIconButton({ 5 | Key? key, 6 | this.onPressed, 7 | required this.color, 8 | required this.icon, 9 | required this.iconColor, 10 | }) : super(key: key); 11 | 12 | final void Function()? onPressed; 13 | final Color color; 14 | final IconData icon; 15 | final Color iconColor; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | width: double.infinity, 21 | child: TextButton( 22 | onPressed: onPressed, 23 | style: ButtonStyle( 24 | tapTargetSize: MaterialTapTargetSize.shrinkWrap, 25 | backgroundColor: MaterialStateProperty.all(color), 26 | ), 27 | child: Padding( 28 | padding: const EdgeInsets.symmetric(vertical: 4), 29 | child: Icon( 30 | icon, 31 | color: iconColor, 32 | ), 33 | ), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/widgets/index.dart: -------------------------------------------------------------------------------- 1 | export 'app.dart'; 2 | export 'async-loading.dart'; 3 | export 'badge.dart'; 4 | export 'button.dart'; 5 | export 'colors.dart'; 6 | export 'empty.dart'; 7 | export 'icon_button.dart'; 8 | export 'index.dart'; 9 | export 'loader.dart'; 10 | export 'refreshing.dart'; 11 | export 'search.dart'; 12 | export 'shadow.dart'; 13 | export 'tabview.dart'; 14 | export 'thumbnail.dart'; 15 | export 'toast.dart'; 16 | -------------------------------------------------------------------------------- /lib/src/widgets/inputs/checkbox.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../index.dart'; 4 | 5 | class CheckboxWidget extends StatelessWidget { 6 | const CheckboxWidget({ 7 | Key? key, 8 | required this.value, 9 | required this.label, 10 | required this.onChanged, 11 | }) : super(key: key); 12 | 13 | final bool value; 14 | final String label; 15 | final Function(bool value) onChanged; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Row( 20 | mainAxisSize: MainAxisSize.min, 21 | children: [ 22 | Checkbox( 23 | value: value, 24 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 25 | activeColor: primary, 26 | onChanged: (value) { 27 | onChanged(value ?? false); 28 | }, 29 | ), 30 | SizedBox(width: 6), 31 | Text( 32 | label, 33 | ), 34 | ], 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/widgets/inputs/custom.switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../index.dart'; 4 | 5 | class ButtonSwith extends StatelessWidget { 6 | const ButtonSwith({ 7 | Key? key, 8 | required this.value, 9 | required this.onChanged, 10 | this.loading = false, 11 | }) : super(key: key); 12 | 13 | final bool value; 14 | final Function(bool value) onChanged; 15 | final bool loading; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return TextButton( 20 | onPressed: loading 21 | ? null 22 | : () { 23 | onChanged(!value); 24 | }, 25 | child: Stack( 26 | alignment: Alignment.center, 27 | children: [ 28 | Row( 29 | mainAxisSize: MainAxisSize.min, 30 | children: [ 31 | Opacity( 32 | opacity: loading ? 0 : 1, 33 | child: Text( 34 | value ? 'Following' : 'Follow', 35 | style: TextStyle( 36 | color: primary, 37 | ), 38 | ), 39 | ), 40 | SizedBox(width: value ? 4 : 0), 41 | !value 42 | ? SizedBox() 43 | : Opacity( 44 | opacity: loading ? 0 : 1, 45 | child: Icon( 46 | Icons.check, 47 | color: primary, 48 | size: 14, 49 | ), 50 | ), 51 | ], 52 | ), 53 | !loading 54 | ? SizedBox() 55 | : Center( 56 | child: SizedBox( 57 | height: 16, 58 | width: 16, 59 | child: CircularProgressIndicator(), 60 | ), 61 | ), 62 | ], 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/widgets/inputs/index.dart: -------------------------------------------------------------------------------- 1 | export 'checkbox.dart'; 2 | export 'custom.switch.dart'; 3 | export 'index.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/widgets/listing/ad.feed_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 3 | import 'package:momentum/momentum.dart'; 4 | 5 | import '../../components/admob/index.dart'; 6 | import '../../components/supporter-subscription/index.dart'; 7 | 8 | class AdFeedTab extends StatefulWidget { 9 | AdFeedTab({Key? key}) : super(key: key); 10 | 11 | @override 12 | _AdFeedTabState createState() => _AdFeedTabState(); 13 | } 14 | 15 | class _AdFeedTabState extends State { 16 | AdmobController? _admobController; 17 | 18 | @override 19 | void didChangeDependencies() { 20 | super.didChangeDependencies(); 21 | _admobController = admobCtrl(context)..loadFeedTabAd(); 22 | } 23 | 24 | @override 25 | void dispose() { 26 | super.dispose(); 27 | _admobController?.disposeFeedTabAd(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return MomentumBuilder( 33 | controllers: [ 34 | AdmobController, 35 | SupporterSubscriptionController, 36 | ], 37 | builder: (context, snapshot) { 38 | final admob = snapshot(); 39 | if (!admob.showFeedTabAd) { 40 | return SizedBox(); 41 | } else { 42 | return SizedBox( 43 | height: 50, 44 | child: AdWidget( 45 | ad: admob.feedTabAd, 46 | ), 47 | ); 48 | } 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/widgets/listing/ad.library_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 3 | import 'package:momentum/momentum.dart'; 4 | 5 | import '../../components/admob/index.dart'; 6 | import '../../components/supporter-subscription/index.dart'; 7 | 8 | class AdLibraryTab extends StatefulWidget { 9 | AdLibraryTab({Key? key}) : super(key: key); 10 | 11 | @override 12 | _AdLibraryTabState createState() => _AdLibraryTabState(); 13 | } 14 | 15 | class _AdLibraryTabState extends State { 16 | AdmobController? _admobController; 17 | 18 | @override 19 | void didChangeDependencies() { 20 | super.didChangeDependencies(); 21 | _admobController = admobCtrl(context)..loadLibraryTabAd(); 22 | } 23 | 24 | @override 25 | void dispose() { 26 | super.dispose(); 27 | _admobController?.disposeLibraryAd(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return MomentumBuilder( 33 | controllers: [ 34 | AdmobController, 35 | SupporterSubscriptionController, 36 | ], 37 | builder: (context, snapshot) { 38 | final admob = snapshot(); 39 | if (!admob.showLibraryTabAd) { 40 | return SizedBox(); 41 | } else { 42 | return SizedBox( 43 | height: 50, 44 | child: AdWidget( 45 | ad: admob.libraryTabAd, 46 | ), 47 | ); 48 | } 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/widgets/listing/ad.sources_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 3 | import 'package:momentum/momentum.dart'; 4 | 5 | import '../../components/admob/index.dart'; 6 | import '../../components/supporter-subscription/index.dart'; 7 | 8 | class AdSourcesTab extends StatefulWidget { 9 | AdSourcesTab({Key? key}) : super(key: key); 10 | 11 | @override 12 | _AdSourcesTabState createState() => _AdSourcesTabState(); 13 | } 14 | 15 | class _AdSourcesTabState extends State { 16 | AdmobController? _admobController; 17 | 18 | @override 19 | void didChangeDependencies() { 20 | super.didChangeDependencies(); 21 | _admobController = admobCtrl(context)..loadSourcesTabAd(); 22 | } 23 | 24 | @override 25 | void dispose() { 26 | super.dispose(); 27 | _admobController?.disposeSourcesTabAd(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return MomentumBuilder( 33 | controllers: [ 34 | AdmobController, 35 | SupporterSubscriptionController, 36 | ], 37 | builder: (context, snapshot) { 38 | final admob = snapshot(); 39 | if (!admob.showSourcesTabAd) { 40 | return SizedBox(); 41 | } else { 42 | return SizedBox( 43 | height: 50, 44 | child: AdWidget( 45 | ad: admob.sourcesTabAd, 46 | ), 47 | ); 48 | } 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/widgets/listing/anime.item-integration.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | import '../../components/animelist/index.dart'; 4 | 5 | import '../../data/mal-user.animelist.dart'; 6 | import '../../data/response.all_anime.dart'; 7 | import '../syncing/mal-updater.dart'; 8 | 9 | class AnimeItemIntegrationAction extends StatelessWidget { 10 | const AnimeItemIntegrationAction({ 11 | Key? key, 12 | required this.item, 13 | required this.fallbackWidget, 14 | }) : super(key: key); 15 | 16 | final AnimeEntry item; 17 | final Widget fallbackWidget; // in case the MAL Status object is invalid or something. 18 | 19 | MalUserAnimeListStatus get malstatus => item.malStatus!; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | Color statusColor = Colors.grey; 24 | IconData icon = Icons.check; 25 | switch (malstatus.status) { 26 | case "watching": 27 | if (malstatus.numEpisodesWatched >= item.latestEpisode) { 28 | statusColor = Colors.green; 29 | } else { 30 | statusColor = Colors.orangeAccent; 31 | icon = Icons.warning_rounded; 32 | } 33 | break; 34 | case "plan_to_watch": 35 | statusColor = Colors.lightBlueAccent; 36 | icon = Icons.info_rounded; 37 | break; 38 | case "on_hold": 39 | statusColor = Colors.orangeAccent; 40 | icon = Icons.info_rounded; 41 | break; 42 | case "dropped": 43 | statusColor = Colors.redAccent; 44 | icon = Icons.info_rounded; 45 | break; 46 | default: 47 | return fallbackWidget; 48 | } 49 | 50 | return ClipRRect( 51 | borderRadius: BorderRadius.circular(100), 52 | child: SizedBox( 53 | height: 48, 54 | width: 48, 55 | child: TextButton( 56 | child: Icon( 57 | icon, 58 | color: statusColor, 59 | size: 24, 60 | ), 61 | onPressed: () { 62 | Momentum.controller(context).getUserAnimeDetails(item); 63 | showMalUpdater(context, item.slug); 64 | }, 65 | ), 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/widgets/listing/anime.list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../data/index.dart'; 4 | import '../index.dart'; 5 | import 'index.dart'; 6 | 7 | const _itemHeight = 66.0; 8 | 9 | class AnimeList extends StatelessWidget { 10 | const AnimeList({ 11 | Key? key, 12 | required this.list, 13 | this.showType = false, 14 | required this.topicLoading, 15 | }) : super(key: key); 16 | 17 | final List list; 18 | final bool showType; 19 | final bool Function(String topic) topicLoading; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | // await Momentum.controller(context).loadList(); 24 | return list.isEmpty 25 | ? EmptyList() 26 | : ListView.builder( 27 | itemCount: list.length, 28 | physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), 29 | itemExtent: _itemHeight, 30 | cacheExtent: _itemHeight * 10, 31 | itemBuilder: (context, index) { 32 | var item = list[index]; 33 | return AnimeItem( 34 | item: item, 35 | following: item.following, 36 | showType: showType, 37 | topicLoading: topicLoading(item.slug), 38 | ); 39 | }, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/widgets/listing/feed.item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:url_launcher/url_launcher.dart'; 3 | 4 | import '../../data/feed.response.dart'; 5 | import '../index.dart'; 6 | 7 | class FeedItemWidget extends StatelessWidget { 8 | const FeedItemWidget({ 9 | Key? key, 10 | required this.item, 11 | }) : super(key: key); 12 | 13 | final QuantzFeedItem item; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Tooltip( 18 | message: item.title, 19 | showDuration: Duration(milliseconds: item.tooltipDuration), 20 | child: ListTile( 21 | title: Text( 22 | item.title, 23 | overflow: TextOverflow.ellipsis, 24 | style: TextStyle( 25 | color: Colors.white.withOpacity(0.85), 26 | ), 27 | maxLines: 1, 28 | ), 29 | subtitle: Row( 30 | children: [ 31 | ClipRRect( 32 | borderRadius: BorderRadius.circular(3), 33 | child: Opacity( 34 | opacity: 0.5, // don't make the icon standout and distract the user. 35 | child: Image.asset( 36 | item.sourceImage, 37 | width: 12, 38 | height: 12, 39 | ), 40 | ), 41 | ), 42 | SizedBox(width: 4), 43 | Text( 44 | item.sourceDomain, 45 | style: TextStyle( 46 | color: Colors.white.withOpacity(0.4), 47 | ), 48 | ), 49 | Spacer(), 50 | Text( 51 | item.ago, 52 | style: TextStyle( 53 | color: Colors.white.withOpacity(0.4), 54 | ), 55 | ), 56 | ], 57 | ), 58 | minVerticalPadding: 8, 59 | onTap: () { 60 | try { 61 | launch(item.permalink); 62 | } catch (e) { 63 | showToast(e.toString(), error: true); 64 | } 65 | }, 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/widgets/listing/index.dart: -------------------------------------------------------------------------------- 1 | export 'ad.library_tab.dart'; 2 | export 'ad.sources_tab.dart'; 3 | export 'anime.item.dart'; 4 | export 'anime.list.dart'; 5 | export 'index.dart'; 6 | export 'menu-item.dart'; 7 | export 'news-source.item.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/widgets/listing/menu-item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MenuListItem extends StatelessWidget { 4 | const MenuListItem({ 5 | Key? key, 6 | this.title = '', 7 | this.titleWidget, 8 | this.subtitle = '', 9 | required this.icon, 10 | this.trail = const SizedBox(), 11 | this.onTap, 12 | }) : super(key: key); 13 | 14 | final String title; 15 | final Widget? titleWidget; 16 | final String subtitle; 17 | final IconData icon; 18 | final Widget trail; 19 | final VoidCallback? onTap; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return ListTile( 24 | leading: Container( 25 | height: 100, 26 | child: Icon(icon), 27 | ), 28 | title: titleWidget ?? 29 | Text( 30 | title, 31 | overflow: TextOverflow.ellipsis, 32 | maxLines: 1, 33 | ), 34 | subtitle: subtitle.isEmpty 35 | ? null 36 | : Text( 37 | subtitle, 38 | overflow: TextOverflow.ellipsis, 39 | maxLines: 1, 40 | ), 41 | trailing: trail, 42 | onTap: onTap ?? () {}, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/widgets/loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class Loader extends StatelessWidget { 6 | const Loader({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | final theme = ThemeData.dark(); 11 | return MaterialApp( 12 | title: 'Quantz', 13 | theme: theme.copyWith( 14 | primaryColor: primary, 15 | colorScheme: theme.colorScheme.copyWith( 16 | secondary: primary, 17 | ), 18 | backgroundColor: background, 19 | ), 20 | themeMode: ThemeMode.dark, 21 | debugShowCheckedModeBanner: false, 22 | home: Scaffold( 23 | body: Center( 24 | child: SizedBox( 25 | height: 36, 26 | width: 36, 27 | child: CircularProgressIndicator(), 28 | ), 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/filter_bottom_sheet/bottom_menu.display.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../../../components/filter/index.dart'; 5 | import '../../index.dart'; 6 | 7 | const _options = DisplayTitle.values; 8 | 9 | class DisplayOption extends StatelessWidget { 10 | const DisplayOption({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Material( 15 | color: Colors.transparent, 16 | child: MomentumBuilder( 17 | controllers: [FilterController], 18 | /* optimization */ 19 | dontRebuildIf: (ctrl, isTimeTravel) { 20 | final controller = ctrl(); 21 | final current = controller.model; 22 | final prev = controller.prevModel; 23 | return current.displayTitle == prev?.displayTitle; 24 | }, 25 | /* optimization */ 26 | builder: (context, snapshot) { 27 | final filter = snapshot(); 28 | return ListView.builder( 29 | itemCount: _options.length, 30 | itemBuilder: (context, index) { 31 | final item = _options[index]; 32 | final isSelected = item == filter.displayTitle; 33 | return ListTile( 34 | leading: isSelected 35 | ? Icon( 36 | Icons.check, 37 | color: primary, 38 | ) 39 | : SizedBox(), 40 | title: Text(displayTitleLabel(item)), 41 | onTap: () { 42 | final controller = Momentum.controller(context); 43 | controller.setDisplayTitle(item); 44 | }, 45 | ); 46 | }, 47 | ); 48 | }, 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/filter_bottom_sheet/bottom_menu.order.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../../../components/filter/index.dart'; 5 | import '../../index.dart'; 6 | 7 | const _options = OrderBy.values; 8 | 9 | class OrderByOption extends StatelessWidget { 10 | const OrderByOption({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Material( 15 | color: Colors.transparent, 16 | child: MomentumBuilder( 17 | controllers: [FilterController], 18 | /* optimization */ 19 | dontRebuildIf: (ctrl, isTimeTravel) { 20 | final controller = ctrl(); 21 | final current = controller.model; 22 | final prev = controller.prevModel; 23 | return current.orderBy == prev?.orderBy; 24 | }, 25 | /* optimization */ 26 | builder: (context, snapshot) { 27 | final filter = snapshot(); 28 | return ListView.builder( 29 | itemCount: _options.length, 30 | itemBuilder: (context, index) { 31 | final item = _options[index]; 32 | final isSelected = item == filter.orderBy; 33 | return ListTile( 34 | leading: isSelected 35 | ? Icon( 36 | Icons.check, 37 | color: primary, 38 | ) 39 | : SizedBox(), 40 | title: Text(orderByLabel(item)), 41 | onTap: () { 42 | final controller = Momentum.controller(context); 43 | controller.setOrderBy(item); 44 | }, 45 | ); 46 | }, 47 | ); 48 | }, 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/filter_bottom_sheet/bottom_menu.sort.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../../../components/filter/index.dart'; 5 | import '../../index.dart'; 6 | 7 | const _options = SortBy.values; 8 | 9 | class SortByOption extends StatelessWidget { 10 | const SortByOption({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Material( 15 | color: Colors.transparent, 16 | child: MomentumBuilder( 17 | controllers: [FilterController], 18 | /* optimization */ 19 | dontRebuildIf: (ctrl, isTimeTravel) { 20 | final controller = ctrl(); 21 | final current = controller.model; 22 | final prev = controller.prevModel; 23 | return current.sortBy == prev?.sortBy; 24 | }, 25 | /* optimization */ 26 | builder: (context, snapshot) { 27 | final filter = snapshot(); 28 | return ListView.builder( 29 | itemCount: _options.length, 30 | itemBuilder: (context, index) { 31 | final item = _options[index]; 32 | final isSelected = item == filter.sortBy; 33 | return ListTile( 34 | leading: isSelected 35 | ? Icon( 36 | Icons.check, 37 | color: primary, 38 | ) 39 | : SizedBox(), 40 | title: Text(sortByLabel(item)), 41 | onTap: () { 42 | final controller = Momentum.controller(context); 43 | controller.setSortBy(item); 44 | }, 45 | ); 46 | }, 47 | ); 48 | }, 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/filter_bottom_sheet/bottom_menu.status.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../../../components/filter/index.dart'; 5 | import '../../index.dart'; 6 | 7 | class StatusOption extends StatelessWidget { 8 | const StatusOption({Key? key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Material( 13 | color: Colors.transparent, 14 | child: MomentumBuilder( 15 | controllers: [FilterController], 16 | /* optimization */ 17 | dontRebuildIf: (ctrl, isTimeTravel) { 18 | final controller = ctrl(); 19 | final current = controller.model; 20 | final prev = controller.prevModel; 21 | return current.showOngoing == prev?.showOngoing && current.showUpcoming == prev?.showUpcoming; 22 | }, 23 | /* optimization */ 24 | builder: (context, snapshot) { 25 | final filter = snapshot(); 26 | return Column( 27 | children: [ 28 | ListTile( 29 | leading: filter.showOngoing 30 | ? Icon( 31 | Icons.check, 32 | color: primary, 33 | ) 34 | : SizedBox(), 35 | title: Text('Currently Airing'), 36 | onTap: () { 37 | final controller = Momentum.controller(context); 38 | controller.toggleOngoing(); 39 | }, 40 | ), 41 | ListTile( 42 | leading: filter.showUpcoming 43 | ? Icon( 44 | Icons.check, 45 | color: primary, 46 | ) 47 | : SizedBox(), 48 | title: Text('Upcoming'), 49 | onTap: () { 50 | final controller = Momentum.controller(context); 51 | controller.toggleUpcoming(); 52 | }, 53 | ), 54 | ], 55 | ); 56 | }, 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/filter_bottom_sheet/bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../index.dart'; 4 | import 'index.dart'; 5 | 6 | void showFilterBottomSheet(BuildContext context) { 7 | showModalBottomSheet( 8 | context: context, 9 | backgroundColor: Colors.transparent, 10 | builder: (context) { 11 | return _BottomSheetWidget(); 12 | }, 13 | ); 14 | } 15 | 16 | const _radius = BorderRadius.only( 17 | topLeft: Radius.circular(7), 18 | topRight: Radius.circular(7), 19 | ); 20 | 21 | class _BottomSheetWidget extends StatefulWidget { 22 | const _BottomSheetWidget({Key? key}) : super(key: key); 23 | 24 | @override 25 | __BottomSheetWidgetState createState() => __BottomSheetWidgetState(); 26 | } 27 | 28 | class __BottomSheetWidgetState extends State<_BottomSheetWidget> with SingleTickerProviderStateMixin { 29 | TabController? _tabController; 30 | TabController get tabController => _tabController!; 31 | 32 | @override 33 | void didChangeDependencies() { 34 | super.didChangeDependencies(); 35 | _tabController = TabController(length: 4, vsync: this); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return Container( 41 | height: 400, 42 | width: double.infinity, 43 | decoration: BoxDecoration( 44 | color: background, 45 | borderRadius: _radius, 46 | ), 47 | child: Column( 48 | children: [ 49 | _TabHeader(tabController: tabController), 50 | Expanded(child: _TabContent(tabController: tabController)), 51 | ], 52 | ), 53 | ); 54 | } 55 | } 56 | 57 | class _TabHeader extends StatelessWidget { 58 | const _TabHeader({ 59 | Key? key, 60 | required this.tabController, 61 | }) : super(key: key); 62 | 63 | final TabController tabController; 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return Container( 68 | decoration: BoxDecoration( 69 | color: secondaryBackground, 70 | borderRadius: _radius, 71 | ), 72 | child: TabBar( 73 | controller: tabController, 74 | indicatorSize: TabBarIndicatorSize.label, 75 | indicatorColor: primary, 76 | tabs: [ 77 | Tab( 78 | text: 'Title', 79 | ), 80 | Tab( 81 | text: 'Order by', 82 | ), 83 | Tab( 84 | text: 'Sort by', 85 | ), 86 | Tab( 87 | text: 'Status', 88 | ), 89 | ], 90 | ), 91 | ); 92 | } 93 | } 94 | 95 | class _TabContent extends StatelessWidget { 96 | const _TabContent({ 97 | Key? key, 98 | required this.tabController, 99 | }) : super(key: key); 100 | 101 | final TabController tabController; 102 | 103 | @override 104 | Widget build(BuildContext context) { 105 | return TabBarView( 106 | controller: tabController, 107 | children: [ 108 | DisplayOption(), 109 | OrderByOption(), 110 | SortByOption(), 111 | StatusOption(), 112 | ], 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/filter_bottom_sheet/index.dart: -------------------------------------------------------------------------------- 1 | export 'bottom_menu.display.dart'; 2 | export 'bottom_menu.order.dart'; 3 | export 'bottom_menu.sort.dart'; 4 | export 'bottom_menu.status.dart'; 5 | export 'bottom_sheet.dart'; 6 | export 'index.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/index.dart: -------------------------------------------------------------------------------- 1 | export 'index.dart'; 2 | export 'refresh_anime_list.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/more_page_menu/index.dart: -------------------------------------------------------------------------------- 1 | export 'index.dart'; 2 | export 'menu.backup-restore.dart'; 3 | export 'menu.mal-integration.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/more_page_menu/menu.backup-restore.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../../../components/cloud-backup/index.dart'; 5 | import '../../../components/google-flow/google-flow.controller.dart'; 6 | import '../../../components/google-flow/google-flow.model.dart'; 7 | import '../../index.dart'; 8 | import '../../listing/index.dart'; 9 | import '../../syncing/index.dart'; 10 | 11 | class MenuBackupRestore extends StatefulWidget { 12 | const MenuBackupRestore({Key? key}) : super(key: key); 13 | 14 | @override 15 | _MenuBackupRestoreState createState() => _MenuBackupRestoreState(); 16 | } 17 | 18 | class _MenuBackupRestoreState extends MomentumState { 19 | GoogleFlowController? flowController; 20 | 21 | @override 22 | void didChangeDependencies() { 23 | super.didChangeDependencies(); 24 | final controller = Momentum.controller(context); 25 | flowController = Momentum.controller(context); 26 | controller.listen( 27 | state: this, 28 | invoke: (event) async { 29 | switch (event) { 30 | case CloudbackupEvents.alreadySignedIn: 31 | final result = await showBackupSettings(context); 32 | controller.model.update(loading: false); 33 | switch (result) { 34 | case CloudbackupEvents.startNewBackup: 35 | showLoading(context, controller.startNewBackup()); 36 | break; 37 | case CloudbackupEvents.restoreFromLatest: 38 | showLoading(context, controller.restoreFromLatest()); 39 | break; 40 | case CloudbackupEvents.logoutGoogle: 41 | showLoading(context, flowController!.signout()); 42 | break; 43 | default: 44 | break; 45 | } 46 | break; 47 | default: 48 | break; 49 | } 50 | }, 51 | ); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return MomentumBuilder( 57 | controllers: [ 58 | CloudBackupController, 59 | GoogleFlowController, 60 | ], 61 | builder: (context, snapshot) { 62 | final cloud = snapshot(); 63 | final googleFlow = snapshot(); 64 | 65 | if (cloud.loading) { 66 | return MenuListItem( 67 | icon: Icons.cloud, 68 | titleWidget: LinearProgressIndicator(), 69 | subtitle: 'Backup & Restore', 70 | ); 71 | } 72 | 73 | return MenuListItem( 74 | title: 'Backup & Restore', 75 | subtitle: cloud.signedIn ? googleFlow.emailObscure : 'Google account required.', 76 | icon: Icons.cloud, 77 | trail: _LoginWidget(signedIn: cloud.signedIn), 78 | onTap: () { 79 | flowController?.showCloudBackup(); 80 | }, 81 | ); 82 | }, 83 | ); 84 | } 85 | } 86 | 87 | class _LoginWidget extends StatelessWidget { 88 | const _LoginWidget({ 89 | Key? key, 90 | required this.signedIn, 91 | }) : super(key: key); 92 | 93 | final bool signedIn; 94 | 95 | @override 96 | Widget build(BuildContext context) { 97 | return !signedIn 98 | ? Image.asset( 99 | 'assets/google_sign_in.png', 100 | width: 128, 101 | ) 102 | : Text( 103 | 'Settings', 104 | style: TextStyle( 105 | color: primary, 106 | ), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/more_page_menu/menu.github.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:url_launcher/url_launcher.dart'; 3 | 4 | import '../../listing/index.dart'; 5 | 6 | class MenuGithubLink extends StatelessWidget { 7 | const MenuGithubLink({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return MenuListItem( 12 | title: 'GitHub Repo', 13 | icon: Icons.link, 14 | onTap: () async { 15 | try { 16 | launch('https://github.com/xamantra/quantz-app'); 17 | } catch (e) { 18 | print(e); 19 | } 20 | }, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/more_page_menu/menu.licenses.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../core/version.dart'; 4 | import '../../listing/index.dart'; 5 | 6 | class MenuAppLicenses extends StatelessWidget { 7 | const MenuAppLicenses({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return MenuListItem( 12 | title: 'License', 13 | icon: Icons.snippet_folder, 14 | onTap: () async { 15 | showLicensePage( 16 | context: context, 17 | applicationName: 'Quantz', 18 | applicationLegalese: 'dev.xamantra.quantz', 19 | applicationVersion: appVersion, 20 | applicationIcon: Center( 21 | child: Padding( 22 | padding: const EdgeInsets.all(8.0), 23 | child: Image.asset( 24 | 'assets/q.png', 25 | height: 48, 26 | ), 27 | ), 28 | ), 29 | ); 30 | }, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/more_page_menu/menu.mal-integration.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | import 'package:url_launcher/url_launcher.dart'; 4 | 5 | import '../../../components/integration/index.dart'; 6 | import '../../index.dart'; 7 | import '../../listing/index.dart'; 8 | import '../../syncing/integration.mal.dart'; 9 | 10 | class MenuMalImport extends StatelessWidget { 11 | const MenuMalImport({Key? key}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return MomentumBuilder( 16 | controllers: [IntegrationController], 17 | builder: (context, snapshot) { 18 | var import = snapshot(); 19 | 20 | final loggedIn = import.loggedIn; 21 | 22 | if (import.loading) { 23 | return MenuListItem( 24 | icon: Icons.sync, 25 | titleWidget: LinearProgressIndicator(), 26 | subtitle: 'MyAnimeList Integration', 27 | ); 28 | } 29 | 30 | return MenuListItem( 31 | title: 'MyAnimeList Integration', 32 | subtitle: !loggedIn ? 'Login required.' : 'You\'re logged in.', 33 | icon: Icons.sync, 34 | trail: Text( 35 | !loggedIn ? 'Import Now' : import.malUsername, 36 | style: TextStyle( 37 | color: primary, 38 | ), 39 | ), 40 | onTap: () async { 41 | try { 42 | final url = await import.controller.getLoginUrl(); 43 | if (url.isEmpty) { 44 | final started = await showImportMAL(context); 45 | if (started) { 46 | await Future.delayed(Duration(seconds: 1)); 47 | await showMalImportProgress(context); 48 | } 49 | return; 50 | } 51 | launch(url); 52 | } catch (e) { 53 | print(e); 54 | } 55 | }, 56 | ); 57 | }, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/more_page_menu/menu.support-dev.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | import '../../../components/google-flow/google-flow.controller.dart'; 4 | 5 | import '../../../components/supporter-subscription/index.dart'; 6 | import '../../index.dart'; 7 | import '../../listing/index.dart'; 8 | 9 | class SupportTheDeveloper extends StatelessWidget { 10 | const SupportTheDeveloper({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return MomentumBuilder( 15 | controllers: [SupporterSubscriptionController], 16 | builder: (context, snapshot) { 17 | var subscription = snapshot(); 18 | final isActive = subscription.subscriptionActive; 19 | 20 | if (subscription.loading) { 21 | return MenuListItem( 22 | icon: Icons.attach_money, 23 | titleWidget: LinearProgressIndicator(), 24 | subtitle: 'Support the developer', 25 | ); 26 | } 27 | 28 | return MenuListItem( 29 | title: 'Support the developer', 30 | subtitle: 'Donate \$1.00 monthly to support development.', 31 | icon: Icons.attach_money, 32 | trail: !isActive 33 | ? Text( 34 | 'Donate', 35 | style: TextStyle( 36 | color: primary, 37 | ), 38 | ) 39 | : Icon( 40 | Icons.check_circle, 41 | color: Colors.green, 42 | ), 43 | onTap: () async { 44 | Momentum.controller(context).getSupporterSubscription(); 45 | }, 46 | ); 47 | }, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/news_guide/guide.item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GuideItem extends StatelessWidget { 4 | const GuideItem({ 5 | Key? key, 6 | required this.title, 7 | required this.body, 8 | }) : super(key: key); 9 | 10 | final String title; 11 | final String body; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Column( 16 | crossAxisAlignment: CrossAxisAlignment.start, 17 | mainAxisSize: MainAxisSize.min, 18 | children: [ 19 | Text( 20 | title, 21 | style: Theme.of(context).textTheme.subtitle1, 22 | ), 23 | SizedBox(height: 2), 24 | Text( 25 | body, 26 | style: Theme.of(context).textTheme.bodyText2?.copyWith( 27 | color: Colors.white.withOpacity(0.7), 28 | ), 29 | ), 30 | ], 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/news_guide/index.dart: -------------------------------------------------------------------------------- 1 | export 'guide.item.dart'; 2 | export 'index.dart'; 3 | export 'sources_guide_prompt.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/news_guide/sources_guide_prompt.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | void showSourcesGuide(BuildContext context) { 6 | showDialog( 7 | context: context, 8 | builder: (context) { 9 | return _SourcesGuidePrompt(); 10 | }, 11 | ); 12 | } 13 | 14 | class _SourcesGuidePrompt extends StatelessWidget { 15 | const _SourcesGuidePrompt({Key? key}) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Dialog( 20 | child: Padding( 21 | padding: const EdgeInsets.all(12), 22 | child: Column( 23 | mainAxisSize: MainAxisSize.min, 24 | children: [ 25 | GuideItem( 26 | title: 'AnimeNewsNetwork', 27 | body: 'Updates for anime, manga, the industry and also covid-19 updates related to anime or manga.', 28 | ), 29 | Divider(), 30 | GuideItem( 31 | title: 'Livechart Headlines', 32 | body: 'Anime news from many different sources curated by the LiveChart.me team to be quickly browsable.', 33 | ), 34 | Divider(), 35 | GuideItem( 36 | title: 'MyAnimeList', 37 | body: 'News about new anime announcement, original, manga, or light novel adaptations. Also includes news about manga and people like authors, voice actors or staffs.', 38 | ), 39 | Divider(), 40 | GuideItem( 41 | title: 'r/anime - Subreddit', 42 | body: 'Notifies for "News" and "Official Media" flair post from the subreddit. There are curated list of trusted users to notify while untrusted users get awaited for 1 hour for post deletion.', 43 | ), 44 | ], 45 | ), 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/widgets/menu_actions/refresh_anime_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../../components/animelist/index.dart'; 5 | 6 | class AnimeListRefresher extends StatelessWidget { 7 | const AnimeListRefresher({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return MomentumBuilder( 12 | controllers: [AnimelistController], 13 | builder: (context, snapshot) { 14 | var animeList = snapshot(); 15 | 16 | if (animeList.refreshingList) { 17 | return Center( 18 | child: Padding( 19 | padding: const EdgeInsets.all(8), 20 | child: SizedBox( 21 | height: 16, 22 | width: 16, 23 | child: CircularProgressIndicator( 24 | valueColor: AlwaysStoppedAnimation(Colors.white), 25 | strokeWidth: 3, 26 | ), 27 | ), 28 | ), 29 | ); 30 | } 31 | 32 | return IconButton( 33 | onPressed: () { 34 | animeList.controller.refreshAnimeList(); 35 | }, 36 | icon: Icon(Icons.refresh), 37 | ); 38 | }, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/widgets/pages/core.dashboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../index.dart'; 4 | import 'index.dart'; 5 | import 'page.feed.dart'; 6 | 7 | class DashboardPage extends StatefulWidget { 8 | const DashboardPage({Key? key}) : super(key: key); 9 | 10 | @override 11 | _DashboardPageState createState() => _DashboardPageState(); 12 | } 13 | 14 | class _DashboardPageState extends State with SingleTickerProviderStateMixin { 15 | double statusBar = 0; 16 | 17 | @override 18 | void didChangeDependencies() { 19 | super.didChangeDependencies(); 20 | statusBar = MediaQuery.of(context).padding.top; 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Scaffold( 26 | backgroundColor: Colors.transparent, 27 | body: Stack( 28 | children: [ 29 | Container( 30 | height: statusBar, 31 | width: double.infinity, 32 | color: secondaryBackground, 33 | ), 34 | SafeArea( 35 | child: Container( 36 | color: background, 37 | child: TabviewWidget( 38 | views: [ 39 | LibraryPage(), 40 | FeedPage(), 41 | SourcesPage(), 42 | MorePage(), 43 | ], 44 | tabs: [ 45 | Tab( 46 | icon: Icon(Icons.collections_bookmark), 47 | iconMargin: EdgeInsets.zero, 48 | text: 'Library', 49 | ), 50 | Tab( 51 | icon: Icon(Icons.rss_feed), 52 | iconMargin: EdgeInsets.zero, 53 | text: 'Feed', 54 | ), 55 | Tab( 56 | icon: Icon(Icons.source), 57 | iconMargin: EdgeInsets.zero, 58 | text: 'Sources', 59 | ), 60 | Tab( 61 | icon: Icon(Icons.more_horiz), 62 | iconMargin: EdgeInsets.zero, 63 | text: 'More', 64 | ), 65 | ], 66 | ), 67 | ), 68 | ), 69 | ], 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/widgets/pages/index.dart: -------------------------------------------------------------------------------- 1 | export 'core.dashboard.dart'; 2 | export 'index.dart'; 3 | export 'page.library.dart'; 4 | export 'page.more.dart'; 5 | export 'page.sources.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/widgets/pages/page.feed.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 4 | import 'package:quantz/src/widgets/refreshing.dart'; 5 | 6 | import '../../components/feed/index.dart'; 7 | import '../colors.dart'; 8 | import '../listing/ad.feed_tab.dart'; 9 | import '../listing/feed.item.dart'; 10 | 11 | class FeedPage extends StatelessWidget { 12 | FeedPage({Key? key}) : super(key: key); 13 | 14 | final RefreshController controller = RefreshController(); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: Text('Feed'), 21 | backgroundColor: secondaryBackground, 22 | elevation: 0.80, 23 | ), 24 | body: MomentumBuilder( 25 | controllers: [FeedController], 26 | builder: (context, snapshot) { 27 | final feed = snapshot(); 28 | final items = feed.feed.items; 29 | return Column( 30 | children: [ 31 | RefreshingWidget( 32 | value: feed.loading, 33 | text: "Getting feed ...", 34 | ), 35 | Expanded( 36 | child: SmartRefresher( 37 | enablePullDown: true, 38 | enablePullUp: true, 39 | physics: BouncingScrollPhysics(), 40 | header: WaterDropMaterialHeader( 41 | backgroundColor: secondaryBackground, 42 | ), 43 | onRefresh: () async { 44 | await feed.controller.loadInitial(refresh: true); 45 | controller.refreshCompleted(); 46 | }, 47 | onLoading: () async { 48 | await feed.controller.loadMore(); 49 | controller.loadComplete(); 50 | }, 51 | controller: controller, 52 | child: ListView.builder( 53 | itemCount: items.length, 54 | physics: BouncingScrollPhysics(), 55 | itemBuilder: (context, index) { 56 | return FeedItemWidget(item: items[index]); 57 | }, 58 | ), 59 | ), 60 | ), 61 | AdFeedTab(), 62 | ], 63 | ); 64 | }, 65 | ), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/widgets/pages/page.more.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../index.dart'; 4 | import '../menu_actions/more_page_menu/index.dart'; 5 | import '../menu_actions/more_page_menu/menu.github.dart'; 6 | import '../menu_actions/more_page_menu/menu.licenses.dart'; 7 | import '../menu_actions/more_page_menu/menu.support-dev.dart'; 8 | 9 | class MorePage extends StatelessWidget { 10 | const MorePage({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text('More'), 17 | backgroundColor: secondaryBackground, 18 | elevation: 0, 19 | ), 20 | body: Column( 21 | children: [ 22 | Expanded( 23 | flex: 16, 24 | child: Container( 25 | width: double.infinity, 26 | decoration: BoxDecoration( 27 | color: secondaryBackground, 28 | boxShadow: [getShadow(-0.5)], 29 | ), 30 | child: Center( 31 | child: Image.asset( 32 | 'assets/q.png', 33 | width: 64, 34 | ), 35 | ), 36 | ), 37 | ), 38 | Expanded( 39 | flex: 80, 40 | child: Container( 41 | width: double.infinity, 42 | child: Column( 43 | children: [ 44 | MenuMalImport(), 45 | Divider(height: 1), 46 | MenuBackupRestore(), 47 | Divider(height: 1), 48 | SupportTheDeveloper(), 49 | Divider(height: 1), 50 | MenuAppLicenses(), 51 | Divider(height: 1), 52 | MenuGithubLink(), 53 | ], 54 | ), 55 | ), 56 | ), 57 | ], 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/widgets/pages/page.sources.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../../components/sources/index.dart'; 5 | import '../index.dart'; 6 | import '../listing/index.dart'; 7 | import '../menu_actions/news_guide/index.dart'; 8 | 9 | class SourcesPage extends StatelessWidget { 10 | const SourcesPage({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text('Sources'), 17 | backgroundColor: secondaryBackground, 18 | elevation: 0.80, 19 | actions: [ 20 | IconButton( 21 | icon: Icon(Icons.help_outline), 22 | onPressed: () { 23 | showSourcesGuide(context); 24 | }, 25 | ), 26 | ], 27 | ), 28 | body: MomentumBuilder( 29 | controllers: [SourcesController], 30 | builder: (context, snapshot) { 31 | var sources = snapshot(); 32 | if (sources.loading) { 33 | return Center( 34 | child: SizedBox( 35 | height: 36, 36 | width: 36, 37 | child: CircularProgressIndicator(), 38 | ), 39 | ); 40 | } 41 | return Column( 42 | children: [ 43 | Expanded( 44 | child: ListView.builder( 45 | itemCount: sourcesList.length, 46 | // physics: BouncingScrollPhysics(), 47 | itemBuilder: (context, index) { 48 | var item = sources.sourcesSubscriptionList[index]; 49 | return NewsSourceItem( 50 | item: item, 51 | following: item.following, 52 | isLastItem: index == sources.sourcesSubscriptionList.length - 1, 53 | ); 54 | }, 55 | ), 56 | ), 57 | AdSourcesTab(), 58 | ], 59 | ); 60 | }, 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/widgets/refreshing.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RefreshingWidget extends StatelessWidget { 4 | const RefreshingWidget({ 5 | Key? key, 6 | required this.value, 7 | this.text, 8 | }) : super(key: key); 9 | 10 | final bool value; 11 | final String? text; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | // if (!value) return SizedBox(); 16 | 17 | return AnimatedContainer( 18 | height: value ? 32 : 0, 19 | width: double.infinity, 20 | duration: Duration(milliseconds: 500), 21 | decoration: BoxDecoration( 22 | color: Theme.of(context).primaryColor, 23 | ), 24 | child: Row( 25 | mainAxisAlignment: MainAxisAlignment.center, 26 | crossAxisAlignment: CrossAxisAlignment.center, 27 | mainAxisSize: MainAxisSize.max, 28 | children: [ 29 | Text( 30 | text ?? 'Refreshing data ...', 31 | style: TextStyle( 32 | fontSize: value ? 14 : 0, 33 | ), 34 | ), 35 | SizedBox(width: 12), 36 | SizedBox( 37 | height: value ? 16 : 0, 38 | width: value ? 16 : 0, 39 | child: CircularProgressIndicator( 40 | color: Colors.white, 41 | ), 42 | ), 43 | ], 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/widgets/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | 4 | import '../components/animelist/index.dart'; 5 | 6 | class SearchWidget extends StatefulWidget { 7 | const SearchWidget({Key? key}) : super(key: key); 8 | 9 | @override 10 | _SearchWidgetState createState() => _SearchWidgetState(); 11 | } 12 | 13 | class _SearchWidgetState extends State { 14 | final controller = TextEditingController(text: ''); 15 | AnimelistController? listController; 16 | 17 | @override 18 | void didChangeDependencies() { 19 | super.didChangeDependencies(); 20 | listController = Momentum.controller(context); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Container( 26 | padding: EdgeInsets.symmetric(vertical: 4, horizontal: 12), 27 | decoration: BoxDecoration( 28 | color: Color(0xff151515), 29 | borderRadius: BorderRadius.circular(100), 30 | ), 31 | child: Row( 32 | children: [ 33 | Expanded( 34 | child: TextFormField( 35 | controller: controller, 36 | decoration: InputDecoration( 37 | border: InputBorder.none, 38 | errorBorder: InputBorder.none, 39 | enabledBorder: InputBorder.none, 40 | focusedBorder: InputBorder.none, 41 | disabledBorder: InputBorder.none, 42 | focusedErrorBorder: InputBorder.none, 43 | hintText: 'Search', 44 | isDense: true, 45 | ), 46 | maxLines: 1, 47 | onChanged: (value) { 48 | listController?.search(value); 49 | }, 50 | ), 51 | ), 52 | Container( 53 | width: 32, 54 | child: TextButton( 55 | child: Icon( 56 | Icons.close, 57 | color: Colors.white.withOpacity(0.7), 58 | ), 59 | style: ButtonStyle( 60 | tapTargetSize: MaterialTapTargetSize.shrinkWrap, 61 | foregroundColor: MaterialStateProperty.all(Colors.white), 62 | padding: MaterialStateProperty.all(EdgeInsets.zero), 63 | ), 64 | onPressed: () { 65 | controller.clear(); 66 | listController?.search(''); 67 | FocusScope.of(context).unfocus(); 68 | }, 69 | ), 70 | ), 71 | ], 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/widgets/shadow.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | BoxShadow getShadow(double offsetY) { 4 | return BoxShadow( 5 | color: Colors.black.withOpacity(0.05), 6 | spreadRadius: 2, 7 | blurRadius: 2, 8 | offset: Offset(0, offsetY), 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/widgets/syncing/cloud-backup-restore.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:momentum/momentum.dart'; 3 | import 'package:timeago/timeago.dart' as timeago; 4 | 5 | import '../../components/cloud-backup/index.dart'; 6 | 7 | Future showBackupSettings(BuildContext context) async { 8 | final popped = await showDialog(context: context, builder: (_) => _CloudBackupRestorePrompt()); 9 | return popped ?? CloudbackupEvents.none; 10 | } 11 | 12 | class _CloudBackupRestorePrompt extends StatelessWidget { 13 | const _CloudBackupRestorePrompt({Key? key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Dialog( 18 | child: MomentumBuilder( 19 | controllers: [CloudBackupController], 20 | builder: (context, snapshot) { 21 | final cloud = snapshot(); 22 | return Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | mainAxisSize: MainAxisSize.min, 25 | children: [ 26 | _NewBackupMenu(cloud: cloud), 27 | _RestoreLatestWidget(cloud: cloud), 28 | _LogoutGoogleWidget(), 29 | ], 30 | ); 31 | }, 32 | ), 33 | ); 34 | } 35 | } 36 | 37 | class _LogoutGoogleWidget extends StatelessWidget { 38 | const _LogoutGoogleWidget({ 39 | Key? key, 40 | }) : super(key: key); 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return ListTile( 45 | title: Text( 46 | 'Signout from Google', 47 | style: TextStyle( 48 | color: Colors.redAccent, 49 | ), 50 | ), 51 | onTap: () { 52 | Navigator.pop(context, CloudbackupEvents.logoutGoogle); 53 | }, 54 | ); 55 | } 56 | } 57 | 58 | class _RestoreLatestWidget extends StatelessWidget { 59 | const _RestoreLatestWidget({ 60 | Key? key, 61 | required this.cloud, 62 | }) : super(key: key); 63 | 64 | final CloudBackupModel cloud; 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return ListTile( 69 | title: Text('Restore from Latest'), 70 | subtitle: cloud.hasLastRestored ? Text(timeago.format(cloud.lastRestore!)) : SizedBox(), 71 | onTap: () { 72 | Navigator.pop(context, CloudbackupEvents.restoreFromLatest); 73 | }, 74 | ); 75 | } 76 | } 77 | 78 | class _NewBackupMenu extends StatelessWidget { 79 | const _NewBackupMenu({ 80 | Key? key, 81 | required this.cloud, 82 | }) : super(key: key); 83 | 84 | final CloudBackupModel cloud; 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | return ListTile( 89 | title: Text('New Backup'), 90 | subtitle: cloud.hasLatestBackup ? Text(timeago.format(cloud.latestBackupInfo.updatedAt!)) : SizedBox(), 91 | onTap: () { 92 | Navigator.pop(context, CloudbackupEvents.startNewBackup); 93 | }, 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/widgets/syncing/index.dart: -------------------------------------------------------------------------------- 1 | export 'cloud-backup-restore.dart'; 2 | export 'integration.mal.dart'; 3 | export 'index.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/widgets/tabview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class TabviewWidget extends StatefulWidget { 6 | const TabviewWidget({ 7 | Key? key, 8 | required this.views, 9 | required this.tabs, 10 | }) : super(key: key); 11 | 12 | final List views; 13 | final List tabs; 14 | 15 | @override 16 | _TabviewWidgetState createState() => _TabviewWidgetState(); 17 | } 18 | 19 | class _TabviewWidgetState extends State with SingleTickerProviderStateMixin { 20 | TabController? tabController; 21 | 22 | @override 23 | void didChangeDependencies() { 24 | super.didChangeDependencies(); 25 | tabController = TabController(initialIndex: 0, length: widget.views.length, vsync: this); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Column( 31 | children: [ 32 | Expanded( 33 | child: TabBarView( 34 | controller: tabController, 35 | children: widget.views, 36 | ), 37 | ), 38 | Container( 39 | height: 56, 40 | decoration: BoxDecoration( 41 | color: secondaryBackground, 42 | boxShadow: [getShadow(-0.5)], 43 | ), 44 | child: TabBar( 45 | controller: tabController, 46 | tabs: widget.tabs, 47 | indicatorSize: TabBarIndicatorSize.label, 48 | indicatorColor: Colors.transparent, 49 | labelPadding: EdgeInsets.zero, 50 | indicatorPadding: EdgeInsets.zero, 51 | labelColor: primary, 52 | unselectedLabelColor: Colors.white.withOpacity(0.7), 53 | indicator: BoxDecoration(color: Colors.transparent), 54 | ), 55 | ), 56 | ], 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/widgets/thumbnail.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ThumbnailImg extends StatelessWidget { 5 | const ThumbnailImg({ 6 | Key? key, 7 | required this.url, 8 | this.background, 9 | required this.size, 10 | }) : super(key: key); 11 | 12 | final String url; 13 | final Color? background; 14 | final double size; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ClipRRect( 19 | borderRadius: BorderRadius.circular(6), 20 | child: CachedNetworkImage( 21 | imageUrl: url, 22 | height: size, 23 | width: size, 24 | fit: BoxFit.fitWidth, 25 | placeholder: (context, url) => Container( 26 | height: size, 27 | width: size, 28 | decoration: BoxDecoration( 29 | color: Colors.white.withOpacity(0.1), 30 | borderRadius: BorderRadius.circular(6), 31 | ), 32 | ), 33 | errorWidget: (context, url, error) => Icon( 34 | Icons.error_outline, 35 | color: Colors.red, 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/widgets/toast.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluttertoast/fluttertoast.dart'; 3 | 4 | void showToast( 5 | String message, { 6 | bool error = false, 7 | }) { 8 | Fluttertoast.showToast( 9 | msg: message, 10 | toastLength: Toast.LENGTH_SHORT, 11 | gravity: ToastGravity.BOTTOM, 12 | timeInSecForIosWeb: 1, 13 | backgroundColor: error ? Colors.red : Colors.green, 14 | textColor: Colors.white, 15 | fontSize: 16.0, 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: quantz 2 | description: A new Flutter project. 3 | publish_to: 'none' 4 | version: 0.0.1+1 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | cupertino_icons: ^1.0.2 14 | 15 | firebase_core: 1.14.0 16 | firebase_messaging: 11.2.12 17 | google_sign_in: 5.2.4 18 | google_mobile_ads: 1.2.0 19 | in_app_purchase: 3.0.2 20 | 21 | momentum: 2.2.1 22 | shared_preferences: 2.0.13 23 | fluttertoast: 8.0.9 24 | flutter_fgbg: 0.2.0 25 | flutter_local_notifications: 9.4.0 26 | dio: 4.0.6 27 | timeago: 3.2.2 28 | url_launcher: 6.0.20 29 | jwt_decoder: 2.0.1 30 | cached_network_image: 3.2.0 31 | uni_links: 0.5.1 32 | random_string: 2.3.1 33 | equatable: 2.0.3 34 | pull_to_refresh: 2.0.0 35 | package_info_plus: 1.4.2 36 | intl: 0.17.0 37 | 38 | dev_dependencies: 39 | flutter_test: 40 | sdk: flutter 41 | 42 | flutter_launcher_icons: 0.9.2 43 | 44 | dependency_overrides: 45 | platform: ^3.1.0 46 | 47 | flutter: 48 | 49 | uses-material-design: true 50 | 51 | assets: 52 | - 'assets/' 53 | - 'assets/sources-icons/' 54 | 55 | flutter_icons: 56 | android: "launcher_icon" 57 | ios: true 58 | image_path: "assets/icon.png" 59 | remove_alpha_ios: true 60 | -------------------------------------------------------------------------------- /test/interface_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:momentum/momentum.dart'; 4 | import 'package:quantz/src/core/index.dart'; 5 | import 'package:quantz/src/services/interface/api.interface.dart'; 6 | import 'package:quantz/src/services/interface/google-api.interface.dart'; 7 | import 'package:quantz/src/services/interface/mal.interface.dart'; 8 | 9 | void main() { 10 | test('services interface should be abstracted properly', () async { 11 | TestWidgetsFlutterBinding.ensureInitialized(); 12 | final tester = MomentumTester( 13 | Momentum( 14 | controllers: [], 15 | services: services(), 16 | ), 17 | ); 18 | 19 | await tester.init(); 20 | 21 | final apiService = tester.service(runtimeType: false); 22 | print(['API_SERVICE', apiService.hashCode]); 23 | expect(apiService, isInstanceOf()); 24 | 25 | final googleApiService = tester.service(runtimeType: false); 26 | print(['GOOGLE_API_SERVICE', googleApiService.hashCode]); 27 | expect(googleApiService, isInstanceOf()); 28 | 29 | final malService = tester.service(runtimeType: false); 30 | print(['MAL_SERVICE', malService.hashCode]); 31 | expect(malService, isInstanceOf()); 32 | }); 33 | 34 | testWidgets('(widget tree) services interface should be abstracted properly', (tester) async { 35 | final widget = Momentum( 36 | child: MaterialApp(home: Scaffold( 37 | body: Builder( 38 | builder: (context) { 39 | Momentum.service(context, runtimeType: false); 40 | Momentum.service(context, runtimeType: false); 41 | Momentum.service(context, runtimeType: false); 42 | // throws an error if interface implementations aren't found. 43 | return Text('WIDGET_BUILT'); // if not, the abstraction worked. 44 | }, 45 | ), 46 | )), 47 | controllers: [], 48 | services: services(), 49 | ); 50 | 51 | await tester.pumpWidget(widget); 52 | await tester.pumpAndSettle(); 53 | 54 | expect(find.text('WIDGET_BUILT'), findsOneWidget); 55 | }); 56 | 57 | testWidgets('(widget tree) services interface should throw an error for non-existent interface', (tester) async { 58 | final widget = Momentum( 59 | child: MaterialApp(home: Scaffold( 60 | body: Builder( 61 | builder: (context) { 62 | // this should throw an error 63 | Momentum.service(context, runtimeType: false); 64 | return Text('WIDGET_BUILT'); 65 | }, 66 | ), 67 | )), 68 | controllers: [], 69 | services: services(), 70 | ); 71 | 72 | await tester.pumpWidget(widget); 73 | await tester.pumpAndSettle(); 74 | 75 | expect(tester.takeException(), isInstanceOf()); 76 | }); 77 | } 78 | 79 | abstract class DummyInterface extends MomentumService {} 80 | --------------------------------------------------------------------------------