├── .github └── workflows │ ├── build.yml │ └── check.yml ├── .gitignore ├── .metadata ├── .travis.yml ├── LICENSE ├── README.md ├── android ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── loshine │ │ │ │ └── flutternga │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── plugins │ │ │ │ └── login │ │ │ │ │ ├── CookiesEventHandler.kt │ │ │ │ │ ├── FlutterCookiesPlugin.kt │ │ │ │ │ ├── FlutterGallerySaverPlugin.kt │ │ │ │ │ └── FlutterLoginPlugin.kt │ │ │ │ └── ui │ │ │ │ └── LoginActivity.kt │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── layout │ │ │ └── activity_login.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── profile │ │ └── AndroidManifest.xml │ │ └── test │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── loshine │ │ └── flutternga │ │ └── ExampleUnitTest.kt ├── build.gradle ├── flutter_nga_android.iml ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── settings_aar.gradle ├── images └── default_forum_icon.png ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── 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 │ └── main.m ├── lib ├── data │ ├── data.dart │ ├── entity │ │ ├── block.dart │ │ ├── child_forum.dart │ │ ├── conversation.dart │ │ ├── emoticon.dart │ │ ├── forum.dart │ │ ├── message.dart │ │ ├── notification.dart │ │ ├── site_api_result.dart │ │ ├── toggle_like_reaction.dart │ │ ├── topic.dart │ │ ├── topic_detail.dart │ │ ├── topic_history.dart │ │ ├── topic_tag.dart │ │ └── user.dart │ ├── http.dart │ ├── repository │ │ ├── expression_repository.dart │ │ ├── forum_repository.dart │ │ ├── message_repository.dart │ │ ├── resource_repository.dart │ │ ├── topic_repository.dart │ │ └── user_repository.dart │ └── usecase │ │ ├── topic │ │ └── get_topic_list_use_case.dart │ │ └── use_case.dart ├── main.dart ├── my_app.dart ├── plugins │ ├── gallery_saver.dart │ └── login.dart ├── store │ ├── common │ │ ├── photo_min_scale_store.dart │ │ └── photo_min_scale_store.g.dart │ ├── forum │ │ ├── child_forum_subscription_store.dart │ │ ├── child_forum_subscription_store.g.dart │ │ ├── favourite_forum_list_store.dart │ │ ├── favourite_forum_list_store.g.dart │ │ ├── favourite_forum_store.dart │ │ ├── favourite_forum_store.g.dart │ │ ├── forum_detail_store.dart │ │ ├── forum_detail_store.g.dart │ │ ├── forum_tag_list_store.dart │ │ └── forum_tag_list_store.g.dart │ ├── home │ │ ├── home_drawer_header_store.dart │ │ ├── home_drawer_header_store.g.dart │ │ ├── home_store.dart │ │ └── home_store.g.dart │ ├── message │ │ ├── conversation_detail_store.dart │ │ ├── conversation_detail_store.g.dart │ │ ├── conversation_list_store.dart │ │ ├── conversation_list_store.g.dart │ │ ├── send_message_store.dart │ │ └── send_message_store.g.dart │ ├── notification │ │ ├── notification_list_store.dart │ │ └── notification_list_store.g.dart │ ├── search │ │ ├── input_deletion_status_store.dart │ │ ├── input_deletion_status_store.g.dart │ │ ├── search_forum_store.dart │ │ ├── search_forum_store.g.dart │ │ ├── search_options_store.dart │ │ ├── search_options_store.g.dart │ │ ├── search_topic_list_store.dart │ │ └── search_topic_list_store.g.dart │ ├── settings │ │ ├── blocklist_settings_store.dart │ │ ├── blocklist_settings_store.g.dart │ │ ├── display_mode_store.dart │ │ ├── display_mode_store.g.dart │ │ ├── interface_settings_store.dart │ │ ├── interface_settings_store.g.dart │ │ ├── theme_store.dart │ │ └── theme_store.g.dart │ ├── topic │ │ ├── favourite_topic_list_store.dart │ │ ├── favourite_topic_list_store.g.dart │ │ ├── topic_detail_store.dart │ │ ├── topic_detail_store.g.dart │ │ ├── topic_history_list_store.dart │ │ ├── topic_history_list_store.g.dart │ │ ├── topic_history_store.dart │ │ ├── topic_history_store.g.dart │ │ ├── topic_reply_store.dart │ │ ├── topic_reply_store.g.dart │ │ ├── topic_single_page_store.dart │ │ └── topic_single_page_store.g.dart │ └── user │ │ ├── account_list_store.dart │ │ ├── account_list_store.g.dart │ │ ├── user_info_store.dart │ │ ├── user_info_store.g.dart │ │ ├── user_replies_store.dart │ │ ├── user_replies_store.g.dart │ │ ├── user_topics_store.dart │ │ └── user_topics_store.g.dart ├── ui │ ├── page │ │ ├── account_management │ │ │ └── account_management_page.dart │ │ ├── content │ │ │ └── content_details_page.dart │ │ ├── conversation │ │ │ ├── conversation_detail_page.dart │ │ │ ├── conversation_item_widget.dart │ │ │ ├── conversation_list_page.dart │ │ │ └── message_item_widget.dart │ │ ├── favourite_topic_list │ │ │ └── favourite_topic_list_page.dart │ │ ├── forum_detail │ │ │ ├── child_forum_item_widget.dart │ │ │ ├── child_forum_list_page.dart │ │ │ ├── forum_detail_page.dart │ │ │ ├── forum_favourite_button_widet.dart │ │ │ └── forum_recommend_topic_list_page.dart │ │ ├── forum_group │ │ │ ├── favourite_forum_group_page.dart │ │ │ ├── forum_group_page.dart │ │ │ └── forum_group_tabs.dart │ │ ├── history │ │ │ └── topic_history_list_page.dart │ │ ├── home │ │ │ ├── home_drawer.dart │ │ │ └── home_page.dart │ │ ├── login │ │ │ └── login_page.dart │ │ ├── notification │ │ │ ├── notification_item_widget.dart │ │ │ └── notification_list_page.dart │ │ ├── photo_preview │ │ │ └── photo_preview_page.dart │ │ ├── publish │ │ │ └── publish_page.dart │ │ ├── search │ │ │ ├── search_forum_page.dart │ │ │ ├── search_page.dart │ │ │ └── search_topic_list_page.dart │ │ ├── send_message │ │ │ ├── contact_edit_dialog.dart │ │ │ └── send_message_page.dart │ │ ├── settings │ │ │ ├── blocklist_edit_dialog.dart │ │ │ ├── blocklist_keywords_page.dart │ │ │ ├── blocklist_settings_page.dart │ │ │ ├── blocklist_users_page.dart │ │ │ ├── interface_settings_page.dart │ │ │ └── settings_page.dart │ │ ├── splash │ │ │ └── splash_page.dart │ │ ├── topic_detail │ │ │ ├── forum_tag_dialog.dart │ │ │ ├── reply_detail_dialog.dart │ │ │ ├── topic_detail_page.dart │ │ │ ├── topic_page_select_dialog.dart │ │ │ ├── topic_reply_comment_item_widget.dart │ │ │ ├── topic_reply_item_widget.dart │ │ │ └── topic_single_page.dart │ │ └── user_info │ │ │ ├── user_info_page.dart │ │ │ ├── user_replies_page.dart │ │ │ └── user_topics_page.dart │ └── widget │ │ ├── attachment_widget.dart │ │ ├── avatar_widget.dart │ │ ├── block_mode_selection_dialog.dart │ │ ├── collapse_widget.dart │ │ ├── custom_forum_dialog.dart │ │ ├── dash.dart │ │ ├── emoticon_group_tabs_widget.dart │ │ ├── font_color_dialog.dart │ │ ├── font_size_dialog.dart │ │ ├── font_style_widget.dart │ │ ├── forum_grid_item_widget.dart │ │ ├── import_cookies_dialog.dart │ │ ├── info_widget.dart │ │ ├── keep_alive_tab_view.dart │ │ ├── line_height_selection_dialog.dart │ │ ├── nga_html_comment_widget.dart │ │ ├── nga_html_content_widget.dart │ │ ├── simple_scroll_behavior.dart │ │ ├── theme_selection_dialog.dart │ │ ├── topic_history_list_item_widget.dart │ │ └── topic_list_item_widget.dart └── utils │ ├── code_utils.dart │ ├── constant.dart │ ├── custom_time_messages.dart │ ├── dimen.dart │ ├── linkroute │ └── link_route.dart │ ├── name_utils.dart │ ├── palette.dart │ ├── parser │ └── content_parser.dart │ ├── picture_utils.dart │ └── route.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the master branch 5 | on: 6 | push: 7 | tags: 8 | - "v*.*.*" # on every version tag will build a new android artifact. 9 | jobs: 10 | build: 11 | name: Build App Bundle 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: subosito/flutter-action@v2 16 | with: 17 | flutter-version: '2.10.2' 18 | channel: 'stable' 19 | - run: flutter --version 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-java@v2 22 | with: 23 | distribution: 'zulu' 24 | java-version: '11' 25 | - uses: subosito/flutter-action@v2 26 | with: 27 | flutter-version: '2.10.2' 28 | env: 29 | run: echo $JKS > ${{ secrets.STORE_FILE }} && echo -e "keyAlias=${{ secrets.KEY_ALIAS }}\nkeyPassword=${{ secrets.KEY_PASSWORD }}\nstoreFile=${{ secrets.STORE_FILE }}\nstorePassword=${{ secrets.STORE_PASSWORD }}" > android/key.properties 30 | - run: flutter pub get 31 | - run: flutter test 32 | - run: flutter build apk 33 | - run: flutter build appbundle 34 | - uses: xresloader/upload-to-github-release@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 37 | with: 38 | file: "*.apk,*.aab" 39 | tags: false 40 | draft: false -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: pull_request 3 | 4 | jobs: 5 | check: 6 | name: Test on ${{ matrix.os }} 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macos-latest] 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-java@v1 14 | with: 15 | java-version: "12.x" 16 | - uses: subosito/flutter-action@v1 17 | with: 18 | channel: 'stable' # or: 'dev' or 'beta' 19 | - run: flutter pub get 20 | - run: flutter test --no-pub test/ 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | .flutter-plugins-dependencies 73 | ios/Flutter/flutter_export_environment.sh 74 | /android/key.properties 75 | !/android/flutter_nga_android.iml 76 | !/pubspec.lock 77 | -------------------------------------------------------------------------------- /.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: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: linux 4 | language: android 5 | licenses: 6 | - 'android-sdk-preview-license-.+' 7 | - 'android-sdk-license-.+' 8 | - 'google-gdk-license-.+' 9 | android: 10 | components: 11 | - tools 12 | - platform-tools 13 | - build-tools-28.0.3 14 | - android-27 15 | - android-28 16 | - sys-img-armeabi-v7a-google_apis-27 17 | - extra-android-m2repository 18 | - extra-google-m2repository 19 | - extra-google-android-support 20 | jdk: oraclejdk8 21 | sudo: false 22 | addons: 23 | apt: 24 | sources: 25 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 26 | packages: 27 | - libstdc++6 28 | - fonts-droid 29 | before_script: 30 | - wget https://downloads.gradle.org/distributions/gradle-4.10.2-bin.zip 31 | - unzip -qq gradle-4.10.2-bin.zip 32 | - export GRADLE_HOME=$PWD/gradle-4.10.2 33 | - export PATH=$GRADLE_HOME/bin:$PATH 34 | - git clone https://github.com/flutter/flutter.git -b beta --depth 1 35 | script: 36 | - ./flutter/bin/flutter -v build apk 37 | 38 | - os: osx 39 | language: generic 40 | osx_image: xcode10.1 41 | before_script: 42 | - pip install six 43 | - brew update 44 | - brew install --HEAD usbmuxd 45 | - brew unlink usbmuxd 46 | - brew link usbmuxd 47 | - brew install --HEAD libimobiledevice 48 | - brew install ideviceinstaller 49 | - brew install ios-deploy 50 | - git clone https://github.com/flutter/flutter.git -b beta --depth 1 51 | script: 52 | - ./flutter/bin/flutter -v build ios --no-codesign 53 | 54 | cache: 55 | directories: 56 | - $HOME/.pub-cache -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_nga 2 | 3 | 现已不再维护。 4 | 5 | ## Flutter 开源版 NGA 6 | 7 | 尚在开发中的 Flutter 实现版 NGA 8 | 9 | ### 功能 10 | 11 | - [x] 版块列表 12 | - [x] 收藏版块 13 | - [x] 登录 (现统一由第三方 Flutter lib 实现) 14 | - [x] Android 版登录 15 | - [x] iOS 版登录 16 | - [x] 登出账号 17 | - [x] 个人详情 18 | - [x] 贴子列表 19 | - [ ] 贴子详情(基本功能完成,细节待完善) 20 | - [x] 发贴 21 | - [x] 发回复 22 | - [x] 私信功能 23 | - [x] 搜索功能 24 | - [x] 子版块 25 | - [x] 精华区 26 | - [ ] App 界面设置 27 | 28 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | ## Flutter wrapper 2 | -keep class io.flutter.app.** { *; } 3 | -keep class io.flutter.plugin.** { *; } 4 | -keep class io.flutter.util.** { *; } 5 | -keep class io.flutter.view.** { *; } 6 | -keep class io.flutter.** { *; } 7 | -keep class io.flutter.plugins.** { *; } 8 | -dontwarn io.flutter.embedding.** 9 | 10 | -keepattributes *Annotation* 11 | -keepclassmembers enum androidx.lifecycle.Lifecycle$Event { 12 | ; 13 | } 14 | -keep !interface * implements androidx.lifecycle.LifecycleObserver { 15 | } 16 | -keep class * implements androidx.lifecycle.LifecycleObserver { 17 | } 18 | -keep class * implements androidx.lifecycle.GeneratedAdapter { 19 | (...); 20 | } 21 | -keepclassmembers class ** { 22 | @androidx.lifecycle.OnLifecycleEvent *; 23 | } 24 | 25 | #Flutter Wrapper 26 | -keep class io.flutter.app.** { *; } 27 | -keep class io.flutter.plugin.** { *; } 28 | -keep class io.flutter.util.** { *; } 29 | -keep class io.flutter.view.** { *; } 30 | -keep class io.flutter.** { *; } 31 | -keep class io.flutter.plugins.** { *; } 32 | -keep class de.prosiebensat1digital.** { *; } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 11 | 12 | 17 | 24 | 25 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/io/github/loshine/flutternga/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.loshine.flutternga 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | import io.flutter.embedding.engine.FlutterEngine 5 | import io.flutter.plugins.GeneratedPluginRegistrant 6 | import io.github.loshine.flutternga.plugins.login.FlutterCookiesPlugin 7 | import io.github.loshine.flutternga.plugins.login.FlutterGallerySaverPlugin 8 | import io.github.loshine.flutternga.plugins.login.FlutterLoginPlugin 9 | 10 | class MainActivity : FlutterActivity() { 11 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 12 | GeneratedPluginRegistrant.registerWith(flutterEngine) 13 | FlutterLoginPlugin.registerWith(flutterEngine, this) 14 | FlutterCookiesPlugin.registerWith(flutterEngine) 15 | FlutterGallerySaverPlugin.registerWith(flutterEngine) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/io/github/loshine/flutternga/plugins/login/CookiesEventHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.loshine.flutternga.plugins.login 2 | 3 | import io.flutter.plugin.common.EventChannel 4 | import kotlinx.coroutines.* 5 | import kotlinx.coroutines.channels.Channel 6 | 7 | object CookiesEventHandler { 8 | 9 | private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) 10 | private val channel = Channel() 11 | 12 | private var job: Job? = null 13 | 14 | fun init(eventSink: EventChannel.EventSink) { 15 | job = coroutineScope.launch { 16 | for (message in channel) { 17 | eventSink.success(message) 18 | } 19 | } 20 | } 21 | 22 | fun dispose() { 23 | // channel.cancel() 24 | job?.cancel() 25 | } 26 | 27 | fun onCookiesChanges(cookies: String) { 28 | coroutineScope.launch { channel.send(cookies) } 29 | } 30 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/io/github/loshine/flutternga/plugins/login/FlutterCookiesPlugin.kt: -------------------------------------------------------------------------------- 1 | package io.github.loshine.flutternga.plugins.login 2 | 3 | import android.util.Log 4 | import io.flutter.embedding.engine.FlutterEngine 5 | import io.flutter.plugin.common.EventChannel 6 | 7 | class FlutterCookiesPlugin : EventChannel.StreamHandler { 8 | 9 | companion object { 10 | const val CHANNEL = "io.github.loshine.flutternga.cookies/plugin" 11 | 12 | fun registerWith(flutterEngine: FlutterEngine) { 13 | val channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) 14 | val instance = FlutterCookiesPlugin() 15 | channel.setStreamHandler(instance) 16 | } 17 | } 18 | 19 | override fun onListen(any: Any?, eventSink: EventChannel.EventSink) { 20 | CookiesEventHandler.init(eventSink) 21 | } 22 | 23 | override fun onCancel(any: Any?) { 24 | Log.i("FlutterCookiesPlugin", "FlutterCookiesPlugin:onCancel") 25 | CookiesEventHandler.dispose() 26 | } 27 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/io/github/loshine/flutternga/plugins/login/FlutterLoginPlugin.kt: -------------------------------------------------------------------------------- 1 | package io.github.loshine.flutternga.plugins.login 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.util.Log 6 | import io.flutter.embedding.engine.FlutterEngine 7 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 8 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 9 | import io.flutter.plugin.common.MethodCall 10 | import io.flutter.plugin.common.MethodChannel 11 | import io.github.loshine.flutternga.ui.LoginActivity 12 | import java.lang.ref.WeakReference 13 | 14 | 15 | class FlutterLoginPlugin : MethodChannel.MethodCallHandler, ActivityAware { 16 | 17 | companion object { 18 | 19 | const val CHANNEL = "io.github.loshine.flutternga.login/plugin" 20 | 21 | fun registerWith(flutterEngine: FlutterEngine, activity: Activity) { 22 | val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) 23 | val instance = FlutterLoginPlugin() 24 | // setMethodCallHandler在此通道上接收方法调用的回调 25 | channel.setMethodCallHandler(instance) 26 | instance.activityRef = WeakReference(activity) 27 | } 28 | } 29 | 30 | private var activityRef: WeakReference? = null 31 | 32 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 33 | // 通过 MethodCall 可以获取参数和方法名 34 | when (call.method) { 35 | "start_login" -> { 36 | Log.d("activity", "$activityRef") 37 | activityRef?.get()?.let { 38 | // 跳转到登录页 39 | val intent = Intent(it, LoginActivity::class.java) 40 | it.startActivity(intent) 41 | // 返回给 flutter 的参数 42 | result.success("success") 43 | } 44 | } 45 | else -> result.notImplemented() 46 | } 47 | } 48 | 49 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 50 | activityRef = WeakReference(binding.activity) 51 | } 52 | 53 | override fun onDetachedFromActivityForConfigChanges() { 54 | activityRef = null 55 | } 56 | 57 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { 58 | activityRef = WeakReference(binding.activity) 59 | } 60 | 61 | override fun onDetachedFromActivity() { 62 | activityRef = null 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | 21 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #795548 4 | #5D4037 5 | #A5D6A7 6 | #fff9e3 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 原生登录 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/test/kotlin/io/github/loshine/flutternga/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.loshine.flutternga 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.junit.Test 5 | import java.io.ByteArrayOutputStream 6 | import java.net.URL 7 | 8 | class ExampleUnitTest { 9 | 10 | @Test 11 | fun testFile() { 12 | runBlocking { 13 | URL("https://img.nga.178.com/attachments/mon_202105/23/dbQ2o-obK18T1kSh2-eb.jpg").openStream() 14 | .use { 15 | val bos = ByteArrayOutputStream() 16 | val b = ByteArray(8192) 17 | var len: Int 18 | while (it.read(b, 0, 8192).also { len = it } != -1) { 19 | bos.write(b, 0, len) 20 | } 21 | println(bos.toByteArray()) 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.21' 3 | repositories { 4 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } 5 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/central' } 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } 17 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/central' } 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/flutter_nga_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Oct 14 10:04:54 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | 7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 8 | def properties = new Properties() 9 | 10 | assert localPropertiesFile.exists() 11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 12 | 13 | def flutterSdkPath = properties.getProperty("flutter.sdk") 14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 16 | -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | -------------------------------------------------------------------------------- /images/default_forum_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/images/default_forum_icon.png -------------------------------------------------------------------------------- /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 "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | pods_ary = [] 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) { |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | pods_ary.push({:name => podname, :path => podpath}); 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | } 32 | return pods_ary 33 | end 34 | 35 | target 'Runner' do 36 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 37 | # referring to absolute paths on developers' machines. 38 | system('rm -rf .symlinks') 39 | system('mkdir -p .symlinks/plugins') 40 | 41 | # Flutter Pods 42 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 43 | if generated_xcode_build_settings.empty? 44 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 45 | end 46 | generated_xcode_build_settings.map { |p| 47 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 48 | symlink = File.join('.symlinks', 'flutter') 49 | File.symlink(File.dirname(p[:path]), symlink) 50 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 51 | end 52 | } 53 | 54 | # Plugin Pods 55 | plugin_pods = parse_KV_file('../.flutter-plugins') 56 | plugin_pods.map { |p| 57 | symlink = File.join('.symlinks', 'plugins', p[:name]) 58 | File.symlink(p[:path], symlink) 59 | pod p[:name], :path => File.join(symlink, 'ios') 60 | } 61 | end 62 | 63 | post_install do |installer| 64 | installer.pods_project.targets.each do |target| 65 | target.build_configurations.each do |config| 66 | config.build_settings['ENABLE_BITCODE'] = 'NO' 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loshine/flutter-nga/1fc6a49fa072b4336be010655a312796bbf77bc3/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 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_nga 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/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/data/entity/block.dart: -------------------------------------------------------------------------------- 1 | class BlockInfoData { 2 | final List blockUserList; 3 | final List blockWordList; 4 | 5 | BlockInfoData(this.blockUserList, this.blockWordList); 6 | 7 | factory BlockInfoData.fromJson(Map map) { 8 | String data = map["0"]; 9 | final userList = []; 10 | final wordList = []; 11 | // 1 代表有屏蔽信息 12 | if (data.startsWith("1\n")) { 13 | // 第一个 \n 后是屏蔽词 14 | final wordsAndUsers = data.substring("1\n".length); 15 | // 第二个 \n 后是屏蔽用户 16 | final secondGapIndex = wordsAndUsers.indexOf("\n"); 17 | // 如果没有第二个 \n 18 | if (secondGapIndex < 0) { 19 | wordList.addAll(wordsAndUsers.split(" ").where((e) => e != "")); 20 | } else { 21 | final words = wordsAndUsers.substring(0, secondGapIndex); 22 | final users = wordsAndUsers.substring(secondGapIndex + 1); 23 | wordList.addAll(words.split(" ").where((e) => e != "")); 24 | userList.addAll(users.split(" ")); 25 | } 26 | } 27 | return BlockInfoData(userList, wordList); 28 | } 29 | 30 | String toData() { 31 | final data = StringBuffer(); 32 | if (blockWordList.isNotEmpty || blockUserList.isNotEmpty) { 33 | data.write("1\n"); 34 | if (blockWordList.isNotEmpty) { 35 | data.write(blockWordList.reduce((value, e) => "$value $e")); 36 | } 37 | if (blockUserList.isNotEmpty) { 38 | data.write("\n"); 39 | data.write(blockUserList.reduce((value, e) => "$value $e")); 40 | } 41 | } 42 | return data.toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/data/entity/child_forum.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/entity/forum.dart'; 2 | 3 | class ChildForum extends Forum { 4 | final int? tid; 5 | final String? desc; 6 | final bool selected; 7 | final int? parentId; 8 | final int type; 9 | 10 | ChildForum(int fid, String name, this.tid, 11 | {this.parentId, this.desc, this.type = 0, this.selected = false}) 12 | : super(fid, name, type: type); 13 | } 14 | -------------------------------------------------------------------------------- /lib/data/entity/conversation.dart: -------------------------------------------------------------------------------- 1 | class Conversation { 2 | final int? mid; 3 | final int? lastModify; 4 | final int? bit; 5 | final String? subject; 6 | final int? from; 7 | final int? time; 8 | final int? lastFrom; 9 | final int? posts; 10 | final int? sbit; 11 | final String? fromUsername; 12 | final String? lastFromUsername; 13 | 14 | String? get realFromUsername => from == 0 ? '#SYSTEM#' : fromUsername; 15 | 16 | String get users => 17 | "$realFromUsername${lastFromUsername != '' ? ", $lastFromUsername" : ''}"; 18 | 19 | bool get isMultiUser => bit! & 1 == 1; 20 | 21 | bool get isUnread => bit! & 2 == 2; 22 | 23 | Conversation( 24 | this.mid, 25 | this.lastModify, 26 | this.bit, 27 | this.subject, 28 | this.from, 29 | this.time, 30 | this.lastFrom, 31 | this.posts, 32 | this.sbit, 33 | this.fromUsername, 34 | this.lastFromUsername, 35 | ); 36 | 37 | // final String allUser; 38 | 39 | factory Conversation.fromJson(Map map) { 40 | return Conversation( 41 | map['mid'], 42 | map['last_modify'], 43 | map['bit'], 44 | map['subject'], 45 | map['from'], 46 | map['time'], 47 | map['last_from'], 48 | map['posts'], 49 | map['sbit'], 50 | map['from_username'], 51 | map['last_from_username']); 52 | } 53 | } 54 | 55 | class ConversationListData { 56 | final int? nextPage; 57 | final int? currentPage; 58 | final int? rowsPerPage; 59 | final List conversationList; 60 | 61 | bool get hasNext => nextPage == 1; 62 | 63 | ConversationListData( 64 | this.nextPage, 65 | this.currentPage, 66 | this.rowsPerPage, 67 | this.conversationList, 68 | ); 69 | 70 | factory ConversationListData.fromJson(Map map) { 71 | Map tempMap = {}; 72 | for (MapEntry entry 73 | in map.entries as Iterable>) { 74 | if (entry.key != 'nextPage' && 75 | entry.key != 'currentPage' && 76 | entry.key != 'rowsPerPage') { 77 | tempMap[entry.key] = Conversation.fromJson(entry.value); 78 | } 79 | } 80 | return ConversationListData( 81 | map['nextPage'], 82 | map['currentPage'], 83 | map['rowsPerPage'], 84 | tempMap.values.toList(), 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/data/entity/emoticon.dart: -------------------------------------------------------------------------------- 1 | /// 表情实体类 2 | class Emoticon { 3 | const Emoticon({required this.content, required this.url}); 4 | 5 | final String content; 6 | final String url; 7 | } 8 | 9 | /// 表情包实体类 10 | class EmoticonGroup { 11 | const EmoticonGroup(this.name, this.expressionList); 12 | 13 | final String name; 14 | final List expressionList; 15 | } 16 | -------------------------------------------------------------------------------- /lib/data/entity/forum.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | /// 版块实体类 4 | class Forum { 5 | const Forum(this.fid, this.name, {this.type = 0}); 6 | 7 | final int fid; 8 | final String name; 9 | final int type; 10 | 11 | factory Forum.fromJson(Map map) { 12 | return Forum(map['fid'], map['name'], type: map['type'] ?? 0); 13 | } 14 | 15 | Map toJson() { 16 | return {'fid': fid, 'name': name, 'type': type}; 17 | } 18 | 19 | String getIconUrl() { 20 | return "https://img4.nga.178.com/ngabbs/nga_classic/f/app/$fid.png"; 21 | } 22 | } 23 | 24 | /// 版块组实体类 25 | class ForumGroup { 26 | const ForumGroup(this.name, this.forumList); 27 | 28 | final String name; 29 | final List forumList; 30 | } 31 | -------------------------------------------------------------------------------- /lib/data/entity/message.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/entity/user.dart'; 2 | 3 | class Message { 4 | final int? id; 5 | final String? subject; 6 | final String content; 7 | final int? from; 8 | final int? time; 9 | final User user; 10 | final dynamic data; 11 | 12 | Message( 13 | this.id, 14 | this.subject, 15 | this.content, 16 | this.from, 17 | this.time, 18 | this.user, 19 | this.data, 20 | ); 21 | 22 | factory Message.fromJson(Map map, List userList) { 23 | return Message( 24 | map['id'], 25 | map['subject'], 26 | map['content'], 27 | map['from'], 28 | map['time'], 29 | userList.firstWhere((e) => e.uid == map['from'], orElse: null), 30 | map['data'], 31 | ); 32 | } 33 | } 34 | 35 | class MessageListData { 36 | final int? length; 37 | final int? nextPage; 38 | final int? currentPage; 39 | final int? subjectBit; 40 | final int? starterUid; 41 | final List messageList; 42 | final List userList; 43 | 44 | bool get hasNext => nextPage != null; 45 | 46 | MessageListData( 47 | this.length, 48 | this.nextPage, 49 | this.currentPage, 50 | this.subjectBit, 51 | this.starterUid, 52 | this.messageList, 53 | this.userList, 54 | ); 55 | 56 | // final String allUsers; 57 | 58 | factory MessageListData.fromJson(Map map) { 59 | dynamic nextPage = map['nextPage']; 60 | Map userMap = map['userInfo']; 61 | List userList = userMap.entries 62 | .where((e) => int.tryParse(e.key) != null) 63 | .map((e) => User.fromJson(e.value)) 64 | .toList(); 65 | List messageList = map.entries 66 | .where((e) => int.tryParse(e.key) != null) 67 | .map((e) => Message.fromJson(e.value, userList)) 68 | .toList(); 69 | return MessageListData( 70 | map['length'], 71 | nextPage is int ? nextPage : null, 72 | map['currentPage'], 73 | map['subjectBit'], 74 | map['starterUid'], 75 | messageList, 76 | userList, 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/data/entity/site_api_result.dart: -------------------------------------------------------------------------------- 1 | class SiteApiResult { 2 | const SiteApiResult(this.data, this.encode, this.time) : assert(data != null); 3 | 4 | final dynamic data; 5 | final String encode; 6 | final int time; 7 | } 8 | -------------------------------------------------------------------------------- /lib/data/entity/toggle_like_reaction.dart: -------------------------------------------------------------------------------- 1 | class ToggleLikeReaction { 2 | const ToggleLikeReaction(this.message, this.countChange); 3 | 4 | Map toJson() { 5 | return {'0': message, '1': countChange}; 6 | } 7 | 8 | factory ToggleLikeReaction.fromJson(Map map) { 9 | return ToggleLikeReaction(map['0'], map['1']); 10 | } 11 | 12 | final String message; 13 | final int countChange; 14 | } 15 | -------------------------------------------------------------------------------- /lib/data/entity/topic_tag.dart: -------------------------------------------------------------------------------- 1 | class TopicTag { 2 | const TopicTag({required this.id, required this.content}); 3 | 4 | final int id; 5 | final String content; 6 | } 7 | -------------------------------------------------------------------------------- /lib/data/repository/resource_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_nga/plugins/gallery_saver.dart'; 5 | 6 | /// 资源资源库 7 | abstract class ResourceRepository { 8 | Future downloadImage(String url); 9 | } 10 | 11 | class ResourceDataRepository implements ResourceRepository { 12 | late Dio _dio; 13 | 14 | ResourceDataRepository() { 15 | _dio = Dio(); 16 | _dio.options.responseType = ResponseType.bytes; 17 | } 18 | 19 | @override 20 | Future downloadImage(String url) async { 21 | // return GallerySaver.saveImage(url, albumName: "NationalGayAlliance"); 22 | return GallerySaver.save(url); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/data/usecase/topic/get_topic_list_use_case.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_nga/data/usecase/use_case.dart'; 5 | 6 | import '../../entity/topic.dart'; 7 | import '../../http.dart'; 8 | 9 | class GetTopicListUseCase extends UseCase { 10 | @override 11 | Future execute(GetTopicListParams? params) async { 12 | var query = "__output=8"; 13 | if (params?.fid != null) { 14 | query += 15 | params?.type == 1 ? "&stid=${params?.fid}" : "&fid=${params?.fid}"; 16 | } 17 | if (params?.authorid != null) { 18 | query += "&authorid=${params?.authorid}"; 19 | } 20 | if (params?.page != null) { 21 | query += "&page=${params?.page}"; 22 | } 23 | if (params?.recommend == true) { 24 | query += "&recommend=1&order_by=postdatedesc"; 25 | } 26 | Response response = await httpClient.get("thread.php?$query"); 27 | return TopicListData.fromJson( 28 | json.decode(response.data ?? "{}")['data'], params?.page); 29 | } 30 | } 31 | 32 | class GetTopicListParams { 33 | final int? fid; 34 | final int? authorid; 35 | final int? page; 36 | final bool recommend; 37 | final int? type; 38 | 39 | GetTopicListParams( 40 | {this.fid, 41 | this.authorid, 42 | this.page, 43 | this.recommend = false, 44 | this.type = 0}); 45 | } 46 | -------------------------------------------------------------------------------- /lib/data/usecase/use_case.dart: -------------------------------------------------------------------------------- 1 | abstract class UseCase { 2 | 3 | Future execute(Params? params); 4 | } 5 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_theme/adaptive_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_displaymode/flutter_displaymode.dart'; 4 | import 'package:flutter_nga/data/http.dart'; 5 | import 'package:mmkv/mmkv.dart'; 6 | import 'package:route_observer_mixin/route_observer_mixin.dart'; 7 | 8 | import 'my_app.dart'; 9 | 10 | void main() async { 11 | WidgetsFlutterBinding.ensureInitialized(); 12 | final savedThemeMode = await AdaptiveTheme.getThemeMode(); 13 | 14 | await FlutterDisplayMode.setHighRefreshRate(); 15 | 16 | // must wait for MMKV to finish initialization 17 | final rootDir = await MMKV.initialize(); 18 | debugPrint('MMKV for flutter with rootDir = $rootDir'); 19 | 20 | // await setUpHttpClient(); 21 | runApp(RouteObserverProvider( 22 | child: MyApp(savedThemeMode: savedThemeMode), 23 | )); 24 | } 25 | -------------------------------------------------------------------------------- /lib/plugins/gallery_saver.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/services.dart'; 4 | 5 | class GallerySaver { 6 | static Future save(String url) async { 7 | if (Platform.isAndroid) { 8 | return _AndroidGallerySaver.save(url); 9 | } 10 | throw Exception("Not implemented!!!"); 11 | } 12 | } 13 | 14 | class _AndroidGallerySaver { 15 | static const _channel = 16 | const MethodChannel('io.github.loshine.flutternga.gallery_saver/plugin'); 17 | 18 | static Future save(String url) async { 19 | return await _channel.invokeMethod('save', {"url": url}); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/plugins/login.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/services.dart'; 4 | 5 | class Login { 6 | static Stream get cookieStream { 7 | if (Platform.isAndroid) { 8 | return _AndroidLogin.cookieStream; 9 | } 10 | throw Exception("Not implemented!!!"); 11 | } 12 | 13 | static Future startLogin() async { 14 | if (Platform.isAndroid) { 15 | return _AndroidLogin.startLogin(); 16 | } 17 | throw Exception("Not implemented!!!"); 18 | } 19 | } 20 | 21 | class _AndroidLogin { 22 | static const loginChannel = 23 | const MethodChannel('io.github.loshine.flutternga.login/plugin'); 24 | static const cookieChannel = 25 | const EventChannel('io.github.loshine.flutternga.cookies/plugin'); 26 | 27 | static Stream get cookieStream => 28 | cookieChannel.receiveBroadcastStream(); 29 | 30 | static Future startLogin() async { 31 | return await loginChannel.invokeMethod('start_login'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/store/common/photo_min_scale_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_nga/utils/picture_utils.dart' as pictureUtils; 4 | import 'package:mobx/mobx.dart'; 5 | 6 | part 'photo_min_scale_store.g.dart'; 7 | 8 | class PhotoMinScaleStore = _PhotoMinScaleStore with _$PhotoMinScaleStore; 9 | 10 | abstract class _PhotoMinScaleStore with Store { 11 | @observable 12 | double minScale = 0; 13 | 14 | @action 15 | void load(String url, double? screenWidth) { 16 | CachedNetworkImageProvider(pictureUtils.getOriginalUrl(url)) 17 | .resolve(ImageConfiguration()) 18 | .addListener(ImageStreamListener( 19 | (ImageInfo info, _) => minScale = screenWidth! / info.image.width)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/store/common/photo_min_scale_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'photo_min_scale_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$PhotoMinScaleStore on _PhotoMinScaleStore, Store { 12 | final _$minScaleAtom = Atom(name: '_PhotoMinScaleStore.minScale'); 13 | 14 | @override 15 | double get minScale { 16 | _$minScaleAtom.reportRead(); 17 | return super.minScale; 18 | } 19 | 20 | @override 21 | set minScale(double value) { 22 | _$minScaleAtom.reportWrite(value, super.minScale, () { 23 | super.minScale = value; 24 | }); 25 | } 26 | 27 | final _$_PhotoMinScaleStoreActionController = 28 | ActionController(name: '_PhotoMinScaleStore'); 29 | 30 | @override 31 | void load(String url, double? screenWidth) { 32 | final _$actionInfo = _$_PhotoMinScaleStoreActionController.startAction( 33 | name: '_PhotoMinScaleStore.load'); 34 | try { 35 | return super.load(url, screenWidth); 36 | } finally { 37 | _$_PhotoMinScaleStoreActionController.endAction(_$actionInfo); 38 | } 39 | } 40 | 41 | @override 42 | String toString() { 43 | return ''' 44 | minScale: ${minScale} 45 | '''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/store/forum/child_forum_subscription_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:fluttertoast/fluttertoast.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'child_forum_subscription_store.g.dart'; 6 | 7 | class ChildForumSubscriptionStore = _ChildForumSubscriptionStore 8 | with _$ChildForumSubscriptionStore; 9 | 10 | abstract class _ChildForumSubscriptionStore with Store { 11 | @observable 12 | bool subscribed = false; 13 | 14 | @action 15 | void setSubscribed(bool subscribed) { 16 | this.subscribed = subscribed; 17 | } 18 | 19 | @action 20 | void addSubscription(int fid, int? parentId) { 21 | Data().forumRepository.addChildForumSubscription(fid, parentId).then((s) { 22 | Fluttertoast.showToast(msg: "订阅成功"); 23 | subscribed = true; 24 | }).catchError((e) { 25 | Fluttertoast.showToast(msg: e.message); 26 | }); 27 | } 28 | 29 | @action 30 | void deleteSubscription(int fid, int? parentId) { 31 | Data() 32 | .forumRepository 33 | .deleteChildForumSubscription(fid, parentId) 34 | .then((s) { 35 | Fluttertoast.showToast(msg: "取消订阅成功"); 36 | subscribed = false; 37 | }).catchError((e) { 38 | Fluttertoast.showToast(msg: e.message); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/store/forum/child_forum_subscription_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'child_forum_subscription_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$ChildForumSubscriptionStore on _ChildForumSubscriptionStore, Store { 12 | final _$subscribedAtom = 13 | Atom(name: '_ChildForumSubscriptionStore.subscribed'); 14 | 15 | @override 16 | bool get subscribed { 17 | _$subscribedAtom.reportRead(); 18 | return super.subscribed; 19 | } 20 | 21 | @override 22 | set subscribed(bool value) { 23 | _$subscribedAtom.reportWrite(value, super.subscribed, () { 24 | super.subscribed = value; 25 | }); 26 | } 27 | 28 | final _$_ChildForumSubscriptionStoreActionController = 29 | ActionController(name: '_ChildForumSubscriptionStore'); 30 | 31 | @override 32 | void setSubscribed(bool subscribed) { 33 | final _$actionInfo = _$_ChildForumSubscriptionStoreActionController 34 | .startAction(name: '_ChildForumSubscriptionStore.setSubscribed'); 35 | try { 36 | return super.setSubscribed(subscribed); 37 | } finally { 38 | _$_ChildForumSubscriptionStoreActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | 42 | @override 43 | void addSubscription(int fid, int? parentId) { 44 | final _$actionInfo = _$_ChildForumSubscriptionStoreActionController 45 | .startAction(name: '_ChildForumSubscriptionStore.addSubscription'); 46 | try { 47 | return super.addSubscription(fid, parentId); 48 | } finally { 49 | _$_ChildForumSubscriptionStoreActionController.endAction(_$actionInfo); 50 | } 51 | } 52 | 53 | @override 54 | void deleteSubscription(int fid, int? parentId) { 55 | final _$actionInfo = _$_ChildForumSubscriptionStoreActionController 56 | .startAction(name: '_ChildForumSubscriptionStore.deleteSubscription'); 57 | try { 58 | return super.deleteSubscription(fid, parentId); 59 | } finally { 60 | _$_ChildForumSubscriptionStoreActionController.endAction(_$actionInfo); 61 | } 62 | } 63 | 64 | @override 65 | String toString() { 66 | return ''' 67 | subscribed: ${subscribed} 68 | '''; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/store/forum/favourite_forum_list_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/forum.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | import 'package:mobx/mobx.dart'; 5 | 6 | part 'favourite_forum_list_store.g.dart'; 7 | 8 | class FavouriteForumListStore = _FavouriteForumListStore 9 | with _$FavouriteForumListStore; 10 | 11 | abstract class _FavouriteForumListStore with Store { 12 | @observable 13 | List list = []; 14 | 15 | @action 16 | void refresh() { 17 | Data().forumRepository.getFavouriteList().then((list) { 18 | this.list = list; 19 | }); 20 | } 21 | 22 | @action 23 | Future add(int fid, String name) async { 24 | final isFavourite = 25 | await Data().forumRepository.isFavourite(Forum(fid, name)); 26 | if (isFavourite) { 27 | Fluttertoast.showToast(msg: "您已添加过该版面"); 28 | } else { 29 | await Data().forumRepository.saveFavourite(Forum(fid, name)); 30 | refresh(); 31 | } 32 | } 33 | 34 | @action 35 | Future delete(int fid) async { 36 | await Data().forumRepository.deleteFavourite(Forum(fid, "")); 37 | refresh(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/store/forum/favourite_forum_list_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'favourite_forum_list_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$FavouriteForumListStore on _FavouriteForumListStore, Store { 12 | final _$listAtom = Atom(name: '_FavouriteForumListStore.list'); 13 | 14 | @override 15 | List get list { 16 | _$listAtom.reportRead(); 17 | return super.list; 18 | } 19 | 20 | @override 21 | set list(List value) { 22 | _$listAtom.reportWrite(value, super.list, () { 23 | super.list = value; 24 | }); 25 | } 26 | 27 | final _$addAsyncAction = AsyncAction('_FavouriteForumListStore.add'); 28 | 29 | @override 30 | Future add(int fid, String name) { 31 | return _$addAsyncAction.run(() => super.add(fid, name)); 32 | } 33 | 34 | final _$deleteAsyncAction = AsyncAction('_FavouriteForumListStore.delete'); 35 | 36 | @override 37 | Future delete(int fid) { 38 | return _$deleteAsyncAction.run(() => super.delete(fid)); 39 | } 40 | 41 | final _$_FavouriteForumListStoreActionController = 42 | ActionController(name: '_FavouriteForumListStore'); 43 | 44 | @override 45 | void refresh() { 46 | final _$actionInfo = _$_FavouriteForumListStoreActionController.startAction( 47 | name: '_FavouriteForumListStore.refresh'); 48 | try { 49 | return super.refresh(); 50 | } finally { 51 | _$_FavouriteForumListStoreActionController.endAction(_$actionInfo); 52 | } 53 | } 54 | 55 | @override 56 | String toString() { 57 | return ''' 58 | list: ${list} 59 | '''; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/store/forum/favourite_forum_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/forum.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'favourite_forum_store.g.dart'; 6 | 7 | class FavouriteForumStore = _FavouriteForumStore with _$FavouriteForumStore; 8 | 9 | abstract class _FavouriteForumStore with Store { 10 | @observable 11 | bool isFavourite = false; 12 | 13 | @action 14 | Future load(int fid, String? name) async { 15 | isFavourite = await Data() 16 | .forumRepository 17 | .isFavourite(Forum(fid, name ?? "", type: 0)); 18 | } 19 | 20 | @action 21 | Future toggle(int fid, String? name, int? type) async { 22 | if (isFavourite) { 23 | await Data() 24 | .forumRepository 25 | .deleteFavourite(Forum(fid, name ?? "", type: type ?? 0)); 26 | } else { 27 | await Data() 28 | .forumRepository 29 | .saveFavourite(Forum(fid, name ?? "", type: type ?? 0)); 30 | } 31 | isFavourite = !isFavourite; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/store/forum/favourite_forum_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'favourite_forum_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$FavouriteForumStore on _FavouriteForumStore, Store { 12 | final _$isFavouriteAtom = Atom(name: '_FavouriteForumStore.isFavourite'); 13 | 14 | @override 15 | bool get isFavourite { 16 | _$isFavouriteAtom.reportRead(); 17 | return super.isFavourite; 18 | } 19 | 20 | @override 21 | set isFavourite(bool value) { 22 | _$isFavouriteAtom.reportWrite(value, super.isFavourite, () { 23 | super.isFavourite = value; 24 | }); 25 | } 26 | 27 | final _$loadAsyncAction = AsyncAction('_FavouriteForumStore.load'); 28 | 29 | @override 30 | Future load(int fid, String? name) { 31 | return _$loadAsyncAction.run(() => super.load(fid, name)); 32 | } 33 | 34 | final _$toggleAsyncAction = AsyncAction('_FavouriteForumStore.toggle'); 35 | 36 | @override 37 | Future toggle(int fid, String? name, int? type) { 38 | return _$toggleAsyncAction.run(() => super.toggle(fid, name, type)); 39 | } 40 | 41 | @override 42 | String toString() { 43 | return ''' 44 | isFavourite: ${isFavourite} 45 | '''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/store/forum/forum_detail_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/topic.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'forum_detail_store.g.dart'; 6 | 7 | class ForumDetailStore = _ForumDetailStore with _$ForumDetailStore; 8 | 9 | abstract class _ForumDetailStore with Store { 10 | @observable 11 | ForumDetailStoreData state = ForumDetailStoreData.initial(); 12 | 13 | @action 14 | Future refresh( 15 | int fid, bool recommend, int? type) async { 16 | try { 17 | TopicListData data = await Data().topicRepository.getTopicList( 18 | fid: fid, 19 | page: 1, 20 | recommend: recommend, 21 | type: type, 22 | ); 23 | state = ForumDetailStoreData( 24 | page: 1, 25 | maxPage: data.maxPage, 26 | enablePullUp: 1 < data.maxPage, 27 | list: data.topicList.values.toList(), 28 | info: data.forum, 29 | ); 30 | return state; 31 | } catch (err) { 32 | rethrow; 33 | } 34 | } 35 | 36 | @action 37 | Future loadMore( 38 | int fid, bool recommend, int? type) async { 39 | try { 40 | TopicListData data = await Data().topicRepository.getTopicList( 41 | fid: fid, 42 | page: state.page + 1, 43 | recommend: recommend, 44 | type: type, 45 | ); 46 | state = ForumDetailStoreData( 47 | page: state.page + 1, 48 | maxPage: data.maxPage, 49 | enablePullUp: state.page + 1 < data.maxPage, 50 | list: state.list..addAll(data.topicList.values), 51 | info: data.forum, 52 | ); 53 | return state; 54 | } catch (err) { 55 | rethrow; 56 | } 57 | } 58 | } 59 | 60 | class ForumDetailStoreData { 61 | final int page; 62 | final int maxPage; 63 | final bool enablePullUp; 64 | final List list; 65 | final ForumInfo? info; 66 | 67 | const ForumDetailStoreData({ 68 | this.page = 1, 69 | this.maxPage = 1, 70 | this.enablePullUp = false, 71 | this.list = const [], 72 | this.info, 73 | }); 74 | 75 | factory ForumDetailStoreData.initial() => ForumDetailStoreData( 76 | page: 1, 77 | maxPage: 1, 78 | enablePullUp: false, 79 | list: [], 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /lib/store/forum/forum_detail_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'forum_detail_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$ForumDetailStore on _ForumDetailStore, Store { 12 | final _$stateAtom = Atom(name: '_ForumDetailStore.state'); 13 | 14 | @override 15 | ForumDetailStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(ForumDetailStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_ForumDetailStore.refresh'); 28 | 29 | @override 30 | Future refresh(int fid, bool recommend, int? type) { 31 | return _$refreshAsyncAction.run(() => super.refresh(fid, recommend, type)); 32 | } 33 | 34 | final _$loadMoreAsyncAction = AsyncAction('_ForumDetailStore.loadMore'); 35 | 36 | @override 37 | Future loadMore(int fid, bool recommend, int? type) { 38 | return _$loadMoreAsyncAction 39 | .run(() => super.loadMore(fid, recommend, type)); 40 | } 41 | 42 | @override 43 | String toString() { 44 | return ''' 45 | state: ${state} 46 | '''; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/store/forum/forum_tag_list_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/topic_tag.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'forum_tag_list_store.g.dart'; 6 | 7 | class ForumTagListStore = _ForumTagListStore with _$ForumTagListStore; 8 | 9 | abstract class _ForumTagListStore with Store { 10 | @observable 11 | List tagList = []; 12 | 13 | @action 14 | void setList(List list) { 15 | tagList = list; 16 | } 17 | 18 | @action 19 | Future> load(int fid) async { 20 | try { 21 | tagList = await Data().topicRepository.getTopicTagList(fid); 22 | return tagList; 23 | } catch (err) { 24 | rethrow; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/store/forum/forum_tag_list_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'forum_tag_list_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$ForumTagListStore on _ForumTagListStore, Store { 12 | final _$tagListAtom = Atom(name: '_ForumTagListStore.tagList'); 13 | 14 | @override 15 | List get tagList { 16 | _$tagListAtom.reportRead(); 17 | return super.tagList; 18 | } 19 | 20 | @override 21 | set tagList(List value) { 22 | _$tagListAtom.reportWrite(value, super.tagList, () { 23 | super.tagList = value; 24 | }); 25 | } 26 | 27 | final _$loadAsyncAction = AsyncAction('_ForumTagListStore.load'); 28 | 29 | @override 30 | Future> load(int fid) { 31 | return _$loadAsyncAction.run(() => super.load(fid)); 32 | } 33 | 34 | final _$_ForumTagListStoreActionController = 35 | ActionController(name: '_ForumTagListStore'); 36 | 37 | @override 38 | void setList(List list) { 39 | final _$actionInfo = _$_ForumTagListStoreActionController.startAction( 40 | name: '_ForumTagListStore.setList'); 41 | try { 42 | return super.setList(list); 43 | } finally { 44 | _$_ForumTagListStoreActionController.endAction(_$actionInfo); 45 | } 46 | } 47 | 48 | @override 49 | String toString() { 50 | return ''' 51 | tagList: ${tagList} 52 | '''; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/store/home/home_drawer_header_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/user.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'home_drawer_header_store.g.dart'; 6 | 7 | class HomeDrawerHeaderStore = _HomeDrawerHeaderStore 8 | with _$HomeDrawerHeaderStore; 9 | 10 | abstract class _HomeDrawerHeaderStore with Store { 11 | @observable 12 | UserInfo? userInfo; 13 | 14 | @action 15 | Future refresh() async { 16 | try { 17 | CacheUser? user = await Data().userRepository.getDefaultUser(); 18 | if (user != null) { 19 | userInfo = await Data().userRepository.getUserInfoByUid(user.uid); 20 | } else { 21 | userInfo = null; 22 | } 23 | } catch (err) { 24 | rethrow; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/store/home/home_drawer_header_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'home_drawer_header_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$HomeDrawerHeaderStore on _HomeDrawerHeaderStore, Store { 12 | final _$userInfoAtom = Atom(name: '_HomeDrawerHeaderStore.userInfo'); 13 | 14 | @override 15 | UserInfo? get userInfo { 16 | _$userInfoAtom.reportRead(); 17 | return super.userInfo; 18 | } 19 | 20 | @override 21 | set userInfo(UserInfo? value) { 22 | _$userInfoAtom.reportWrite(value, super.userInfo, () { 23 | super.userInfo = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_HomeDrawerHeaderStore.refresh'); 28 | 29 | @override 30 | Future refresh() { 31 | return _$refreshAsyncAction.run(() => super.refresh()); 32 | } 33 | 34 | @override 35 | String toString() { 36 | return ''' 37 | userInfo: ${userInfo} 38 | '''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/store/home/home_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'home_store.g.dart'; 4 | 5 | class HomeStore = _HomeStore with _$HomeStore; 6 | 7 | abstract class _HomeStore with Store { 8 | @observable 9 | var index = 0; 10 | 11 | @action 12 | void setIndex(int index) { 13 | this.index = index; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/store/home/home_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'home_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$HomeStore on _HomeStore, Store { 12 | final _$indexAtom = Atom(name: '_HomeStore.index'); 13 | 14 | @override 15 | int get index { 16 | _$indexAtom.reportRead(); 17 | return super.index; 18 | } 19 | 20 | @override 21 | set index(int value) { 22 | _$indexAtom.reportWrite(value, super.index, () { 23 | super.index = value; 24 | }); 25 | } 26 | 27 | final _$_HomeStoreActionController = ActionController(name: '_HomeStore'); 28 | 29 | @override 30 | void setIndex(int index) { 31 | final _$actionInfo = 32 | _$_HomeStoreActionController.startAction(name: '_HomeStore.setIndex'); 33 | try { 34 | return super.setIndex(index); 35 | } finally { 36 | _$_HomeStoreActionController.endAction(_$actionInfo); 37 | } 38 | } 39 | 40 | @override 41 | String toString() { 42 | return ''' 43 | index: ${index} 44 | '''; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/store/message/conversation_detail_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/data/data.dart'; 3 | import 'package:flutter_nga/data/entity/message.dart'; 4 | import 'package:mobx/mobx.dart'; 5 | 6 | part 'conversation_detail_store.g.dart'; 7 | 8 | class ConversationDetailStore = _ConversationDetailStore 9 | with _$ConversationDetailStore; 10 | 11 | abstract class _ConversationDetailStore with Store { 12 | @observable 13 | ConversationDetailStoreData state = ConversationDetailStoreData.initial(); 14 | 15 | @action 16 | Future refresh( 17 | BuildContext context, int? mid) async { 18 | try { 19 | MessageListData data = 20 | await Data().messageRepository.getMessageList(mid, 1); 21 | state = ConversationDetailStoreData( 22 | page: 1, 23 | enablePullUp: data.hasNext, 24 | list: data.messageList.toList(), 25 | ); 26 | return state; 27 | } catch (err) { 28 | rethrow; 29 | } 30 | } 31 | 32 | @action 33 | Future loadMore( 34 | BuildContext context, int? mid) async { 35 | try { 36 | MessageListData data = 37 | await Data().messageRepository.getMessageList(mid, state.page + 1); 38 | state = ConversationDetailStoreData( 39 | page: state.page + 1, 40 | enablePullUp: data.hasNext, 41 | list: state.list..addAll(data.messageList), 42 | ); 43 | return state; 44 | } catch (err) { 45 | rethrow; 46 | } 47 | } 48 | } 49 | 50 | class ConversationDetailStoreData { 51 | final int page; 52 | final bool enablePullUp; 53 | final List list; 54 | 55 | const ConversationDetailStoreData({ 56 | this.page = 1, 57 | this.enablePullUp = false, 58 | this.list = const [], 59 | }); 60 | 61 | factory ConversationDetailStoreData.initial() => ConversationDetailStoreData( 62 | page: 1, 63 | enablePullUp: false, 64 | list: [], 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /lib/store/message/conversation_detail_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'conversation_detail_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$ConversationDetailStore on _ConversationDetailStore, Store { 12 | final _$stateAtom = Atom(name: '_ConversationDetailStore.state'); 13 | 14 | @override 15 | ConversationDetailStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(ConversationDetailStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_ConversationDetailStore.refresh'); 28 | 29 | @override 30 | Future refresh(BuildContext context, int? mid) { 31 | return _$refreshAsyncAction.run(() => super.refresh(context, mid)); 32 | } 33 | 34 | final _$loadMoreAsyncAction = 35 | AsyncAction('_ConversationDetailStore.loadMore'); 36 | 37 | @override 38 | Future loadMore(BuildContext context, int? mid) { 39 | return _$loadMoreAsyncAction.run(() => super.loadMore(context, mid)); 40 | } 41 | 42 | @override 43 | String toString() { 44 | return ''' 45 | state: ${state} 46 | '''; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/store/message/conversation_list_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/conversation.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'conversation_list_store.g.dart'; 6 | 7 | class ConversationListStore = _ConversationListStore 8 | with _$ConversationListStore; 9 | 10 | abstract class _ConversationListStore with Store { 11 | @observable 12 | ConversationListStoreData state = ConversationListStoreData.initial(); 13 | 14 | @action 15 | Future refresh() async { 16 | try { 17 | ConversationListData data = 18 | await Data().messageRepository.getConversationList(1); 19 | state = ConversationListStoreData( 20 | page: 1, 21 | enablePullUp: data.hasNext, 22 | list: data.conversationList.toList(), 23 | ); 24 | return state; 25 | } catch (err) { 26 | rethrow; 27 | } 28 | } 29 | 30 | @action 31 | Future loadMore() async { 32 | try { 33 | ConversationListData data = 34 | await Data().messageRepository.getConversationList(state.page + 1); 35 | state = ConversationListStoreData( 36 | page: state.page + 1, 37 | enablePullUp: data.hasNext, 38 | list: state.list..addAll(data.conversationList), 39 | ); 40 | return state; 41 | } catch (err) { 42 | rethrow; 43 | } 44 | } 45 | } 46 | 47 | class ConversationListStoreData { 48 | final int page; 49 | final bool enablePullUp; 50 | final List list; 51 | 52 | const ConversationListStoreData({ 53 | this.page = 1, 54 | this.enablePullUp = false, 55 | this.list = const [], 56 | }); 57 | 58 | factory ConversationListStoreData.initial() => ConversationListStoreData( 59 | page: 1, 60 | enablePullUp: false, 61 | list: [], 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /lib/store/message/conversation_list_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'conversation_list_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$ConversationListStore on _ConversationListStore, Store { 12 | final _$stateAtom = Atom(name: '_ConversationListStore.state'); 13 | 14 | @override 15 | ConversationListStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(ConversationListStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_ConversationListStore.refresh'); 28 | 29 | @override 30 | Future refresh() { 31 | return _$refreshAsyncAction.run(() => super.refresh()); 32 | } 33 | 34 | final _$loadMoreAsyncAction = AsyncAction('_ConversationListStore.loadMore'); 35 | 36 | @override 37 | Future loadMore() { 38 | return _$loadMoreAsyncAction.run(() => super.loadMore()); 39 | } 40 | 41 | @override 42 | String toString() { 43 | return ''' 44 | state: ${state} 45 | '''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/store/message/send_message_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_nga/data/data.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | import 'package:mobx/mobx.dart'; 5 | 6 | part 'send_message_store.g.dart'; 7 | 8 | class SendMessageStore = _SendMessageStore with _$SendMessageStore; 9 | 10 | abstract class _SendMessageStore with Store { 11 | @observable 12 | List contacts = []; 13 | 14 | @action 15 | add(String contact) { 16 | contacts.add(contact); 17 | this.contacts = contacts; 18 | } 19 | 20 | @action 21 | remove(String contact) { 22 | contacts.removeWhere((element) => element == contact); 23 | this.contacts = contacts; 24 | } 25 | 26 | Future send(int? mid, String subject, String content) async { 27 | try { 28 | return await Data() 29 | .messageRepository 30 | .sendMessage(mid, contacts, subject, content); 31 | } catch (err) { 32 | if (err is DioError) { 33 | Fluttertoast.showToast(msg: err.message); 34 | } else { 35 | Fluttertoast.showToast(msg: err.toString()); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/store/message/send_message_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'send_message_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$SendMessageStore on _SendMessageStore, Store { 12 | final _$contactsAtom = Atom(name: '_SendMessageStore.contacts'); 13 | 14 | @override 15 | List get contacts { 16 | _$contactsAtom.reportRead(); 17 | return super.contacts; 18 | } 19 | 20 | @override 21 | set contacts(List value) { 22 | _$contactsAtom.reportWrite(value, super.contacts, () { 23 | super.contacts = value; 24 | }); 25 | } 26 | 27 | final _$_SendMessageStoreActionController = 28 | ActionController(name: '_SendMessageStore'); 29 | 30 | @override 31 | dynamic add(String contact) { 32 | final _$actionInfo = _$_SendMessageStoreActionController.startAction( 33 | name: '_SendMessageStore.add'); 34 | try { 35 | return super.add(contact); 36 | } finally { 37 | _$_SendMessageStoreActionController.endAction(_$actionInfo); 38 | } 39 | } 40 | 41 | @override 42 | dynamic remove(String contact) { 43 | final _$actionInfo = _$_SendMessageStoreActionController.startAction( 44 | name: '_SendMessageStore.remove'); 45 | try { 46 | return super.remove(contact); 47 | } finally { 48 | _$_SendMessageStoreActionController.endAction(_$actionInfo); 49 | } 50 | } 51 | 52 | @override 53 | String toString() { 54 | return ''' 55 | contacts: ${contacts} 56 | '''; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/store/notification/notification_list_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/notification.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'notification_list_store.g.dart'; 6 | 7 | class NotificationListStore = _NotificationListStore 8 | with _$NotificationListStore; 9 | 10 | abstract class _NotificationListStore with Store { 11 | @observable 12 | NotificationInfoListData state = NotificationInfoListData( 13 | replyNotificationList: [], 14 | messageNotificationList: [], 15 | systemNotificationList: [], 16 | unread: 0, 17 | ); 18 | 19 | int get count => 20 | 3 + 21 | state.replyNotificationList!.length + 22 | state.messageNotificationList!.length + 23 | state.systemNotificationList!.length; 24 | 25 | @action 26 | Future refresh() async { 27 | try { 28 | return Data() 29 | .messageRepository 30 | .getNotificationList() 31 | .then((v) => state = v); 32 | } catch (err) { 33 | rethrow; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/store/notification/notification_list_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'notification_list_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$NotificationListStore on _NotificationListStore, Store { 12 | final _$stateAtom = Atom(name: '_NotificationListStore.state'); 13 | 14 | @override 15 | NotificationInfoListData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(NotificationInfoListData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_NotificationListStore.refresh'); 28 | 29 | @override 30 | Future refresh() { 31 | return _$refreshAsyncAction.run(() => super.refresh()); 32 | } 33 | 34 | @override 35 | String toString() { 36 | return ''' 37 | state: ${state} 38 | '''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/store/search/input_deletion_status_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'input_deletion_status_store.g.dart'; 4 | 5 | class InputDeletionStatusStore = _InputDeletionStatusStore 6 | with _$InputDeletionStatusStore; 7 | 8 | abstract class _InputDeletionStatusStore with Store { 9 | @observable 10 | bool visible = false; 11 | 12 | @action 13 | void setVisible(bool val) { 14 | visible = val; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/store/search/input_deletion_status_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'input_deletion_status_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$InputDeletionStatusStore on _InputDeletionStatusStore, Store { 12 | final _$visibleAtom = Atom(name: '_InputDeletionStatusStore.visible'); 13 | 14 | @override 15 | bool get visible { 16 | _$visibleAtom.reportRead(); 17 | return super.visible; 18 | } 19 | 20 | @override 21 | set visible(bool value) { 22 | _$visibleAtom.reportWrite(value, super.visible, () { 23 | super.visible = value; 24 | }); 25 | } 26 | 27 | final _$_InputDeletionStatusStoreActionController = 28 | ActionController(name: '_InputDeletionStatusStore'); 29 | 30 | @override 31 | void setVisible(bool val) { 32 | final _$actionInfo = _$_InputDeletionStatusStoreActionController 33 | .startAction(name: '_InputDeletionStatusStore.setVisible'); 34 | try { 35 | return super.setVisible(val); 36 | } finally { 37 | _$_InputDeletionStatusStoreActionController.endAction(_$actionInfo); 38 | } 39 | } 40 | 41 | @override 42 | String toString() { 43 | return ''' 44 | visible: ${visible} 45 | '''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/store/search/search_forum_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/forum.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'search_forum_store.g.dart'; 6 | 7 | class SearchForumStore = _SearchForumStore with _$SearchForumStore; 8 | 9 | abstract class _SearchForumStore with Store { 10 | @observable 11 | List forums = []; 12 | 13 | @action 14 | Future> search(String keyword) async { 15 | List results = await Data().forumRepository.getForumByName(keyword); 16 | forums = results; 17 | return results; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/store/search/search_forum_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'search_forum_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$SearchForumStore on _SearchForumStore, Store { 12 | final _$forumsAtom = Atom(name: '_SearchForumStore.forums'); 13 | 14 | @override 15 | List get forums { 16 | _$forumsAtom.reportRead(); 17 | return super.forums; 18 | } 19 | 20 | @override 21 | set forums(List value) { 22 | _$forumsAtom.reportWrite(value, super.forums, () { 23 | super.forums = value; 24 | }); 25 | } 26 | 27 | final _$searchAsyncAction = AsyncAction('_SearchForumStore.search'); 28 | 29 | @override 30 | Future> search(String keyword) { 31 | return _$searchAsyncAction.run(() => super.search(keyword)); 32 | } 33 | 34 | @override 35 | String toString() { 36 | return ''' 37 | forums: ${forums} 38 | '''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/store/search/search_options_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'search_options_store.g.dart'; 4 | 5 | class SearchOptionsStore = _SearchOptionsStore with _$SearchOptionsStore; 6 | 7 | abstract class _SearchOptionsStore with Store { 8 | @observable 9 | SearchStoreData state = SearchStoreData.allForumTopic(); 10 | 11 | @action 12 | void checkFirstRadio(int value) { 13 | state = SearchStoreData( 14 | value, state.content, state.topicRadio, state.userRadio); 15 | } 16 | 17 | @action 18 | void checkTopicRadio(int value) { 19 | state = SearchStoreData( 20 | state.firstRadio, state.content, value, state.userRadio); 21 | } 22 | 23 | @action 24 | void checkUserRadio(int value) { 25 | state = SearchStoreData( 26 | state.firstRadio, state.content, state.topicRadio, value); 27 | } 28 | 29 | @action 30 | void checkContent(bool value) { 31 | state = SearchStoreData( 32 | state.firstRadio, value, state.topicRadio, state.userRadio); 33 | } 34 | } 35 | 36 | class SearchStoreData { 37 | static const int FIRST_RADIO_TOPIC = 1; 38 | static const int FIRST_RADIO_FORUM = 2; 39 | static const int FIRST_RADIO_USER = 3; 40 | 41 | static const int TOPIC_RADIO_ALL_FORUM = 4; 42 | static const int TOPIC_RADIO_CURRENT_FORUM = 5; 43 | 44 | static const int USER_RADIO_NAME = 6; 45 | static const int USER_RADIO_UID = 7; 46 | 47 | final int firstRadio; 48 | final bool content; 49 | final int topicRadio; 50 | final int userRadio; 51 | 52 | const SearchStoreData( 53 | this.firstRadio, this.content, this.topicRadio, this.userRadio); 54 | 55 | factory SearchStoreData.allForumTopic() => SearchStoreData( 56 | FIRST_RADIO_TOPIC, false, TOPIC_RADIO_ALL_FORUM, USER_RADIO_NAME); 57 | 58 | factory SearchStoreData.currentForumTopic() => SearchStoreData( 59 | FIRST_RADIO_TOPIC, false, TOPIC_RADIO_CURRENT_FORUM, USER_RADIO_NAME); 60 | 61 | factory SearchStoreData.forum() => SearchStoreData( 62 | FIRST_RADIO_FORUM, false, TOPIC_RADIO_ALL_FORUM, USER_RADIO_NAME); 63 | 64 | factory SearchStoreData.user() => SearchStoreData( 65 | FIRST_RADIO_USER, false, TOPIC_RADIO_ALL_FORUM, USER_RADIO_NAME); 66 | } 67 | -------------------------------------------------------------------------------- /lib/store/search/search_options_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'search_options_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$SearchOptionsStore on _SearchOptionsStore, Store { 12 | final _$stateAtom = Atom(name: '_SearchOptionsStore.state'); 13 | 14 | @override 15 | SearchStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(SearchStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$_SearchOptionsStoreActionController = 28 | ActionController(name: '_SearchOptionsStore'); 29 | 30 | @override 31 | void checkFirstRadio(int value) { 32 | final _$actionInfo = _$_SearchOptionsStoreActionController.startAction( 33 | name: '_SearchOptionsStore.checkFirstRadio'); 34 | try { 35 | return super.checkFirstRadio(value); 36 | } finally { 37 | _$_SearchOptionsStoreActionController.endAction(_$actionInfo); 38 | } 39 | } 40 | 41 | @override 42 | void checkTopicRadio(int value) { 43 | final _$actionInfo = _$_SearchOptionsStoreActionController.startAction( 44 | name: '_SearchOptionsStore.checkTopicRadio'); 45 | try { 46 | return super.checkTopicRadio(value); 47 | } finally { 48 | _$_SearchOptionsStoreActionController.endAction(_$actionInfo); 49 | } 50 | } 51 | 52 | @override 53 | void checkUserRadio(int value) { 54 | final _$actionInfo = _$_SearchOptionsStoreActionController.startAction( 55 | name: '_SearchOptionsStore.checkUserRadio'); 56 | try { 57 | return super.checkUserRadio(value); 58 | } finally { 59 | _$_SearchOptionsStoreActionController.endAction(_$actionInfo); 60 | } 61 | } 62 | 63 | @override 64 | void checkContent(bool value) { 65 | final _$actionInfo = _$_SearchOptionsStoreActionController.startAction( 66 | name: '_SearchOptionsStore.checkContent'); 67 | try { 68 | return super.checkContent(value); 69 | } finally { 70 | _$_SearchOptionsStoreActionController.endAction(_$actionInfo); 71 | } 72 | } 73 | 74 | @override 75 | String toString() { 76 | return ''' 77 | state: ${state} 78 | '''; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/store/search/search_topic_list_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/topic.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'search_topic_list_store.g.dart'; 6 | 7 | class SearchTopicListStore = _SearchTopicListStore with _$SearchTopicListStore; 8 | 9 | abstract class _SearchTopicListStore with Store { 10 | @observable 11 | SearchTopicListStoreData state = SearchTopicListStoreData.initial(); 12 | 13 | @action 14 | Future refresh( 15 | String keyword, int? fid, bool content) async { 16 | try { 17 | TopicListData data = 18 | await Data().topicRepository.searchTopic(keyword, fid, content, 1); 19 | state = SearchTopicListStoreData( 20 | page: 1, 21 | maxPage: data.maxPage, 22 | enablePullUp: 1 < data.maxPage, 23 | list: data.topicList.values.toList(), 24 | ); 25 | return state; 26 | } catch (err) { 27 | rethrow; 28 | } 29 | } 30 | 31 | @action 32 | Future loadMore( 33 | String keyword, int? fid, bool content) async { 34 | try { 35 | TopicListData data = await Data() 36 | .topicRepository 37 | .searchTopic(keyword, fid, content, state.page); 38 | state = SearchTopicListStoreData( 39 | page: state.page + 1, 40 | maxPage: data.maxPage, 41 | enablePullUp: state.page + 1 < data.maxPage, 42 | list: state.list..addAll(data.topicList.values), 43 | ); 44 | return state; 45 | } catch (err) { 46 | rethrow; 47 | } 48 | } 49 | } 50 | 51 | class SearchTopicListStoreData { 52 | final int page; 53 | final int maxPage; 54 | final bool enablePullUp; 55 | final List list; 56 | 57 | const SearchTopicListStoreData({ 58 | this.page = 1, 59 | this.maxPage = 1, 60 | this.enablePullUp = false, 61 | this.list = const [], 62 | }); 63 | 64 | factory SearchTopicListStoreData.initial() => SearchTopicListStoreData( 65 | page: 1, 66 | maxPage: 1, 67 | enablePullUp: false, 68 | list: [], 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /lib/store/search/search_topic_list_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'search_topic_list_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$SearchTopicListStore on _SearchTopicListStore, Store { 12 | final _$stateAtom = Atom(name: '_SearchTopicListStore.state'); 13 | 14 | @override 15 | SearchTopicListStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(SearchTopicListStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_SearchTopicListStore.refresh'); 28 | 29 | @override 30 | Future refresh( 31 | String keyword, int? fid, bool content) { 32 | return _$refreshAsyncAction.run(() => super.refresh(keyword, fid, content)); 33 | } 34 | 35 | final _$loadMoreAsyncAction = AsyncAction('_SearchTopicListStore.loadMore'); 36 | 37 | @override 38 | Future loadMore( 39 | String keyword, int? fid, bool content) { 40 | return _$loadMoreAsyncAction 41 | .run(() => super.loadMore(keyword, fid, content)); 42 | } 43 | 44 | @override 45 | String toString() { 46 | return ''' 47 | state: ${state} 48 | '''; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/store/settings/theme_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_theme/adaptive_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'theme_store.g.dart'; 6 | 7 | class ThemeStore = _ThemeStore with _$ThemeStore; 8 | 9 | abstract class _ThemeStore with Store { 10 | @observable 11 | AdaptiveThemeMode? mode; 12 | 13 | String get modeName { 14 | switch (mode) { 15 | case AdaptiveThemeMode.light: 16 | return "亮色主题"; 17 | case AdaptiveThemeMode.dark: 18 | return "暗色主题"; 19 | case AdaptiveThemeMode.system: 20 | return "跟随系统"; 21 | default: 22 | return "亮色主题"; 23 | } 24 | } 25 | 26 | @action 27 | Future refresh() async { 28 | mode = await AdaptiveTheme.getThemeMode() ?? mode; 29 | } 30 | 31 | @action 32 | void update(BuildContext context, AdaptiveThemeMode adaptiveThemeMode) { 33 | AdaptiveTheme.of(context).setThemeMode(adaptiveThemeMode); 34 | mode = adaptiveThemeMode; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/store/settings/theme_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'theme_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$ThemeStore on _ThemeStore, Store { 12 | final _$modeAtom = Atom(name: '_ThemeStore.mode'); 13 | 14 | @override 15 | AdaptiveThemeMode? get mode { 16 | _$modeAtom.reportRead(); 17 | return super.mode; 18 | } 19 | 20 | @override 21 | set mode(AdaptiveThemeMode? value) { 22 | _$modeAtom.reportWrite(value, super.mode, () { 23 | super.mode = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_ThemeStore.refresh'); 28 | 29 | @override 30 | Future refresh() { 31 | return _$refreshAsyncAction.run(() => super.refresh()); 32 | } 33 | 34 | final _$_ThemeStoreActionController = ActionController(name: '_ThemeStore'); 35 | 36 | @override 37 | void update(BuildContext context, AdaptiveThemeMode adaptiveThemeMode) { 38 | final _$actionInfo = 39 | _$_ThemeStoreActionController.startAction(name: '_ThemeStore.update'); 40 | try { 41 | return super.update(context, adaptiveThemeMode); 42 | } finally { 43 | _$_ThemeStoreActionController.endAction(_$actionInfo); 44 | } 45 | } 46 | 47 | @override 48 | String toString() { 49 | return ''' 50 | mode: ${mode} 51 | '''; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/store/topic/favourite_topic_list_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/topic.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'favourite_topic_list_store.g.dart'; 6 | 7 | class FavouriteTopicListStore = _FavouriteTopicListStore 8 | with _$FavouriteTopicListStore; 9 | 10 | abstract class _FavouriteTopicListStore with Store { 11 | @observable 12 | FavouriteTopicListStoreData state = FavouriteTopicListStoreData.initial(); 13 | 14 | @action 15 | Future refresh() async { 16 | try { 17 | TopicListData data = 18 | await Data().topicRepository.getFavouriteTopicList(1); 19 | state = FavouriteTopicListStoreData( 20 | size: data.topicRows, 21 | page: 1, 22 | maxPage: data.maxPage, 23 | enablePullUp: 1 < data.maxPage, 24 | list: data.topicList.values.toList(), 25 | ); 26 | return state; 27 | } catch (err) { 28 | rethrow; 29 | } 30 | } 31 | 32 | @action 33 | Future loadMore() async { 34 | try { 35 | TopicListData data = 36 | await Data().topicRepository.getFavouriteTopicList(state.page + 1); 37 | state = FavouriteTopicListStoreData( 38 | size: data.topicRows, 39 | page: state.page + 1, 40 | maxPage: data.maxPage, 41 | enablePullUp: state.page + 1 < data.maxPage, 42 | list: state.list..addAll(data.topicList.values), 43 | ); 44 | return state; 45 | } catch (err) { 46 | rethrow; 47 | } 48 | } 49 | 50 | @action 51 | Future delete(Topic topic) async { 52 | try { 53 | String? message = await Data() 54 | .topicRepository 55 | .deleteFavouriteTopic(topic.tid, topic.page); 56 | state = FavouriteTopicListStoreData( 57 | size: state.size, 58 | page: state.page, 59 | maxPage: state.maxPage, 60 | enablePullUp: state.enablePullUp, 61 | list: state.list..removeWhere((t) => t.tid == topic.tid), 62 | ); 63 | return message; 64 | } catch (err) { 65 | rethrow; 66 | } 67 | } 68 | } 69 | 70 | class FavouriteTopicListStoreData { 71 | final int size; 72 | final int page; 73 | final int maxPage; 74 | final bool enablePullUp; 75 | final List list; 76 | 77 | const FavouriteTopicListStoreData({ 78 | this.size = 35, 79 | this.page = 1, 80 | this.maxPage = 1, 81 | this.enablePullUp = false, 82 | this.list = const [], 83 | }); 84 | 85 | factory FavouriteTopicListStoreData.initial() => FavouriteTopicListStoreData( 86 | size: 35, 87 | page: 1, 88 | maxPage: 1, 89 | enablePullUp: false, 90 | list: [], 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /lib/store/topic/favourite_topic_list_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'favourite_topic_list_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$FavouriteTopicListStore on _FavouriteTopicListStore, Store { 12 | final _$stateAtom = Atom(name: '_FavouriteTopicListStore.state'); 13 | 14 | @override 15 | FavouriteTopicListStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(FavouriteTopicListStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_FavouriteTopicListStore.refresh'); 28 | 29 | @override 30 | Future refresh() { 31 | return _$refreshAsyncAction.run(() => super.refresh()); 32 | } 33 | 34 | final _$loadMoreAsyncAction = 35 | AsyncAction('_FavouriteTopicListStore.loadMore'); 36 | 37 | @override 38 | Future loadMore() { 39 | return _$loadMoreAsyncAction.run(() => super.loadMore()); 40 | } 41 | 42 | final _$deleteAsyncAction = AsyncAction('_FavouriteTopicListStore.delete'); 43 | 44 | @override 45 | Future delete(Topic topic) { 46 | return _$deleteAsyncAction.run(() => super.delete(topic)); 47 | } 48 | 49 | @override 50 | String toString() { 51 | return ''' 52 | state: ${state} 53 | '''; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/store/topic/topic_detail_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/topic.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'topic_detail_store.g.dart'; 6 | 7 | class TopicDetailStore = _TopicDetailStore with _$TopicDetailStore; 8 | 9 | abstract class _TopicDetailStore with Store { 10 | @observable 11 | int currentPage = 1; 12 | @observable 13 | int maxPage = 1; 14 | @observable 15 | int maxFloor = 1; 16 | @observable 17 | Topic? topic; 18 | 19 | String? get subject => topic != null ? topic!.subject : ""; 20 | 21 | @action 22 | void setMaxPage(int maxPage) { 23 | this.maxPage = maxPage; 24 | } 25 | 26 | @action 27 | void setMaxFloor(int maxFloor) { 28 | this.maxFloor = maxFloor; 29 | } 30 | 31 | @action 32 | void setCurrentPage(int currentPage) { 33 | this.currentPage = currentPage; 34 | } 35 | 36 | @action 37 | void setTopic(Topic? topic) { 38 | if (topic == null || (this.topic != null && this.topic!.tid == topic.tid)) 39 | return; 40 | this.topic = topic; 41 | } 42 | 43 | Future addFavourite(int? tid) async { 44 | try { 45 | String? message = await Data().topicRepository.addFavouriteTopic(tid); 46 | return message; 47 | } catch (err) { 48 | rethrow; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/store/topic/topic_history_list_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'topic_history_list_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$TopicHistoryListStore on _TopicHistoryListStore, Store { 12 | final _$stateAtom = Atom(name: '_TopicHistoryListStore.state'); 13 | 14 | @override 15 | TopicHistoryListStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(TopicHistoryListStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_TopicHistoryListStore.refresh'); 28 | 29 | @override 30 | Future refresh() { 31 | return _$refreshAsyncAction.run(() => super.refresh()); 32 | } 33 | 34 | final _$loadMoreAsyncAction = AsyncAction('_TopicHistoryListStore.loadMore'); 35 | 36 | @override 37 | Future loadMore() { 38 | return _$loadMoreAsyncAction.run(() => super.loadMore()); 39 | } 40 | 41 | final _$_TopicHistoryListStoreActionController = 42 | ActionController(name: '_TopicHistoryListStore'); 43 | 44 | @override 45 | Future delete(int id) { 46 | final _$actionInfo = _$_TopicHistoryListStoreActionController.startAction( 47 | name: '_TopicHistoryListStore.delete'); 48 | try { 49 | return super.delete(id); 50 | } finally { 51 | _$_TopicHistoryListStoreActionController.endAction(_$actionInfo); 52 | } 53 | } 54 | 55 | @override 56 | Future clean() { 57 | final _$actionInfo = _$_TopicHistoryListStoreActionController.startAction( 58 | name: '_TopicHistoryListStore.clean'); 59 | try { 60 | return super.clean(); 61 | } finally { 62 | _$_TopicHistoryListStoreActionController.endAction(_$actionInfo); 63 | } 64 | } 65 | 66 | @override 67 | String toString() { 68 | return ''' 69 | state: ${state} 70 | '''; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/store/topic/topic_history_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/topic_history.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'topic_history_store.g.dart'; 6 | 7 | class TopicHistoryStore = _TopicHistoryStore with _$TopicHistoryStore; 8 | 9 | abstract class _TopicHistoryStore with Store { 10 | @action 11 | void insertHistory(TopicHistory history) { 12 | Data().topicRepository.insertTopicHistory(history); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/store/topic/topic_history_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'topic_history_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$TopicHistoryStore on _TopicHistoryStore, Store { 12 | final _$_TopicHistoryStoreActionController = 13 | ActionController(name: '_TopicHistoryStore'); 14 | 15 | @override 16 | void insertHistory(TopicHistory history) { 17 | final _$actionInfo = _$_TopicHistoryStoreActionController.startAction( 18 | name: '_TopicHistoryStore.insertHistory'); 19 | try { 20 | return super.insertHistory(history); 21 | } finally { 22 | _$_TopicHistoryStoreActionController.endAction(_$actionInfo); 23 | } 24 | } 25 | 26 | @override 27 | String toString() { 28 | return ''' 29 | 30 | '''; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/store/topic/topic_reply_store.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_nga/data/data.dart'; 5 | import 'package:flutter_nga/data/entity/topic_detail.dart'; 6 | import 'package:flutter_nga/data/entity/user.dart'; 7 | import 'package:mobx/mobx.dart'; 8 | 9 | part 'topic_reply_store.g.dart'; 10 | 11 | class TopicReplyStore = _TopicReplyStore with _$TopicReplyStore; 12 | 13 | abstract class _TopicReplyStore with Store { 14 | @observable 15 | TopicReplyStoreData state = TopicReplyStoreData.initial(); 16 | 17 | Future load(BuildContext context, int? pid) async { 18 | try { 19 | TopicDetailData data = await Data().topicRepository.getTopicReplies(pid); 20 | List replyList = []; 21 | data.replyList.values.forEach((reply) { 22 | replyList.add(reply); 23 | }); 24 | List userList = data.userList.values.toList(); 25 | Set groups = data.groupList.values.toSet(); 26 | Set medals = data.medalList.values.toSet(); 27 | state = TopicReplyStoreData( 28 | replyList: replyList, 29 | userList: userList, 30 | groupSet: groups, 31 | medalSet: medals, 32 | ); 33 | return state; 34 | } catch (err) { 35 | rethrow; 36 | } 37 | } 38 | } 39 | 40 | class TopicReplyStoreData { 41 | final List replyList; 42 | final List userList; 43 | final Set groupSet; 44 | final Set medalSet; 45 | 46 | TopicReplyStoreData({ 47 | this.replyList = const [], 48 | this.userList = const [], 49 | this.groupSet = const {}, 50 | this.medalSet = const {}, 51 | }); 52 | 53 | factory TopicReplyStoreData.initial() { 54 | return TopicReplyStoreData( 55 | replyList: [], 56 | userList: [], 57 | groupSet: Set(), 58 | medalSet: HashSet(), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/store/topic/topic_reply_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'topic_reply_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$TopicReplyStore on _TopicReplyStore, Store { 12 | final _$stateAtom = Atom(name: '_TopicReplyStore.state'); 13 | 14 | @override 15 | TopicReplyStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(TopicReplyStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | @override 28 | String toString() { 29 | return ''' 30 | state: ${state} 31 | '''; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/store/topic/topic_single_page_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'topic_single_page_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$TopicSinglePageStore on _TopicSinglePageStore, Store { 12 | final _$stateAtom = Atom(name: '_TopicSinglePageStore.state'); 13 | 14 | @override 15 | TopicSinglePageStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(TopicSinglePageStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_TopicSinglePageStore.refresh'); 28 | 29 | @override 30 | Future refresh( 31 | BuildContext context, int tid, int page, int? authorid) { 32 | return _$refreshAsyncAction 33 | .run(() => super.refresh(context, tid, page, authorid)); 34 | } 35 | 36 | @override 37 | String toString() { 38 | return ''' 39 | state: ${state} 40 | '''; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/store/user/account_list_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/user.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'account_list_store.g.dart'; 6 | 7 | class AccountListStore = _AccountListStore with _$AccountListStore; 8 | 9 | abstract class _AccountListStore with Store { 10 | @observable 11 | List list = []; 12 | 13 | @action 14 | Future refresh() async { 15 | List accountList = await Data().userRepository.getAllLoginUser(); 16 | list = accountList; 17 | } 18 | 19 | @action 20 | Future quitAll() { 21 | return Data().userRepository.quitAllLoginUser(); 22 | } 23 | 24 | @action 25 | Future setDefault(CacheUser cacheUser) { 26 | return Data().userRepository.setDefault(cacheUser); 27 | } 28 | 29 | @action 30 | Future delete(CacheUser cacheUser) { 31 | return Data().userRepository.deleteCacheUser(cacheUser); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/store/user/account_list_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'account_list_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$AccountListStore on _AccountListStore, Store { 12 | final _$listAtom = Atom(name: '_AccountListStore.list'); 13 | 14 | @override 15 | List get list { 16 | _$listAtom.reportRead(); 17 | return super.list; 18 | } 19 | 20 | @override 21 | set list(List value) { 22 | _$listAtom.reportWrite(value, super.list, () { 23 | super.list = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_AccountListStore.refresh'); 28 | 29 | @override 30 | Future refresh() { 31 | return _$refreshAsyncAction.run(() => super.refresh()); 32 | } 33 | 34 | final _$_AccountListStoreActionController = 35 | ActionController(name: '_AccountListStore'); 36 | 37 | @override 38 | Future quitAll() { 39 | final _$actionInfo = _$_AccountListStoreActionController.startAction( 40 | name: '_AccountListStore.quitAll'); 41 | try { 42 | return super.quitAll(); 43 | } finally { 44 | _$_AccountListStoreActionController.endAction(_$actionInfo); 45 | } 46 | } 47 | 48 | @override 49 | Future setDefault(CacheUser cacheUser) { 50 | final _$actionInfo = _$_AccountListStoreActionController.startAction( 51 | name: '_AccountListStore.setDefault'); 52 | try { 53 | return super.setDefault(cacheUser); 54 | } finally { 55 | _$_AccountListStoreActionController.endAction(_$actionInfo); 56 | } 57 | } 58 | 59 | @override 60 | Future delete(CacheUser cacheUser) { 61 | final _$actionInfo = _$_AccountListStoreActionController.startAction( 62 | name: '_AccountListStore.delete'); 63 | try { 64 | return super.delete(cacheUser); 65 | } finally { 66 | _$_AccountListStoreActionController.endAction(_$actionInfo); 67 | } 68 | } 69 | 70 | @override 71 | String toString() { 72 | return ''' 73 | list: ${list} 74 | '''; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/store/user/user_info_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_info_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$UserInfoStore on _UserInfoStore, Store { 12 | final _$stateAtom = Atom(name: '_UserInfoStore.state'); 13 | 14 | @override 15 | UserInfoStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(UserInfoStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$loadByNameAsyncAction = AsyncAction('_UserInfoStore.loadByName'); 28 | 29 | @override 30 | Future loadByName(String? username) { 31 | return _$loadByNameAsyncAction.run(() => super.loadByName(username)); 32 | } 33 | 34 | final _$loadByUidAsyncAction = AsyncAction('_UserInfoStore.loadByUid'); 35 | 36 | @override 37 | Future loadByUid(String? uid) { 38 | return _$loadByUidAsyncAction.run(() => super.loadByUid(uid)); 39 | } 40 | 41 | @override 42 | String toString() { 43 | return ''' 44 | state: ${state} 45 | '''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/store/user/user_replies_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/topic.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'user_replies_store.g.dart'; 6 | 7 | class UserRepliesStore = _UserRepliesStore with _$UserRepliesStore; 8 | 9 | abstract class _UserRepliesStore with Store { 10 | @observable 11 | UserRepliesStoreData state = UserRepliesStoreData.initial(); 12 | 13 | @action 14 | Future refresh(int authorid) async { 15 | try { 16 | TopicListData data = 17 | await Data().topicRepository.getUserReplies(authorid, 1); 18 | state = UserRepliesStoreData( 19 | size: data.rRows, 20 | page: 1, 21 | enablePullUp: data.topicList.length == data.rRows, 22 | list: data.topicList.values.toList(), 23 | ); 24 | return state; 25 | } catch (err) { 26 | rethrow; 27 | } 28 | } 29 | 30 | @action 31 | Future loadMore(int authorid) async { 32 | try { 33 | TopicListData data = 34 | await Data().topicRepository.getUserReplies(authorid, state.page + 1); 35 | state = UserRepliesStoreData( 36 | size: data.rRows, 37 | page: state.page + 1, 38 | enablePullUp: state.list.length + data.topicList.length == 39 | data.rRows * (state.page + 1), 40 | list: state.list..addAll(data.topicList.values), 41 | ); 42 | return state; 43 | } catch (err) { 44 | rethrow; 45 | } 46 | } 47 | } 48 | 49 | class UserRepliesStoreData { 50 | final int size; 51 | final int page; 52 | final bool enablePullUp; 53 | final List list; 54 | 55 | const UserRepliesStoreData({ 56 | this.size = 20, 57 | this.page = 1, 58 | this.enablePullUp = false, 59 | this.list = const [], 60 | }); 61 | 62 | factory UserRepliesStoreData.initial() => UserRepliesStoreData( 63 | size: 20, 64 | page: 1, 65 | enablePullUp: false, 66 | list: [], 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /lib/store/user/user_replies_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_replies_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$UserRepliesStore on _UserRepliesStore, Store { 12 | final _$stateAtom = Atom(name: '_UserRepliesStore.state'); 13 | 14 | @override 15 | UserRepliesStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(UserRepliesStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_UserRepliesStore.refresh'); 28 | 29 | @override 30 | Future refresh(int authorid) { 31 | return _$refreshAsyncAction.run(() => super.refresh(authorid)); 32 | } 33 | 34 | final _$loadMoreAsyncAction = AsyncAction('_UserRepliesStore.loadMore'); 35 | 36 | @override 37 | Future loadMore(int authorid) { 38 | return _$loadMoreAsyncAction.run(() => super.loadMore(authorid)); 39 | } 40 | 41 | @override 42 | String toString() { 43 | return ''' 44 | state: ${state} 45 | '''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/store/user/user_topics_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_nga/data/data.dart'; 2 | import 'package:flutter_nga/data/entity/topic.dart'; 3 | import 'package:mobx/mobx.dart'; 4 | 5 | part 'user_topics_store.g.dart'; 6 | 7 | class UserTopicsStore = _UserTopicsStore with _$UserTopicsStore; 8 | 9 | abstract class _UserTopicsStore with Store { 10 | @observable 11 | UserTopicsStoreData state = UserTopicsStoreData.initial(); 12 | 13 | @action 14 | Future refresh(int authorid) async { 15 | try { 16 | TopicListData data = await Data() 17 | .topicRepository 18 | .getTopicList(authorid: authorid, page: 1); 19 | state = UserTopicsStoreData( 20 | size: data.topicRows, 21 | page: 1, 22 | maxPage: data.maxPage, 23 | enablePullUp: 1 < data.maxPage, 24 | list: data.topicList.values.toList(), 25 | ); 26 | return state; 27 | } catch (err) { 28 | rethrow; 29 | } 30 | } 31 | 32 | @action 33 | Future loadMore(int authorid) async { 34 | try { 35 | TopicListData data = await Data() 36 | .topicRepository 37 | .getTopicList(authorid: authorid, page: state.page + 1); 38 | state = UserTopicsStoreData( 39 | size: data.topicRows, 40 | page: state.page + 1, 41 | maxPage: data.maxPage, 42 | enablePullUp: state.page + 1 < data.maxPage, 43 | list: state.list..addAll(data.topicList.values), 44 | ); 45 | return state; 46 | } catch (err) { 47 | rethrow; 48 | } 49 | } 50 | } 51 | 52 | class UserTopicsStoreData { 53 | final int size; 54 | final int page; 55 | final int maxPage; 56 | final bool enablePullUp; 57 | final List list; 58 | 59 | const UserTopicsStoreData({ 60 | this.size = 35, 61 | this.page = 1, 62 | this.maxPage = 1, 63 | this.enablePullUp = false, 64 | this.list = const [], 65 | }); 66 | 67 | factory UserTopicsStoreData.initial() => UserTopicsStoreData( 68 | size: 35, 69 | page: 1, 70 | maxPage: 1, 71 | enablePullUp: false, 72 | list: [], 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /lib/store/user/user_topics_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_topics_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$UserTopicsStore on _UserTopicsStore, Store { 12 | final _$stateAtom = Atom(name: '_UserTopicsStore.state'); 13 | 14 | @override 15 | UserTopicsStoreData get state { 16 | _$stateAtom.reportRead(); 17 | return super.state; 18 | } 19 | 20 | @override 21 | set state(UserTopicsStoreData value) { 22 | _$stateAtom.reportWrite(value, super.state, () { 23 | super.state = value; 24 | }); 25 | } 26 | 27 | final _$refreshAsyncAction = AsyncAction('_UserTopicsStore.refresh'); 28 | 29 | @override 30 | Future refresh(int authorid) { 31 | return _$refreshAsyncAction.run(() => super.refresh(authorid)); 32 | } 33 | 34 | final _$loadMoreAsyncAction = AsyncAction('_UserTopicsStore.loadMore'); 35 | 36 | @override 37 | Future loadMore(int authorid) { 38 | return _$loadMoreAsyncAction.run(() => super.loadMore(authorid)); 39 | } 40 | 41 | @override 42 | String toString() { 43 | return ''' 44 | state: ${state} 45 | '''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/ui/page/content/content_details_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/utils/dimen.dart'; 3 | 4 | class ContentDetailsPage extends StatelessWidget { 5 | final List children; 6 | 7 | const ContentDetailsPage(this.children, {Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | body: Padding( 13 | padding: EdgeInsets.all(16), 14 | child: Center( 15 | child: DefaultTextStyle.merge( 16 | child: Wrap(children: children), 17 | style: TextStyle( 18 | fontSize: Dimen.display1.toDouble(), 19 | ), 20 | ), 21 | ), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/ui/page/conversation/conversation_detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/message/conversation_detail_store.dart'; 4 | import 'package:flutter_nga/utils/route.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 7 | 8 | import 'message_item_widget.dart'; 9 | 10 | class ConversationDetailPage extends StatefulWidget { 11 | final int? mid; 12 | 13 | const ConversationDetailPage({Key? key, this.mid}) : super(key: key); 14 | 15 | @override 16 | _ConversationDetailState createState() => _ConversationDetailState(); 17 | } 18 | 19 | class _ConversationDetailState extends State { 20 | final _store = ConversationDetailStore(); 21 | late RefreshController _refreshController; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | _refreshController = RefreshController(initialRefresh: true); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | _refreshController.dispose(); 32 | super.dispose(); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Scaffold( 38 | appBar: AppBar( 39 | title: Text('消息详情'), 40 | ), 41 | body: Observer( 42 | builder: (_) { 43 | return SmartRefresher( 44 | onLoading: _onLoading, 45 | controller: _refreshController, 46 | enablePullUp: _store.state.enablePullUp, 47 | onRefresh: _onRefresh, 48 | child: ListView.builder( 49 | itemCount: _store.state.list.length, 50 | itemBuilder: (context, index) => 51 | MessageItemWidget(message: _store.state.list[index]), 52 | ), 53 | ); 54 | }, 55 | ), 56 | floatingActionButton: FloatingActionButton( 57 | tooltip: '回复', 58 | onPressed: () => Routes.navigateTo( 59 | context, "${Routes.SEND_MESSAGE}?mid=${widget.mid}"), 60 | child: Icon( 61 | Icons.reply, 62 | color: Colors.white, 63 | ), 64 | ), 65 | ); 66 | } 67 | 68 | _onRefresh() { 69 | _store.refresh(context, widget.mid).catchError((err) { 70 | _refreshController.refreshFailed(); 71 | Fluttertoast.showToast( 72 | msg: err.toString(), 73 | ); 74 | }).whenComplete( 75 | () => _refreshController.refreshCompleted(resetFooterState: true)); 76 | } 77 | 78 | _onLoading() async { 79 | _store.loadMore(context, widget.mid).then((state) { 80 | if (state.enablePullUp) { 81 | _refreshController.loadComplete(); 82 | } else { 83 | _refreshController.loadNoData(); 84 | } 85 | }).catchError((_) => _refreshController.loadFailed()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/ui/page/conversation/conversation_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/message/conversation_list_store.dart'; 4 | import 'package:fluttertoast/fluttertoast.dart'; 5 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 6 | 7 | import 'conversation_item_widget.dart'; 8 | 9 | class ConversationListPage extends StatefulWidget { 10 | @override 11 | _ConversationListState createState() => _ConversationListState(); 12 | } 13 | 14 | class _ConversationListState extends State { 15 | final _store = ConversationListStore(); 16 | late RefreshController _refreshController; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | _refreshController = RefreshController(initialRefresh: true); 22 | } 23 | 24 | @override 25 | void dispose() { 26 | _refreshController.dispose(); 27 | super.dispose(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Observer( 33 | builder: (_) { 34 | return SmartRefresher( 35 | onLoading: _onLoading, 36 | controller: _refreshController, 37 | enablePullUp: _store.state.enablePullUp, 38 | onRefresh: _onRefresh, 39 | child: ListView.builder( 40 | itemCount: _store.state.list.length, 41 | itemBuilder: (context, index) => 42 | ConversationItemWidget(conversation: _store.state.list[index]), 43 | ), 44 | ); 45 | }, 46 | ); 47 | } 48 | 49 | _onRefresh() { 50 | _store.refresh().catchError((err) { 51 | _refreshController.refreshFailed(); 52 | Fluttertoast.showToast( 53 | msg: err.toString(), 54 | ); 55 | }).whenComplete( 56 | () => _refreshController.refreshCompleted(resetFooterState: true)); 57 | } 58 | 59 | _onLoading() async { 60 | _store.loadMore().then((state) { 61 | if (state.enablePullUp) { 62 | _refreshController.loadComplete(); 63 | } else { 64 | _refreshController.loadNoData(); 65 | } 66 | }).catchError((_) => _refreshController.loadFailed()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/ui/page/conversation/message_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/data/entity/message.dart'; 3 | import 'package:flutter_nga/ui/widget/avatar_widget.dart'; 4 | import 'package:flutter_nga/ui/widget/nga_html_content_widget.dart'; 5 | import 'package:flutter_nga/utils/code_utils.dart' as codeUtils; 6 | import 'package:flutter_nga/utils/dimen.dart'; 7 | 8 | class MessageItemWidget extends StatelessWidget { 9 | final Message message; 10 | 11 | const MessageItemWidget({Key? key, required this.message}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final children = []; 16 | children.add(Padding( 17 | padding: EdgeInsets.all(16), 18 | child: Row( 19 | mainAxisAlignment: MainAxisAlignment.start, 20 | crossAxisAlignment: CrossAxisAlignment.start, 21 | children: [ 22 | AvatarWidget( 23 | message.user.avatar, 24 | size: 24, 25 | username: message.user.username, 26 | ), 27 | Expanded( 28 | child: Padding( 29 | padding: EdgeInsets.symmetric(horizontal: 16), 30 | child: Text( 31 | message.user.getShowName(), 32 | style: TextStyle(color: Colors.black), 33 | ), 34 | ), 35 | ), 36 | Text( 37 | codeUtils.formatPostDate(message.time! * 1000), 38 | style: TextStyle( 39 | color: Theme.of(context).textTheme.bodyText2?.color, 40 | fontSize: Dimen.caption, 41 | ), 42 | ), 43 | ], 44 | ), 45 | )); 46 | if (!codeUtils.isStringEmpty(message.subject)) { 47 | children.add(Padding( 48 | padding: EdgeInsets.symmetric(horizontal: 16), 49 | child: Text( 50 | codeUtils.unescapeHtml(message.subject), 51 | style: TextStyle( 52 | fontSize: Dimen.title, 53 | fontWeight: FontWeight.bold, 54 | color: Theme.of(context).textTheme.bodyText1?.color, 55 | ), 56 | ), 57 | )); 58 | } 59 | children.add(Padding( 60 | padding: EdgeInsets.all(16), 61 | child: NgaHtmlContentWidget(content: message.content), 62 | )); 63 | children.add(Divider(height: 1)); 64 | return Column( 65 | crossAxisAlignment: CrossAxisAlignment.start, 66 | children: children, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/ui/page/forum_detail/child_forum_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/data/entity/topic.dart'; 3 | import 'package:flutter_nga/utils/dimen.dart'; 4 | 5 | import 'child_forum_item_widget.dart'; 6 | 7 | class ChildForumListPage extends StatefulWidget { 8 | final ForumInfo? forumInfo; 9 | 10 | const ChildForumListPage(this.forumInfo, {Key? key}) : super(key: key); 11 | 12 | @override 13 | _ChildForumListPage createState() => _ChildForumListPage(); 14 | } 15 | 16 | class _ChildForumListPage extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return MediaQuery.removePadding( 20 | context: context, 21 | removeTop: true, 22 | child: widget.forumInfo == null || widget.forumInfo!.subForums!.isEmpty 23 | ? Center( 24 | child: Text( 25 | "本版暂无子版", 26 | style: TextStyle( 27 | fontSize: Dimen.subheading, 28 | color: Theme.of(context).textTheme.bodyText2?.color, 29 | ), 30 | ), 31 | ) 32 | : ListView.builder( 33 | itemCount: widget.forumInfo == null 34 | ? 0 35 | : widget.forumInfo!.subForums!.length, 36 | itemBuilder: (_, index) => 37 | ChildForumItemWidget(widget.forumInfo!.subForums![index]), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/ui/page/forum_detail/forum_favourite_button_widet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/forum/favourite_forum_list_store.dart'; 4 | import 'package:flutter_nga/store/forum/favourite_forum_store.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class ForumFavouriteButtonWidget extends StatefulWidget { 9 | const ForumFavouriteButtonWidget( 10 | {this.name, required this.fid, Key? key, this.type}) 11 | : super(key: key); 12 | 13 | final String? name; 14 | final int fid; 15 | final int? type; 16 | 17 | @override 18 | State createState() => _ForumFavouriteButtonState(); 19 | } 20 | 21 | class _ForumFavouriteButtonState extends State { 22 | final FavouriteForumStore _favouriteForumStore = FavouriteForumStore(); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Observer( 27 | builder: (_) { 28 | return IconButton( 29 | icon: Icon( 30 | _favouriteForumStore.isFavourite ? Icons.star : Icons.star_border, 31 | color: Colors.white, 32 | ), 33 | onPressed: () { 34 | _favouriteForumStore 35 | .toggle(widget.fid, widget.name, widget.type) 36 | .then((_) { 37 | Provider.of(context, listen: false) 38 | .refresh(); 39 | }).catchError((err) { 40 | Fluttertoast.showToast(msg: err.message); 41 | }); 42 | }, 43 | ); 44 | }, 45 | ); 46 | } 47 | 48 | @override 49 | void initState() { 50 | super.initState(); 51 | _favouriteForumStore.load(widget.fid, widget.name ?? ""); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/ui/page/forum_detail/forum_recommend_topic_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/forum/forum_detail_store.dart'; 4 | import 'package:flutter_nga/ui/widget/topic_list_item_widget.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 7 | 8 | class ForumRecommendTopicListPage extends StatefulWidget { 9 | final int fid; 10 | final int? type; 11 | 12 | const ForumRecommendTopicListPage(this.fid, {this.type, Key? key}) 13 | : super(key: key); 14 | 15 | @override 16 | _ForumRecommendTopicListState createState() => 17 | _ForumRecommendTopicListState(); 18 | } 19 | 20 | class _ForumRecommendTopicListState extends State { 21 | final _store = ForumDetailStore(); 22 | late RefreshController _refreshController; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _refreshController = RefreshController(initialRefresh: true); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | _refreshController.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Observer( 39 | builder: (_) { 40 | return SmartRefresher( 41 | onLoading: _onLoading, 42 | controller: _refreshController, 43 | enablePullUp: _store.state.enablePullUp, 44 | onRefresh: _onRefresh, 45 | child: ListView.builder( 46 | itemCount: _store.state.list.length, 47 | itemBuilder: (context, index) => TopicListItemWidget( 48 | topic: _store.state.list[index],), 49 | ), 50 | ); 51 | }, 52 | ); 53 | } 54 | 55 | _onRefresh() { 56 | _store.refresh(widget.fid, true, widget.type).catchError((err) { 57 | _refreshController.refreshFailed(); 58 | Fluttertoast.showToast(msg: err.message); 59 | }).whenComplete( 60 | () => _refreshController.refreshCompleted(resetFooterState: true)); 61 | } 62 | 63 | _onLoading() async { 64 | _store.loadMore(widget.fid, true, widget.type).then((state) { 65 | if (state.page + 1 < state.maxPage) { 66 | _refreshController.loadComplete(); 67 | } else { 68 | _refreshController.loadNoData(); 69 | } 70 | }).catchError((_) => _refreshController.loadFailed()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/ui/page/forum_group/favourite_forum_group_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/forum/favourite_forum_list_store.dart'; 4 | import 'package:flutter_nga/ui/widget/forum_grid_item_widget.dart'; 5 | import 'package:flutter_nga/utils/route.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class FavouriteForumGroupPage extends StatefulWidget { 9 | @override 10 | _FavouriteForumGroupState createState() => _FavouriteForumGroupState(); 11 | } 12 | 13 | class _FavouriteForumGroupState extends State { 14 | late FavouriteForumListStore _store; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | Future.delayed(const Duration()).then((_) { 20 | _store.refresh(); 21 | }); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | _store = Provider.of(context); 27 | final size = MediaQuery.of(context).size; 28 | 29 | /*24 is for notification bar on Android*/ 30 | final double itemHeight = 96; 31 | final double itemWidth = size.width / 3; 32 | 33 | return Observer(builder: (_) { 34 | return GridView.builder( 35 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 36 | crossAxisCount: 3, 37 | childAspectRatio: itemWidth / itemHeight, 38 | ), 39 | itemCount: _store.list.length, 40 | itemBuilder: (_, index) => ForumGridItemWidget( 41 | _store.list[index], 42 | onLongPress: () => _showDeleteDialog(_store.list[index].fid), 43 | ), 44 | ); 45 | }); 46 | } 47 | 48 | @override 49 | void deactivate() { 50 | _store.refresh(); 51 | super.deactivate(); 52 | } 53 | 54 | void _showDeleteDialog(int fid) { 55 | showDialog( 56 | context: context, 57 | builder: (_) { 58 | return AlertDialog( 59 | title: Text("提示"), 60 | content: Text("是否删除该版面"), 61 | actions: [ 62 | TextButton( 63 | onPressed: () => Routes.pop(context), 64 | child: Text("取消"), 65 | ), 66 | TextButton( 67 | onPressed: () { 68 | _store.delete(fid).whenComplete(() => Routes.pop(context)); 69 | }, 70 | child: Text("确认"), 71 | ), 72 | ], 73 | ); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/ui/page/forum_group/forum_group_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/data/entity/forum.dart'; 3 | import 'package:flutter_nga/ui/widget/forum_grid_item_widget.dart'; 4 | 5 | class ForumGroupPage extends StatefulWidget { 6 | const ForumGroupPage({this.group, Key? key}) : super(key: key); 7 | 8 | final ForumGroup? group; 9 | 10 | @override 11 | State createState() => _ForumGroupState(); 12 | } 13 | 14 | class _ForumGroupState extends State { 15 | @override 16 | Widget build(BuildContext context) { 17 | final size = MediaQuery.of(context).size; 18 | 19 | /*24 is for notification bar on Android*/ 20 | final double itemHeight = 96; 21 | final double itemWidth = size.width / 3; 22 | return GridView.builder( 23 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 24 | crossAxisCount: 3, 25 | childAspectRatio: itemWidth / itemHeight, 26 | ), 27 | itemCount: widget.group!.forumList.length, 28 | itemBuilder: (_, index) => 29 | ForumGridItemWidget(widget.group!.forumList[index]), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/page/forum_group/forum_group_tabs.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/data/data.dart'; 3 | import 'package:flutter_nga/ui/page/forum_group/favourite_forum_group_page.dart'; 4 | import 'package:flutter_nga/ui/widget/keep_alive_tab_view.dart'; 5 | 6 | import 'forum_group_page.dart'; 7 | 8 | class ForumGroupTabsPage extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | List _tabs = [Tab(text: "我的收藏")]; 12 | List _tabBarViews = [ 13 | KeepAliveTabView(child: FavouriteForumGroupPage()) 14 | ]; 15 | 16 | final list = Data().forumRepository.getForumGroups(); 17 | 18 | _tabs.addAll(list.map((group) => Tab(text: group.name))); 19 | _tabBarViews.addAll(list 20 | .map((group) => KeepAliveTabView(child: ForumGroupPage(group: group)))); 21 | 22 | return DefaultTabController( 23 | length: _tabs.length, 24 | child: Scaffold( 25 | appBar: PreferredSize( 26 | child: AppBar( 27 | bottom: TabBar( 28 | isScrollable: true, 29 | tabs: _tabs, 30 | ), 31 | ), 32 | preferredSize: Size.fromHeight(kToolbarHeight), 33 | ), 34 | body: TabBarView(children: _tabBarViews), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/ui/page/notification/notification_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/data/entity/notification.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | 5 | class ReplyNotificationItemWidget extends StatelessWidget { 6 | final ReplyNotification notification; 7 | 8 | const ReplyNotificationItemWidget({Key? key, required this.notification}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return InkWell( 14 | onTap: _onTap, 15 | child: Column( 16 | children: [ 17 | Container( 18 | width: double.infinity, 19 | padding: EdgeInsets.all(16), 20 | child: Text(notification.getNotificationMessage()), 21 | ), 22 | Divider(height: 1), 23 | ], 24 | ), 25 | ); 26 | } 27 | 28 | _onTap() { 29 | if (notification.type == SystemNotification.TYPE_SELF_KEYWORD) { 30 | Fluttertoast.showToast(msg: notification.toString()); 31 | } else { 32 | Fluttertoast.showToast(msg: notification.toString()); 33 | } 34 | } 35 | } 36 | 37 | class SystemNotificationItemWidget extends StatelessWidget { 38 | final SystemNotification notification; 39 | 40 | const SystemNotificationItemWidget({Key? key, required this.notification}) 41 | : super(key: key); 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return InkWell( 46 | onTap: _onTap, 47 | child: Column( 48 | children: [ 49 | Container( 50 | width: double.infinity, 51 | padding: EdgeInsets.all(16), 52 | child: Text(notification.getNotificationMessage()), 53 | ), 54 | Divider(height: 1), 55 | ], 56 | ), 57 | ); 58 | } 59 | 60 | _onTap() { 61 | if (notification.type == SystemNotification.TYPE_SELF_KEYWORD) { 62 | Fluttertoast.showToast(msg: notification.toString()); 63 | } else { 64 | Fluttertoast.showToast(msg: notification.toString()); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/ui/page/photo_preview/photo_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:community_material_icon/community_material_icon.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_mobx/flutter_mobx.dart'; 5 | import 'package:flutter_nga/data/data.dart'; 6 | import 'package:flutter_nga/store/common/photo_min_scale_store.dart'; 7 | import 'package:flutter_nga/utils/picture_utils.dart' as pictureUtils; 8 | import 'package:fluttertoast/fluttertoast.dart'; 9 | import 'package:photo_view/photo_view.dart'; 10 | 11 | class PhotoPreviewPage extends StatefulWidget { 12 | const PhotoPreviewPage({Key? key, this.url, this.screenWidth}) 13 | : super(key: key); 14 | 15 | final String? url; 16 | final double? screenWidth; 17 | 18 | @override 19 | _PhotoPreviewState createState() => _PhotoPreviewState(); 20 | } 21 | 22 | class _PhotoPreviewState extends State { 23 | final _store = PhotoMinScaleStore(); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | appBar: AppBar( 29 | title: Text("查看图片"), 30 | actions: [ 31 | IconButton( 32 | icon: Icon( 33 | CommunityMaterialIcons.content_save, 34 | color: Colors.white, 35 | ), 36 | onPressed: () => _save(), 37 | tooltip: "保存", 38 | ), 39 | ], 40 | ), 41 | body: Observer( 42 | builder: (_) { 43 | return _store.minScale == 0 44 | ? Center(child: CircularProgressIndicator()) 45 | : PhotoView( 46 | imageProvider: CachedNetworkImageProvider( 47 | pictureUtils.getOriginalUrl(widget.url!)), 48 | minScale: _store.minScale, 49 | ); 50 | }, 51 | ), 52 | ); 53 | } 54 | 55 | @override 56 | void initState() { 57 | super.initState(); 58 | _store.load(widget.url!, widget.screenWidth); 59 | } 60 | 61 | _save() async { 62 | final success = await Data() 63 | .resourceRepository 64 | .downloadImage(pictureUtils.getOriginalUrl(widget.url!)); 65 | Fluttertoast.showToast(msg: success == true ? "保存到相册成功" : "保存到相册失败"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/ui/page/search/search_forum_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/data/entity/forum.dart'; 4 | import 'package:flutter_nga/store/search/search_forum_store.dart'; 5 | import 'package:flutter_nga/utils/code_utils.dart'; 6 | import 'package:flutter_nga/utils/route.dart'; 7 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 8 | 9 | class SearchForumPage extends StatefulWidget { 10 | const SearchForumPage(this.keyword, {Key? key}) : super(key: key); 11 | 12 | final String keyword; 13 | 14 | @override 15 | _SearchForumState createState() => _SearchForumState(); 16 | } 17 | 18 | class _SearchForumState extends State { 19 | final _store = SearchForumStore(); 20 | late RefreshController _refreshController; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _refreshController = RefreshController(initialRefresh: true); 26 | } 27 | 28 | @override 29 | void dispose() { 30 | _refreshController.dispose(); 31 | super.dispose(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | title: Text("搜索板块:${widget.keyword}"), 39 | ), 40 | body: Observer( 41 | builder: (_) => SmartRefresher( 42 | onRefresh: _onRefresh, 43 | enablePullUp: false, 44 | controller: _refreshController, 45 | child: ListView.builder( 46 | itemBuilder: (_, index) => _buildForumWidget(_store.forums[index]), 47 | itemCount: _store.forums.length, 48 | ), 49 | ), 50 | ), 51 | ); 52 | } 53 | 54 | _onRefresh() { 55 | _store 56 | .search(widget.keyword) 57 | .whenComplete(() => _refreshController.refreshCompleted()) 58 | .catchError((_) => _refreshController.refreshFailed()); 59 | } 60 | 61 | Widget _buildForumWidget(Forum forum) { 62 | return InkWell( 63 | child: Column( 64 | crossAxisAlignment: CrossAxisAlignment.start, 65 | children: [ 66 | Padding( 67 | padding: EdgeInsets.all(16), 68 | child: Text(forum.name), 69 | ), 70 | Divider(height: 1), 71 | ], 72 | ), 73 | onTap: () => Routes.navigateTo( 74 | context, 75 | "${Routes.FORUM_DETAIL}?fid=${forum.fid}" 76 | "&name=${fluroCnParamsEncode(forum.name)}" 77 | // "&type=${widget.forum.type}", 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/ui/page/search/search_topic_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/search/search_topic_list_store.dart'; 4 | import 'package:flutter_nga/ui/widget/topic_list_item_widget.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 7 | 8 | class SearchTopicListPage extends StatefulWidget { 9 | const SearchTopicListPage(this.keyword, 10 | {this.fid, this.content = false, Key? key}) 11 | : super(key: key); 12 | 13 | final int? fid; 14 | final String keyword; 15 | final bool content; 16 | 17 | @override 18 | State createState() => _SearchTopicListSate(); 19 | } 20 | 21 | class _SearchTopicListSate extends State { 22 | final _store = SearchTopicListStore(); 23 | late RefreshController _refreshController; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | appBar: AppBar( 29 | title: Text("搜索贴子:${widget.keyword}"), 30 | ), 31 | body: Observer( 32 | builder: (_) => SmartRefresher( 33 | onRefresh: _onRefresh, 34 | onLoading: _onLoadMore, 35 | enablePullUp: _store.state.enablePullUp, 36 | controller: _refreshController, 37 | child: ListView.builder( 38 | itemBuilder: (_, index) => TopicListItemWidget( 39 | topic: _store.state.list[index], 40 | ), 41 | itemCount: _store.state.list.length, 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | 48 | @override 49 | void initState() { 50 | super.initState(); 51 | _refreshController = RefreshController( 52 | initialRefresh: true, 53 | ); 54 | } 55 | 56 | @override 57 | void dispose() { 58 | _refreshController.dispose(); 59 | super.dispose(); 60 | } 61 | 62 | _onRefresh() { 63 | _store 64 | .refresh(widget.keyword, widget.fid, widget.content) 65 | .whenComplete(() => _refreshController.refreshCompleted()) 66 | .catchError((e) { 67 | Fluttertoast.showToast(msg: e.message); 68 | _refreshController.refreshFailed(); 69 | }); 70 | } 71 | 72 | _onLoadMore() { 73 | _store 74 | .loadMore(widget.keyword, widget.fid, widget.content) 75 | .whenComplete(() => _refreshController.loadComplete()) 76 | .catchError((e) { 77 | Fluttertoast.showToast(msg: e.message); 78 | _refreshController.loadFailed(); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/ui/page/send_message/contact_edit_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/utils/route.dart'; 3 | 4 | typedef EditCallback = void Function(String text); 5 | 6 | class ContactEditDialog extends StatelessWidget { 7 | final EditCallback? callback; 8 | 9 | final _controller = TextEditingController(); 10 | 11 | ContactEditDialog({Key? key, this.callback}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return AlertDialog( 16 | title: Text("添加收信人"), 17 | content: TextField( 18 | maxLines: 1, 19 | controller: _controller, 20 | decoration: InputDecoration( 21 | labelText: "UID 或 用户名", 22 | ), 23 | keyboardType: TextInputType.text, 24 | ), 25 | actions: [ 26 | TextButton( 27 | onPressed: () => Routes.pop(context), 28 | child: Text( 29 | '取消', 30 | style: 31 | TextStyle(color: Theme.of(context).textTheme.bodyText2?.color), 32 | ), 33 | ), 34 | TextButton( 35 | onPressed: () { 36 | callback?.call(_controller.text); 37 | Routes.pop(context); 38 | }, 39 | child: Text('确定'), 40 | ) 41 | ], 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/ui/page/settings/blocklist_edit_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/utils/route.dart'; 3 | 4 | typedef EditCallback = void Function(String text); 5 | 6 | class BlocklistEditDialog extends StatelessWidget { 7 | final EditCallback? callback; 8 | 9 | final String title; 10 | final String inputHint; 11 | 12 | final _controller = TextEditingController(); 13 | 14 | BlocklistEditDialog( 15 | {Key? key, this.callback, required this.title, required this.inputHint}) 16 | : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return AlertDialog( 21 | title: Text(title), 22 | content: TextField( 23 | maxLines: 1, 24 | controller: _controller, 25 | decoration: InputDecoration( 26 | labelText: inputHint, 27 | ), 28 | keyboardType: TextInputType.text, 29 | ), 30 | actions: [ 31 | TextButton( 32 | onPressed: () => Routes.pop(context), 33 | child: Text( 34 | '取消', 35 | style: 36 | TextStyle(color: Theme.of(context).textTheme.bodyText2?.color), 37 | ), 38 | ), 39 | TextButton( 40 | onPressed: () { 41 | callback?.call(_controller.text); 42 | Routes.pop(context); 43 | }, 44 | child: Text('确定'), 45 | ) 46 | ], 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/ui/page/settings/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/settings/display_mode_store.dart'; 4 | import 'package:flutter_nga/store/settings/theme_store.dart'; 5 | import 'package:flutter_nga/ui/widget/theme_selection_dialog.dart'; 6 | import 'package:flutter_nga/utils/route.dart'; 7 | 8 | class SettingsPage extends StatefulWidget { 9 | @override 10 | _SettingsState createState() => _SettingsState(); 11 | } 12 | 13 | class _SettingsState extends State { 14 | ThemeStore _themeStore = ThemeStore(); 15 | DisplayModeStore _displayModeStore = DisplayModeStore(); 16 | 17 | @override 18 | void initState() { 19 | _themeStore.refresh(); 20 | _displayModeStore.refresh(); 21 | super.initState(); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Scaffold( 27 | appBar: AppBar(title: Text("设置")), 28 | body: ListView( 29 | shrinkWrap: true, 30 | children: [ 31 | ListTile( 32 | title: Text("账号管理"), 33 | subtitle: Text("管理您的账号"), 34 | onTap: () => Routes.navigateTo(context, Routes.ACCOUNT_MANAGEMENT), 35 | ), 36 | ListTile( 37 | title: Text("主题模式"), 38 | subtitle: Observer( 39 | builder: (context) => Text("当前主题模式: ${_themeStore.modeName}"), 40 | ), 41 | onTap: showThemeSelectionDialog, 42 | ), 43 | ListTile( 44 | title: Text("界面设置"), 45 | subtitle: Text("设置文字大小等界面元素"), 46 | onTap: () => Routes.navigateTo(context, Routes.INTERFACE_SETTINGS), 47 | ), 48 | ListTile( 49 | title: Text("屏蔽设置"), 50 | subtitle: Text("屏蔽用户、关键词等选项"), 51 | onTap: () => Routes.navigateTo(context, Routes.BLOCKLIST_SETTINGS), 52 | ), 53 | ], 54 | ), 55 | ); 56 | } 57 | 58 | showThemeSelectionDialog() { 59 | showDialog( 60 | context: context, 61 | builder: (_) => ThemeSelectionDialog(themeStore: _themeStore), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/ui/page/splash/splash_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluro/fluro.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_nga/data/data.dart'; 4 | import 'package:flutter_nga/utils/custom_time_messages.dart'; 5 | import 'package:flutter_nga/utils/route.dart'; 6 | import 'package:timeago/timeago.dart' as timeAgo; 7 | 8 | class SplashPage extends StatefulWidget { 9 | @override 10 | _SplashState createState() => _SplashState(); 11 | } 12 | 13 | class _SplashState extends State { 14 | @override 15 | void initState() { 16 | timeAgo.setLocaleMessages('en', CustomTimeMessages()); 17 | Data().init().then((_) { 18 | Routes.navigateTo(context, Routes.HOME, 19 | replace: true, transition: TransitionType.fadeIn); 20 | }); 21 | super.initState(); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Scaffold( 27 | body: Center( 28 | child: Text("欢迎使用"), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/page/topic_detail/forum_tag_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/data/entity/topic_tag.dart'; 4 | import 'package:flutter_nga/store/forum/forum_tag_list_store.dart'; 5 | import 'package:flutter_nga/utils/dimen.dart'; 6 | import 'package:fluttertoast/fluttertoast.dart'; 7 | 8 | typedef TagSelectedCallback = void Function(String tag); 9 | typedef TagLoadCompleteCallback = void Function(List tagList); 10 | 11 | class ForumTagDialog extends StatefulWidget { 12 | const ForumTagDialog({ 13 | required this.fid, 14 | this.tagList = const [], 15 | this.onSelected, 16 | this.onLoadComplete, 17 | Key? key, 18 | }) : super(key: key); 19 | final int fid; 20 | final List tagList; 21 | final TagSelectedCallback? onSelected; 22 | final TagLoadCompleteCallback? onLoadComplete; 23 | 24 | @override 25 | _ForumTagDialogState createState() => _ForumTagDialogState(); 26 | } 27 | 28 | class _ForumTagDialogState extends State { 29 | final _store = ForumTagListStore(); 30 | 31 | @override 32 | void initState() { 33 | _store.setList(widget.tagList); 34 | 35 | if (widget.tagList.isEmpty) { 36 | _store.load(widget.fid).then((value) { 37 | widget.onLoadComplete?.call(value); 38 | }).catchError((err) { 39 | Fluttertoast.showToast(msg: err.message); 40 | }); 41 | } 42 | super.initState(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return AlertDialog( 48 | title: Text("主题分类"), 49 | content: Observer(builder: (context) { 50 | return SizedBox( 51 | width: double.maxFinite, 52 | child: ListView.builder( 53 | shrinkWrap: true, 54 | itemCount: _store.tagList.length, 55 | itemBuilder: (context, position) { 56 | final tag = _store.tagList[position].content; 57 | return InkWell( 58 | child: Padding( 59 | padding: EdgeInsets.symmetric(vertical: 16), 60 | child: Text( 61 | "$tag", 62 | style: TextStyle( 63 | color: Theme.of(context).textTheme.bodyText1?.color, 64 | fontSize: Dimen.subheading, 65 | ), 66 | ), 67 | ), 68 | onTap: () { 69 | if (widget.onSelected != null) { 70 | widget.onSelected!(tag); 71 | } 72 | }, 73 | ); 74 | }, 75 | ), 76 | ); 77 | }), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/ui/page/topic_detail/topic_reply_comment_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/data/entity/topic_detail.dart'; 3 | import 'package:flutter_nga/data/entity/user.dart'; 4 | import 'package:flutter_nga/ui/widget/avatar_widget.dart'; 5 | import 'package:flutter_nga/ui/widget/nga_html_comment_widget.dart'; 6 | import 'package:flutter_nga/utils/dimen.dart'; 7 | 8 | class TopicReplyCommentItemWidget extends StatelessWidget { 9 | const TopicReplyCommentItemWidget(this.reply, this.user, {Key? key}) 10 | : super(key: key); 11 | 12 | final User? user; 13 | final Reply reply; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | padding: EdgeInsets.all(8), 19 | decoration: BoxDecoration( 20 | border: Border( 21 | left: BorderSide(color: Theme.of(context).dividerColor), 22 | right: BorderSide(color: Theme.of(context).dividerColor), 23 | bottom: BorderSide(color: Theme.of(context).dividerColor), 24 | ), 25 | ), 26 | child: Column( 27 | children: [ 28 | Row( 29 | crossAxisAlignment: CrossAxisAlignment.center, 30 | children: [ 31 | Padding( 32 | padding: EdgeInsets.only(right: 8), 33 | child: AvatarWidget( 34 | user!.avatar, 35 | size: 36, 36 | username: user!.username, 37 | ), 38 | ), 39 | Expanded( 40 | child: Column( 41 | crossAxisAlignment: CrossAxisAlignment.start, 42 | children: [ 43 | Padding( 44 | padding: EdgeInsets.only(bottom: 4), 45 | child: Text( 46 | user!.getShowName(), 47 | style: TextStyle( 48 | color: 49 | Theme.of(context).textTheme.bodyText1?.color), 50 | ), 51 | ), 52 | Text( 53 | reply.postDate!, 54 | style: TextStyle( 55 | fontSize: Dimen.caption, 56 | color: Theme.of(context).textTheme.bodyText2?.color, 57 | ), 58 | ), 59 | ], 60 | ), 61 | ), 62 | ], 63 | ), 64 | Padding( 65 | padding: EdgeInsets.only(top: 8), 66 | child: NgaHtmlCommentWidget(content: reply.content), 67 | ), 68 | ], 69 | ), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/ui/page/user_info/user_replies_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/user/user_replies_store.dart'; 4 | import 'package:flutter_nga/ui/widget/topic_list_item_widget.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 7 | 8 | class UserRepliesPage extends StatefulWidget { 9 | final int uid; 10 | final String username; 11 | 12 | const UserRepliesPage({ 13 | Key? key, 14 | required this.uid, 15 | required this.username, 16 | }) : super(key: key); 17 | 18 | @override 19 | _UserRepliesPageState createState() => _UserRepliesPageState(); 20 | } 21 | 22 | class _UserRepliesPageState extends State { 23 | final _store = UserRepliesStore(); 24 | late RefreshController _refreshController; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _refreshController = RefreshController(initialRefresh: true); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | _refreshController.dispose(); 35 | super.dispose(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return Scaffold( 41 | appBar: AppBar(title: Text("${widget.username}发布的回复")), 42 | body: Observer( 43 | builder: (_) { 44 | return SmartRefresher( 45 | onLoading: _onLoading, 46 | controller: _refreshController, 47 | enablePullUp: _store.state.enablePullUp, 48 | onRefresh: _onRefresh, 49 | child: ListView.builder( 50 | itemCount: _store.state.list.length, 51 | itemBuilder: (context, index) => 52 | TopicListItemWidget(topic: _store.state.list[index]), 53 | ), 54 | ); 55 | }, 56 | ), 57 | ); 58 | } 59 | 60 | _onRefresh() { 61 | _store.refresh(widget.uid).catchError((err) { 62 | _refreshController.refreshFailed(); 63 | Fluttertoast.showToast(msg: err.message); 64 | }).whenComplete( 65 | () => _refreshController.refreshCompleted(resetFooterState: true)); 66 | } 67 | 68 | _onLoading() async { 69 | _store.loadMore(widget.uid).then((state) { 70 | if (state.list.length == state.page * state.size) { 71 | _refreshController.loadComplete(); 72 | } else { 73 | _refreshController.loadNoData(); 74 | } 75 | }).catchError((err) { 76 | Fluttertoast.showToast(msg: err.message); 77 | debugPrintStack(stackTrace: err.stackTrace); 78 | _refreshController.loadFailed(); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/ui/page/user_info/user_topics_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/user/user_topics_store.dart'; 4 | import 'package:flutter_nga/ui/widget/topic_list_item_widget.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 7 | 8 | class UserTopicsPage extends StatefulWidget { 9 | final int uid; 10 | final String username; 11 | 12 | const UserTopicsPage({ 13 | Key? key, 14 | required this.uid, 15 | required this.username, 16 | }) : super(key: key); 17 | 18 | @override 19 | _UserTopicsPageState createState() => _UserTopicsPageState(); 20 | } 21 | 22 | class _UserTopicsPageState extends State { 23 | final _store = UserTopicsStore(); 24 | late RefreshController _refreshController; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _refreshController = RefreshController(initialRefresh: true); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | _refreshController.dispose(); 35 | super.dispose(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return Scaffold( 41 | appBar: AppBar(title: Text("${widget.username}发布的主题")), 42 | body: Observer( 43 | builder: (_) { 44 | return SmartRefresher( 45 | onLoading: _onLoading, 46 | controller: _refreshController, 47 | enablePullUp: _store.state.enablePullUp, 48 | onRefresh: _onRefresh, 49 | child: ListView.builder( 50 | itemCount: _store.state.list.length, 51 | itemBuilder: (context, index) => 52 | TopicListItemWidget(topic: _store.state.list[index]), 53 | ), 54 | ); 55 | }, 56 | ), 57 | ); 58 | } 59 | 60 | _onRefresh() { 61 | _store.refresh(widget.uid).catchError((err) { 62 | _refreshController.refreshFailed(); 63 | Fluttertoast.showToast(msg: err.message); 64 | }).whenComplete( 65 | () => _refreshController.refreshCompleted(resetFooterState: true)); 66 | } 67 | 68 | _onLoading() async { 69 | _store.loadMore(widget.uid).then((state) { 70 | if (state.page + 1 < state.maxPage) { 71 | _refreshController.loadComplete(); 72 | } else { 73 | _refreshController.loadNoData(); 74 | } 75 | }).catchError((_) { 76 | _refreshController.loadFailed(); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/ui/widget/avatar_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_nga/utils/code_utils.dart' as codeUtils; 4 | import 'package:flutter_nga/utils/route.dart'; 5 | 6 | class AvatarWidget extends StatelessWidget { 7 | const AvatarWidget(this.avatar, {this.size = 48, this.username, Key? key}) 8 | : super(key: key); 9 | 10 | final String? avatar; 11 | final double size; 12 | final String? username; 13 | 14 | String get _realAvatarUrl { 15 | if (avatar == null) { 16 | return ""; 17 | } else if (avatar!.startsWith("http://")) { 18 | return avatar!.replaceAll("http://", "https://"); 19 | } else { 20 | return avatar!; 21 | } 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return ClipOval( 27 | child: codeUtils.isStringEmpty(username) 28 | ? _getAvatarImage() 29 | : Material( 30 | color: Colors.transparent, 31 | child: InkWell( 32 | onTap: () => Routes.navigateTo(context, 33 | "${Routes.USER}?name=${codeUtils.fluroCnParamsEncode(username!)}"), 34 | child: _getAvatarImage(), 35 | ), 36 | ), 37 | ); 38 | } 39 | 40 | Widget _getAvatarImage() { 41 | return avatar != null 42 | ? CachedNetworkImage( 43 | width: size, 44 | height: size, 45 | fit: BoxFit.cover, 46 | imageUrl: _realAvatarUrl, 47 | placeholder: (context, url) => Image.asset( 48 | 'images/default_forum_icon.png', 49 | width: size, 50 | height: size, 51 | ), 52 | errorWidget: (context, url, err) => Image.asset( 53 | 'images/default_forum_icon.png', 54 | width: size, 55 | height: size, 56 | ), 57 | ) 58 | : Image.asset( 59 | 'images/default_forum_icon.png', 60 | width: size, 61 | height: size, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/ui/widget/block_mode_selection_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/settings/blocklist_settings_store.dart'; 4 | import 'package:flutter_nga/utils/route.dart'; 5 | 6 | class BlockModeSelectionDialog extends StatefulWidget { 7 | final BlocklistSettingsStore store; 8 | 9 | const BlockModeSelectionDialog({Key? key, required this.store}) 10 | : super(key: key); 11 | 12 | @override 13 | _BlockModeSelectionDialogState createState() => 14 | _BlockModeSelectionDialogState(); 15 | } 16 | 17 | class _BlockModeSelectionDialogState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return AlertDialog( 21 | title: Text("选择屏蔽模式"), 22 | content: SizedBox( 23 | width: double.maxFinite, 24 | // height: double.minPositive, 25 | child: Observer( 26 | builder: (_) { 27 | return ListView( 28 | shrinkWrap: true, 29 | children: BlockMode.values 30 | .map((e) => RadioListTile( 31 | title: Text(e.name), 32 | value: e, 33 | groupValue: widget.store.blockMode, 34 | onChanged: _onChanged, 35 | )) 36 | .toList(), 37 | ); 38 | }, 39 | ), 40 | ), 41 | actions: [ 42 | TextButton( 43 | onPressed: () => Routes.pop(context), 44 | child: Text('关闭'), 45 | ) 46 | ], 47 | ); 48 | } 49 | 50 | _onChanged(BlockMode? mode) { 51 | widget.store.updateBlockMode(mode ?? BlockMode.COLLAPSE); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/ui/widget/collapse_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CollapseWidget extends StatefulWidget { 4 | const CollapseWidget.fromNodes({this.title, this.child, Key? key}) 5 | : super(key: key); 6 | 7 | final String? title; 8 | final Widget? child; 9 | 10 | @override 11 | _CollapseState createState() => _CollapseState(); 12 | } 13 | 14 | class _CollapseState extends State { 15 | var _collapsed = true; // 默认收起 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | // 因为要占据一行,所以必须 infinity 20 | return SizedBox( 21 | width: double.infinity, 22 | child: Column( 23 | crossAxisAlignment: CrossAxisAlignment.start, 24 | children: [ 25 | ElevatedButton( 26 | style: ButtonStyle(elevation: MaterialStateProperty.all(0)), 27 | onPressed: () => setState(() => _collapsed = !_collapsed), 28 | child: 29 | Text("${_collapsed ? "点击展开" : "点击收起"}:${widget.title ?? ""}"), 30 | ), 31 | SizedBox( 32 | height: _collapsed ? 0 : null, 33 | child: widget.child, 34 | ), 35 | ], 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/ui/widget/custom_forum_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/store/forum/favourite_forum_list_store.dart'; 3 | import 'package:flutter_nga/utils/route.dart'; 4 | import 'package:fluttertoast/fluttertoast.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class CustomForumDialog extends StatelessWidget { 8 | final _fidController = TextEditingController(); 9 | final _nameController = TextEditingController(); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return AlertDialog( 14 | title: Text("添加自定义版面"), 15 | content: Column( 16 | mainAxisSize: MainAxisSize.min, 17 | children: [ 18 | TextField( 19 | maxLines: 1, 20 | controller: _fidController, 21 | decoration: InputDecoration(labelText: "fid"), 22 | keyboardType: TextInputType.number, 23 | ), 24 | TextField( 25 | maxLines: 1, 26 | controller: _nameController, 27 | decoration: InputDecoration(labelText: "名称"), 28 | keyboardType: TextInputType.text, 29 | ), 30 | ], 31 | ), 32 | actions: [ 33 | TextButton( 34 | onPressed: () { 35 | final fid = int.tryParse(_fidController.text)!; 36 | Provider.of(context, listen: false) 37 | .add(fid, _nameController.text) 38 | .catchError((e) { 39 | debugPrint(e.toString()); 40 | Fluttertoast.showToast(msg: "添加自定义板块失败"); 41 | }).whenComplete(() => Routes.pop(context)); 42 | }, 43 | child: Text("确定"), 44 | ) 45 | ], 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/ui/widget/dash.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Dash extends StatelessWidget { 4 | const Dash( 5 | {Key? key, 6 | this.direction = Axis.horizontal, 7 | this.dashColor = Colors.black, 8 | this.length = 200, 9 | this.dashGap = 3, 10 | this.dashLength = 6, 11 | this.dashThickness = 1, 12 | this.dashBorderRadius = 0}) 13 | : super(key: key); 14 | 15 | final Axis direction; 16 | final Color dashColor; 17 | final double length; 18 | final double dashGap; 19 | final double dashLength; 20 | final double dashThickness; 21 | final double dashBorderRadius; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | var dashes = []; 26 | double n = (length + dashGap) / (dashGap + dashLength); 27 | int newN = n.round(); 28 | double newDashGap = (length - dashLength * newN) / (newN - 1); 29 | for (var i = newN; i > 0; i--) { 30 | dashes.add(step(i, newDashGap)); 31 | } 32 | if (direction == Axis.horizontal) { 33 | return SizedBox( 34 | width: length, 35 | child: Row( 36 | children: dashes, 37 | )); 38 | } else { 39 | return Column(children: dashes); 40 | } 41 | } 42 | 43 | Widget step(int index, double newDashGap) { 44 | bool isHorizontal = direction == Axis.horizontal; 45 | return Padding( 46 | padding: EdgeInsets.fromLTRB( 47 | 0, 48 | 0, 49 | isHorizontal && index != 1 ? newDashGap : 0, 50 | isHorizontal || index == 1 ? 0 : newDashGap), 51 | child: SizedBox( 52 | width: isHorizontal ? dashLength : dashThickness, 53 | height: isHorizontal ? dashThickness : dashLength, 54 | child: DecoratedBox( 55 | decoration: BoxDecoration( 56 | color: dashColor, 57 | borderRadius: 58 | BorderRadius.all(Radius.circular(dashBorderRadius))), 59 | ), 60 | )); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/ui/widget/font_color_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/ui/widget/font_style_widget.dart'; 3 | import 'package:flutter_nga/utils/constant.dart'; 4 | import 'package:flutter_nga/utils/route.dart'; 5 | 6 | class FontColorDialog extends StatelessWidget { 7 | const FontColorDialog({this.callback, Key? key}) : super(key: key); 8 | final InputCallback? callback; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final keyList = TEXT_COLOR_MAP.keys.toList(); 13 | final valueList = TEXT_COLOR_MAP.values.toList(); 14 | return AlertDialog( 15 | title: Text("颜色"), 16 | content: SizedBox( 17 | width: 0, 18 | height: 408, 19 | child: ListView.builder( 20 | itemCount: TEXT_COLOR_MAP.length, 21 | itemBuilder: (context, position) { 22 | return InkWell( 23 | child: Padding( 24 | padding: EdgeInsets.symmetric(vertical: 16), 25 | child: Text( 26 | "${keyList[position]}", 27 | style: TextStyle(color: valueList[position]), 28 | ), 29 | ), 30 | onTap: () { 31 | callback?.call( 32 | "[color=${keyList[position]}]", "[/color]", true); 33 | Routes.pop(context); 34 | }, 35 | ); 36 | }, 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/ui/widget/font_size_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/ui/widget/font_style_widget.dart'; 3 | import 'package:flutter_nga/utils/route.dart'; 4 | 5 | class FontSizeDialog extends StatelessWidget { 6 | const FontSizeDialog({this.callback, Key? key}) : super(key: key); 7 | final InputCallback? callback; 8 | 9 | static const sizeList = [ 10 | "110%", 11 | "120%", 12 | "130%", 13 | "150%", 14 | "200%", 15 | "300%", 16 | "400%", 17 | "500%", 18 | ]; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return AlertDialog( 23 | title: Text("字号"), 24 | content: SizedBox( 25 | width: 0, 26 | height: 408, 27 | child: ListView.builder( 28 | itemCount: sizeList.length, 29 | itemBuilder: (context, position) { 30 | return InkWell( 31 | child: Padding( 32 | padding: EdgeInsets.symmetric(vertical: 16), 33 | child: Text("${sizeList[position]}"), 34 | ), 35 | onTap: () { 36 | callback?.call("[size=${sizeList[position]}]", "[/size]", true); 37 | Routes.pop(context); 38 | }, 39 | ); 40 | }, 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/ui/widget/forum_grid_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_nga/data/entity/forum.dart'; 4 | import 'package:flutter_nga/utils/code_utils.dart'; 5 | import 'package:flutter_nga/utils/route.dart'; 6 | 7 | class ForumGridItemWidget extends StatelessWidget { 8 | final Forum forum; 9 | final GestureLongPressCallback? onLongPress; 10 | 11 | const ForumGridItemWidget(this.forum, {Key? key, this.onLongPress}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Material( 17 | color: Colors.transparent, 18 | child: InkWell( 19 | onTap: () => Routes.navigateTo(context, 20 | "${Routes.FORUM_DETAIL}?fid=${forum.fid}&name=${fluroCnParamsEncode(forum.name)}&type=${forum.type}"), 21 | onLongPress: onLongPress, 22 | child: Column( 23 | mainAxisAlignment: MainAxisAlignment.center, 24 | children: [ 25 | CachedNetworkImage( 26 | width: 48, 27 | height: 48, 28 | imageUrl: forum.getIconUrl(), 29 | placeholder: (context, url) => Image.asset( 30 | 'images/default_forum_icon.png', 31 | width: 48, 32 | height: 48, 33 | ), 34 | errorWidget: (context, url, err) => Image.asset( 35 | 'images/default_forum_icon.png', 36 | width: 48, 37 | height: 48, 38 | ), 39 | ), 40 | Padding( 41 | padding: EdgeInsets.only(top: 8), 42 | child: Text( 43 | forum.name, 44 | overflow: TextOverflow.ellipsis, 45 | ), 46 | ) 47 | ], 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/ui/widget/import_cookies_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/utils/route.dart'; 3 | 4 | class ImportCookiesDialog extends StatelessWidget { 5 | final Function(String) cookiesCallback; 6 | final _controller = TextEditingController(); 7 | 8 | ImportCookiesDialog({Key? key, required this.cookiesCallback}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return AlertDialog( 14 | title: Text("导入 Cookies"), 15 | content: TextField( 16 | maxLines: 1, 17 | controller: _controller, 18 | decoration: InputDecoration(labelText: "cookies"), 19 | keyboardType: TextInputType.text, 20 | ), 21 | actions: [ 22 | TextButton( 23 | onPressed: () => Routes.pop(context), 24 | child: Text("取消"), 25 | ), 26 | TextButton( 27 | onPressed: () { 28 | if (_controller.text.isNotEmpty) { 29 | cookiesCallback(_controller.text); 30 | } 31 | Routes.pop(context); 32 | }, 33 | child: Text("确定"), 34 | ) 35 | ], 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/ui/widget/info_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class InfoWidget extends StatelessWidget { 4 | const InfoWidget({this.title, this.subTitle, Key? key}) : super(key: key); 5 | 6 | final String? title; 7 | final String? subTitle; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Row( 12 | children: [ 13 | Text( 14 | title!, 15 | style: TextStyle(fontSize: 14), 16 | ), 17 | Text( 18 | subTitle!, 19 | style: TextStyle( 20 | fontSize: 14, 21 | color: Theme.of(context).textTheme.bodyText2?.color, 22 | ), 23 | ) 24 | ], 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/ui/widget/keep_alive_tab_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class KeepAliveTabView extends StatefulWidget { 4 | final Widget? child; 5 | final bool Function()? keepAlive; 6 | 7 | const KeepAliveTabView({this.child, this.keepAlive, Key? key}) 8 | : super(key: key); 9 | 10 | @override 11 | _KeepAliveTabState createState() => _KeepAliveTabState(); 12 | } 13 | 14 | class _KeepAliveTabState extends State 15 | with AutomaticKeepAliveClientMixin { 16 | @override 17 | Widget build(BuildContext context) { 18 | super.build(context); 19 | return widget.child!; 20 | } 21 | 22 | @override 23 | bool get wantKeepAlive { 24 | if (widget.keepAlive != null) { 25 | return widget.keepAlive!(); 26 | } else { 27 | return true; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/widget/line_height_selection_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:flutter_nga/store/settings/interface_settings_store.dart'; 4 | import 'package:flutter_nga/utils/route.dart'; 5 | 6 | class LineHeightSelectionDialog extends StatefulWidget { 7 | final InterfaceSettingsStore interfaceSettingsStore; 8 | 9 | const LineHeightSelectionDialog( 10 | {Key? key, required this.interfaceSettingsStore}) 11 | : super(key: key); 12 | 13 | @override 14 | _LineHeightSelectionDialogState createState() => 15 | _LineHeightSelectionDialogState(); 16 | } 17 | 18 | class _LineHeightSelectionDialogState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | return AlertDialog( 22 | title: Text("选择行间距"), 23 | content: SizedBox( 24 | width: double.maxFinite, 25 | // height: double.minPositive, 26 | child: Observer( 27 | builder: (_) { 28 | return ListView( 29 | shrinkWrap: true, 30 | children: [ 31 | CustomLineHeight.NORMAL, 32 | CustomLineHeight.MEDIUM, 33 | CustomLineHeight.LARGE, 34 | CustomLineHeight.XLARGE, 35 | CustomLineHeight.XXLARGE, 36 | ].map((e) { 37 | return RadioListTile( 38 | value: e, 39 | groupValue: widget.interfaceSettingsStore.lineHeight, 40 | onChanged: _onChanged, 41 | title: Text(e.nameWithSize), 42 | ); 43 | }).toList(), 44 | ); 45 | }, 46 | ), 47 | ), 48 | actions: [ 49 | TextButton( 50 | onPressed: () => Routes.pop(context), 51 | child: Text('关闭'), 52 | ) 53 | ], 54 | ); 55 | } 56 | 57 | _onChanged(CustomLineHeight? customLineHeight) { 58 | widget.interfaceSettingsStore.setLineHeight(customLineHeight?.index ?? 0); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/ui/widget/nga_html_comment_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_html/flutter_html.dart'; 3 | import 'package:flutter_nga/store/settings/interface_settings_store.dart'; 4 | import 'package:flutter_nga/utils/dimen.dart'; 5 | import 'package:flutter_nga/utils/parser/content_parser.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class NgaHtmlCommentWidget extends StatelessWidget { 9 | final String content; 10 | 11 | const NgaHtmlCommentWidget({Key? key, required this.content}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Html( 17 | data: NgaContentParser.parseComment(content), 18 | style: { 19 | 'body': Style( 20 | fontSize: FontSize(Dimen.body * 21 | Provider.of(context).contentSizeMultiple), 22 | lineHeight: LineHeight( 23 | Provider.of(context).lineHeight.size), 24 | padding: EdgeInsets.all(0), 25 | margin: EdgeInsets.all(0), 26 | color: Theme.of(context).textTheme.bodyText1?.color, 27 | ), 28 | }, 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/ui/widget/simple_scroll_behavior.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SimpleScrollBehavior extends ScrollBehavior { 4 | @override 5 | Widget buildViewportChrome( 6 | BuildContext context, Widget child, AxisDirection axisDirection) { 7 | return child; 8 | } 9 | } -------------------------------------------------------------------------------- /lib/ui/widget/theme_selection_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_theme/adaptive_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_mobx/flutter_mobx.dart'; 4 | import 'package:flutter_nga/store/settings/theme_store.dart'; 5 | import 'package:flutter_nga/utils/route.dart'; 6 | 7 | class ThemeSelectionDialog extends StatefulWidget { 8 | final ThemeStore themeStore; 9 | 10 | const ThemeSelectionDialog({Key? key, required this.themeStore}) 11 | : super(key: key); 12 | 13 | @override 14 | _ThemeSelectionDialogState createState() => _ThemeSelectionDialogState(); 15 | } 16 | 17 | class _ThemeSelectionDialogState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return AlertDialog( 21 | title: Text("选择主题模式"), 22 | content: SizedBox( 23 | width: double.maxFinite, 24 | // height: double.minPositive, 25 | child: Observer( 26 | builder: (_) { 27 | return ListView( 28 | shrinkWrap: true, 29 | children: [ 30 | RadioListTile( 31 | value: AdaptiveThemeMode.system, 32 | groupValue: widget.themeStore.mode, 33 | onChanged: _onChanged, 34 | title: Text("跟随系统"), 35 | ), 36 | RadioListTile( 37 | value: AdaptiveThemeMode.light, 38 | groupValue: widget.themeStore.mode, 39 | onChanged: _onChanged, 40 | title: Text("亮色主题"), 41 | ), 42 | RadioListTile( 43 | value: AdaptiveThemeMode.dark, 44 | groupValue: widget.themeStore.mode, 45 | onChanged: _onChanged, 46 | title: Text("暗色主题"), 47 | ), 48 | ], 49 | ); 50 | }, 51 | ), 52 | ), 53 | actions: [ 54 | TextButton( 55 | onPressed: () => Routes.pop(context), 56 | child: Text('关闭'), 57 | ) 58 | ], 59 | ); 60 | } 61 | 62 | _onChanged(AdaptiveThemeMode? adaptiveThemeMode) { 63 | widget.themeStore 64 | .update(context, adaptiveThemeMode ?? AdaptiveThemeMode.light); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/utils/constant.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | const String DOMAIN_WITHOUT_HTTPS = "bbs.nga.cn"; 4 | const String DOMAIN = "https://$DOMAIN_WITHOUT_HTTPS/"; 5 | 6 | const String LOGIN_URL = DOMAIN + "nuke.php?__lib=login&__act=account&login"; 7 | 8 | const int TOPIC_MASK_FONT_COLOR_DEFAULT = 0; 9 | const int TOPIC_MASK_FONT_COLOR_RED = 1; 10 | const int TOPIC_MASK_FONT_COLOR_BLUE = 2; 11 | const int TOPIC_MASK_FONT_COLOR_GREEN = 4; 12 | const int TOPIC_MASK_FONT_COLOR_ORANGE = 8; 13 | const int TOPIC_MASK_FONT_COLOR_SILVER = 16; 14 | 15 | const int TOPIC_MASK_FONT_STYLE_BOLD = 32; 16 | const int TOPIC_MASK_FONT_STYLE_ITALIC = 64; 17 | const int TOPIC_MASK_FONT_STYLE_UNDERLINE = 128; 18 | 19 | const int TOPIC_MASK_TYPE_LOCK = 1024; // 主题被锁定 2^10 20 | const int TOPIC_MASK_TYPE_ATTACHMENT = 8192; // 主题中有附件 2^13 21 | const int TOPIC_MASK_TYPE_ASSEMBLE = 32768; // 合集 2^15 22 | 23 | const TEXT_COLOR_MAP = { 24 | "skyblue": Color(0xFF87CEEB), 25 | "royalblue": Color(0xFF4169E1), 26 | "blue": Color(0xFF0000FF), 27 | "darkblue": Color(0xFF00008B), 28 | "orange": Color(0xFFFFA500), 29 | "orangered": Color(0xFFFF4500), 30 | "crimson": Color(0xFFDC143C), 31 | "red": Color(0xFFFF0000), 32 | "firebrick": Color(0xFFB22222), 33 | "darkred": Color(0xFF8B0000), 34 | "green": Color(0xFF008000), 35 | "limegreen": Color(0xFF32CD32), 36 | "seagreen": Color(0xFF2E8B57), 37 | "teal": Color(0xFF008080), 38 | "deeppink": Color(0xFFFF1493), 39 | "tomato": Color(0xFFFF6347), 40 | "coral": Color(0xFFFF7F50), 41 | "purple": Color(0xFF800080), 42 | "indigo": Color(0xFF4B0082), 43 | "burlywood": Color(0xFFDEB887), 44 | "sandybrown": Color(0xFFF4A460), 45 | "chocolate": Color(0xFFD2691E), 46 | "silver": Color(0xFFC0C0C0), 47 | }; 48 | -------------------------------------------------------------------------------- /lib/utils/custom_time_messages.dart: -------------------------------------------------------------------------------- 1 | import 'package:timeago/timeago.dart'; 2 | 3 | class CustomTimeMessages implements LookupMessages { 4 | String prefixAgo() => ''; 5 | 6 | String prefixFromNow() => '刚刚'; 7 | 8 | String suffixAgo() => '前'; 9 | 10 | String suffixFromNow() => '后'; 11 | 12 | String lessThanOneMinute(int seconds) => '1分钟'; 13 | 14 | String aboutAMinute(int minutes) => '1分钟'; 15 | 16 | String minutes(int minutes) => '$minutes分钟'; 17 | 18 | String aboutAnHour(int minutes) => '1小时'; 19 | 20 | String hours(int hours) => '$hours小时'; 21 | 22 | String aDay(int hours) => '1天'; 23 | 24 | String days(int days) => '$days天'; 25 | 26 | String aboutAMonth(int days) => '1个月'; 27 | 28 | String months(int months) => '$months个月'; 29 | 30 | String aboutAYear(int year) => '1年'; 31 | 32 | String years(int years) => 'years年'; 33 | 34 | String wordSeparator() => ''; 35 | } 36 | -------------------------------------------------------------------------------- /lib/utils/dimen.dart: -------------------------------------------------------------------------------- 1 | /// 数值 2 | class Dimen { 3 | static final double button = 14; 4 | static final double caption = 12; // 说明文字 5 | static final double body = 14; 6 | static final double subheading = 16; 7 | static final double title = 20; 8 | static final double headline = 24; 9 | 10 | static final double display1 = 34; 11 | static final double display2 = 45; 12 | static final double display3 = 56; 13 | static final double display4 = 112; 14 | 15 | static final double bottomPanelHeight = 240; 16 | static final double icon = 12; // 普通 icon 17 | } 18 | -------------------------------------------------------------------------------- /lib/utils/linkroute/link_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_nga/ui/page/topic_detail/reply_detail_dialog.dart'; 3 | import 'package:flutter_nga/utils/route.dart'; 4 | 5 | /// 超链接对应 Route 6 | abstract class LinkRoute { 7 | String matchRegExp(); 8 | 9 | void handleMatch(BuildContext context, Match match); 10 | } 11 | 12 | /// 话题超链接 13 | class TopicLinkRoute extends LinkRoute { 14 | @override 15 | void handleMatch(BuildContext context, Match match) { 16 | Routes.navigateTo(context, "${Routes.TOPIC_DETAIL}?tid=${match.group(1)}"); 17 | } 18 | 19 | @override 20 | String matchRegExp() { 21 | return "read\\.php\\?tid=(\\d+)?"; 22 | } 23 | } 24 | 25 | /// 用户超链接 26 | class UserLinkRoute extends LinkRoute { 27 | @override 28 | void handleMatch(BuildContext context, Match match) { 29 | Routes.navigateTo(context, "${Routes.USER}?uid=${match.group(1)}"); 30 | } 31 | 32 | @override 33 | String matchRegExp() { 34 | return "nuke\\.php\\?func=ucp&uid=(\\d+)?"; 35 | } 36 | } 37 | 38 | /// 回复超链接 39 | class ReplyLinkRoute extends LinkRoute { 40 | @override 41 | void handleMatch(BuildContext context, Match match) { 42 | showDialog( 43 | context: context, 44 | builder: (_) => ReplyDetailDialog(pid: int.tryParse(match.group(1)!))); 45 | } 46 | 47 | @override 48 | String matchRegExp() { 49 | return "read\\.php\\?searchpost=1&pid=(\\d+)?"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/utils/name_utils.dart: -------------------------------------------------------------------------------- 1 | String getShowName(String username) { 2 | if (username.startsWith("#anony_")) { 3 | final prefix = "甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥"; 4 | final suffix = "王李张刘陈杨黄吴赵周徐孙马朱胡林郭何高罗郑梁谢宋唐许邓冯韩曹曾彭萧蔡潘田董袁于" 5 | "余叶蒋杜苏魏程吕丁沈任姚卢傅钟姜崔谭廖范汪陆金石戴贾韦夏邱方侯邹熊孟秦白江阎薛" 6 | "尹段雷黎史龙陶贺顾毛郝龚邵万钱严赖覃洪武莫孔汤向常温康施文牛樊葛邢安齐易乔伍庞" 7 | "颜倪庄聂章鲁岳翟殷詹申欧耿关兰焦俞左柳甘祝包宁尚符舒阮柯纪梅童凌毕单季裴霍涂成" 8 | "苗谷盛曲翁冉骆蓝路游辛靳管柴蒙鲍华喻祁蒲房滕屈饶解牟艾尤阳时穆农司卓古吉缪简车" 9 | "项连芦麦褚娄窦戚岑景党宫费卜冷晏席卫米柏宗瞿桂全佟应臧闵苟邬边卞姬师和仇栾隋商" 10 | "刁沙荣巫寇桑郎甄丛仲虞敖巩明佘池查麻苑迟邝"; 11 | final buffer = new StringBuffer(); 12 | var i = 6; 13 | for (var j = 0; j <= 5; j++) { 14 | if (j == 0 || j == 3) { 15 | int pos = int.tryParse(username.substring(i + 1, i + 2), radix: 16)!; 16 | if (pos >= prefix.length) { 17 | pos = prefix.length - 1; 18 | } else if (pos < 0) { 19 | pos = 0; 20 | } 21 | buffer.write(prefix[pos]); 22 | } else { 23 | int pos = int.tryParse(username.substring(i, i + 2), radix: 16)!; 24 | if (pos >= suffix.length) { 25 | pos = suffix.length - 1; 26 | } else if (pos < 0) { 27 | pos = 0; 28 | } 29 | buffer.write(suffix[pos]); 30 | } 31 | i += 2; 32 | } 33 | return buffer.toString(); 34 | } else { 35 | return username; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/utils/palette.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 调色板 4 | class Palette { 5 | static final colorPrimary = Colors.brown; 6 | static final colorDarkPrimary = Colors.teal; 7 | static final colorBackground = Color(0xFFFFF9E3); 8 | static final colorDivider = Color(0xFFBDBDBD); 9 | static final colorIcon = Colors.black45; 10 | static final colorSplash = Colors.black12; 11 | static final colorHighlight = Colors.black12; 12 | 13 | static final colorTextPrimary = Colors.black87; 14 | static final colorTextSecondary = Colors.black54; 15 | static final colorTextHintWhite = Colors.white54; 16 | static final colorTextSubTitle = Color(0xFF591804); 17 | static final colorTextLock = Color(0xFFC58080); 18 | static final colorTextAssemble = Color(0xFFA0B4F0); 19 | 20 | static final colorWhite = Colors.white; 21 | 22 | static final _colorThumbBackground = Color(0xFFE0C19E); 23 | 24 | static final _colorQuoteBackground = Color(0xFFF9EFD6); 25 | static final _colorAlbumBorder = Color(0xFF91B262); 26 | static final _colorAlbumBackground = Color(0xFFd6dcae); 27 | 28 | static Color getColorPrimary(BuildContext context) { 29 | return isDark(context) ? colorDarkPrimary : colorPrimary; 30 | } 31 | 32 | static Color getColorThumbBackground(BuildContext context) { 33 | return isDark(context) ? Colors.white24 : _colorThumbBackground; 34 | } 35 | 36 | static Color getColorQuoteBackground(BuildContext context) { 37 | return isDark(context) ? Colors.white24 : _colorQuoteBackground; 38 | } 39 | 40 | static Color getColorAlbumBorder(BuildContext context) { 41 | return isDark(context) ? Palette.colorDivider : _colorAlbumBorder; 42 | } 43 | 44 | static Color getColorAlbumBackground(BuildContext context) { 45 | return isDark(context) ? Colors.white24 : _colorAlbumBackground; 46 | } 47 | 48 | static Color? getColorUserInfoCard(BuildContext context) { 49 | return isDark(context) ? null : Color(0xFFF5E8CB); 50 | } 51 | 52 | static Color getColorTextSubtitle(BuildContext context) { 53 | return isDark(context) ? Colors.white : colorTextSubTitle; 54 | } 55 | 56 | static Color? getColorDrawerListTileBackground(BuildContext context) { 57 | return isDark(context) ? Colors.white70 : null; 58 | } 59 | 60 | static bool isDark(BuildContext context) { 61 | return Theme.of(context).brightness == Brightness.dark; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/utils/picture_utils.dart: -------------------------------------------------------------------------------- 1 | const _THUMB_SUFFIX = ".thumb.jpg"; 2 | const _THUMB_S_SUFFIX = ".thumb_s.jpg"; 3 | const _THUMB_SS_SUFFIX = ".thumb_ss.jpg"; 4 | const _MEDIUM_SUFFIX = ".medium.jpg"; 5 | 6 | /// 是否是原图模式 7 | bool isOriginalUrl(String url) { 8 | bool originalMode = true; 9 | if (url.endsWith(_THUMB_SUFFIX) || 10 | url.endsWith(_THUMB_S_SUFFIX) || 11 | url.endsWith(_THUMB_SS_SUFFIX) || 12 | url.endsWith(_MEDIUM_SUFFIX)) { 13 | originalMode = false; 14 | } 15 | return originalMode; 16 | } 17 | 18 | String getOriginalUrl(String url) { 19 | if (url.endsWith(_THUMB_SUFFIX)) { 20 | return url.substring(0, url.length - _THUMB_SUFFIX.length); 21 | } else if (url.endsWith(_THUMB_S_SUFFIX)) { 22 | return url.substring(0, url.length - _THUMB_S_SUFFIX.length); 23 | } else if (url.endsWith(_THUMB_SS_SUFFIX)) { 24 | return url.substring(0, url.length - _THUMB_SS_SUFFIX.length); 25 | } else if (url.endsWith(_MEDIUM_SUFFIX)) { 26 | return url.substring(0, url.length - _MEDIUM_SUFFIX.length); 27 | } else { 28 | return url; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | void main() { 11 | // testWidgets('Counter increments smoke test', (WidgetTester tester) async { 12 | // // Build our app and trigger a frame. 13 | // await tester.pumpWidget(MyApp()); 14 | // 15 | // // Verify that our counter starts at 0. 16 | // expect(find.text('0'), findsOneWidget); 17 | // expect(find.text('1'), findsNothing); 18 | // 19 | // // Tap the '+' icon and trigger a frame. 20 | // await tester.tap(find.byIcon(Icons.add)); 21 | // await tester.pump(); 22 | // 23 | // // Verify that our counter has incremented. 24 | // expect(find.text('1'), findsOneWidget); 25 | // expect(find.text('0'), findsNothing); 26 | // }); 27 | 28 | test('Substring test', () { 29 | String url = "./mon_201901/23/7nQ5-1s5nK1kT1kSb9-45.png.thumb.jpg"; 30 | expect(url.substring(0, url.length - 10), 31 | "./mon_201901/23/7nQ5-1s5nK1kT1kSb9-45.png"); 32 | }); 33 | 34 | test('RegExp test', () { 35 | RegExp regExp = RegExp( 36 | "\\[b]Reply to \\[tid=(\\d+)?]Topic\\[/tid] Post by \\[uid]([\\s\\S]*?)\\[/uid]\\[color=gray]\\(([\\s\\S]*?)\\)\\[/color] \\(([\\s\\S]*?)\\)\\[/b]"); 37 | String content = 38 | "[b]Reply to [tid=16893894]Topic[/tid] Post by [uid]#anony_6b0df884c0e44bf854e195a52cbc3a0e[/uid][color=gray](0楼)[/color] (2019-04-08 14:29)[/b] 哈哈哈哈哈哈"; 39 | expect(content.replaceAll(regExp, ""), " 哈哈哈哈哈哈"); 40 | }); 41 | 42 | test('Anony RegExp test', () { 43 | RegExp regExp = RegExp( 44 | "\\[pid=(\\d+)?,(\\d+)?,(\\d+)?]Reply\\[/pid] \\[b]Post by \\[uid]#anony_([0-9a-zA-Z]*)\\[/uid]\\[color=gray]\\((\\d+)?楼\\)\\[/color] \\(([\\s\\S]*?)\\):\\[/b]"); 45 | String content = 46 | "[pid=445996637,23005426,1]Reply[/pid] [b]Post by [uid]#anony_7dc5258240df2301fdae75153712d174[/uid][color=gray](6楼)[/color] (2020-08-18 15:04):[/b]"; 47 | expect(content.replaceAll(regExp, ""), ""); 48 | }); 49 | } 50 | --------------------------------------------------------------------------------