├── README.md ├── android ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── app │ ├── src │ │ └── main │ │ │ ├── res │ │ │ ├── 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 │ │ │ │ └── styles.xml │ │ │ └── drawable │ │ │ │ └── launch_background.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── tain335 │ │ │ │ └── nodebb │ │ │ │ └── MainActivity.java │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── .gitignore ├── settings.gradle ├── build.gradle ├── gradlew.bat └── gradlew ├── assets └── images │ ├── flutter_avatar.png │ └── flutter_cover.jpg ├── lib ├── enums │ └── enums.dart ├── socket_io │ ├── socket_io.dart │ ├── errors.dart │ ├── sio_client.dart │ ├── eio_parser.dart │ ├── eio_client.dart │ ├── sio_parser.dart │ ├── sio_socket.dart │ └── eio_socket.dart ├── models │ ├── models.dart │ ├── notification.dart │ ├── unread_info.dart │ ├── teaser.dart │ ├── category.dart │ ├── message.dart │ ├── app_state.dart │ ├── post.dart │ ├── teaser.g.dart │ ├── user.dart │ ├── room.dart │ ├── topic.dart │ ├── category.g.dart │ ├── message.g.dart │ ├── notification.g.dart │ ├── unread_info.g.dart │ ├── post.g.dart │ ├── room.g.dart │ ├── app_state.g.dart │ ├── topic.g.dart │ └── user.g.dart ├── views │ ├── register_page.dart │ ├── recent_views_page.dart │ ├── base.dart │ ├── user_info_page.dart │ ├── home_page.dart │ ├── personal_fragment.dart │ ├── comment_page.dart │ ├── login_page.dart │ ├── bookmarks_page.dart │ ├── search_user_page.dart │ ├── chat_page.dart │ ├── topics_fragment.dart │ └── messages_fragment.dart ├── errors │ └── errors.dart ├── application │ └── application.dart ├── widgets │ ├── animation_page_route.dart │ └── builders.dart ├── utils │ └── utils.dart ├── services │ ├── cookie_jar.dart │ └── remote_service.dart ├── actions │ └── actions.dart └── mutations │ └── mutations.dart ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── AppDelegate.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 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 │ │ │ └── Contents.json │ ├── main.m │ ├── AppDelegate.m │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ └── contents.xcworkspacedata ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── .gitignore └── Podfile ├── .gitignore ├── .metadata ├── android.iml ├── tool ├── build.dart └── watch.dart ├── test └── widget_test.dart ├── nodebb_android.iml ├── pubspec.yaml └── nodebb.iml /README.md: -------------------------------------------------------------------------------- 1 | # nodebb 2 | 3 | Flutter中文论坛客户端 -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /assets/images/flutter_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/assets/images/flutter_avatar.png -------------------------------------------------------------------------------- /assets/images/flutter_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/assets/images/flutter_cover.jpg -------------------------------------------------------------------------------- /lib/enums/enums.dart: -------------------------------------------------------------------------------- 1 | enum RequestStatus { PENDING, ERROR, SUCCESS, EMPTY } 2 | 3 | enum DialogMode { ALERT, CONFIRM } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/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/flutter-dev/nodebb/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter-dev/nodebb/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea 4 | .vscode/ 5 | .packages 6 | .pub/ 7 | build/ 8 | ios/.generated/ 9 | packages 10 | .flutter-plugins 11 | $cachePath 12 | *.log 13 | pubspec.lock 14 | -------------------------------------------------------------------------------- /lib/socket_io/socket_io.dart: -------------------------------------------------------------------------------- 1 | export 'eio_client.dart'; 2 | export 'eio_parser.dart'; 3 | export 'eio_socket.dart'; 4 | export 'sio_client.dart'; 5 | export 'sio_parser.dart'; 6 | export 'sio_socket.dart'; -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /lib/models/models.dart: -------------------------------------------------------------------------------- 1 | library model; 2 | 3 | export 'app_state.dart'; 4 | export 'category.dart'; 5 | export 'post.dart'; 6 | export 'topic.dart'; 7 | export 'user.dart'; 8 | export 'message.dart'; 9 | export 'unread_info.dart'; 10 | export 'room.dart'; 11 | export 'teaser.dart'; 12 | export 'notification.dart'; -------------------------------------------------------------------------------- /.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: 4353297079c80b17a6cb6c4ee12486f0e52f3c37 8 | channel: master 9 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/tain335/nodebb/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tain335.nodebb; 2 | 3 | import android.os.Bundle; 4 | 5 | import io.flutter.app.FlutterActivity; 6 | import io.flutter.plugins.GeneratedPluginRegistrant; 7 | 8 | public class MainActivity extends FlutterActivity { 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | GeneratedPluginRegistrant.registerWith(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 7 | [GeneratedPluginRegistrant registerWithRegistry:self]; 8 | // Override point for customization after application launch. 9 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /lib/models/notification.dart: -------------------------------------------------------------------------------- 1 | library notification; 2 | 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:flutter_wills_gen/wills.dart'; 5 | 6 | 7 | part 'notification.g.dart'; 8 | 9 | @wills 10 | class NodeBBNotification extends Object with Reactive { 11 | 12 | bool newReply; 13 | 14 | bool newChat; 15 | 16 | bool newFollow; 17 | 18 | bool groupInvite; 19 | 20 | bool newTopic; 21 | 22 | NodeBBNotification.$(); 23 | 24 | factory NodeBBNotification() = _$NodeBBNotification; 25 | } -------------------------------------------------------------------------------- /lib/views/register_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:nodebb/views/base.dart'; 4 | 5 | class RegisterPage extends BasePage { 6 | RegisterPage({Key key}) : super(key: key); 7 | 8 | @override 9 | _RegisterPageState createState() => new _RegisterPageState(); 10 | } 11 | 12 | class _RegisterPageState extends State { 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return new Text('注册'); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.0.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /lib/socket_io/errors.dart: -------------------------------------------------------------------------------- 1 | class _BaseException implements Exception { 2 | final dynamic reason; 3 | 4 | const _BaseException(this.reason); 5 | 6 | String toString() => '$runtimeType.reason: $reason'; 7 | } 8 | 9 | class SocketIOStateException extends _BaseException { 10 | const SocketIOStateException([reason]): super(reason); 11 | } 12 | 13 | class SocketIOParseException extends _BaseException { 14 | const SocketIOParseException([reason]): super(reason); 15 | } 16 | 17 | class EngineIOReconnectFailException extends _BaseException { 18 | const EngineIOReconnectFailException([reason]): super(reason); 19 | } -------------------------------------------------------------------------------- /android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | *.pbxuser 16 | *.mode1v3 17 | *.mode2v3 18 | *.perspectivev3 19 | 20 | !default.pbxuser 21 | !default.mode1v3 22 | !default.mode2v3 23 | !default.perspectivev3 24 | 25 | xcuserdata 26 | 27 | *.moved-aside 28 | 29 | *.pyc 30 | *sync/ 31 | Icon? 32 | .tags* 33 | 34 | /Flutter/app.flx 35 | /Flutter/app.zip 36 | /Flutter/flutter_assets/ 37 | /Flutter/App.framework 38 | /Flutter/Flutter.framework 39 | /Flutter/Generated.xcconfig 40 | /ServiceDefinitions.json 41 | 42 | Pods/ 43 | -------------------------------------------------------------------------------- /lib/models/unread_info.dart: -------------------------------------------------------------------------------- 1 | library unread_info; 2 | 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:flutter_wills_gen/wills.dart'; 5 | 6 | part 'unread_info.g.dart'; 7 | 8 | @wills 9 | class UnreadInfo extends Object with Reactive { 10 | 11 | int unreadTopicCount; 12 | 13 | int unreadNewTopicCount; 14 | 15 | int unreadChatCount; 16 | 17 | int unreadWatchedTopicCount; 18 | 19 | int unreadNotificationCount; 20 | 21 | UnreadInfo.$(); 22 | 23 | factory UnreadInfo({ 24 | int unreadTopicCount, 25 | int unreadNewTopicCount, 26 | int unreadChatCount, 27 | int unreadWatchedTopicCount, 28 | int unreadNotificationCount 29 | }) = _$UnreadInfo; 30 | } -------------------------------------------------------------------------------- /lib/models/teaser.dart: -------------------------------------------------------------------------------- 1 | library teaser; 2 | 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:flutter_wills_gen/wills.dart'; 5 | import 'package:nodebb/models/user.dart'; 6 | 7 | part 'teaser.g.dart'; 8 | 9 | @wills 10 | class Teaser extends Object with Reactive { 11 | 12 | User fromUser; 13 | 14 | String content; 15 | 16 | int timestamp; 17 | 18 | Teaser.$(); 19 | 20 | factory Teaser.fromJSON(Map json) { 21 | Teaser teaser = new _$Teaser( 22 | fromUser: json['user'] != null ? new User.fromJSON(json['user']) : null, 23 | content: json['content'] ?? '', 24 | timestamp: json['timestamp'] ?? 0 25 | ); 26 | return teaser; 27 | } 28 | 29 | factory Teaser() = _$Teaser; 30 | 31 | } -------------------------------------------------------------------------------- /lib/models/category.dart: -------------------------------------------------------------------------------- 1 | library category; 2 | import 'package:flutter_wills_gen/wills.dart'; 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/utils/utils.dart' as utils; 5 | 6 | part 'category.g.dart'; 7 | 8 | @wills 9 | abstract class Category extends Object with Reactive { 10 | 11 | int cid; 12 | 13 | String name; 14 | 15 | String bgColor; 16 | 17 | String color; 18 | 19 | String image; 20 | 21 | Category.$(); 22 | 23 | factory Category.fromJSON(Map json) { 24 | Category category = new _$Category( 25 | cid: utils.convertToInteger(json['cid']), 26 | name: json['name'], 27 | bgColor: json['bgcolor'], 28 | image: json['img'] 29 | ); 30 | return category; 31 | } 32 | 33 | factory Category() = _$Category; 34 | } -------------------------------------------------------------------------------- /tool/build.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:build_runner/build_runner.dart'; 4 | import 'package:build_runner/src/generate/build_impl.dart' as build_impl; 5 | import 'package:source_gen/source_gen.dart'; 6 | import 'package:flutter_wills_gen/flutter_wills_gen.dart'; 7 | 8 | /// Example of how to use source_gen with [BuiltValueGenerator]. 9 | /// 10 | /// Import the generators you want and pass them to [build] as shown, 11 | /// specifying which files in which packages you want to run against. 12 | Future main(List args) async { 13 | await build_impl.build([ 14 | new BuildAction( 15 | new PartBuilder([ 16 | new WillsGenerator() 17 | ]), 18 | 'nodebb', 19 | inputs: const ['lib/models/*.dart']) 20 | ], deleteFilesByDefault: true, skipBuildScriptCheck: true); 21 | } -------------------------------------------------------------------------------- /lib/models/message.dart: -------------------------------------------------------------------------------- 1 | library message; 2 | 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:flutter_wills_gen/wills.dart'; 5 | import 'package:nodebb/models/user.dart'; 6 | 7 | part 'message.g.dart'; 8 | 9 | enum MessageType { SEND, RECEIVE, SEND_PENDING } 10 | 11 | @wills 12 | class Message extends Object with Reactive { 13 | 14 | int id; 15 | 16 | User user; 17 | 18 | DateTime timestamp; 19 | 20 | String content; 21 | 22 | MessageType type; 23 | 24 | Message.$(); 25 | 26 | factory Message({User user, DateTime timestamp, String content, MessageType type}) = _$Message; 27 | 28 | factory Message.fromJSON(Map json) { 29 | Message msg = new _$Message( 30 | id: json['messageId'], 31 | user: new User.fromJSON(json['fromUser']), 32 | timestamp: new DateTime.fromMillisecondsSinceEpoch(json['timestamp']), 33 | content: json['cleanedContent'], 34 | type: MessageType.RECEIVE 35 | ); 36 | return msg; 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /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 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | MinimumOSVersion 28 | 8.0 29 | 30 | 31 | -------------------------------------------------------------------------------- /tool/watch.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Google Inc. Please see the AUTHORS file for details. 2 | // All rights reserved. Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'package:build_runner/build_runner.dart'; 7 | import 'package:build_runner/src/generate/watch_impl.dart' as watch_impl; 8 | import 'package:flutter_wills_gen/flutter_wills_gen.dart'; 9 | import 'package:source_gen/source_gen.dart'; 10 | 11 | /// Example of how to use source_gen with [BuiltValueGenerator]. 12 | /// 13 | /// This script runs a watcher that continuously rebuilds generated source. 14 | /// 15 | /// Import the generators you want and pass them to [watch] as shown, 16 | /// specifying which files in which packages you want to run against. 17 | Future main(List args) async { 18 | await watch_impl.watch([ 19 | new BuildAction( 20 | new PartBuilder([ 21 | new WillsGenerator() 22 | ]), 23 | 'nodebb', 24 | inputs: const ['lib/models/*.dart']) 25 | ], deleteFilesByDefault: true, skipBuildScriptCheck: true); 26 | } -------------------------------------------------------------------------------- /lib/errors/errors.dart: -------------------------------------------------------------------------------- 1 | class _BaseException implements Exception { 2 | final dynamic reason; 3 | 4 | const _BaseException(this.reason); 5 | 6 | String toString() => '$runtimeType.reason: $reason'; 7 | } 8 | 9 | class NodeBBLoginFailException extends _BaseException { 10 | const NodeBBLoginFailException([reason]): super(reason); 11 | } 12 | 13 | //class RequestFailException extends _BaseException { 14 | // const RequestFailException([reason]): super(reason); 15 | //} 16 | 17 | class NodeBBServiceNotAvailableException extends _BaseException { 18 | const NodeBBServiceNotAvailableException([reason]): super(reason); 19 | } 20 | 21 | 22 | class NodeBBException extends _BaseException { 23 | const NodeBBException([reason]): super(reason); 24 | } 25 | 26 | class NodeBBBookmarkedException extends _BaseException { 27 | const NodeBBBookmarkedException([reason]): super(reason); 28 | } 29 | 30 | class NodeBBNoUserInRoomException extends _BaseException { 31 | const NodeBBNoUserInRoomException([reason]): super(reason); 32 | } 33 | 34 | class ApplicationException extends _BaseException { 35 | const ApplicationException([reason]): super(reason); 36 | } -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | import 'package:nodebb/main.dart'; 11 | 12 | void main() { 13 | // testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // // Build our app and trigger a frame. 15 | // await tester.pumpWidget(new MyApp()); 16 | // 17 | // // Verify that our counter starts at 0. 18 | // expect(find.text('0'), findsOneWidget); 19 | // expect(find.text('1'), findsNothing); 20 | // 21 | // // Tap the '+' icon and trigger a frame. 22 | // await tester.tap(find.byIcon(Icons.add)); 23 | // await tester.pump(); 24 | // 25 | // // Verify that our counter has incremented. 26 | // expect(find.text('0'), findsNothing); 27 | // expect(find.text('1'), findsOneWidget); 28 | // }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/application/application.dart: -------------------------------------------------------------------------------- 1 | import 'package:logging/logging.dart'; 2 | import 'package:nodebb/services/io_service.dart'; 3 | import 'package:nodebb/services/remote_service.dart'; 4 | import 'package:nodebb/socket_io/sio_client.dart'; 5 | 6 | 7 | 8 | class Application { 9 | 10 | static setup() { 11 | Logger.root.level = Level.ALL; 12 | Logger.root.onRecord.listen((LogRecord rec) { 13 | print('${rec.level.name}: ${rec.message}'); 14 | }); 15 | 16 | RemoteService.getInstance().setup(Application.host); 17 | SocketIOClient client = new SocketIOClient( 18 | uri: 'ws://${Application.host}/socket.io/?EIO=3&transport=websocket', 19 | jar: RemoteService.getInstance().jar 20 | ); 21 | IOService.getInstance().setup(client); 22 | } 23 | 24 | // static final store = new Store( 25 | // reducerBuilder.build(), 26 | // new AppState(), 27 | // new AppActions(), 28 | // middleware: [ 29 | // createAppStoreMiddleware() 30 | // ] 31 | // ); 32 | 33 | static final Logger logger = new Logger('Application'); 34 | 35 | // static final host = '172.18.4.19:4567'; 36 | static final host = 'flutter-dev.com'; 37 | } -------------------------------------------------------------------------------- /lib/models/app_state.dart: -------------------------------------------------------------------------------- 1 | library app_state; 2 | import 'package:flutter_wills_gen/wills.dart'; 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/models/models.dart'; 5 | import 'package:nodebb/models/notification.dart'; 6 | import 'package:nodebb/models/room.dart'; 7 | import 'package:nodebb/models/unread_info.dart'; 8 | import 'package:nodebb/socket_io/socket_io.dart'; 9 | 10 | part 'app_state.g.dart'; 11 | 12 | @wills 13 | abstract class AppState extends Object with Reactive { 14 | 15 | User activeUser; 16 | 17 | UnreadInfo unreadInfo; 18 | 19 | NodeBBNotification notification; 20 | 21 | ObservableMap topics; 22 | 23 | ObservableMap categories; 24 | 25 | ObservableMap users; 26 | 27 | ObservableMap rooms; 28 | 29 | ObservableMap shareStorage; 30 | 31 | ObservableList recentViews; 32 | 33 | AppState.$(); 34 | 35 | factory AppState({ 36 | UnreadInfo unreadInfo, 37 | User activeUser, 38 | NodeBBNotification notification, 39 | ObservableMap topics, 40 | ObservableMap categories, 41 | ObservableMap users, 42 | ObservableMap rooms, 43 | ObservableMap shareStorage, 44 | ObservableList recentViews 45 | }) = _$AppState; 46 | } -------------------------------------------------------------------------------- /lib/models/post.dart: -------------------------------------------------------------------------------- 1 | library post; 2 | import 'package:flutter_wills_gen/wills.dart'; 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/models/user.dart'; 5 | import 'package:nodebb/utils/utils.dart' as utils; 6 | part 'post.g.dart'; 7 | 8 | @wills 9 | abstract class Post extends Object with Reactive { 10 | 11 | int tid; //关联Topic ID 12 | 13 | int pid; //post id 14 | 15 | User user; //用户 ID 16 | 17 | bool downVoted; //已踩 18 | 19 | bool upVoted; //已赞 20 | 21 | int upVotes; //点赞数 22 | 23 | int downVotes; //踩数 24 | 25 | int votes; //赞总数 26 | 27 | bool isMainPost; 28 | 29 | DateTime timestamp; 30 | 31 | String content; //内容 32 | 33 | Post.$(); 34 | 35 | factory Post.fromJSON(Map json) { 36 | Post post = new _$Post( 37 | content: json['content'], 38 | tid: utils.convertToInteger(json['tid']), 39 | pid: utils.convertToInteger(json['pid']), 40 | user: json['user'] != null ? new User.fromJSON(json['user']) : null, 41 | downVotes: json['downvotes'] ?? 0, 42 | upVotes: json['upvotes'] ?? 0, 43 | upVoted: json['upvoted'] ?? false, 44 | downVoted: json['downvoted'] ?? false, 45 | isMainPost: json['isMainPost'] ?? false, 46 | votes: json['votes'] ?? 0, 47 | timestamp: new DateTime.fromMillisecondsSinceEpoch(json['timestamp']) 48 | ); 49 | return post; 50 | } 51 | 52 | factory Post() = _$Post; 53 | } -------------------------------------------------------------------------------- /lib/models/teaser.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of teaser; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$Teaser extends Teaser { 10 | User _fromUser; 11 | User get fromUser { 12 | $observe('fromUser'); 13 | return _fromUser; 14 | } 15 | 16 | set fromUser(User fromUser) { 17 | if (fromUser == _fromUser) return; 18 | _fromUser = fromUser; 19 | $notify('fromUser'); 20 | } 21 | 22 | String _content; 23 | String get content { 24 | $observe('content'); 25 | return _content; 26 | } 27 | 28 | set content(String content) { 29 | if (content == _content) return; 30 | _content = content; 31 | $notify('content'); 32 | } 33 | 34 | int _timestamp; 35 | int get timestamp { 36 | $observe('timestamp'); 37 | return _timestamp; 38 | } 39 | 40 | set timestamp(int timestamp) { 41 | if (timestamp == _timestamp) return; 42 | _timestamp = timestamp; 43 | $notify('timestamp'); 44 | } 45 | 46 | _$Teaser.$() : super.$(); 47 | factory _$Teaser({ 48 | User fromUser, 49 | String content, 50 | int timestamp, 51 | }) { 52 | return new _$Teaser.$() 53 | .._fromUser = fromUser 54 | .._content = content ?? '' 55 | .._timestamp = timestamp ?? 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/models/user.dart: -------------------------------------------------------------------------------- 1 | library user; 2 | import 'package:flutter_wills_gen/wills.dart'; 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/utils/utils.dart' as utils; 5 | 6 | part 'user.g.dart'; 7 | 8 | @wills 9 | abstract class User extends Object with Reactive { 10 | 11 | int get uid; 12 | 13 | String get userName; 14 | 15 | String get status; 16 | 17 | String get picture; 18 | 19 | String get cover; //封面 20 | 21 | int get followerCount; //粉丝 22 | 23 | int get followingCount; //关注 24 | 25 | int get reputation; //声望 26 | 27 | int get topicCount; //主题数量 28 | 29 | String get iconBgColor; 30 | 31 | String get iconText; 32 | 33 | String get signature; //签名 34 | 35 | User.$(); 36 | 37 | factory User.fromJSON(Map json) { 38 | User user = new _$User( 39 | userName: json['username'], 40 | uid: utils.convertToInteger(json['uid']), 41 | topicCount: json['topiccount'] ?? 0, 42 | picture: json['picture'], 43 | reputation: json['reputation'] ?? 0, 44 | status: json['status'], 45 | signature: json['signature'] ?? '', 46 | iconText: json['icon:text'], 47 | iconBgColor: json['icon:bgColor'], 48 | cover: json['cover:url'] ?? '', 49 | followerCount: utils.convertToInteger(json['followerCount'] ?? 0), 50 | followingCount: utils.convertToInteger(json['followingCount'] ?? 0) 51 | ); 52 | return user; 53 | } 54 | 55 | factory User() = _$User; 56 | } -------------------------------------------------------------------------------- /lib/models/room.dart: -------------------------------------------------------------------------------- 1 | library room; 2 | 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/models/message.dart'; 5 | import 'package:nodebb/models/teaser.dart'; 6 | import 'package:flutter_wills_gen/wills.dart'; 7 | import 'package:nodebb/models/user.dart'; 8 | 9 | part 'room.g.dart'; 10 | 11 | @wills 12 | class Room extends Object with Reactive { 13 | 14 | int owner; 15 | 16 | int roomId; 17 | 18 | String roomName; 19 | 20 | ObservableList users; 21 | 22 | bool groupChat; 23 | 24 | bool unread; 25 | 26 | String ownerName; 27 | 28 | Teaser teaser; 29 | 30 | int maxChatMessageLength; 31 | 32 | ObservableList messages; 33 | 34 | Room.$(); 35 | 36 | factory Room.fromJSON(Map json) { 37 | List datas = json['users']; 38 | ObservableList users = new ObservableList(); 39 | for(var data in datas) { 40 | users.add(new User.fromJSON(data)); 41 | } 42 | Room room = new _$Room( 43 | owner: json['owner'], 44 | roomId: json['roomId'], 45 | roomName: json['roomName'], 46 | ownerName: json['usernames'], 47 | groupChat: json['groupChat'], 48 | unread: json['unread'], 49 | users: users, 50 | maxChatMessageLength: json['maximumChatMessageLength'] ?? 1000, 51 | teaser: json['teaser'] != null ? new Teaser.fromJSON(json['teaser']) : new Teaser.fromJSON({}), 52 | messages: new ObservableList() 53 | ); 54 | return room; 55 | } 56 | 57 | factory Room() = _$Room; 58 | 59 | } -------------------------------------------------------------------------------- /nodebb_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/models/topic.dart: -------------------------------------------------------------------------------- 1 | library topic; 2 | import 'package:flutter_wills_gen/wills.dart'; 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/models/post.dart'; 5 | import 'package:nodebb/models/user.dart'; 6 | import 'package:nodebb/utils/utils.dart' as utils; 7 | part 'topic.g.dart'; 8 | 9 | @wills 10 | abstract class Topic extends Object with Reactive { 11 | 12 | int cid; //分类 ID 13 | 14 | int tid; //Topic ID 15 | 16 | int mainPid; 17 | 18 | User user; 19 | 20 | bool isOwner; 21 | 22 | String title; 23 | 24 | DateTime lastPostTime; //最后回复 25 | 26 | int postCount; 27 | 28 | DateTime timestamp; //发布时间 29 | 30 | int viewCount; //阅读次数 31 | 32 | int upVotes; //点赞 33 | 34 | int downVotes; //踩 35 | 36 | // ObservableList posts; //posts 37 | 38 | Topic.$(); 39 | 40 | factory Topic.fromJSON(Map json) { 41 | Topic topic = new _$Topic( 42 | tid: utils.convertToInteger(json['tid']), 43 | isOwner: json['isOwner'], 44 | cid: utils.convertToInteger(json['cid']), 45 | mainPid: utils.convertToInteger(json['mainPid']), 46 | lastPostTime: new DateTime.fromMillisecondsSinceEpoch(json['lastposttime']), 47 | downVotes: json['downvotes'], 48 | upVotes: json['upvotes'], 49 | timestamp: new DateTime.fromMillisecondsSinceEpoch(json['timestamp']), 50 | postCount: json['postcount'], 51 | viewCount: json['viewcount'], 52 | title: json['title'], 53 | //uid: utils.convertToInteger(json['uid']) 54 | user: new User.fromJSON(json['user']) 55 | ); 56 | return topic; 57 | } 58 | 59 | factory Topic() = _$Topic; 60 | } -------------------------------------------------------------------------------- /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 | nodebb 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | arm64 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 27 19 | 20 | lintOptions { 21 | disable 'InvalidPackage' 22 | } 23 | 24 | defaultConfig { 25 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 26 | applicationId "com.tain335.nodebb" 27 | minSdkVersion 16 28 | targetSdkVersion 27 29 | versionCode 1 30 | versionName "1.0" 31 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 32 | } 33 | 34 | buildTypes { 35 | release { 36 | // TODO: Add your own signing config for the release build. 37 | // Signing with the debug keys for now, so `flutter run --release` works. 38 | signingConfig signingConfigs.debug 39 | } 40 | } 41 | } 42 | 43 | flutter { 44 | source '../..' 45 | } 46 | 47 | dependencies { 48 | testImplementation 'junit:junit:4.12' 49 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 50 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 51 | } 52 | -------------------------------------------------------------------------------- /lib/socket_io/sio_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:nodebb/services/cookie_jar.dart'; 4 | import 'package:nodebb/socket_io/eio_client.dart'; 5 | import 'package:nodebb/socket_io/eio_socket.dart'; 6 | import 'package:nodebb/socket_io/sio_socket.dart'; 7 | 8 | class SocketIOEvent {} 9 | 10 | class SocketIOClient { 11 | 12 | String uri; 13 | 14 | int connectTimeout; 15 | 16 | bool autoReconnect; 17 | 18 | int reconnectInterval; 19 | 20 | int maxReconnectTrys; 21 | 22 | CookieJar jar; 23 | 24 | List sockets = new List(); 25 | 26 | //StreamController _eventController = new StreamController.broadcast(); 27 | 28 | //StreamSubscription _sub; 29 | 30 | EngineIOClient engine; 31 | 32 | //Stream get eventStream => _eventController.stream; 33 | 34 | SocketIOClient({ 35 | this.uri, 36 | this.autoReconnect = true, 37 | this.reconnectInterval = 10000, 38 | this.maxReconnectTrys = 3, 39 | this.connectTimeout = 8000, 40 | this.jar 41 | }) { 42 | if(this.jar == null) { 43 | this.jar = new CookieJar(); 44 | } 45 | engine = new EngineIOClient( 46 | autoReconnect: autoReconnect, 47 | reconnectInterval: reconnectInterval, 48 | maxReconnectTrys: maxReconnectTrys, 49 | jar: jar 50 | ); 51 | } 52 | 53 | Future of({String namespace = '/', Map query}) async { 54 | EngineIOSocket io = await engine.connect(uri, true); 55 | SocketIOSocket socket = new SocketIOSocket( 56 | io: io, 57 | namespace: namespace, 58 | query: query 59 | ); 60 | sockets.add(socket); 61 | return socket; 62 | } 63 | 64 | closeAll() { 65 | engine.closeAll(); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /lib/models/category.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of category; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$Category extends Category { 10 | int _cid; 11 | int get cid { 12 | $observe('cid'); 13 | return _cid; 14 | } 15 | 16 | set cid(int cid) { 17 | if (cid == _cid) return; 18 | _cid = cid; 19 | $notify('cid'); 20 | } 21 | 22 | String _name; 23 | String get name { 24 | $observe('name'); 25 | return _name; 26 | } 27 | 28 | set name(String name) { 29 | if (name == _name) return; 30 | _name = name; 31 | $notify('name'); 32 | } 33 | 34 | String _bgColor; 35 | String get bgColor { 36 | $observe('bgColor'); 37 | return _bgColor; 38 | } 39 | 40 | set bgColor(String bgColor) { 41 | if (bgColor == _bgColor) return; 42 | _bgColor = bgColor; 43 | $notify('bgColor'); 44 | } 45 | 46 | String _color; 47 | String get color { 48 | $observe('color'); 49 | return _color; 50 | } 51 | 52 | set color(String color) { 53 | if (color == _color) return; 54 | _color = color; 55 | $notify('color'); 56 | } 57 | 58 | String _image; 59 | String get image { 60 | $observe('image'); 61 | return _image; 62 | } 63 | 64 | set image(String image) { 65 | if (image == _image) return; 66 | _image = image; 67 | $notify('image'); 68 | } 69 | 70 | _$Category.$() : super.$(); 71 | factory _$Category({ 72 | int cid, 73 | String name, 74 | String bgColor, 75 | String color, 76 | String image, 77 | }) { 78 | return new _$Category.$() 79 | .._cid = cid ?? 0 80 | .._name = name ?? '' 81 | .._bgColor = bgColor ?? '' 82 | .._color = color ?? '' 83 | .._image = image ?? ''; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/models/message.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of message; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$Message extends Message { 10 | int _id; 11 | int get id { 12 | $observe('id'); 13 | return _id; 14 | } 15 | 16 | set id(int id) { 17 | if (id == _id) return; 18 | _id = id; 19 | $notify('id'); 20 | } 21 | 22 | User _user; 23 | User get user { 24 | $observe('user'); 25 | return _user; 26 | } 27 | 28 | set user(User user) { 29 | if (user == _user) return; 30 | _user = user; 31 | $notify('user'); 32 | } 33 | 34 | DateTime _timestamp; 35 | DateTime get timestamp { 36 | $observe('timestamp'); 37 | return _timestamp; 38 | } 39 | 40 | set timestamp(DateTime timestamp) { 41 | if (timestamp == _timestamp) return; 42 | _timestamp = timestamp; 43 | $notify('timestamp'); 44 | } 45 | 46 | String _content; 47 | String get content { 48 | $observe('content'); 49 | return _content; 50 | } 51 | 52 | set content(String content) { 53 | if (content == _content) return; 54 | _content = content; 55 | $notify('content'); 56 | } 57 | 58 | MessageType _type; 59 | MessageType get type { 60 | $observe('type'); 61 | return _type; 62 | } 63 | 64 | set type(MessageType type) { 65 | if (type == _type) return; 66 | _type = type; 67 | $notify('type'); 68 | } 69 | 70 | _$Message.$() : super.$(); 71 | factory _$Message({ 72 | int id, 73 | User user, 74 | DateTime timestamp, 75 | String content, 76 | MessageType type, 77 | }) { 78 | return new _$Message.$() 79 | .._id = id ?? 0 80 | .._user = user 81 | .._timestamp = timestamp 82 | .._content = content ?? '' 83 | .._type = type; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/models/notification.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of notification; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$NodeBBNotification extends NodeBBNotification { 10 | bool _newReply; 11 | bool get newReply { 12 | $observe('newReply'); 13 | return _newReply; 14 | } 15 | 16 | set newReply(bool newReply) { 17 | if (newReply == _newReply) return; 18 | _newReply = newReply; 19 | $notify('newReply'); 20 | } 21 | 22 | bool _newChat; 23 | bool get newChat { 24 | $observe('newChat'); 25 | return _newChat; 26 | } 27 | 28 | set newChat(bool newChat) { 29 | if (newChat == _newChat) return; 30 | _newChat = newChat; 31 | $notify('newChat'); 32 | } 33 | 34 | bool _newFollow; 35 | bool get newFollow { 36 | $observe('newFollow'); 37 | return _newFollow; 38 | } 39 | 40 | set newFollow(bool newFollow) { 41 | if (newFollow == _newFollow) return; 42 | _newFollow = newFollow; 43 | $notify('newFollow'); 44 | } 45 | 46 | bool _groupInvite; 47 | bool get groupInvite { 48 | $observe('groupInvite'); 49 | return _groupInvite; 50 | } 51 | 52 | set groupInvite(bool groupInvite) { 53 | if (groupInvite == _groupInvite) return; 54 | _groupInvite = groupInvite; 55 | $notify('groupInvite'); 56 | } 57 | 58 | bool _newTopic; 59 | bool get newTopic { 60 | $observe('newTopic'); 61 | return _newTopic; 62 | } 63 | 64 | set newTopic(bool newTopic) { 65 | if (newTopic == _newTopic) return; 66 | _newTopic = newTopic; 67 | $notify('newTopic'); 68 | } 69 | 70 | _$NodeBBNotification.$() : super.$(); 71 | factory _$NodeBBNotification({ 72 | bool newReply, 73 | bool newChat, 74 | bool newFollow, 75 | bool groupInvite, 76 | bool newTopic, 77 | }) { 78 | return new _$NodeBBNotification.$() 79 | .._newReply = newReply ?? false 80 | .._newChat = newChat ?? false 81 | .._newFollow = newFollow ?? false 82 | .._groupInvite = groupInvite ?? false 83 | .._newTopic = newTopic ?? false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /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 | def parse_KV_file(file, separator='=') 8 | file_abs_path = File.expand_path(file) 9 | if !File.exists? file_abs_path 10 | return []; 11 | end 12 | pods_ary = [] 13 | skip_line_start_symbols = ["#", "/"] 14 | File.foreach(file_abs_path) { |line| 15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 16 | plugin = line.split(pattern=separator) 17 | if plugin.length == 2 18 | podname = plugin[0].strip() 19 | path = plugin[1].strip() 20 | podpath = File.expand_path("#{path}", file_abs_path) 21 | pods_ary.push({:name => podname, :path => podpath}); 22 | else 23 | puts "Invalid plugin specification: #{line}" 24 | end 25 | } 26 | return pods_ary 27 | end 28 | 29 | target 'Runner' do 30 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 31 | # referring to absolute paths on developers' machines. 32 | system('rm -rf .symlinks') 33 | system('mkdir -p .symlinks/plugins') 34 | 35 | # Flutter Pods 36 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 37 | if generated_xcode_build_settings.empty? 38 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 39 | end 40 | generated_xcode_build_settings.map { |p| 41 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 42 | symlink = File.join('.symlinks', 'flutter') 43 | File.symlink(File.dirname(p[:path]), symlink) 44 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 45 | end 46 | } 47 | 48 | # Plugin Pods 49 | plugin_pods = parse_KV_file('../.flutter-plugins') 50 | plugin_pods.map { |p| 51 | symlink = File.join('.symlinks', 'plugins', p[:name]) 52 | File.symlink(p[:path], symlink) 53 | pod p[:name], :path => File.join(symlink, 'ios') 54 | } 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['ENABLE_BITCODE'] = 'NO' 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nodebb 2 | description: Flutter Dev Forum Flutter Client 3 | 4 | dependencies: 5 | flutter: 6 | sdk: flutter 7 | 8 | # The following adds the Cupertino Icons font to your application. 9 | # Use with the CupertinoIcons class for iOS style icons. 10 | cupertino_icons: 0.1.0 11 | flutter_markdown: 0.1.5 12 | string_scanner: 1.0.2 13 | flutter_wills: 14 | path: '../flutter_wills' 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | shared_preferences: 0.4.1 20 | build_runner: 0.8.9 21 | source_gen: 0.7.4+3 22 | quiver: '0.29.0+1' 23 | flutter_wills_gen: 24 | path: '../flutter_wills_gen' 25 | 26 | 27 | # For information on the generic Dart part of this file, see the 28 | # following page: https://www.dartlang.org/tools/pub/pubspec 29 | 30 | # The following section is specific to Flutter. 31 | flutter: 32 | 33 | # The following line ensures that the Material Icons font is 34 | # included with your application, so that you can use the icons in 35 | # the material Icons class. 36 | uses-material-design: true 37 | 38 | # To add assets to your application, add an assets section, like this: 39 | # assets: 40 | # - images/a_dot_burr.jpeg 41 | # - images/a_dot_ham.jpeg 42 | 43 | # An image asset can refer to one or more resolution-specific "variants", see 44 | # https://flutter.io/assets-and-images/#resolution-aware. 45 | 46 | # For details regarding adding assets from package dependencies, see 47 | # https://flutter.io/assets-and-images/#from-packages 48 | 49 | # To add custom fonts to your application, add a fonts section here, 50 | # in this "flutter" section. Each entry in this list should have a 51 | # "family" key with the font family name, and a "fonts" key with a 52 | # list giving the asset and other descriptors for the font. For 53 | # example: 54 | # fonts: 55 | # - family: Schyler 56 | # fonts: 57 | # - asset: fonts/Schyler-Regular.ttf 58 | # - asset: fonts/Schyler-Italic.ttf 59 | # style: italic 60 | # - family: Trajan Pro 61 | # fonts: 62 | # - asset: fonts/TrajanPro.ttf 63 | # - asset: fonts/TrajanPro_Bold.ttf 64 | # weight: 700 65 | # 66 | # For details regarding fonts from package dependencies, 67 | # see https://flutter.io/custom-fonts/#from-packages 68 | assets: 69 | - assets/images/flutter_cover.jpg 70 | - assets/images/flutter_avatar.png 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /nodebb.iml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /lib/widgets/animation_page_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AnimationPageRoute extends MaterialPageRoute { 4 | /// 5 | Tween slideTween; 6 | 7 | Tween fadeTween; 8 | 9 | Tween scaleTween; 10 | 11 | Tween rotationTween; 12 | 13 | AnimationPageRoute({ 14 | WidgetBuilder builder, 15 | this.slideTween, 16 | this.fadeTween, 17 | this.scaleTween, 18 | this.rotationTween, 19 | RouteSettings settings, 20 | bool maintainState: true, 21 | bool fullscreenDialog: false, 22 | }) : super( 23 | builder: builder, 24 | settings: settings, 25 | maintainState: maintainState, 26 | fullscreenDialog: fullscreenDialog); 27 | 28 | @override 29 | Widget buildTransitions(BuildContext context, Animation animation, 30 | Animation secondaryAnimation, Widget child) { 31 | Widget widget = new SlideTransition( 32 | child: new FadeTransition( 33 | child: new ScaleTransition( 34 | child: new RotationTransition( 35 | child: child, 36 | turns: getRotationAnimation(animation), 37 | ), 38 | scale: getScaleAnimation(animation), 39 | ), 40 | opacity: getFadeAnimation(animation), 41 | ), 42 | position: _getSlideAnimation(animation), 43 | ); 44 | return widget; 45 | } 46 | 47 | Animation getRotationAnimation(Animation animation) { 48 | if(rotationTween == null){ 49 | rotationTween = new Tween(begin: 1.0, end: 1.0); 50 | } 51 | return rotationTween.animate(animation); 52 | } 53 | 54 | Animation getScaleAnimation(Animation animation) { 55 | if(scaleTween == null){ 56 | scaleTween = new Tween(begin: 1.0, end: 1.0); 57 | } 58 | return scaleTween.animate(animation); 59 | } 60 | 61 | Animation getFadeAnimation(Animation animation) { 62 | if (fadeTween == null) { 63 | fadeTween = new Tween(begin: 1.0, end: 1.0); 64 | } 65 | return fadeTween.animate(new CurvedAnimation( 66 | parent: animation, 67 | curve: Curves.easeIn, 68 | )); 69 | } 70 | 71 | Animation _getSlideAnimation(Animation animation) { 72 | if (slideTween == null) { 73 | slideTween = new Tween( 74 | begin: new Offset(0.0, 0.0), 75 | end: Offset.zero, 76 | ); 77 | } 78 | return slideTween.animate(new CurvedAnimation( 79 | parent: animation, // The route's linear 0.0 - 1.0 animation. 80 | curve: Curves.fastOutSlowIn, 81 | )); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:nodebb/errors/errors.dart'; 5 | 6 | bool isEmpty(val) { 7 | return val == null || val == ''; 8 | } 9 | 10 | int convertToInteger([num = -1]) { 11 | if(null == num) return 0; 12 | return num.runtimeType == int ? num : int.parse(num); 13 | } 14 | 15 | Color parseColorFromStr([String colorStr = '#000000']) { 16 | int colorVal = int.parse('ff' + colorStr.substring(1, colorStr.length), radix: 16); 17 | return new Color(colorVal); 18 | } 19 | 20 | String encodeUriQuery(Map query) { 21 | if(query == null) return ''; 22 | StringBuffer sb = new StringBuffer(); 23 | query.forEach((key, value) { 24 | if(sb.length > 0) sb.write('&'); 25 | sb.write(Uri.encodeQueryComponent(key)); 26 | sb.write('='); 27 | sb.write(Uri.encodeQueryComponent(value)); 28 | }); 29 | return sb.toString(); 30 | } 31 | 32 | //https://github.com/dartist/express/blob/master/lib/utils.dart 33 | Map pathMatcher(String routePath, String matchesPath){ 34 | Map params = {}; 35 | if (routePath == matchesPath) return params; 36 | List pathComponents = matchesPath.split("/"); 37 | List routeComponents = routePath.split("/"); 38 | if (pathComponents.length == routeComponents.length){ 39 | for (int i=0; i matches = exp.allMatches(res); 57 | var json; 58 | if(matches.first != null) { 59 | json = { 60 | matches.first.group(1): matches.first.group(2) 61 | }; 62 | } 63 | return json; 64 | } 65 | 66 | _throwException(reason) { 67 | switch(reason) { 68 | case 'invalid-login-credentials': 69 | case 'invalid-username-or-password': 70 | throw new NodeBBLoginFailException(reason); 71 | break; 72 | } 73 | } 74 | 75 | dynamic decodeJSON(String data) { 76 | if(data.length == 0) return {}; 77 | var json; 78 | try { 79 | json = jsonDecode(data); 80 | } catch(e) { 81 | json = handleNodeBBResponse(data); 82 | if(json == null) { 83 | throw e; 84 | } 85 | if(json is Map && json['error'] != null) { 86 | _throwException(json['error']); 87 | } 88 | } 89 | return json; 90 | } 91 | 92 | void noop() {} -------------------------------------------------------------------------------- /lib/views/recent_views_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/enums/enums.dart'; 5 | import 'package:nodebb/models/models.dart'; 6 | import 'package:nodebb/mutations/mutations.dart'; 7 | import 'package:nodebb/services/remote_service.dart'; 8 | import 'package:nodebb/views/base.dart'; 9 | import 'package:nodebb/widgets/builders.dart'; 10 | import 'package:nodebb/widgets/widgets.dart'; 11 | 12 | class RecentViewsPage extends BaseReactivePage { 13 | RecentViewsPage({Key key, routeParams}) : super(key: key, routeParams: routeParams); 14 | 15 | @override 16 | _RegisterViewPageState createState() => new _RegisterViewPageState(); 17 | } 18 | 19 | class _RegisterViewPageState extends BaseReactiveState { 20 | 21 | List topics = []; 22 | 23 | List posts = []; 24 | 25 | bool initial = false; 26 | 27 | ReactiveProp status = new ReactiveProp(); 28 | 29 | _fetchContent() { 30 | status.self = RequestStatus.PENDING; 31 | RemoteService.getInstance().fetchTopicsCollection($store.state.recentViews.reversed.toList()).then((List data) { 32 | data = data ?? []; 33 | if(data.isNotEmpty) { 34 | List topicsFromData = data.cast(); 35 | for (var data in topicsFromData) { 36 | topics.add(new Topic.fromJSON(data)); 37 | List ps = data['posts']; 38 | posts.add(new Post.fromJSON(ps.cast()[0])); 39 | } 40 | status.self = RequestStatus.SUCCESS; 41 | } else { 42 | status.self = RequestStatus.EMPTY; 43 | } 44 | }).catchError((err) { 45 | status.self = RequestStatus.ERROR; 46 | }); 47 | } 48 | 49 | @override 50 | Widget render(BuildContext context) { 51 | if(!initial) { 52 | _fetchContent(); 53 | initial = true; 54 | } 55 | return new Scaffold( 56 | appBar: new AppBar(title: const Text('最近浏览'),), 57 | body: new Container( 58 | child: buildPendingBody(status: status.self, bodyBuilder: () { 59 | return new ListView.builder( 60 | //padding: const EdgeInsets.all(16.0), 61 | itemCount: topics.length, 62 | itemBuilder: (BuildContext context, int index) { 63 | return new TopicsSummaryItem( 64 | topic: topics[index], 65 | post: posts[index], 66 | onTap: () { 67 | //$store.commit(new AddRecentViewTopic(topics[index].tid)); 68 | Navigator.of(context).pushNamed('/topic/${posts[index].tid}'); 69 | }, 70 | ); 71 | }, 72 | ); 73 | }, onRetry: () { 74 | _fetchContent(); 75 | }), 76 | ), 77 | ); 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /lib/models/unread_info.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of unread_info; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$UnreadInfo extends UnreadInfo { 10 | int _unreadTopicCount; 11 | int get unreadTopicCount { 12 | $observe('unreadTopicCount'); 13 | return _unreadTopicCount; 14 | } 15 | 16 | set unreadTopicCount(int unreadTopicCount) { 17 | if (unreadTopicCount == _unreadTopicCount) return; 18 | _unreadTopicCount = unreadTopicCount; 19 | $notify('unreadTopicCount'); 20 | } 21 | 22 | int _unreadNewTopicCount; 23 | int get unreadNewTopicCount { 24 | $observe('unreadNewTopicCount'); 25 | return _unreadNewTopicCount; 26 | } 27 | 28 | set unreadNewTopicCount(int unreadNewTopicCount) { 29 | if (unreadNewTopicCount == _unreadNewTopicCount) return; 30 | _unreadNewTopicCount = unreadNewTopicCount; 31 | $notify('unreadNewTopicCount'); 32 | } 33 | 34 | int _unreadChatCount; 35 | int get unreadChatCount { 36 | $observe('unreadChatCount'); 37 | return _unreadChatCount; 38 | } 39 | 40 | set unreadChatCount(int unreadChatCount) { 41 | if (unreadChatCount == _unreadChatCount) return; 42 | _unreadChatCount = unreadChatCount; 43 | $notify('unreadChatCount'); 44 | } 45 | 46 | int _unreadWatchedTopicCount; 47 | int get unreadWatchedTopicCount { 48 | $observe('unreadWatchedTopicCount'); 49 | return _unreadWatchedTopicCount; 50 | } 51 | 52 | set unreadWatchedTopicCount(int unreadWatchedTopicCount) { 53 | if (unreadWatchedTopicCount == _unreadWatchedTopicCount) return; 54 | _unreadWatchedTopicCount = unreadWatchedTopicCount; 55 | $notify('unreadWatchedTopicCount'); 56 | } 57 | 58 | int _unreadNotificationCount; 59 | int get unreadNotificationCount { 60 | $observe('unreadNotificationCount'); 61 | return _unreadNotificationCount; 62 | } 63 | 64 | set unreadNotificationCount(int unreadNotificationCount) { 65 | if (unreadNotificationCount == _unreadNotificationCount) return; 66 | _unreadNotificationCount = unreadNotificationCount; 67 | $notify('unreadNotificationCount'); 68 | } 69 | 70 | _$UnreadInfo.$() : super.$(); 71 | factory _$UnreadInfo({ 72 | int unreadTopicCount, 73 | int unreadNewTopicCount, 74 | int unreadChatCount, 75 | int unreadWatchedTopicCount, 76 | int unreadNotificationCount, 77 | }) { 78 | return new _$UnreadInfo.$() 79 | .._unreadTopicCount = unreadTopicCount ?? 0 80 | .._unreadNewTopicCount = unreadNewTopicCount ?? 0 81 | .._unreadChatCount = unreadChatCount ?? 0 82 | .._unreadWatchedTopicCount = unreadWatchedTopicCount ?? 0 83 | .._unreadNotificationCount = unreadNotificationCount ?? 0; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/views/base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_wills/flutter_wills.dart'; 5 | import 'package:nodebb/enums/enums.dart'; 6 | import 'package:nodebb/errors/errors.dart'; 7 | import 'package:nodebb/models/models.dart'; 8 | 9 | abstract class BasePage extends StatefulWidget { 10 | final Map routeParams; 11 | BasePage({key, routeParams}) 12 | : routeParams = routeParams, 13 | super(key: key); 14 | } 15 | 16 | abstract class BaseReactivePage extends BaseReactiveWidget { 17 | final Map routeParams; 18 | BaseReactivePage({key, routeParams}) 19 | : routeParams = routeParams, 20 | super(key: key); 21 | } 22 | 23 | abstract class BaseMixin { 24 | get context; 25 | Store get $store; 26 | $confirm(String content, { 27 | onConfirm, 28 | onCancel, 29 | String onConfirmBtnTxt = '确定', 30 | String onCancelBtnTxt = '取消', 31 | DialogMode mode = DialogMode.CONFIRM 32 | }) { 33 | List children = new List(); 34 | if(mode == DialogMode.CONFIRM) { 35 | children.add(new FlatButton( 36 | child: new Text(onCancelBtnTxt), 37 | onPressed: () async { 38 | if(onCancel != null) { 39 | await onCancel(); 40 | } 41 | Navigator.pop(context, false); 42 | } 43 | )); 44 | } 45 | children.add(new FlatButton( 46 | child: new Text(onConfirmBtnTxt), 47 | onPressed: () async { 48 | if(onConfirm != null) { 49 | await onConfirm(); 50 | } 51 | Navigator.pop(context, true); 52 | } 53 | )); 54 | return showDialog( 55 | context: context, 56 | builder: (context) { 57 | return new AlertDialog( 58 | content: new Text(content), 59 | actions: children, 60 | ); 61 | } 62 | ); 63 | } 64 | 65 | $alert(String content, { 66 | onConfirm, 67 | String onConfirmBtnTxt = '确定' 68 | }) { 69 | return $confirm(content, onConfirm: onConfirm, onConfirmBtnTxt: onConfirmBtnTxt, mode: DialogMode.ALERT); 70 | } 71 | 72 | $checkLogin() { 73 | Completer completer = new Completer(); 74 | if($store.state.activeUser == null) { 75 | $confirm('请登录后操作~', onConfirm: () { 76 | new Timer(const Duration(milliseconds: 300), () { 77 | Navigator.of(context).pushNamed('/login'); 78 | }); 79 | }, onConfirmBtnTxt: '登录'); 80 | completer.completeError(new ApplicationException('Not logged in')); 81 | } else { 82 | completer.complete($store.state.activeUser); 83 | } 84 | return completer.future; 85 | } 86 | } 87 | 88 | abstract class BaseReactiveState extends ReactiveState, W> with BaseMixin {} 89 | 90 | abstract class BaseReactiveWidget extends ReactiveWidget { 91 | 92 | BaseReactiveWidget({key}): super(key: key); 93 | 94 | @override 95 | BaseReactiveState createState(); 96 | 97 | 98 | } -------------------------------------------------------------------------------- /lib/services/cookie_jar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class CookieRecord { 4 | 5 | DateTime createTime; 6 | 7 | Cookie cookie; 8 | 9 | CookieRecord({this.cookie, this.createTime}) { 10 | if(createTime == null) { 11 | createTime = new DateTime.now(); 12 | } 13 | } 14 | 15 | } 16 | 17 | class CookieJar { 18 | Map> store = new Map(); 19 | 20 | CookieJar(); 21 | 22 | add(Cookie cookie) { 23 | 24 | // if(cookie.domain != null) { 25 | // cookie.domain = 26 | // cookie.domain.startsWith('.') ? cookie.domain.substring(1) : cookie.domain; 27 | // } 28 | 29 | if(store[cookie.domain] == null) { 30 | store[cookie.domain] = new List(); 31 | } 32 | 33 | List records = store[cookie.domain]; 34 | for(int i = 0; i < records.length; i++) { 35 | if(records[i].cookie.name == cookie.name 36 | && records[i].cookie.path == cookie.path 37 | && records[i].cookie.domain == cookie.domain 38 | && records[i].cookie.secure == cookie.secure) { 39 | records[i].cookie = cookie; 40 | records[i].createTime = new DateTime.now(); 41 | return; 42 | } 43 | } 44 | records.add(new CookieRecord(cookie: cookie)); 45 | 46 | } 47 | 48 | List getCookies(Uri uri) { 49 | List records = new List(); 50 | store.keys.forEach((domain) { 51 | if(domain == uri.host) { 52 | records.addAll(store[domain]); 53 | } 54 | //var d = domain.startsWith('.') ? domain : '.' + domain; 55 | if(domain.startsWith('.') && uri.host.endsWith(domain)) { 56 | records.addAll(store[domain]); 57 | } 58 | }); 59 | List cookies = new List(); 60 | for(CookieRecord record in records) { 61 | if(record.cookie.maxAge != null) { 62 | if(record.createTime.add(new Duration(seconds: record.cookie.maxAge)) 63 | .compareTo(new DateTime.now()) <= 0) { 64 | continue; 65 | } 66 | } 67 | if(record.cookie.maxAge == null && record.cookie.expires != null) { 68 | if(record.cookie.expires.compareTo(new DateTime.now()) <= 0) { 69 | continue; 70 | } 71 | } 72 | if((record.cookie.secure && (uri.scheme != 'https' && uri.scheme != 'wss')) 73 | || (!record.cookie.secure && (uri.scheme != 'http' && uri.scheme != 'ws'))) { 74 | continue; 75 | } 76 | if(!uri.path.startsWith(record.cookie.path)) { 77 | continue; 78 | } 79 | cookies.add(record.cookie); 80 | } 81 | return cookies.length > 0 ? cookies : null; 82 | } 83 | 84 | String serializeCookies(List cookies) { 85 | if(cookies == null) return null; 86 | StringBuffer sb = new StringBuffer(); 87 | cookies.forEach((cookie) { 88 | if(sb.length > 0) { 89 | sb.write(';'); 90 | } 91 | sb.write('${cookie.name}=${cookie.value}'); 92 | }); 93 | return sb.toString(); 94 | } 95 | 96 | void clear() { 97 | store.clear(); 98 | } 99 | } -------------------------------------------------------------------------------- /lib/views/user_info_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/actions/actions.dart'; 5 | import 'package:nodebb/enums/enums.dart'; 6 | import 'package:nodebb/models/models.dart'; 7 | import 'package:nodebb/services/io_service.dart'; 8 | import 'package:nodebb/services/remote_service.dart'; 9 | import 'package:nodebb/views/base.dart'; 10 | import 'package:nodebb/widgets/builders.dart'; 11 | 12 | class UserInfoPage extends BaseReactivePage { 13 | UserInfoPage({Key key, routeParams}) : super(key: key, routeParams: routeParams); 14 | 15 | @override 16 | _UserInfoPageState createState() => new _UserInfoPageState(); 17 | } 18 | 19 | class _UserInfoPageState extends BaseReactiveState { 20 | 21 | ReactiveProp status = new ReactiveProp(); 22 | 23 | ReactiveProp user = new ReactiveProp(); 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | this._fetchContent(); 29 | } 30 | 31 | _fetchContent() { 32 | status.self = RequestStatus.PENDING; 33 | RemoteService.getInstance().fetchUserInfo(int.parse(widget.routeParams['uid'])).then((Map json) { 34 | try { 35 | user.self = new User.fromJSON(json); 36 | status.self = RequestStatus.SUCCESS; 37 | } catch(err) { 38 | status.self = RequestStatus.ERROR; 39 | } 40 | }).catchError((err) { 41 | status.self = RequestStatus.ERROR; 42 | }); 43 | } 44 | 45 | _startChat(User user) async { 46 | var existsRoomId = await IOService.getInstance().hasPrivateChat(user.uid); 47 | if(existsRoomId == -1) { 48 | var newRoomId = await IOService.getInstance().newRoom(user.uid); 49 | await $store.dispatch(new FetchRecentChatAction()); 50 | Navigator.of(context).pushNamed('/chat/$newRoomId'); 51 | } else if(!$store.state.rooms.containsKey(existsRoomId)) { 52 | await $store.dispatch(new FetchRecentChatAction()); 53 | Navigator.of(context).pushNamed('/chat/$existsRoomId'); 54 | } else { 55 | Navigator.of(context).pushNamed('/chat/$existsRoomId'); 56 | } 57 | } 58 | 59 | @override 60 | Widget render(BuildContext context) { 61 | String userName = ($store.state.shareStorage['user_info_page'] as User).userName; 62 | return new Scaffold( 63 | appBar: new AppBar(title: new Text("$userName")), 64 | body: buildPendingBody(status: status.self, bodyBuilder: () { 65 | return new Stack( 66 | children: [ 67 | new Column( 68 | children: [ 69 | buildCover(user.self), 70 | buildAvatar(user.self), 71 | buildUserInfo(user.self) 72 | ], 73 | ), 74 | new Positioned( 75 | bottom: 8.0, 76 | right: 16.0, 77 | left: 16.0, 78 | child: new MaterialButton( 79 | height: 48.0, 80 | color: user.self.uid == $store.state.activeUser?.uid ? Colors.grey : Colors.green, 81 | onPressed: () { 82 | $checkLogin().then((_) { 83 | if(user.self.uid == $store.state.activeUser?.uid) return; 84 | _startChat(user.self); 85 | }); 86 | }, 87 | child: new Text('打招呼', style: const TextStyle(color: Colors.white, fontSize: 18.0),), 88 | ), 89 | ) 90 | ], 91 | ); 92 | }) 93 | ); 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /lib/views/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nodebb/models/models.dart'; 3 | import 'package:nodebb/views/base.dart'; 4 | import 'package:nodebb/views/messages_fragment.dart'; 5 | import 'package:nodebb/views/personal_fragment.dart'; 6 | import 'package:nodebb/views/topics_fragment.dart'; 7 | 8 | 9 | class HomePage extends BaseReactivePage { 10 | HomePage({Key key, this.title}) : super(key: key); 11 | 12 | final String title; 13 | 14 | @override 15 | _HomePageState createState() => new _HomePageState(); 16 | } 17 | 18 | class _HomePageState extends BaseReactiveState with TickerProviderStateMixin { 19 | 20 | int _currentIndex = 0; 21 | 22 | TabController _controller; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | this._controller = new TabController(initialIndex: this._currentIndex, length: 3, vsync: this); 28 | this._controller.addListener(() { 29 | this.setState(() { 30 | this._currentIndex = this._controller.index; 31 | }); 32 | }); 33 | } 34 | 35 | Widget _buildBottomNavBarIcon(IconData icon, [bool marked = false]) { 36 | var children = []; 37 | if(marked) { 38 | children.add(new Positioned( 39 | right: -4.0, 40 | top: -4.0, 41 | child: marked ? new Container( 42 | width: 8.0, 43 | height: 8.0, 44 | decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle), 45 | ) : null, 46 | )); 47 | } 48 | children.add(new Icon(icon)); 49 | return new Stack( 50 | overflow: Overflow.visible, 51 | children: children 52 | ); 53 | } 54 | 55 | 56 | BottomNavigationBar _buildBottomNavBar() { 57 | UnreadInfo info = $store.state.unreadInfo; 58 | return new BottomNavigationBar( 59 | currentIndex: this._currentIndex, 60 | items: [ 61 | new BottomNavigationBarItem( 62 | icon: _buildBottomNavBarIcon(Icons.explore, $store.state.notification.newTopic), 63 | title: const Text('话题', style: const TextStyle(fontSize: 12.0)) 64 | ), 65 | new BottomNavigationBarItem( 66 | icon: _buildBottomNavBarIcon(Icons.message, info.unreadChatCount > 0), 67 | title: const Text('消息', style: const TextStyle(fontSize: 12.0)) 68 | ), 69 | new BottomNavigationBarItem( 70 | icon: _buildBottomNavBarIcon(Icons.person, false), 71 | title: const Text('个人', style: const TextStyle(fontSize: 12.0)) 72 | ) 73 | ], 74 | onTap: (int index) { 75 | this._controller.animateTo(index); 76 | } 77 | ); 78 | } 79 | 80 | 81 | @override 82 | Widget render(BuildContext context) { 83 | return new Scaffold( 84 | appBar: new AppBar( 85 | title: new Text(widget.title) 86 | ), 87 | body: new NotificationListener( 88 | onNotification: (ScrollNotification) { 89 | //print(ScrollNotification.toString()); 90 | }, 91 | child: new TabBarView( 92 | controller: this._controller, 93 | children: [ 94 | new TopicsFragment(key: new ValueKey('TopicsFragment')), 95 | new MessagesFragment(key: new ValueKey('MessagesFragment')), 96 | new PersonalFragment(key: new ValueKey('PersonalFragment')) 97 | ] 98 | ) 99 | ), 100 | bottomNavigationBar: this._buildBottomNavBar(), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/views/personal_fragment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nodebb/actions/actions.dart'; 3 | import 'package:nodebb/views/base.dart'; 4 | import 'package:nodebb/widgets/builders.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | class PersonalFragment extends BaseReactiveWidget { 8 | PersonalFragment({Key key}) : super(key: key); 9 | 10 | @override 11 | BaseReactiveState createState() { 12 | return new _PersonalFragmentState(); 13 | } 14 | } 15 | 16 | class _PersonalFragmentState extends BaseReactiveState { 17 | 18 | _resetUser() { 19 | SharedPreferences.getInstance().then((prefs) { 20 | prefs.setString('username', ''); 21 | prefs.setString('password', ''); 22 | }); 23 | } 24 | 25 | _buildSelectItem({title, icon, divider = true, onTap}) { 26 | return new InkWell( 27 | onTap: onTap, 28 | child: new Container( 29 | padding: const EdgeInsets.symmetric(horizontal: 24.0), 30 | child: new Container( 31 | decoration: divider ? buildBottomDividerDecoration(context) : null, 32 | padding: const EdgeInsets.symmetric(vertical: 20.0), 33 | child: new Row( 34 | children: [ 35 | new Expanded( 36 | child: new Text(title, style: const TextStyle(fontSize: 16.0),) 37 | ), 38 | new Icon(icon) 39 | ], 40 | ), 41 | ) 42 | ) 43 | ); 44 | } 45 | 46 | _buildLogoutButton() { 47 | if($store.state.activeUser != null) { 48 | return new Container( 49 | padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), 50 | child: new MaterialButton( 51 | height: 44.0, 52 | color: Colors.red, 53 | textColor: Colors.white, 54 | onPressed: () { 55 | $confirm('确认要退出?', onConfirm: () { 56 | $store.dispatch(new LogoutAction()).then((_) { 57 | _resetUser(); 58 | }); 59 | }); 60 | }, 61 | child: new Text('退出', style: const TextStyle(fontSize: 18.0),), 62 | ), 63 | ); 64 | } else { 65 | return new Container(); 66 | } 67 | } 68 | 69 | @override 70 | Widget render(BuildContext context) { 71 | return new ListView( 72 | children: [ 73 | buildCover($store.state.activeUser), 74 | buildAvatar($store.state.activeUser), 75 | buildUserInfo($store.state.activeUser), 76 | _buildSelectItem(title: '我的收藏', icon: Icons.star, onTap: () { 77 | $checkLogin().then((_) { 78 | Navigator.of(context).pushNamed('/bookmarks'); 79 | }); 80 | }), 81 | _buildSelectItem(title: '最近浏览', icon: Icons.remove_red_eye, onTap: () { 82 | $checkLogin().then((_) { 83 | Navigator.of(context).pushNamed('/recent_views'); 84 | }); 85 | }), 86 | // _buildSelectItem(title: '设置', icon: Icons.settings), 87 | _buildSelectItem(title: '关于', icon: Icons.group, onTap: () { 88 | showAboutDialog( 89 | context: context, 90 | applicationName: 'Flutter中文论坛客户端', 91 | applicationVersion: '0.0.1', 92 | applicationIcon: new SizedBox( 93 | width: 48.0, 94 | height: 48.0, 95 | child: new Image.asset('assets/images/flutter_avatar.png') 96 | ) 97 | ); 98 | }), 99 | _buildLogoutButton() 100 | ], 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /lib/actions/actions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/models/models.dart'; 5 | import 'package:nodebb/mutations/mutations.dart'; 6 | import 'package:nodebb/services/io_service.dart'; 7 | import 'package:nodebb/services/remote_service.dart'; 8 | 9 | abstract class BaseAction extends WillsAction, S> {} 10 | 11 | abstract class BaseRunLastAction extends WillsRunLastAction, S> {} 12 | 13 | abstract class BaseRunUniqueAction extends WillsRunUniqueAction, S> {} 14 | 15 | abstract class BaseRunQueueAction extends WillsRunQueueAction, S> {} 16 | 17 | class FetchTopicsAction extends BaseRunLastAction { 18 | 19 | int start; 20 | 21 | int count; 22 | 23 | bool clearBefore; 24 | 25 | FetchTopicsAction({this.start = 0, this.count = 20, this.clearBefore = false}); 26 | 27 | @override 28 | Stream exec() async* { 29 | var data; 30 | yield data = await RemoteService.getInstance().fetchTopics(start: start, count: count); 31 | yield data; 32 | List topicsFromData = data['topics'] ?? []; 33 | var topics = new List(); 34 | for(var topic in topicsFromData) { 35 | topics.add(new Topic.fromJSON(topic)); 36 | } 37 | if(clearBefore) { 38 | $store.commit(new ClearTopicsMutation()); 39 | } 40 | $store.commit(new AddTopicsMutation(topics)); 41 | yield topics; 42 | } 43 | 44 | } 45 | 46 | 47 | class LoginAction extends BaseRunUniqueAction { 48 | 49 | String username; 50 | 51 | String password; 52 | 53 | LoginAction(this.username, this.password); 54 | 55 | @override 56 | Stream exec() async* { 57 | var data; 58 | yield data = await RemoteService.getInstance().doLogin(username, password); 59 | yield data; 60 | User user = new User.fromJSON(data); 61 | $store.commit(new SetActiveUserMutation(user)); 62 | yield user; 63 | } 64 | } 65 | 66 | class LogoutAction extends BaseRunUniqueAction { 67 | 68 | @override 69 | Stream exec() async* { 70 | yield await RemoteService.getInstance().doLogout(); 71 | yield null; 72 | $store.commit(new SetActiveUserMutation(null)); 73 | } 74 | 75 | } 76 | 77 | class FetchUnreadInfoAction extends BaseRunLastAction { 78 | 79 | @override 80 | Stream exec() async* { 81 | UnreadInfo info; 82 | yield info = await IOService.getInstance().getUserUnreadCounts(); 83 | yield info; 84 | $store.commit(new SetUnreadInfoMutation(info)); 85 | } 86 | 87 | } 88 | 89 | class FetchRecentChatAction extends BaseRunLastAction { 90 | 91 | @override 92 | Stream exec() async* { 93 | Map data; 94 | yield data = await IOService.getInstance().getRecentChat(uid: $store.state.activeUser.uid, after: 0); 95 | yield data; 96 | //nextStart = data['nextStart']; 97 | $store.commit(new AddRoomsMutation(data['rooms'])); 98 | } 99 | 100 | } 101 | 102 | class PostCommentAction extends BaseAction { 103 | 104 | int topicId; 105 | 106 | int postId; 107 | 108 | String comment; 109 | 110 | PostCommentAction({this.topicId, this.postId, this.comment}); 111 | 112 | @override 113 | Stream exec() async* { 114 | Post post; 115 | yield post = await IOService.getInstance().reply(topicId: topicId, postId: postId, content: comment); 116 | yield post; 117 | } 118 | 119 | } 120 | 121 | class DoBookmarkAction extends BaseAction { 122 | 123 | int topicId; 124 | 125 | int postId; 126 | 127 | DoBookmarkAction({this.topicId, this.postId}); 128 | 129 | @override 130 | Stream exec() async* { 131 | var data; 132 | yield data = await IOService.getInstance().bookmark(topicId: topicId, postId: postId); 133 | yield data; 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /lib/models/post.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of post; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$Post extends Post { 10 | int _tid; 11 | int get tid { 12 | $observe('tid'); 13 | return _tid; 14 | } 15 | 16 | set tid(int tid) { 17 | if (tid == _tid) return; 18 | _tid = tid; 19 | $notify('tid'); 20 | } 21 | 22 | int _pid; 23 | int get pid { 24 | $observe('pid'); 25 | return _pid; 26 | } 27 | 28 | set pid(int pid) { 29 | if (pid == _pid) return; 30 | _pid = pid; 31 | $notify('pid'); 32 | } 33 | 34 | User _user; 35 | User get user { 36 | $observe('user'); 37 | return _user; 38 | } 39 | 40 | set user(User user) { 41 | if (user == _user) return; 42 | _user = user; 43 | $notify('user'); 44 | } 45 | 46 | bool _downVoted; 47 | bool get downVoted { 48 | $observe('downVoted'); 49 | return _downVoted; 50 | } 51 | 52 | set downVoted(bool downVoted) { 53 | if (downVoted == _downVoted) return; 54 | _downVoted = downVoted; 55 | $notify('downVoted'); 56 | } 57 | 58 | bool _upVoted; 59 | bool get upVoted { 60 | $observe('upVoted'); 61 | return _upVoted; 62 | } 63 | 64 | set upVoted(bool upVoted) { 65 | if (upVoted == _upVoted) return; 66 | _upVoted = upVoted; 67 | $notify('upVoted'); 68 | } 69 | 70 | int _upVotes; 71 | int get upVotes { 72 | $observe('upVotes'); 73 | return _upVotes; 74 | } 75 | 76 | set upVotes(int upVotes) { 77 | if (upVotes == _upVotes) return; 78 | _upVotes = upVotes; 79 | $notify('upVotes'); 80 | } 81 | 82 | int _downVotes; 83 | int get downVotes { 84 | $observe('downVotes'); 85 | return _downVotes; 86 | } 87 | 88 | set downVotes(int downVotes) { 89 | if (downVotes == _downVotes) return; 90 | _downVotes = downVotes; 91 | $notify('downVotes'); 92 | } 93 | 94 | int _votes; 95 | int get votes { 96 | $observe('votes'); 97 | return _votes; 98 | } 99 | 100 | set votes(int votes) { 101 | if (votes == _votes) return; 102 | _votes = votes; 103 | $notify('votes'); 104 | } 105 | 106 | bool _isMainPost; 107 | bool get isMainPost { 108 | $observe('isMainPost'); 109 | return _isMainPost; 110 | } 111 | 112 | set isMainPost(bool isMainPost) { 113 | if (isMainPost == _isMainPost) return; 114 | _isMainPost = isMainPost; 115 | $notify('isMainPost'); 116 | } 117 | 118 | DateTime _timestamp; 119 | DateTime get timestamp { 120 | $observe('timestamp'); 121 | return _timestamp; 122 | } 123 | 124 | set timestamp(DateTime timestamp) { 125 | if (timestamp == _timestamp) return; 126 | _timestamp = timestamp; 127 | $notify('timestamp'); 128 | } 129 | 130 | String _content; 131 | String get content { 132 | $observe('content'); 133 | return _content; 134 | } 135 | 136 | set content(String content) { 137 | if (content == _content) return; 138 | _content = content; 139 | $notify('content'); 140 | } 141 | 142 | _$Post.$() : super.$(); 143 | factory _$Post({ 144 | int tid, 145 | int pid, 146 | User user, 147 | bool downVoted, 148 | bool upVoted, 149 | int upVotes, 150 | int downVotes, 151 | int votes, 152 | bool isMainPost, 153 | DateTime timestamp, 154 | String content, 155 | }) { 156 | return new _$Post.$() 157 | .._tid = tid ?? 0 158 | .._pid = pid ?? 0 159 | .._user = user 160 | .._downVoted = downVoted ?? false 161 | .._upVoted = upVoted ?? false 162 | .._upVotes = upVotes ?? 0 163 | .._downVotes = downVotes ?? 0 164 | .._votes = votes ?? 0 165 | .._isMainPost = isMainPost ?? false 166 | .._timestamp = timestamp 167 | .._content = content ?? ''; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /lib/models/room.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of room; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$Room extends Room { 10 | int _owner; 11 | int get owner { 12 | $observe('owner'); 13 | return _owner; 14 | } 15 | 16 | set owner(int owner) { 17 | if (owner == _owner) return; 18 | _owner = owner; 19 | $notify('owner'); 20 | } 21 | 22 | int _roomId; 23 | int get roomId { 24 | $observe('roomId'); 25 | return _roomId; 26 | } 27 | 28 | set roomId(int roomId) { 29 | if (roomId == _roomId) return; 30 | _roomId = roomId; 31 | $notify('roomId'); 32 | } 33 | 34 | String _roomName; 35 | String get roomName { 36 | $observe('roomName'); 37 | return _roomName; 38 | } 39 | 40 | set roomName(String roomName) { 41 | if (roomName == _roomName) return; 42 | _roomName = roomName; 43 | $notify('roomName'); 44 | } 45 | 46 | ObservableList _users; 47 | ObservableList get users { 48 | $observe('users'); 49 | return _users; 50 | } 51 | 52 | set users(ObservableList users) { 53 | if (users == _users) return; 54 | _users = users; 55 | $notify('users'); 56 | } 57 | 58 | bool _groupChat; 59 | bool get groupChat { 60 | $observe('groupChat'); 61 | return _groupChat; 62 | } 63 | 64 | set groupChat(bool groupChat) { 65 | if (groupChat == _groupChat) return; 66 | _groupChat = groupChat; 67 | $notify('groupChat'); 68 | } 69 | 70 | bool _unread; 71 | bool get unread { 72 | $observe('unread'); 73 | return _unread; 74 | } 75 | 76 | set unread(bool unread) { 77 | if (unread == _unread) return; 78 | _unread = unread; 79 | $notify('unread'); 80 | } 81 | 82 | String _ownerName; 83 | String get ownerName { 84 | $observe('ownerName'); 85 | return _ownerName; 86 | } 87 | 88 | set ownerName(String ownerName) { 89 | if (ownerName == _ownerName) return; 90 | _ownerName = ownerName; 91 | $notify('ownerName'); 92 | } 93 | 94 | Teaser _teaser; 95 | Teaser get teaser { 96 | $observe('teaser'); 97 | return _teaser; 98 | } 99 | 100 | set teaser(Teaser teaser) { 101 | if (teaser == _teaser) return; 102 | _teaser = teaser; 103 | $notify('teaser'); 104 | } 105 | 106 | int _maxChatMessageLength; 107 | int get maxChatMessageLength { 108 | $observe('maxChatMessageLength'); 109 | return _maxChatMessageLength; 110 | } 111 | 112 | set maxChatMessageLength(int maxChatMessageLength) { 113 | if (maxChatMessageLength == _maxChatMessageLength) return; 114 | _maxChatMessageLength = maxChatMessageLength; 115 | $notify('maxChatMessageLength'); 116 | } 117 | 118 | ObservableList _messages; 119 | ObservableList get messages { 120 | $observe('messages'); 121 | return _messages; 122 | } 123 | 124 | set messages(ObservableList messages) { 125 | if (messages == _messages) return; 126 | _messages = messages; 127 | $notify('messages'); 128 | } 129 | 130 | _$Room.$() : super.$(); 131 | factory _$Room({ 132 | int owner, 133 | int roomId, 134 | String roomName, 135 | ObservableList users, 136 | bool groupChat, 137 | bool unread, 138 | String ownerName, 139 | Teaser teaser, 140 | int maxChatMessageLength, 141 | ObservableList messages, 142 | }) { 143 | return new _$Room.$() 144 | .._owner = owner ?? 0 145 | .._roomId = roomId ?? 0 146 | .._roomName = roomName ?? '' 147 | .._users = users 148 | .._groupChat = groupChat ?? false 149 | .._unread = unread ?? false 150 | .._ownerName = ownerName ?? '' 151 | .._teaser = teaser 152 | .._maxChatMessageLength = maxChatMessageLength ?? 0 153 | .._messages = messages; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/views/comment_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | import 'package:nodebb/actions/actions.dart'; 7 | import 'package:nodebb/views/base.dart'; 8 | 9 | class CommentPage extends BaseReactivePage { 10 | 11 | 12 | CommentPage({Key key, routeParams}) : super(key: key, routeParams: routeParams); 13 | 14 | @override 15 | BaseReactiveState createState() => new _CommentPageState(); 16 | } 17 | 18 | class _CommentPageState extends BaseReactiveState { 19 | String comment; 20 | 21 | _doPost(BuildContext context) { 22 | SystemChannels.textInput.invokeMethod('TextInput.hide'); 23 | $store.dispatch(new PostCommentAction( 24 | topicId: int.parse(widget.routeParams['tid']), 25 | postId: null, 26 | comment: comment 27 | )).then((post) { 28 | Scaffold.of(context).showSnackBar(new SnackBar( 29 | content: new Text('提交成功!'), 30 | backgroundColor: Colors.green, 31 | )); 32 | new Timer(const Duration(seconds: 1), () { 33 | Navigator.of(context).pop(post); 34 | }); 35 | }).catchError((err) { 36 | print(err); 37 | Scaffold.of(context).showSnackBar(new SnackBar( 38 | content: new Text('提交失败!请重试'), 39 | backgroundColor: Colors.red, 40 | )); 41 | }); 42 | } 43 | 44 | @override 45 | Widget render(BuildContext context) { 46 | return new Scaffold( 47 | appBar: new AppBar(title: new Text('回复:${$store.state.topics[int.parse(this.widget.routeParams['tid'])].title}')), 48 | body: new Container( 49 | margin: const EdgeInsets.all(16.0), 50 | child: new Form( 51 | child: new Column( 52 | children: [ 53 | new TextFormField( 54 | maxLength: 200, 55 | maxLines: 5, 56 | maxLengthEnforced: true, 57 | decoration: new InputDecoration(border: const OutlineInputBorder(borderSide: const BorderSide(width: 1.0))), 58 | validator: (String val) { 59 | if(val.length == 0) { 60 | return '回复不能为空'; 61 | } 62 | if(val.length > 200) { 63 | return '回复长度越界'; 64 | } 65 | if(val.length < 10) { 66 | return '回复长度太短'; 67 | } 68 | }, 69 | onSaved: (String val) { 70 | comment = val; 71 | }, 72 | ), 73 | new Builder( 74 | builder: (BuildContext context) { 75 | return new Row( 76 | children: [ 77 | new Expanded( 78 | child: new Container( 79 | padding: const EdgeInsets.only(top: 16.0), 80 | child: new MaterialButton( 81 | height: 44.0, 82 | color: Theme.of(context).primaryColor, 83 | textColor: Colors.white, 84 | onPressed: () { 85 | FormState state = Form.of(context); 86 | if(state.validate()) { 87 | state.save(); 88 | _doPost(context); 89 | } 90 | }, 91 | child: new Text('提交', style: const TextStyle(fontSize: 18.0),), 92 | ), 93 | ) 94 | ) 95 | ], 96 | ); 97 | }, 98 | ), 99 | ], 100 | ) 101 | ), 102 | ) 103 | ); 104 | } 105 | 106 | @override 107 | void dispose() { 108 | super.dispose(); 109 | SystemChannels.textInput.invokeMethod('TextInput.hide'); 110 | } 111 | 112 | 113 | } -------------------------------------------------------------------------------- /lib/socket_io/eio_parser.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:nodebb/socket_io/errors.dart'; 3 | import 'package:nodebb/utils/utils.dart' as utils; 4 | 5 | enum EngineIOPacketType { OPEN, CLOSE, PING, PONG, MESSAGE, UPGRADE, NOOP } 6 | 7 | class EngineIOPacket { 8 | 9 | EngineIOPacketType type; 10 | 11 | bool useBase64Encoder; 12 | 13 | dynamic data; 14 | 15 | EngineIOPacket({this.type, this.data = '', this.useBase64Encoder = false}); 16 | 17 | @override 18 | String toString() { 19 | return 'EngineIOPacket{type: $type, data: $data}'; 20 | } 21 | 22 | } 23 | 24 | EngineIOPacketType getEngineIOPacketType(int t) { 25 | EngineIOPacketType type; 26 | try { 27 | type = EngineIOPacketType.values[t]; 28 | } catch (e) { 29 | throw new SocketIOParseException('unsupport engineio packet type ${t}'); 30 | } 31 | return type; 32 | } 33 | 34 | class EngineIOPacketDecoder extends Converter { 35 | 36 | @override 37 | EngineIOPacket convert(input) { //todo binary support 38 | if(input is String) { 39 | String _input = input; 40 | bool _isBase64 = false; 41 | if('b' == _input[0]) { //base64 42 | _isBase64 = true; 43 | _input = _input.substring(1); 44 | } 45 | EngineIOPacketType type = getEngineIOPacketType(utils.convertToInteger(_input[0])); 46 | return new EngineIOPacket( 47 | type: type, 48 | data: _isBase64 ? UTF8.decode(BASE64.decode(_input.substring(1))) : _input.substring(1), 49 | // dataType: EngineIOPacketDataType.STRING 50 | ); 51 | } else if(input is List) { 52 | List _input = input; 53 | EngineIOPacketType type = getEngineIOPacketType(utils.convertToInteger(_input[0])); 54 | return new EngineIOPacket( 55 | type: type, 56 | data: _input.skip(1).toList()); 57 | } else { 58 | throw new SocketIOParseException("packet type: ${input.type}, its data type must be List or String, data:${input.data}"); 59 | } 60 | } 61 | 62 | @override 63 | Sink startChunkedConversion(Sink sink) { 64 | return new _EngineIOPacketDecoderSink(sink, this); 65 | } 66 | 67 | } 68 | 69 | class _EngineIOPacketDecoderSink extends ChunkedConversionSink { 70 | 71 | Sink _sink; 72 | 73 | EngineIOPacketDecoder _decoder; 74 | 75 | _EngineIOPacketDecoderSink(this._sink, this._decoder); 76 | 77 | @override 78 | void add(chunk) { 79 | this._sink.add(_decoder.convert(chunk)); 80 | } 81 | 82 | @override 83 | void close() { 84 | this._sink.close(); 85 | } 86 | 87 | } 88 | 89 | class EngineIOPacketEncoder extends Converter { 90 | 91 | @override 92 | dynamic convert(EngineIOPacket packet) { 93 | if(packet.data is String) { 94 | StringBuffer sb = new StringBuffer(); 95 | if(packet.useBase64Encoder) { 96 | sb.write('b'); 97 | } 98 | sb.write(packet.type.index); 99 | if(packet.useBase64Encoder) { 100 | sb.write(BASE64.encode(UTF8.encode(packet.data))); 101 | } else { 102 | sb.write(packet.data); 103 | } 104 | return sb.toString(); 105 | } else if(packet.data is List) { 106 | List data = packet.data; 107 | data.insert(0, packet.type.index); 108 | return data; 109 | } else { 110 | throw new SocketIOParseException("packet type: ${packet.type}, its data type must be List or String, data:${packet.data}"); 111 | } 112 | } 113 | 114 | @override 115 | Sink startChunkedConversion(Sink sink) { 116 | return new _EngineIOPacketEncoderSink(sink, this); 117 | } 118 | 119 | } 120 | 121 | class _EngineIOPacketEncoderSink extends ChunkedConversionSink { 122 | 123 | EngineIOPacketEncoder _encoder; 124 | 125 | Sink _sink; 126 | 127 | _EngineIOPacketEncoderSink(this._sink, this._encoder); 128 | 129 | @override 130 | void add(EngineIOPacket packet) { 131 | this._sink.add(_encoder.convert(packet)); 132 | } 133 | 134 | @override 135 | void close() { 136 | this._sink.close(); 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /lib/models/app_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of app_state; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$AppState extends AppState { 10 | User _activeUser; 11 | User get activeUser { 12 | $observe('activeUser'); 13 | return _activeUser; 14 | } 15 | 16 | set activeUser(User activeUser) { 17 | if (activeUser == _activeUser) return; 18 | _activeUser = activeUser; 19 | $notify('activeUser'); 20 | } 21 | 22 | UnreadInfo _unreadInfo; 23 | UnreadInfo get unreadInfo { 24 | $observe('unreadInfo'); 25 | return _unreadInfo; 26 | } 27 | 28 | set unreadInfo(UnreadInfo unreadInfo) { 29 | if (unreadInfo == _unreadInfo) return; 30 | _unreadInfo = unreadInfo; 31 | $notify('unreadInfo'); 32 | } 33 | 34 | NodeBBNotification _notification; 35 | NodeBBNotification get notification { 36 | $observe('notification'); 37 | return _notification; 38 | } 39 | 40 | set notification(NodeBBNotification notification) { 41 | if (notification == _notification) return; 42 | _notification = notification; 43 | $notify('notification'); 44 | } 45 | 46 | ObservableMap _topics; 47 | ObservableMap get topics { 48 | $observe('topics'); 49 | return _topics; 50 | } 51 | 52 | set topics(ObservableMap topics) { 53 | if (topics == _topics) return; 54 | _topics = topics; 55 | $notify('topics'); 56 | } 57 | 58 | ObservableMap _categories; 59 | ObservableMap get categories { 60 | $observe('categories'); 61 | return _categories; 62 | } 63 | 64 | set categories(ObservableMap categories) { 65 | if (categories == _categories) return; 66 | _categories = categories; 67 | $notify('categories'); 68 | } 69 | 70 | ObservableMap _users; 71 | ObservableMap get users { 72 | $observe('users'); 73 | return _users; 74 | } 75 | 76 | set users(ObservableMap users) { 77 | if (users == _users) return; 78 | _users = users; 79 | $notify('users'); 80 | } 81 | 82 | ObservableMap _rooms; 83 | ObservableMap get rooms { 84 | $observe('rooms'); 85 | return _rooms; 86 | } 87 | 88 | set rooms(ObservableMap rooms) { 89 | if (rooms == _rooms) return; 90 | _rooms = rooms; 91 | $notify('rooms'); 92 | } 93 | 94 | ObservableMap _shareStorage; 95 | ObservableMap get shareStorage { 96 | $observe('shareStorage'); 97 | return _shareStorage; 98 | } 99 | 100 | set shareStorage(ObservableMap shareStorage) { 101 | if (shareStorage == _shareStorage) return; 102 | _shareStorage = shareStorage; 103 | $notify('shareStorage'); 104 | } 105 | 106 | ObservableList _recentViews; 107 | ObservableList get recentViews { 108 | $observe('recentViews'); 109 | return _recentViews; 110 | } 111 | 112 | set recentViews(ObservableList recentViews) { 113 | if (recentViews == _recentViews) return; 114 | _recentViews = recentViews; 115 | $notify('recentViews'); 116 | } 117 | 118 | _$AppState.$() : super.$(); 119 | factory _$AppState({ 120 | User activeUser, 121 | UnreadInfo unreadInfo, 122 | NodeBBNotification notification, 123 | ObservableMap topics, 124 | ObservableMap categories, 125 | ObservableMap users, 126 | ObservableMap rooms, 127 | ObservableMap shareStorage, 128 | ObservableList recentViews, 129 | }) { 130 | return new _$AppState.$() 131 | .._activeUser = activeUser 132 | .._unreadInfo = unreadInfo 133 | .._notification = notification 134 | .._topics = topics 135 | .._categories = categories 136 | .._users = users 137 | .._rooms = rooms 138 | .._shareStorage = shareStorage 139 | .._recentViews = recentViews; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/services/remote_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:async'; 3 | import 'dart:io'; 4 | import 'package:http/http.dart'; 5 | import 'package:nodebb/errors/errors.dart'; 6 | import 'package:nodebb/services/cookie_jar.dart'; 7 | import 'package:nodebb/utils/utils.dart' as utils; 8 | 9 | 10 | class RemoteService { 11 | 12 | String _host; 13 | 14 | bool _security; 15 | 16 | final Client client = new Client(); 17 | 18 | final CookieJar jar = new CookieJar(); 19 | 20 | static final RemoteService service = new RemoteService._(); 21 | 22 | RemoteService._(); 23 | 24 | //http://dart.goodev.org/guides/language/effective-dart/design 25 | //虽然推荐用工厂构造函数 26 | //但是还是Java的比较直观 27 | static RemoteService getInstance() { 28 | return service; 29 | } 30 | 31 | setup(String host, [bool security = false]) { 32 | this._host = host; 33 | this._security = security; 34 | } 35 | 36 | Future open(Uri uri, {String method = 'get', Map body}) async { 37 | List cookies = jar.getCookies(uri) ?? []; 38 | Map headers = new Map(); 39 | headers[HttpHeaders.COOKIE] = jar.serializeCookies(cookies); 40 | Response res; 41 | if(method == 'get') { 42 | res = await client.get(uri, headers: headers); 43 | } else if(method == 'post') { 44 | res = await client.post(uri, headers: headers, body: body); 45 | } 46 | Cookie cookie; 47 | if(res.headers[HttpHeaders.SET_COOKIE] != null) { 48 | cookie = new Cookie.fromSetCookieValue(res.headers[HttpHeaders.SET_COOKIE]); 49 | } 50 | if(cookie != null) { 51 | cookie.domain = cookie.domain ?? uri.host; 52 | jar.add(cookie); 53 | } 54 | return res; 55 | } 56 | 57 | Future get(Uri uri) async { 58 | return open(uri); 59 | } 60 | 61 | Future post(Uri uri, [Map body]) async { 62 | Response res = await open(uri, method: 'post', body: body); 63 | if(res.statusCode >= 500) { 64 | throw new NodeBBServiceNotAvailableException(res.statusCode); 65 | } 66 | return res; 67 | } 68 | 69 | Uri _buildUrl(String path, [Map params]) { 70 | if(_security) { 71 | return new Uri.https(_host, path, params); 72 | } else { 73 | return new Uri.http(_host, path, params); 74 | } 75 | } 76 | 77 | Future fetchTopics({int start = 0, int count = 9}) async { 78 | var params = {'after': start.toString(), 'count': count.toString()}; 79 | Response res = await get(_buildUrl('/api/mobile/v1/topics', params)); 80 | return utils.decodeJSON(res.body); 81 | } 82 | 83 | Future fetchTopicDetail(int tid) async { 84 | Response res = await get(_buildUrl('/api/mobile/v1/topics/$tid')); 85 | return utils.decodeJSON(res.body); 86 | } 87 | 88 | Future fetchBookmarks(int uid) async { 89 | Response res = await get(_buildUrl('/api/mobile/v1/users/$uid/bookmarks')); 90 | return utils.decodeJSON(res.body); 91 | } 92 | 93 | Future fetchUsers({int start = 0, int stop = 30}) async { 94 | var params = {'start': start.toString(), 'stop': stop.toString()}; 95 | Response res = await get(_buildUrl('/api/mobile/v1/users', params)); 96 | return utils.decodeJSON(res.body); 97 | } 98 | 99 | Future fetchUserInfo(int uid) async { 100 | Response res = await get(_buildUrl('/api/mobile/v1/users/$uid')); 101 | return utils.decodeJSON(res.body); 102 | } 103 | 104 | Future doLogin(usernameOrEmail, password) async { 105 | Response res = await post(_buildUrl('/api/mobile/v1/auth/login'), 106 | {'username': usernameOrEmail, 'password': password}); 107 | return utils.decodeJSON(res.body); 108 | } 109 | 110 | Future fetchTopicsCollection(List tids) async { 111 | var params = {'tids': jsonEncode(tids)}; 112 | Response res = await get(_buildUrl('/api/mobile/v1/topics/collection', params)); 113 | return utils.decodeJSON(res.body); 114 | } 115 | 116 | Future doLogout() async { 117 | Response res = await post(_buildUrl('/api/mobile/v1/auth/logout'), {}); 118 | return utils.decodeJSON(res.body); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/models/topic.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of topic; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$Topic extends Topic { 10 | int _cid; 11 | int get cid { 12 | $observe('cid'); 13 | return _cid; 14 | } 15 | 16 | set cid(int cid) { 17 | if (cid == _cid) return; 18 | _cid = cid; 19 | $notify('cid'); 20 | } 21 | 22 | int _tid; 23 | int get tid { 24 | $observe('tid'); 25 | return _tid; 26 | } 27 | 28 | set tid(int tid) { 29 | if (tid == _tid) return; 30 | _tid = tid; 31 | $notify('tid'); 32 | } 33 | 34 | int _mainPid; 35 | int get mainPid { 36 | $observe('mainPid'); 37 | return _mainPid; 38 | } 39 | 40 | set mainPid(int mainPid) { 41 | if (mainPid == _mainPid) return; 42 | _mainPid = mainPid; 43 | $notify('mainPid'); 44 | } 45 | 46 | User _user; 47 | User get user { 48 | $observe('user'); 49 | return _user; 50 | } 51 | 52 | set user(User user) { 53 | if (user == _user) return; 54 | _user = user; 55 | $notify('user'); 56 | } 57 | 58 | bool _isOwner; 59 | bool get isOwner { 60 | $observe('isOwner'); 61 | return _isOwner; 62 | } 63 | 64 | set isOwner(bool isOwner) { 65 | if (isOwner == _isOwner) return; 66 | _isOwner = isOwner; 67 | $notify('isOwner'); 68 | } 69 | 70 | String _title; 71 | String get title { 72 | $observe('title'); 73 | return _title; 74 | } 75 | 76 | set title(String title) { 77 | if (title == _title) return; 78 | _title = title; 79 | $notify('title'); 80 | } 81 | 82 | DateTime _lastPostTime; 83 | DateTime get lastPostTime { 84 | $observe('lastPostTime'); 85 | return _lastPostTime; 86 | } 87 | 88 | set lastPostTime(DateTime lastPostTime) { 89 | if (lastPostTime == _lastPostTime) return; 90 | _lastPostTime = lastPostTime; 91 | $notify('lastPostTime'); 92 | } 93 | 94 | int _postCount; 95 | int get postCount { 96 | $observe('postCount'); 97 | return _postCount; 98 | } 99 | 100 | set postCount(int postCount) { 101 | if (postCount == _postCount) return; 102 | _postCount = postCount; 103 | $notify('postCount'); 104 | } 105 | 106 | DateTime _timestamp; 107 | DateTime get timestamp { 108 | $observe('timestamp'); 109 | return _timestamp; 110 | } 111 | 112 | set timestamp(DateTime timestamp) { 113 | if (timestamp == _timestamp) return; 114 | _timestamp = timestamp; 115 | $notify('timestamp'); 116 | } 117 | 118 | int _viewCount; 119 | int get viewCount { 120 | $observe('viewCount'); 121 | return _viewCount; 122 | } 123 | 124 | set viewCount(int viewCount) { 125 | if (viewCount == _viewCount) return; 126 | _viewCount = viewCount; 127 | $notify('viewCount'); 128 | } 129 | 130 | int _upVotes; 131 | int get upVotes { 132 | $observe('upVotes'); 133 | return _upVotes; 134 | } 135 | 136 | set upVotes(int upVotes) { 137 | if (upVotes == _upVotes) return; 138 | _upVotes = upVotes; 139 | $notify('upVotes'); 140 | } 141 | 142 | int _downVotes; 143 | int get downVotes { 144 | $observe('downVotes'); 145 | return _downVotes; 146 | } 147 | 148 | set downVotes(int downVotes) { 149 | if (downVotes == _downVotes) return; 150 | _downVotes = downVotes; 151 | $notify('downVotes'); 152 | } 153 | 154 | _$Topic.$() : super.$(); 155 | factory _$Topic({ 156 | int cid, 157 | int tid, 158 | int mainPid, 159 | User user, 160 | bool isOwner, 161 | String title, 162 | DateTime lastPostTime, 163 | int postCount, 164 | DateTime timestamp, 165 | int viewCount, 166 | int upVotes, 167 | int downVotes, 168 | }) { 169 | return new _$Topic.$() 170 | .._cid = cid ?? 0 171 | .._tid = tid ?? 0 172 | .._mainPid = mainPid ?? 0 173 | .._user = user 174 | .._isOwner = isOwner ?? false 175 | .._title = title ?? '' 176 | .._lastPostTime = lastPostTime 177 | .._postCount = postCount ?? 0 178 | .._timestamp = timestamp 179 | .._viewCount = viewCount ?? 0 180 | .._upVotes = upVotes ?? 0 181 | .._downVotes = downVotes ?? 0; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/views/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:nodebb/actions/actions.dart'; 6 | import 'package:nodebb/errors/errors.dart'; 7 | import 'package:nodebb/views/base.dart'; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | 10 | class LoginPage extends BaseReactivePage { 11 | LoginPage({Key key, routeParams}) : super(key: key, routeParams: routeParams); 12 | 13 | @override 14 | _LoginPageState createState() => new _LoginPageState(); 15 | } 16 | 17 | class _LoginPageState extends BaseReactiveState { 18 | 19 | String username; 20 | 21 | String password; 22 | 23 | _saveUser(username, password) { 24 | SharedPreferences.getInstance().then((prefs) { 25 | prefs.setString('username', username); 26 | prefs.setString('password', password); 27 | }); 28 | } 29 | 30 | _doLogin(BuildContext context) async { 31 | FocusScope.of(context).requestFocus(new FocusNode()); 32 | try { 33 | await $store.dispatch(new LoginAction(username, password)); 34 | } on NodeBBLoginFailException { 35 | Scaffold.of(context).showSnackBar(new SnackBar( 36 | content: new Text('登录失败,请检查用户名和密码是否正确') 37 | )); 38 | } catch(err) { 39 | 40 | } 41 | _saveUser(username, password); 42 | Scaffold.of(context).showSnackBar(new SnackBar( 43 | content: new Text('登录成功! ${$store.state.activeUser.userName} 欢迎回来'), 44 | backgroundColor: Colors.green, 45 | )); 46 | new Timer(const Duration(seconds: 1), () { 47 | Navigator.of(context).pop(); 48 | }); 49 | } 50 | 51 | @override 52 | Widget render(BuildContext context) { 53 | return new Scaffold( 54 | appBar: new AppBar( 55 | title: new Text('登录') 56 | ), 57 | body: new Form( 58 | autovalidate: false, 59 | child: new ListView( //只有ListView才可以在键盘弹出的情况下,内容可以滚动,Column是不行的 60 | padding: const EdgeInsets.fromLTRB(24.0, 96.0, 24.0, 0.0), 61 | children: [ 62 | new TextFormField( 63 | style: new TextStyle(fontSize: 18.0, color: Theme.of(context).textTheme.body1.color), 64 | decoration: new InputDecoration( 65 | labelText: '用户名', 66 | contentPadding: new EdgeInsets.only(bottom: 6.0), 67 | ), 68 | validator: (String val) { 69 | if(val.length == 0) { 70 | return '用户名不能为空'; 71 | } 72 | if(val.length > 12) { 73 | return '用户名长度越界'; 74 | } 75 | }, 76 | onSaved: (String val) { 77 | username = val; 78 | }, 79 | ), 80 | new TextFormField( 81 | obscureText: true, 82 | style: new TextStyle(fontSize: 18.0, color: Theme.of(context).textTheme.body1.color), 83 | decoration: new InputDecoration( 84 | labelText: '密码', 85 | contentPadding: new EdgeInsets.only(bottom: 6.0), 86 | ), 87 | validator: (String val) { 88 | if(val.length == 0) { 89 | return '密码不能为空'; 90 | } 91 | if(val.length > 24) { 92 | return '密码长度越界'; 93 | } 94 | }, 95 | onSaved: (String val) { 96 | password = val; 97 | }, 98 | ), 99 | new Builder( 100 | builder: (BuildContext context) { 101 | return new MaterialButton( 102 | height: 44.0, 103 | color: Theme.of(context).primaryColor, 104 | textColor: Colors.white, 105 | onPressed: () { 106 | FormState state = Form.of(context); 107 | if(state.validate()) { 108 | state.save(); 109 | _doLogin(context); 110 | } 111 | }, 112 | child: new Text('登录', style: const TextStyle(fontSize: 18.0),), 113 | ); 114 | }, 115 | ), 116 | ].map((child) { 117 | return new Container( 118 | padding: const EdgeInsets.symmetric(vertical: 12.0), 119 | child: child, 120 | ); 121 | }).toList() 122 | ), 123 | ), 124 | ); 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /lib/models/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of user; 4 | 5 | // ************************************************************************** 6 | // Generator: WillsGenerator 7 | // ************************************************************************** 8 | 9 | class _$User extends User { 10 | int _uid; 11 | int get uid { 12 | $observe('uid'); 13 | return _uid; 14 | } 15 | 16 | set uid(int uid) { 17 | if (uid == _uid) return; 18 | _uid = uid; 19 | $notify('uid'); 20 | } 21 | 22 | String _userName; 23 | String get userName { 24 | $observe('userName'); 25 | return _userName; 26 | } 27 | 28 | set userName(String userName) { 29 | if (userName == _userName) return; 30 | _userName = userName; 31 | $notify('userName'); 32 | } 33 | 34 | String _status; 35 | String get status { 36 | $observe('status'); 37 | return _status; 38 | } 39 | 40 | set status(String status) { 41 | if (status == _status) return; 42 | _status = status; 43 | $notify('status'); 44 | } 45 | 46 | String _picture; 47 | String get picture { 48 | $observe('picture'); 49 | return _picture; 50 | } 51 | 52 | set picture(String picture) { 53 | if (picture == _picture) return; 54 | _picture = picture; 55 | $notify('picture'); 56 | } 57 | 58 | String _cover; 59 | String get cover { 60 | $observe('cover'); 61 | return _cover; 62 | } 63 | 64 | set cover(String cover) { 65 | if (cover == _cover) return; 66 | _cover = cover; 67 | $notify('cover'); 68 | } 69 | 70 | int _followerCount; 71 | int get followerCount { 72 | $observe('followerCount'); 73 | return _followerCount; 74 | } 75 | 76 | set followerCount(int followerCount) { 77 | if (followerCount == _followerCount) return; 78 | _followerCount = followerCount; 79 | $notify('followerCount'); 80 | } 81 | 82 | int _followingCount; 83 | int get followingCount { 84 | $observe('followingCount'); 85 | return _followingCount; 86 | } 87 | 88 | set followingCount(int followingCount) { 89 | if (followingCount == _followingCount) return; 90 | _followingCount = followingCount; 91 | $notify('followingCount'); 92 | } 93 | 94 | int _reputation; 95 | int get reputation { 96 | $observe('reputation'); 97 | return _reputation; 98 | } 99 | 100 | set reputation(int reputation) { 101 | if (reputation == _reputation) return; 102 | _reputation = reputation; 103 | $notify('reputation'); 104 | } 105 | 106 | int _topicCount; 107 | int get topicCount { 108 | $observe('topicCount'); 109 | return _topicCount; 110 | } 111 | 112 | set topicCount(int topicCount) { 113 | if (topicCount == _topicCount) return; 114 | _topicCount = topicCount; 115 | $notify('topicCount'); 116 | } 117 | 118 | String _iconBgColor; 119 | String get iconBgColor { 120 | $observe('iconBgColor'); 121 | return _iconBgColor; 122 | } 123 | 124 | set iconBgColor(String iconBgColor) { 125 | if (iconBgColor == _iconBgColor) return; 126 | _iconBgColor = iconBgColor; 127 | $notify('iconBgColor'); 128 | } 129 | 130 | String _iconText; 131 | String get iconText { 132 | $observe('iconText'); 133 | return _iconText; 134 | } 135 | 136 | set iconText(String iconText) { 137 | if (iconText == _iconText) return; 138 | _iconText = iconText; 139 | $notify('iconText'); 140 | } 141 | 142 | String _signature; 143 | String get signature { 144 | $observe('signature'); 145 | return _signature; 146 | } 147 | 148 | set signature(String signature) { 149 | if (signature == _signature) return; 150 | _signature = signature; 151 | $notify('signature'); 152 | } 153 | 154 | _$User.$() : super.$(); 155 | factory _$User({ 156 | int uid, 157 | String userName, 158 | String status, 159 | String picture, 160 | String cover, 161 | int followerCount, 162 | int followingCount, 163 | int reputation, 164 | int topicCount, 165 | String iconBgColor, 166 | String iconText, 167 | String signature, 168 | }) { 169 | return new _$User.$() 170 | .._uid = uid ?? 0 171 | .._userName = userName ?? '' 172 | .._status = status ?? '' 173 | .._picture = picture ?? '' 174 | .._cover = cover ?? '' 175 | .._followerCount = followerCount ?? 0 176 | .._followingCount = followingCount ?? 0 177 | .._reputation = reputation ?? 0 178 | .._topicCount = topicCount ?? 0 179 | .._iconBgColor = iconBgColor ?? '' 180 | .._iconText = iconText ?? '' 181 | .._signature = signature ?? ''; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/socket_io/eio_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:nodebb/application/application.dart'; 3 | import 'package:nodebb/services/cookie_jar.dart'; 4 | import 'package:nodebb/socket_io/eio_socket.dart'; 5 | 6 | enum EngineIOEventType { OPEN, CLOSE, ERROR, RECEIVE_PACKET, SEND_PACKET } 7 | 8 | class EngineIOEvent { 9 | 10 | EngineIOEventType type; 11 | 12 | dynamic data; 13 | 14 | EngineIOEvent(this.type, [this.data]); 15 | 16 | @override 17 | String toString() { 18 | return '{type: $type, data: $data}'; 19 | } 20 | } 21 | 22 | class _EngineIOSocketRecord { 23 | 24 | EngineIOSocket socket; 25 | 26 | _EngineIOSocketRecord(this.socket); 27 | } 28 | 29 | class EngineIOClient { 30 | 31 | bool autoReconnect; 32 | 33 | int maxReconnectTrys; 34 | 35 | int reconnectInterval; 36 | 37 | CookieJar jar; 38 | 39 | Map sockets = new Map(); 40 | 41 | StreamController _eventController = new StreamController.broadcast(); 42 | 43 | Stream get eventStream => _eventController.stream; 44 | 45 | EngineIOClient({ 46 | this.autoReconnect = true, 47 | this.maxReconnectTrys = 3, 48 | this.reconnectInterval = 1000, 49 | this.jar 50 | }); 51 | 52 | get socketsCount => sockets.values.length; 53 | 54 | connect(String uri, [forceNew = false]) async { 55 | EngineIOSocket existsSocket = getExistsSocket(uri); 56 | if(!forceNew && existsSocket != null) return existsSocket; 57 | EngineIOSocket socket = new EngineIOSocket( 58 | uri: uri, 59 | owner: this, 60 | autoReconnect: autoReconnect, 61 | reconnectInterval: reconnectInterval, 62 | maxReconnectTrys: maxReconnectTrys 63 | ); 64 | StreamSubscription sub; 65 | sub = socket.eventStream.listen((EngineIOSocketEvent event) async { 66 | switch(event.type) { 67 | case EngineIOSocketEventType.CLOSE: 68 | _EngineIOSocketRecord record = sockets[socket.sid]; 69 | if(record == null) return; 70 | // if(autoReconnect && !record.socket.forceClose) { 71 | // if(record.reconnectTrys < maxReconnectTry) { 72 | // String _oldSid = socket.sid; 73 | // _eventController.add(new EngineIOEvent(EngineIOEventType.RECONNECT, record.socket)); 74 | // while(record.reconnectTrys < maxReconnectTry) { 75 | // try { 76 | // Application.logger.fine('socket: ${socket.sid} try reconnet ${record.reconnectTrys}'); 77 | // _eventController.add(new EngineIOEvent(EngineIOEventType.RECONNECT_ATTEMPT, record.socket)); 78 | // await record.socket.connect(); 79 | // sockets.remove(_oldSid); 80 | // return; 81 | // } catch(err) { 82 | // Application.logger.fine('socket: ${record.socket.sid} error: $err'); 83 | // record.reconnectTrys++; 84 | // await new Future.delayed(new Duration(milliseconds: reconnectInterval)); 85 | // } 86 | // } 87 | // } 88 | // _eventController.add(new EngineIOEvent(EngineIOEventType.RECONNECT_FAIL, socket)); 89 | // Application.logger.fine('socket: ${socket.sid} exceed max retry: $maxReconnectTry'); 90 | // } 91 | sockets.remove(record.socket.sid); 92 | record.socket.owner = null; 93 | sub.cancel(); 94 | _eventController.add(new EngineIOEvent(EngineIOEventType.CLOSE, record.socket)); 95 | break; 96 | case EngineIOSocketEventType.OPEN: 97 | if(!sockets.containsKey(socket.sid)) { 98 | sockets[socket.sid] = new _EngineIOSocketRecord(socket); 99 | } 100 | _eventController.add(new EngineIOEvent(EngineIOEventType.OPEN, socket)); 101 | break; 102 | case EngineIOSocketEventType.SEND: 103 | _eventController.add(new EngineIOEvent(EngineIOEventType.SEND_PACKET, event.data)); 104 | break; 105 | case EngineIOSocketEventType.RECEIVE: 106 | _eventController.add(new EngineIOEvent(EngineIOEventType.RECEIVE_PACKET, event.data)); 107 | break; 108 | case EngineIOSocketEventType.ERROR: 109 | _eventController.add(new EngineIOEvent(EngineIOEventType.ERROR, event.data)); 110 | break; 111 | default: 112 | break; 113 | } 114 | }); 115 | await socket.connect(); 116 | return socket; 117 | } 118 | 119 | getExistsSocket(uri) { 120 | for(_EngineIOSocketRecord record in sockets.values) { 121 | if(record.socket.uri == uri) { 122 | return record.socket; 123 | } 124 | } 125 | return null; 126 | } 127 | 128 | closeAll() async { 129 | for(_EngineIOSocketRecord record in sockets.values) { 130 | await record.socket.close(); 131 | } 132 | sockets.clear(); 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /lib/views/bookmarks_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_wills/flutter_wills.dart'; 4 | import 'package:nodebb/enums/enums.dart'; 5 | import 'package:nodebb/models/models.dart'; 6 | import 'package:nodebb/mutations/mutations.dart'; 7 | import 'package:nodebb/services/io_service.dart'; 8 | import 'package:nodebb/services/remote_service.dart'; 9 | import 'package:nodebb/views/base.dart'; 10 | import 'package:nodebb/widgets/builders.dart'; 11 | import 'package:nodebb/widgets/widgets.dart'; 12 | 13 | class BookmarksPage extends BaseReactivePage { 14 | BookmarksPage({Key key, Map routeParams}) : super(key: key, routeParams: routeParams); 15 | 16 | @override 17 | _BookmarksPageState createState() => new _BookmarksPageState(); 18 | } 19 | 20 | 21 | class _BookmarksPageState extends BaseReactiveState { 22 | 23 | ObservableList topics = new ObservableList(); 24 | 25 | List posts = []; 26 | 27 | bool initial = false; 28 | 29 | ReactiveProp status = new ReactiveProp(); 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | status.self = RequestStatus.PENDING; 35 | 36 | } 37 | 38 | _fetchContent() { 39 | status.self = RequestStatus.PENDING; 40 | RemoteService.getInstance().fetchBookmarks($store.state.activeUser.uid).then((List data) { 41 | data = data ?? []; 42 | if(data.isNotEmpty) { 43 | List topicsFromData = data.cast(); 44 | for (var data in topicsFromData) { 45 | topics.add(new Topic.fromJSON(data)); 46 | List ps = data['posts']; 47 | posts.add(new Post.fromJSON(ps.cast()[0])); 48 | } 49 | status.self = RequestStatus.SUCCESS; 50 | } else { 51 | status.self = RequestStatus.EMPTY; 52 | } 53 | }).catchError((_) { 54 | status.self = RequestStatus.ERROR; 55 | }); 56 | } 57 | 58 | @override 59 | Widget render(BuildContext context) { 60 | if(!initial) { 61 | _fetchContent(); 62 | initial = true; 63 | } 64 | return new Scaffold( 65 | appBar: new AppBar(title: const Text("我的收藏")), 66 | body: new Container( 67 | child: buildPendingBody(status: status.self, bodyBuilder: () { 68 | return new ListView.builder( 69 | //padding: const EdgeInsets.all(16.0), 70 | itemCount: topics.length, 71 | itemBuilder: (BuildContext context, int index) { 72 | return new TopicsSummaryItem( 73 | topic: topics[index], 74 | post: posts[index], 75 | onTap: () { 76 | $store.commit(new AddRecentViewTopic(topics[index].tid)); 77 | Navigator.of(context).pushNamed('/topic/${posts[index].tid}'); 78 | }, 79 | onLongPress: (BuildContext context, TapDownDetails details) { 80 | RenderBox box = context.findRenderObject(); 81 | //var topLeftPosition = box.localToGlobal(Offset.zero); 82 | showMenu( 83 | context: context, 84 | position: new RelativeRect.fromLTRB( 85 | details.globalPosition.dx, 86 | details.globalPosition.dy, 87 | box.size.width - details.globalPosition.dx, 88 | 0.0 89 | ), 90 | items: [ 91 | new PopupMenuItem( 92 | value: 0, 93 | child: new Text('删除'), 94 | ), 95 | ] 96 | ).then((val) { 97 | if(val == 0) { 98 | IOService.getInstance().unbookmark( 99 | topicId: topics[index].tid, 100 | postId: topics[index].mainPid 101 | ).then((_) { 102 | topics.removeAt(index); 103 | if(topics.isEmpty) { 104 | status.self = RequestStatus.EMPTY; 105 | } 106 | Scaffold.of(context).showSnackBar(new SnackBar( 107 | content: new Text('删除成功!'), 108 | backgroundColor: Colors.green, 109 | )); 110 | }).catchError((err) { 111 | Scaffold.of(context).showSnackBar(new SnackBar( 112 | content: new Text('删除失败,请重试!'), 113 | backgroundColor: Colors.red, 114 | )); 115 | }); 116 | } 117 | }); 118 | }, 119 | ); 120 | }, 121 | ); 122 | }, onRetry: () { 123 | _fetchContent(); 124 | }) 125 | ) 126 | ); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /lib/widgets/builders.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:nodebb/application/application.dart'; 5 | import 'package:nodebb/enums/enums.dart'; 6 | import 'package:nodebb/models/models.dart'; 7 | import 'package:nodebb/widgets/widgets.dart'; 8 | import 'package:nodebb/utils/utils.dart' as utils; 9 | 10 | buildBottomDividerDecoration(BuildContext context) { 11 | return new BoxDecoration( 12 | border: new Border(bottom: new BorderSide( 13 | color: Theme.of(context).dividerColor 14 | )) 15 | ); 16 | } 17 | 18 | buildCover(User user) { 19 | if(user == null || user.cover == null || user.cover.length == 0) { 20 | return new Image.asset('assets/images/flutter_cover.jpg', 21 | width: ui.window.physicalSize.width / ui.window.devicePixelRatio, 22 | height: 160.0, 23 | fit: BoxFit.cover, 24 | alignment: Alignment.center, 25 | ); 26 | } else { 27 | return new Image.network( 28 | 'http://${Application.host}${user.cover}', 29 | width: ui.window.physicalSize.width / ui.window.devicePixelRatio, 30 | height: 160.0, 31 | fit: BoxFit.cover, 32 | alignment: Alignment.topLeft, 33 | ); 34 | } 35 | } 36 | 37 | buildAvatar(User user) { 38 | var avatar; 39 | if(user == null) { 40 | avatar = new CircleAvatar( 41 | backgroundImage: new AssetImage('assets/images/flutter_avatar.png'), 42 | backgroundColor: utils.parseColorFromStr('#ffffff'), 43 | ); 44 | } else { 45 | avatar = new NodeBBAvatar( 46 | picture: user?.picture, 47 | iconText: user?.iconText, 48 | iconBgColor: user?.iconBgColor 49 | ); 50 | } 51 | return new SizedBox( 52 | height: 42.0, 53 | child: new Stack( 54 | alignment: AlignmentDirectional.center, 55 | overflow: Overflow.visible, 56 | children: [ 57 | new Positioned( 58 | top: -42.0, 59 | width: 84.0, 60 | height: 84.0, 61 | child: avatar, 62 | ) 63 | ], 64 | )); 65 | } 66 | 67 | buildTextColumn({title, content}) { 68 | return new Column( 69 | mainAxisAlignment: MainAxisAlignment.center, 70 | crossAxisAlignment: CrossAxisAlignment.center, 71 | children: [ 72 | new Text(title, style: const TextStyle(fontSize: 18.0)), 73 | new Padding(padding: const EdgeInsets.only(top: 12.0)), 74 | new Text('$content', style: const TextStyle(fontSize: 22.0, fontWeight: FontWeight.w400),) 75 | ], 76 | ); 77 | } 78 | 79 | buildUserInfo(User user) { 80 | if(user == null) { 81 | return new Container( 82 | padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), 83 | child: new Builder(builder: (BuildContext context) { 84 | return new MaterialButton( 85 | height: 44.0, 86 | color: Theme.of(context).primaryColor, 87 | textColor: Colors.white, 88 | onPressed: () { 89 | Navigator.of(context).pushNamed('/login'); 90 | }, 91 | child: new Text('立即登录', style: const TextStyle(fontSize: 18.0),), 92 | ); 93 | }) 94 | ); 95 | 96 | } else { 97 | return new Container( 98 | padding: const EdgeInsets.symmetric(horizontal: 24.0), 99 | child: new Builder(builder: (BuildContext context) { 100 | return new Container( 101 | padding: const EdgeInsets.fromLTRB(0.0, 24.0, 0.0, 12.0), 102 | decoration: buildBottomDividerDecoration(context), 103 | child: new Row( 104 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 105 | children: [ 106 | buildTextColumn(title: '声望', content: user.reputation), 107 | buildTextColumn(title: '粉丝', content: user.followerCount), 108 | buildTextColumn(title: '话题', content: user.topicCount) 109 | ], 110 | ), 111 | ); 112 | }) 113 | ); 114 | } 115 | } 116 | 117 | buildPendingBody({RequestStatus status, bodyBuilder, onRetry}) { 118 | Widget body; 119 | switch (status) { 120 | case RequestStatus.SUCCESS: 121 | body = bodyBuilder(); 122 | break; 123 | case RequestStatus.ERROR: 124 | body = new Builder( 125 | builder: (BuildContext context) { 126 | var children = [new Text('出错了!')]; 127 | if(onRetry != null) { 128 | children.add(new MaterialButton( 129 | color: Theme.of(context).primaryColor, 130 | textColor: Colors.white, 131 | onPressed: onRetry, 132 | child: new Text('重试'), 133 | )); 134 | } 135 | return new Center( 136 | child: new Row( 137 | crossAxisAlignment: CrossAxisAlignment.center, 138 | mainAxisAlignment: MainAxisAlignment.center, 139 | children: children 140 | )); 141 | } 142 | ); 143 | break; 144 | case RequestStatus.EMPTY: 145 | body = new Center(child: new Text('╮(๑•́ ₃•̀๑)╭没有内容')); 146 | break; 147 | default: 148 | body = new Center(child: new Text('加载中...')); 149 | break; 150 | } 151 | return body; 152 | } -------------------------------------------------------------------------------- /lib/views/search_user_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_wills/flutter_wills.dart'; 6 | import 'package:nodebb/enums/enums.dart'; 7 | import 'package:nodebb/models/models.dart'; 8 | import 'package:nodebb/services/io_service.dart'; 9 | import 'package:nodebb/services/remote_service.dart'; 10 | import 'package:nodebb/views/base.dart'; 11 | import 'package:nodebb/widgets/builders.dart'; 12 | import 'package:nodebb/widgets/widgets.dart'; 13 | import 'package:nodebb/utils/utils.dart' as utils; 14 | 15 | class SearchUserPage extends BaseReactivePage { 16 | 17 | SearchUserPage({Key key, routeParams}) : super(key: key, routeParams: routeParams); 18 | 19 | @override 20 | _SearchUserPageState createState() => new _SearchUserPageState(); 21 | } 22 | 23 | class _SearchUserPageState extends BaseReactiveState { 24 | 25 | ReactiveProp status = new ReactiveProp(); 26 | 27 | ObservableList users = new ObservableList(); 28 | 29 | TextEditingController _textController = new TextEditingController(); 30 | 31 | Timer _timer; 32 | 33 | SliverGridDelegate _delegate = new SliverGridDelegateWithMaxCrossAxisExtent( 34 | maxCrossAxisExtent: 90.0, 35 | mainAxisSpacing: 6.0, 36 | crossAxisSpacing: 2.0 37 | ); 38 | 39 | 40 | @override 41 | void initState() { 42 | super.initState(); 43 | _fetchContent(); 44 | _textController.addListener(this._updateContent); 45 | } 46 | 47 | _fetchContent() { 48 | status.self = RequestStatus.PENDING; 49 | RemoteService.getInstance().fetchUsers().then((results) { 50 | List jsons = results['users'] ?? []; 51 | users.clear(); 52 | jsons.forEach((data) { 53 | users.add(new User.fromJSON(data)); 54 | }); 55 | if(users.isNotEmpty) { 56 | status.self = RequestStatus.SUCCESS; 57 | } else { 58 | status.self = RequestStatus.EMPTY; 59 | } 60 | }).catchError((err) { 61 | status.self = RequestStatus.ERROR; 62 | }); 63 | } 64 | 65 | _updateContent() async { 66 | _timer?.cancel(); 67 | _timer = new Timer(const Duration(milliseconds: 200), () async { 68 | if(!utils.isEmpty(_textController.text)) { 69 | status.self = RequestStatus.PENDING; 70 | List data = await IOService.getInstance().searchUser( 71 | query: _textController.text); 72 | users.clear(); 73 | users.addAll(data); 74 | if(users.isNotEmpty) { 75 | status.self = RequestStatus.SUCCESS; 76 | } else { 77 | status.self = RequestStatus.EMPTY; 78 | } 79 | } else { 80 | _fetchContent(); 81 | } 82 | }); 83 | } 84 | 85 | _buildAvatar(User user) { 86 | return new Column( 87 | crossAxisAlignment: CrossAxisAlignment.center, 88 | children: [ 89 | new Expanded( 90 | child: new Container( 91 | width: 80.0, 92 | height: 80.0, 93 | alignment: Alignment.center, 94 | child: new NodeBBAvatar( 95 | picture: user.picture, 96 | iconBgColor: user.iconBgColor, 97 | iconText: user.iconText, 98 | ) 99 | ) 100 | ), 101 | new Padding(padding: const EdgeInsets.only(top: 2.0)), 102 | new Text(user.userName, overflow: TextOverflow.ellipsis, maxLines: 1,) 103 | ], 104 | ); 105 | } 106 | 107 | @override 108 | Widget render(BuildContext context) { 109 | return new Scaffold( 110 | appBar: new AppBar(title: new Text('查找用户')), 111 | body: new Container( 112 | padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), 113 | child: new Column( 114 | children: [ 115 | new Container( 116 | height: 56.0, 117 | child: new TextField(controller: _textController, maxLines: 1) 118 | ), 119 | new Expanded( 120 | child: buildPendingBody(status: status.self, bodyBuilder: () { 121 | return new ScrollConfiguration( 122 | behavior: new CustomScrollBehavior(), 123 | child: new GridView.builder( 124 | gridDelegate: _delegate, 125 | itemCount: users.length, 126 | itemBuilder: (BuildContext context, int index) { 127 | User user = users[index]; 128 | return new GestureDetector( 129 | child: _buildAvatar(user), 130 | onTap: () { 131 | $store.state.shareStorage['user_info_page'] = user; 132 | Navigator.of(context).pushNamed('/users/${user.uid}'); 133 | } 134 | ); 135 | }) 136 | ); 137 | }) 138 | ) 139 | ], 140 | ) 141 | ) 142 | ); 143 | } 144 | 145 | @override 146 | void dispose() { 147 | _timer?.cancel(); 148 | SystemChannels.textInput.invokeMethod('TextInput.hide'); 149 | _textController.removeListener(this._updateContent); 150 | super.dispose(); 151 | } 152 | } -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /lib/mutations/mutations.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wills/flutter_wills.dart'; 2 | import 'package:nodebb/models/models.dart'; 3 | import 'package:nodebb/models/room.dart'; 4 | 5 | abstract class BaseMutation extends WillsMutation> {} 6 | 7 | class AddUsersMutation extends BaseMutation { 8 | 9 | List users; 10 | 11 | AddUsersMutation(this.users); 12 | 13 | @override 14 | exec() { 15 | users.forEach((user) { 16 | $store.state.users[user.uid] = user; 17 | }); 18 | } 19 | 20 | } 21 | 22 | class AddTopicsMutation extends BaseMutation { 23 | 24 | List topics; 25 | 26 | AddTopicsMutation(this.topics); 27 | 28 | @override 29 | exec() { 30 | topics.forEach((topic) { 31 | $store.state.topics[topic.tid] = topic; 32 | }); 33 | } 34 | 35 | } 36 | 37 | class SetActiveUserMutation extends BaseMutation { 38 | 39 | User user; 40 | 41 | SetActiveUserMutation(this.user); 42 | 43 | @override 44 | exec() { 45 | $store.state.activeUser = user; 46 | $store.commit(new SetUnreadInfoMutation(new UnreadInfo())); 47 | $store.commit(new ClearRoomsMutation()); 48 | } 49 | 50 | } 51 | 52 | class AddRoomsMutation extends BaseMutation { 53 | 54 | List rooms; 55 | 56 | AddRoomsMutation(this.rooms); 57 | 58 | @override 59 | exec() { 60 | rooms.forEach((room) { 61 | $store.state.rooms[room.roomId] = room; 62 | }); 63 | } 64 | 65 | } 66 | 67 | class SetUnreadInfoMutation extends BaseMutation { 68 | 69 | UnreadInfo info; 70 | 71 | SetUnreadInfoMutation(this.info); 72 | 73 | @override 74 | exec() { 75 | $store.state.unreadInfo = info; 76 | } 77 | 78 | } 79 | 80 | class AddMessagesToRoomMutation extends BaseMutation { 81 | 82 | List messages; 83 | 84 | int roomId; 85 | 86 | AddMessagesToRoomMutation(this.roomId, this.messages); 87 | 88 | @override 89 | exec() { 90 | $store.state.rooms[roomId].messages.addAll(messages); 91 | } 92 | 93 | 94 | } 95 | 96 | class ClearMessagesFromRoomMutation extends BaseMutation { 97 | 98 | int roomId; 99 | 100 | ClearMessagesFromRoomMutation(this.roomId); 101 | 102 | @override 103 | exec() { 104 | $store.state.rooms[roomId]?.messages?.clear(); 105 | } 106 | 107 | } 108 | 109 | class ClearRoomsMutation extends BaseMutation { 110 | 111 | ClearRoomsMutation(); 112 | 113 | @override 114 | exec() { 115 | $store.state.rooms.clear(); 116 | } 117 | 118 | } 119 | 120 | class UpdateUnreadChatCountMutation extends BaseMutation { 121 | 122 | int unreadChatCount = 0; 123 | 124 | UpdateUnreadChatCountMutation(this.unreadChatCount); 125 | 126 | @override 127 | exec() { 128 | $store.state.unreadInfo.unreadChatCount = unreadChatCount; 129 | } 130 | 131 | } 132 | 133 | class UpdateRoomUnreadStatusMutation extends BaseMutation { 134 | 135 | bool unread; 136 | 137 | int roomId; 138 | 139 | UpdateRoomUnreadStatusMutation(this.roomId, this.unread); 140 | 141 | @override 142 | exec() { 143 | $store.state.rooms[this.roomId].unread = unread; 144 | } 145 | 146 | } 147 | 148 | class UpdateRoomTeaserContentMutation extends BaseMutation { 149 | 150 | int roomId; 151 | 152 | String content; 153 | 154 | UpdateRoomTeaserContentMutation(this.roomId, this.content); 155 | 156 | @override 157 | exec() { 158 | Room room = $store.state.rooms[this.roomId]; 159 | if(room != null) { 160 | room.teaser?.content = content; 161 | } 162 | } 163 | 164 | } 165 | 166 | 167 | 168 | class ClearTopicsMutation extends BaseMutation { 169 | 170 | @override 171 | exec() { 172 | $store.state.topics.clear(); 173 | } 174 | 175 | } 176 | 177 | class DeleteRoomMutation extends BaseMutation { 178 | int roomId; 179 | 180 | DeleteRoomMutation(this.roomId); 181 | 182 | @override 183 | exec() { 184 | if($store.state.rooms.containsKey(this.roomId)) { 185 | $store.state.rooms.remove(this.roomId); 186 | } 187 | } 188 | 189 | } 190 | 191 | class UpdateNotificationMutation extends BaseMutation { 192 | 193 | bool newReply; 194 | 195 | bool newChat; 196 | 197 | bool newFollow; 198 | 199 | bool groupInvite; 200 | 201 | bool newTopic; 202 | 203 | UpdateNotificationMutation({this.newReply, this.newChat, this.newFollow, this.groupInvite, this.newTopic}); 204 | 205 | @override 206 | exec() { 207 | if(newReply != null) { 208 | $store.state.notification.newReply = newReply; 209 | } 210 | if(newChat != null) { 211 | $store.state.notification.newChat = newChat; 212 | } 213 | if(newFollow != null) { 214 | $store.state.notification.newFollow = newFollow; 215 | } 216 | if(groupInvite != null) { 217 | $store.state.notification.groupInvite = groupInvite; 218 | } 219 | if(newTopic != null) { 220 | $store.state.notification.newTopic = newTopic; 221 | } 222 | } 223 | 224 | } 225 | 226 | class AddRecentViewTopic extends BaseMutation { 227 | 228 | int tid; 229 | 230 | AddRecentViewTopic(this.tid); 231 | 232 | @override 233 | exec() { 234 | if($store.state.recentViews.length >= 8) { 235 | if($store.state.recentViews.contains(this.tid)) { 236 | $store.state.recentViews.remove(this.tid); 237 | } else { 238 | $store.state.recentViews.removeAt(0); 239 | } 240 | $store.state.recentViews.add(this.tid); 241 | } else { 242 | $store.state.recentViews.add(this.tid); 243 | } 244 | } 245 | 246 | } 247 | 248 | //class AddNewPostToTopicMutation extends BaseMutation { 249 | // Post post; 250 | // 251 | // AddNewPostToTopicMutation({this.post}); 252 | // 253 | // @override 254 | // exec() { 255 | // $store.state.topics[this.post.tid]. 256 | // } 257 | //} -------------------------------------------------------------------------------- /lib/views/chat_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:nodebb/models/models.dart'; 6 | import 'package:nodebb/mutations/mutations.dart'; 7 | import 'package:nodebb/services/io_service.dart'; 8 | import 'package:nodebb/views/base.dart'; 9 | import 'package:nodebb/widgets/widgets.dart'; 10 | import 'package:nodebb/errors/errors.dart'; 11 | import 'package:nodebb/utils/utils.dart' as utils; 12 | 13 | class ChatPage extends BaseReactivePage { 14 | ChatPage({Key key, routeParams}) : super(key: key, routeParams: routeParams); 15 | 16 | @override 17 | BaseReactiveState createState() => new _RegisterPageState(); 18 | } 19 | 20 | class _RegisterPageState extends BaseReactiveState { 21 | 22 | Room room; 23 | 24 | StreamSubscription chatSub; 25 | 26 | TextEditingController _textController = new TextEditingController(); 27 | 28 | _handleSubmit(String content) { 29 | if(content == null || content.length == 0) return; 30 | _textController.clear(); 31 | Message msg = new Message( 32 | content: content, 33 | type: MessageType.SEND_PENDING, 34 | user: $store.state.activeUser, 35 | timestamp: new DateTime.now() 36 | ); 37 | IOService.getInstance().sendMessage(roomId: room.roomId, content: content).then((Message message) { 38 | msg.id = message.id; 39 | msg.type = MessageType.SEND; 40 | }).catchError((err) { 41 | if(err is NodeBBNoUserInRoomException) { 42 | Scaffold.of(context).showSnackBar(new SnackBar(content: new Text('房间没有用户!'), backgroundColor: Colors.red,)); 43 | } else { 44 | Scaffold.of(context).showSnackBar(new SnackBar(content: new Text('未知错误,请重试!'), backgroundColor: Colors.red,)); 45 | } 46 | }); 47 | room.messages.insert(0, msg); 48 | //FocusScope.of(context).requestFocus(new FocusNode()); //收起键盘 49 | //SystemChannels.textInput.invokeMethod('TextInput.hide'); 50 | } 51 | 52 | 53 | @override 54 | void initState() { 55 | super.initState(); 56 | } 57 | 58 | 59 | @override 60 | void didChangeDependencies() { 61 | super.didChangeDependencies(); 62 | room = $store.state.rooms[int.parse(widget.routeParams['roomId'])]; 63 | IOService.getInstance().loadRoom(roomId: room.roomId, uid: $store.state.activeUser.uid).then((List messages) { 64 | messages = messages.map((Message msg) { 65 | if(msg.user.uid == $store.state.activeUser.uid) { 66 | msg.type = MessageType.SEND; 67 | } 68 | return msg; 69 | }).toList().reversed.toList(); 70 | $store.commit(new ClearMessagesFromRoomMutation(room.roomId)); 71 | $store.commit(new AddMessagesToRoomMutation(room.roomId, messages)); 72 | IOService.getInstance().markRead(room.roomId); 73 | }); 74 | chatSub?.cancel(); 75 | chatSub = IOService.getInstance().eventStream.listen(null)..onData((NodeBBEvent event) { 76 | if(event.type == NodeBBEventType.RECEIVE_CHATS) { 77 | Map data = event.data; 78 | if(utils.convertToInteger(data['roomId']) == room.roomId 79 | && data['fromUid'] != $store.state.activeUser.uid) { 80 | room.messages.insert(0, new Message.fromJSON(data['message'])); 81 | event.ack(); 82 | } 83 | if(utils.convertToInteger(data['roomId']) == room.roomId) { 84 | IOService.getInstance().markRead(room.roomId); 85 | } 86 | } 87 | }); 88 | } 89 | 90 | @override 91 | Widget render(BuildContext context) { 92 | return new Scaffold( 93 | appBar: new AppBar(title: new Text(utils.isEmpty(room.roomName) ? room.ownerName : room.roomName)), 94 | body: new Column( 95 | children: [ 96 | new Expanded( 97 | child: new ListView.builder( 98 | reverse: true, 99 | itemBuilder: (BuildContext context, int index) { 100 | $enterScope(); 101 | if(index >= room.messages.length) { 102 | $leaveScope(); 103 | return null; 104 | } else { 105 | Widget w = new MessageWidget(room.messages[index]); 106 | $leaveScope(); 107 | return w; 108 | } 109 | } 110 | ) 111 | ), 112 | new SizedBox( 113 | height: 54.0, 114 | child: new Column( 115 | children: [ 116 | new Divider(height: 0.0,), 117 | new Expanded( 118 | child: new Row( 119 | crossAxisAlignment: CrossAxisAlignment.center, 120 | children: [ 121 | new Expanded( 122 | child: new Padding( 123 | padding: const EdgeInsets.only(left: 14.0), 124 | child: new TextField( 125 | controller: _textController, 126 | maxLines: 1, 127 | style: new TextStyle(fontSize: 18.0, color: Theme.of(context).textTheme.body1.color), 128 | decoration: const InputDecoration(contentPadding: const EdgeInsets.only(bottom: 6.0, top: 6.0)), 129 | onSubmitted: _handleSubmit, 130 | ), 131 | ), 132 | ), 133 | new InkWell( 134 | onTap: () { 135 | _handleSubmit(_textController.text); 136 | }, 137 | borderRadius: const BorderRadius.all(const Radius.circular(54.0)), 138 | child: new SizedBox( 139 | width: 54.0, 140 | height: 54.0, 141 | child: new Icon(Icons.send, size: 32.0,), 142 | ) 143 | ) 144 | ], 145 | ), 146 | ), 147 | ], 148 | ), 149 | ) 150 | ], 151 | ) 152 | ); 153 | } 154 | 155 | 156 | 157 | @override 158 | void dispose() { 159 | chatSub?.cancel(); 160 | SystemChannels.textInput.invokeMethod('TextInput.hide'); 161 | super.dispose(); 162 | //FocusScope.of(context).requestFocus(new FocusNode()); 163 | } 164 | 165 | 166 | } 167 | -------------------------------------------------------------------------------- /lib/views/topics_fragment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wills/flutter_wills.dart'; 3 | import 'package:nodebb/actions/actions.dart'; 4 | import 'package:nodebb/models/models.dart'; 5 | import 'package:nodebb/views/base.dart'; 6 | import 'package:nodebb/widgets/widgets.dart'; 7 | import 'package:nodebb/mutations/mutations.dart'; 8 | 9 | class TopicsFragment extends StatefulWidget { 10 | TopicsFragment({Key key}) : super(key: key); 11 | 12 | @override 13 | State createState() { 14 | return new _TopicsFragmentState(); 15 | } 16 | } 17 | 18 | class _TopicsFragmentState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | return new Container(child: new TopicList()); 22 | } 23 | } 24 | 25 | class TopicList extends BaseReactiveWidget { 26 | @override 27 | BaseReactiveState createState() { 28 | return new _TopicListState(); 29 | } 30 | } 31 | 32 | class _TopicListState extends BaseReactiveState { 33 | bool isLoadingMore = false; 34 | 35 | bool isCannotLoadMore = true; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | } 41 | 42 | @override 43 | Widget render(BuildContext context) { 44 | return new RefreshIndicator( 45 | child: new ListView.builder( 46 | itemCount: $store.state.topics.values.length + 1, 47 | itemBuilder: (BuildContext context, int index) { 48 | if (isCannotLoadMore && 49 | !isLoadingMore && 50 | $store.state.topics.values.length - index <= 10) { 51 | isLoadingMore = true; 52 | int oldListLength = $store.state.topics.values.length; 53 | $store 54 | .dispatch(new FetchTopicsAction( 55 | start: oldListLength, count: 19, clearBefore: false)) 56 | .then((_) { 57 | if ($store.state.topics.values.length - oldListLength < 20) { 58 | isCannotLoadMore = false; 59 | } 60 | }).whenComplete(() { 61 | isLoadingMore = false; 62 | setState(() {}); 63 | }); 64 | } 65 | if (index == $store.state.topics.values.length) { 66 | if (isCannotLoadMore) { 67 | return new Container( 68 | height: 64.0, 69 | child: new Row( 70 | mainAxisAlignment: MainAxisAlignment.center, 71 | crossAxisAlignment: CrossAxisAlignment.center, 72 | children: [new Text('加载中...')], 73 | )); 74 | } else { 75 | return new Container( 76 | height: 64.0, 77 | child: new Row( 78 | mainAxisAlignment: MainAxisAlignment.center, 79 | crossAxisAlignment: CrossAxisAlignment.center, 80 | children: [new Text('# end #')], 81 | )); 82 | } 83 | } else if (index > $store.state.topics.values.length) { 84 | return null; 85 | } 86 | Topic topic = $store.state.topics.values.toList()[index]; 87 | User user = topic.user; 88 | return getListItem(user, topic, context); 89 | }, 90 | ), 91 | onRefresh: () async { 92 | await $store.dispatch( 93 | new FetchTopicsAction(start: 0, count: 20, clearBefore: true)); 94 | //$store.state.notification.newTopic 95 | //$store.commit(new UpdateNotificationMutation(newTopic: false)); 96 | }); 97 | } 98 | 99 | Widget getListItem(User user, Topic topic, BuildContext context) { 100 | return new Card( 101 | shape: new RoundedRectangleBorder( 102 | borderRadius: const BorderRadius.all(const Radius.circular(0.0)), 103 | ), 104 | margin: new EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 5.0), 105 | child: new InkWell( 106 | child: new Padding( 107 | padding: new EdgeInsets.all(10.0), 108 | child: new Row( 109 | crossAxisAlignment: CrossAxisAlignment.start, 110 | children: [ 111 | new SizedBox( 112 | width: 56.0, 113 | height: 56.0, 114 | child: new NodeBBAvatar( 115 | picture: user?.picture, 116 | iconText: user.iconText, 117 | iconBgColor: user.iconBgColor), 118 | ), 119 | new Expanded( 120 | child: new Padding( 121 | padding: new EdgeInsets.only(left: 10.0), 122 | child: new Column( 123 | crossAxisAlignment: CrossAxisAlignment.start, 124 | children: [ 125 | new Padding( 126 | padding: new EdgeInsets.only(bottom: 5.0), 127 | child: new Row( 128 | children: [ 129 | new Text(user.userName, 130 | style: new TextStyle( 131 | fontWeight: FontWeight.bold, 132 | )), 133 | ], 134 | ), 135 | ), 136 | new Text( 137 | topic.title, 138 | style: new TextStyle(fontSize: 16.0), 139 | ), 140 | new Padding( 141 | padding: new EdgeInsets.only(top: 5.0), 142 | child: new Text( 143 | getTopicPostTimeDesc(topic.timestamp), 144 | style: new TextStyle( 145 | fontSize: 12.0, color: Colors.blue.shade500), 146 | ), 147 | ) 148 | ], 149 | ), 150 | ), 151 | ) 152 | ], 153 | ), 154 | ), 155 | onTap: () { 156 | Navigator.of(context).pushNamed('/topic/${topic.tid}'); 157 | }, 158 | )); 159 | } 160 | 161 | String getTopicPostTimeDesc(DateTime time) { 162 | var now = new DateTime.now(); 163 | if (now.year > time.year) { 164 | return "发布于 ${now.year-time.year} 年前"; 165 | } 166 | if (now.month > time.month) { 167 | return "发布于 ${now.month-time.month} 月前"; 168 | } 169 | if (now.day > time.day) { 170 | return "发布于 ${now.day-time.day} 天前"; 171 | } 172 | if (now.hour > time.hour) { 173 | return "发布于 ${now.hour-time.hour} 小时前"; 174 | } 175 | if (now.minute > time.minute) { 176 | return "发布于 ${now.minute-time.minute} 分钟前"; 177 | } 178 | if (now.second > time.second) { 179 | return "发布于 ${now.second-time.second} 秒前"; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/views/messages_fragment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nodebb/models/models.dart'; 3 | import 'package:nodebb/mutations/mutations.dart'; 4 | import 'package:nodebb/services/io_service.dart'; 5 | import 'package:nodebb/views/base.dart'; 6 | import 'package:nodebb/widgets/builders.dart'; 7 | import 'package:nodebb/widgets/widgets.dart'; 8 | 9 | class MessagesFragment extends BaseReactiveWidget { 10 | MessagesFragment({Key key}): super(key: key); 11 | 12 | @override 13 | BaseReactiveState createState() { 14 | return new _MessagesFragmentState(); 15 | } 16 | } 17 | 18 | class _MessagesFragmentState extends BaseReactiveState { 19 | 20 | Widget _buildSystemItem({icon, iconColor, title, onTap}) { 21 | return new InkWell( 22 | onTap: onTap, 23 | child: new SizedBox( 24 | height: 64.0, 25 | child: new Container( 26 | padding: const EdgeInsets.only(left: 8.0, right: 12.0), 27 | child: new Row( 28 | crossAxisAlignment: CrossAxisAlignment.center, 29 | children: [ 30 | new Icon(icon, size: 36.0, color: iconColor,), 31 | new Padding(padding: const EdgeInsets.only(right: 8.0)), 32 | new Expanded( 33 | child: new Container( 34 | decoration: buildBottomDividerDecoration(context), 35 | padding: const EdgeInsets.only(left: 6.0), 36 | child: new Align( 37 | alignment: Alignment.centerLeft, 38 | child: new Text(title, style: const TextStyle(fontSize: 16.0),), 39 | ) 40 | ) 41 | ) 42 | ], 43 | ) 44 | ) 45 | ) 46 | ); 47 | } 48 | 49 | Widget _buildRoomItem({Room room, onTap, onLongPress}) { 50 | User user; 51 | if(room.users.isNotEmpty) { 52 | user = room.users[0]; 53 | } else { 54 | user = $store.state.activeUser; 55 | } 56 | String content = room.teaser?.content ?? ''; 57 | TapDownDetails _details; 58 | return new Builder( 59 | builder: (BuildContext context) { 60 | return new InkWell( 61 | onTap: onTap, 62 | onLongPress: () { 63 | onLongPress(context, _details); 64 | }, 65 | onTapDown: (TapDownDetails details) { 66 | _details = details; 67 | }, 68 | child: new SizedBox( 69 | height: 64.0, 70 | child: new Container( 71 | padding: const EdgeInsets.only(left: 8.0, right: 12.0), 72 | child: new Row( 73 | crossAxisAlignment: CrossAxisAlignment.center, 74 | children: [ 75 | new SizedBox( 76 | width: 42.0, 77 | height: 42.0, 78 | child: new NodeBBAvatar( 79 | picture: user.picture, 80 | iconText: user.iconText, 81 | iconBgColor: user.iconBgColor, 82 | marked: room.unread, 83 | ) 84 | ), 85 | new Padding(padding: const EdgeInsets.only(right: 4.0)), 86 | new Expanded( 87 | child: new Container( 88 | decoration: buildBottomDividerDecoration(context), 89 | padding: const EdgeInsets.only(left: 6.0), 90 | child: new Column( 91 | crossAxisAlignment: CrossAxisAlignment.start, 92 | mainAxisAlignment: MainAxisAlignment.center, 93 | children: [ 94 | new Text(user.userName, style: const TextStyle(fontSize: 16.0),), 95 | new Padding(padding: const EdgeInsets.only(top: 4.0)), 96 | new Text(content, maxLines: 1, overflow:TextOverflow.ellipsis, style: const TextStyle(fontSize: 14.0, color: Colors.grey)) 97 | ], 98 | ) 99 | ) 100 | ) 101 | ], 102 | ) 103 | ) 104 | ) 105 | ); 106 | }, 107 | ); 108 | 109 | } 110 | 111 | @override 112 | Widget render(BuildContext context) { 113 | List contents = new List(); 114 | contents.addAll([ 115 | _buildSystemItem( 116 | icon: Icons.search, 117 | title: '查找用户', 118 | iconColor: Theme.of(context).primaryColor, 119 | onTap: () { 120 | Navigator.of(context).pushNamed('/search_user'); 121 | } 122 | ), 123 | // _buildSystemItem( 124 | // icon: Icons.rss_feed, 125 | // title: '系统通知', 126 | // iconColor: Theme.of(context).primaryColor 127 | // ), 128 | // _buildSystemItem( 129 | // icon: Icons.send, 130 | // title: '管理员', 131 | // iconColor: Theme.of(context).primaryColor, 132 | // onTap: () { 133 | // Navigator.of(context).pushNamed('/chat/0'); 134 | // } 135 | // ) 136 | ]); 137 | $store.state.rooms.values.forEach((Room room) { 138 | contents.add(_buildRoomItem(room: room, 139 | onTap: () { 140 | for(User user in room.users) { 141 | if(user.uid != $store.state.activeUser?.uid) { 142 | $store.commit(new AddUsersMutation([user])); 143 | } 144 | } 145 | Navigator.of(context).pushNamed('/chat/${room.roomId}'); 146 | }, 147 | onLongPress: (BuildContext context, TapDownDetails details) { 148 | RenderBox box = context.findRenderObject(); 149 | var topLeftPosition = box.localToGlobal(Offset.zero); 150 | showMenu( 151 | context: context, 152 | position: new RelativeRect.fromLTRB(details.globalPosition.dx, topLeftPosition.dy, box.size.width - details.globalPosition.dx, 0.0), 153 | items: [ 154 | new PopupMenuItem( 155 | value: 0, 156 | child: new Text('标记已读'), 157 | ), 158 | new PopupMenuItem( 159 | value: 1, 160 | child: new Text('删除'), 161 | ), 162 | ] 163 | ).then((val) { 164 | if(val == 1) { 165 | IOService.getInstance().markRead(room.roomId); 166 | IOService.getInstance().leaveRoom(room.roomId).then((_) { 167 | $store.commit(new DeleteRoomMutation(room.roomId)); 168 | }).catchError((err) { 169 | Scaffold.of(context).showSnackBar(new SnackBar( 170 | content: new Text('删除失败,请重试!'), 171 | backgroundColor: Colors.red, 172 | )); 173 | }); 174 | } else if(val == 0) { 175 | IOService.getInstance().markRead(room.roomId); 176 | } 177 | }); 178 | } 179 | )); 180 | }); 181 | return new ListView( 182 | children: contents, 183 | ); 184 | } 185 | 186 | } -------------------------------------------------------------------------------- /lib/socket_io/sio_parser.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:nodebb/application/application.dart'; 3 | import 'package:nodebb/socket_io/errors.dart'; 4 | import 'package:nodebb/utils/utils.dart' as utils; 5 | import 'package:nodebb/socket_io/eio_parser.dart'; 6 | 7 | enum SocketIOPacketType { CONNECT, DISCONNECT, EVENT, ACK, ERROR, BINARY_EVENT, BINARY_ACK } 8 | 9 | SocketIOPacketType getSocketIOPacketType(int t) { 10 | SocketIOPacketType type; 11 | try { 12 | type = SocketIOPacketType.values[t]; 13 | } catch (e) { 14 | throw new SocketIOParseException('unsupport socketio packet type ${t}'); 15 | } 16 | return type; 17 | } 18 | 19 | class SocketIOPacket { 20 | 21 | bool useBase64Encoder; 22 | 23 | SocketIOPacketType type; 24 | 25 | String namespace; 26 | 27 | int id; 28 | 29 | int attachments; 30 | 31 | dynamic data; 32 | 33 | SocketIOPacket({this.type, this.data, this.namespace = '/', this.attachments = 0, this.useBase64Encoder = false, this.id}); 34 | 35 | @override 36 | String toString() { 37 | return '{type: $type, id: $id, data: $data, namespace: $namespace, attachments: $attachments, useBase64Encoder: $useBase64Encoder}'; 38 | } 39 | 40 | 41 | } 42 | 43 | class SocketIOPacketDecoder extends Converter { 44 | 45 | @override 46 | SocketIOPacket convert(input) { 47 | if(input.data is List) { 48 | List data = input.data; 49 | SocketIOPacketType type = getSocketIOPacketType(data[0]); 50 | SocketIOPacket packet = new SocketIOPacket(type: type, data: data.skip(1).toList()); 51 | return packet; 52 | } else { // String 53 | String str = input.data; 54 | int i = 0; 55 | SocketIOPacketType type = getSocketIOPacketType(utils.convertToInteger(str[0])); 56 | SocketIOPacket packet = new SocketIOPacket(type: type); 57 | if (type == SocketIOPacketType.BINARY_EVENT 58 | || type == SocketIOPacketType.BINARY_ACK) { //get attachments 59 | StringBuffer _attachments = new StringBuffer(); 60 | while(++i < str.length) { 61 | if(str[i] == '-') break; 62 | _attachments.write(str[i]); 63 | } 64 | packet.attachments = utils.convertToInteger(_attachments.toString()); 65 | if(packet.attachments == 0 || str[i] != '-') { 66 | throw new SocketIOParseException('illegal attachments'); 67 | } 68 | } 69 | if(i + 1 < str.length) { 70 | if (str[i + 1] == '/') { //get namespace 71 | StringBuffer sb = new StringBuffer(); 72 | while (++i < str.length) { 73 | if (str[i] == ',') break; 74 | sb.write(str[i]); 75 | } 76 | packet.namespace = sb.toString(); 77 | } else { 78 | packet.namespace = '/'; 79 | } 80 | } 81 | if(i + 1 < str.length) { 82 | int next = str.codeUnitAt(i + 1); //get id 83 | if (next >= 48 && next <= 57) { 84 | StringBuffer sb = new StringBuffer(); 85 | while (++i < str.length) { 86 | next = str.codeUnitAt(i); 87 | if (!(next >= 48 && next <= 57)) { 88 | i--; 89 | break; 90 | } 91 | sb.write(str[i]); 92 | } 93 | packet.id = utils.convertToInteger(sb.toString()); 94 | } 95 | } 96 | if(++i < str.length) { 97 | try { 98 | packet.data = JSON.decode(str.substring(i)); 99 | } catch(e) { 100 | Application.logger.fine('packet data decode fail data: ${str.substring(i)}'); 101 | } 102 | } 103 | return packet; 104 | } 105 | } 106 | 107 | @override 108 | Sink startChunkedConversion(Sink sink) { 109 | return new _SocketIOPacketDecoderSink(sink, this); 110 | } 111 | 112 | } 113 | 114 | class _SocketIOPacketDecoderSink extends ChunkedConversionSink { 115 | 116 | Sink _sink; 117 | 118 | SocketIOPacketDecoder _decoder; 119 | 120 | _SocketIOPacketDecoderSink(this._sink, this._decoder); 121 | 122 | SocketIOPacket _pendingPacket; 123 | 124 | List _buffers = new List(); 125 | 126 | void reconstructPendingPacket() { 127 | reconstruct(obj) { 128 | if(obj is Map) { 129 | if (obj.containsKey('_placeholder') && 130 | obj.containsKey('num')) { //placeholder 131 | return _buffers[obj['num']].data; 132 | } else { 133 | obj.forEach((key, value) { 134 | if (value is Map) { 135 | obj[key] = reconstruct(value); 136 | } 137 | }); 138 | } 139 | } else if(obj is List) { 140 | for(int i = 0; i < obj.length; i++) { 141 | obj[i] = reconstruct(obj[i]); 142 | } 143 | } 144 | return obj; 145 | } 146 | _pendingPacket.data = reconstruct(_pendingPacket.data); 147 | } 148 | 149 | @override 150 | void add(EngineIOPacket chunk) { 151 | SocketIOPacket packet = this._decoder.convert(chunk); 152 | if(_pendingPacket != null && packet.data is List) { 153 | _buffers.add(packet); 154 | if(_buffers.length == _pendingPacket.attachments) { 155 | try { 156 | reconstructPendingPacket(); 157 | this._sink.add(_pendingPacket); 158 | } catch(e) { 159 | Application.logger.fine('pending packet reconstruct fail, packet: $_pendingPacket, buffers: $_buffers'); 160 | } 161 | _pendingPacket = null; 162 | _buffers.clear(); 163 | } 164 | } 165 | if((packet.type == SocketIOPacketType.BINARY_EVENT 166 | || packet.type == SocketIOPacketType.BINARY_ACK) 167 | && packet.attachments > 0) { //wait for seq pack 168 | _pendingPacket = packet; 169 | } else { 170 | this._sink.add(packet); 171 | } 172 | } 173 | 174 | @override 175 | void close() { 176 | this._sink.close(); 177 | } 178 | 179 | } 180 | 181 | class SocketIOPacketEncoder extends Converter> { 182 | 183 | EngineIOPacket _convertToEngineIOPacket(SocketIOPacket input) { 184 | StringBuffer sb = new StringBuffer(); 185 | sb.write(input.type.index); 186 | if(input.type == SocketIOPacketType.BINARY_ACK 187 | || input.type == SocketIOPacketType.BINARY_EVENT) { 188 | sb.write(input.attachments); 189 | sb.write('-'); 190 | } 191 | if(input.namespace != '/') { 192 | sb.write(input.namespace); 193 | sb.write(','); 194 | } 195 | if(input.id != null) { 196 | sb.write(input.id); 197 | } 198 | if(input.data != null) { 199 | sb.write(JSON.encode(input.data)); 200 | } 201 | return new EngineIOPacket( 202 | type: EngineIOPacketType.MESSAGE, 203 | data: sb.toString(), 204 | useBase64Encoder: input.useBase64Encoder 205 | ); 206 | } 207 | 208 | List _destructBinaryPacket(SocketIOPacket packet) { 209 | 210 | List engineIOPackets = []; 211 | 212 | removeBinary(data) { 213 | if(data is List) { 214 | Map placeholder = {'_placholder': true, 'num': engineIOPackets.length}; 215 | engineIOPackets.add( 216 | new EngineIOPacket( 217 | type: EngineIOPacketType.MESSAGE, 218 | data: data 219 | ) 220 | ); 221 | return placeholder; 222 | } else if(data is Map) { 223 | data.forEach((key, value) { 224 | data[key] = removeBinary(value); 225 | }); 226 | } else if(data is List) { 227 | for(int i = 0; i < data.length; i++) { 228 | data[i] = removeBinary(data[i]); 229 | } 230 | } 231 | return data; 232 | } 233 | packet.data = removeBinary(packet.data); 234 | engineIOPackets.insert(0, _convertToEngineIOPacket(packet)); 235 | return engineIOPackets; 236 | } 237 | 238 | @override 239 | List convert(SocketIOPacket input) { 240 | List outs = new List(); 241 | if(input.type == SocketIOPacketType.BINARY_ACK 242 | || input.type == SocketIOPacketType.BINARY_EVENT) { 243 | outs.addAll(_destructBinaryPacket(input)); 244 | } else { 245 | outs.add(_convertToEngineIOPacket(input)); 246 | } 247 | return outs; 248 | } 249 | 250 | @override 251 | Sink startChunkedConversion(Sink> sink) { 252 | return new _SocketIOPacketEncoderSink(sink, this); 253 | } 254 | 255 | } 256 | 257 | class _SocketIOPacketEncoderSink extends ChunkedConversionSink { 258 | 259 | Sink _sink; 260 | 261 | SocketIOPacketEncoder _encoder; 262 | 263 | _SocketIOPacketEncoderSink(this._sink, this._encoder); 264 | 265 | @override 266 | void add(SocketIOPacket chunk) { 267 | _encoder.convert(chunk).forEach((packet) { 268 | _sink.add(packet); 269 | }); 270 | } 271 | 272 | @override 273 | void close() { 274 | _sink.close(); 275 | } 276 | 277 | } -------------------------------------------------------------------------------- /lib/socket_io/sio_socket.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:nodebb/application/application.dart'; 4 | import 'package:nodebb/socket_io/eio_parser.dart'; 5 | import 'package:nodebb/socket_io/eio_socket.dart'; 6 | import 'package:nodebb/socket_io/errors.dart'; 7 | import 'package:nodebb/socket_io/sio_client.dart'; 8 | import 'package:nodebb/socket_io/sio_parser.dart'; 9 | import 'package:nodebb/utils/utils.dart' as utils; 10 | 11 | 12 | enum SocketIOSocketEventType { 13 | CONNECT, 14 | CONNECT_ERROR, 15 | CONNECT_TIMEOUT, 16 | CONNECTING, 17 | DISCONNECT, 18 | ERROR, 19 | RECONNECT, 20 | RECONNECT_ATTEMPT, 21 | RECONNECT_FAIL, 22 | RECONNECTING, 23 | PING, 24 | PONG, 25 | //自定义事件 26 | SEND, 27 | RECEIVE, 28 | CLOSE 29 | } 30 | 31 | class SocketIOSocketEvent { 32 | 33 | SocketIOSocketEventType type; 34 | 35 | dynamic data; 36 | 37 | AckHandler ack; 38 | 39 | SocketIOSocketEvent(this.type, [this.data, this.ack]); 40 | 41 | @override 42 | String toString() { 43 | return '{type: $type, data: $data}'; 44 | } 45 | } 46 | 47 | typedef OnAckHandler(SocketIOPacket packet); 48 | 49 | typedef AckHandler(); 50 | 51 | class SocketIOSocket { 52 | 53 | SocketIOClient owner; 54 | 55 | String namespace; 56 | 57 | bool ready = false; 58 | 59 | EngineIOSocket io; 60 | 61 | List sendBuffer = new List(); 62 | 63 | List receiveBuffer = new List(); 64 | 65 | SocketIOPacketDecoder decoder = new SocketIOPacketDecoder(); 66 | 67 | SocketIOPacketEncoder encoder = new SocketIOPacketEncoder(); 68 | 69 | Map query; 70 | 71 | Map acks = new Map(); 72 | 73 | int ids = 0; 74 | 75 | StreamSubscription _sub; 76 | 77 | StreamController _eventController = new StreamController.broadcast(); 78 | 79 | Stream get eventStream => _eventController.stream; 80 | 81 | SocketIOSocket({this.io, this.namespace = '/', this.owner, this.query}) { 82 | _eventController.add(new SocketIOSocketEvent(SocketIOSocketEventType.CONNECTING)); 83 | if(io.readyStatus == EngineIOSocketStatus.OPEN) { 84 | onOpen(); 85 | } 86 | setupEvents(); 87 | } 88 | 89 | setupEvents() { 90 | Application.logger.fine('socketio namespace: $namespace listen socket: ${io.sid}'); 91 | _sub = this.io.eventStream.listen(null)..onData((EngineIOSocketEvent e) { 92 | if(e.type == EngineIOSocketEventType.ERROR) { 93 | onError(e.data); 94 | return; 95 | } 96 | switch(e.type) { 97 | case EngineIOSocketEventType.OPEN: 98 | onOpen(); 99 | break; 100 | case EngineIOSocketEventType.FLUSH: 101 | break; 102 | case EngineIOSocketEventType.RECEIVE: 103 | if(e.data != null && e.data.type == EngineIOPacketType.MESSAGE) { 104 | SocketIOPacket p = decoder.convert(e.data); 105 | Application.logger.fine('receive socketio packet $p'); 106 | if (p?.namespace != namespace) return; //not match 107 | onPacket(p); 108 | } 109 | break; 110 | case EngineIOSocketEventType.SEND: 111 | break; 112 | case EngineIOSocketEventType.CLOSE: 113 | onClose(); 114 | break; 115 | default: 116 | break; 117 | } 118 | }); 119 | } 120 | 121 | teardownEvents() { 122 | _sub?.cancel(); 123 | } 124 | 125 | onPacket(SocketIOPacket packet) { 126 | switch(packet.type) { 127 | case SocketIOPacketType.ERROR: 128 | onError(packet); 129 | break; 130 | case SocketIOPacketType.BINARY_EVENT: 131 | onEvent(packet); 132 | break; 133 | case SocketIOPacketType.BINARY_ACK: 134 | onAck(packet); 135 | break; 136 | case SocketIOPacketType.ACK: 137 | onAck(packet); 138 | break; 139 | case SocketIOPacketType.CONNECT: 140 | onConnect(packet); 141 | break; 142 | case SocketIOPacketType.DISCONNECT: 143 | onDisconnect(packet); 144 | break; 145 | case SocketIOPacketType.EVENT: 146 | onEvent(packet); 147 | break; 148 | } 149 | } 150 | 151 | onOpen() { 152 | connect(); 153 | } 154 | 155 | onClose() { 156 | ready = false; 157 | teardownEvents(); 158 | _eventController.add(new SocketIOSocketEvent(SocketIOSocketEventType.DISCONNECT)); 159 | } 160 | 161 | onError(e) { 162 | _eventController.add(new SocketIOSocketEvent(SocketIOSocketEventType.ERROR, e.data)); 163 | } 164 | 165 | onEvent(SocketIOPacket packet) { 166 | bool sent = false; 167 | _eventController.add(new SocketIOSocketEvent( 168 | SocketIOSocketEventType.RECEIVE, 169 | packet, 170 | () { 171 | if(!sent && packet.id != null) { 172 | SocketIOPacketType type; 173 | if(packet.type == SocketIOPacketType.EVENT) { 174 | type = SocketIOPacketType.ACK; 175 | } else if(packet.type == SocketIOPacketType.BINARY_EVENT) { 176 | type = SocketIOPacketType.BINARY_ACK; 177 | } 178 | send(new SocketIOPacket( 179 | type: type, 180 | id: packet.id 181 | )); 182 | sent = true; 183 | } 184 | } 185 | )); 186 | } 187 | 188 | onAck(SocketIOPacket packet) { 189 | if(packet.id != null && acks[packet.id] != null) { 190 | Application.logger.fine('call ack ${packet.id} with ${packet.data}'); 191 | acks[packet.id](packet); 192 | acks.remove(packet.id); 193 | } else { 194 | Application.logger.fine('bad ack ${packet.id}'); 195 | } 196 | } 197 | 198 | onDisconnect(SocketIOPacket packet) { 199 | ready = false; 200 | _eventController.add(new SocketIOSocketEvent( 201 | SocketIOSocketEventType.DISCONNECT, 202 | packet 203 | )); 204 | } 205 | 206 | onConnect(SocketIOPacket packet) { 207 | ready = true; 208 | flush(); 209 | _eventController.add(new SocketIOSocketEvent( 210 | SocketIOSocketEventType.CONNECT, 211 | packet 212 | )); 213 | } 214 | 215 | 216 | send(SocketIOPacket packet, [OnAckHandler handler]) { 217 | if(handler != null) { 218 | int id = ids++; 219 | acks[id] = handler; 220 | packet.id = id; 221 | } 222 | sendBuffer.add(packet); 223 | if(ready) flush(); 224 | } 225 | 226 | // ack(SocketIOPacket packet) { 227 | // SocketIOPacketType type; 228 | // if(packet.type == SocketIOPacketType.EVENT) { 229 | // type = SocketIOPacketType.ACK; 230 | // } else if(packet.type == SocketIOPacketType.BINARY_EVENT) { 231 | // type = SocketIOPacketType.BINARY_ACK; 232 | // } else { 233 | // throw new SocketIOStateException('packet type must be SocketIOPacketType.EVENT or SocketIOPacketType.BINARY_EVENT'); 234 | // } 235 | // if(packet.id == null) { 236 | // throw new SocketIOParseException('paket id not found'); 237 | // } 238 | // Application.logger.fine('send socketio packet ack: ${packet.id}'); 239 | // send(new SocketIOPacket(type: type, id: packet.id)); 240 | // } 241 | 242 | _sendPacket(SocketIOPacket packet) { 243 | if(packet.namespace == null || packet.namespace == '') { 244 | packet.namespace = namespace; 245 | } 246 | _eventController.add(new SocketIOSocketEvent(SocketIOSocketEventType.SEND, packet)); 247 | Application.logger.fine('send socketio packet $packet'); 248 | encoder.convert(packet).forEach((EngineIOPacket p) { 249 | io.sendPacket(p); 250 | }); 251 | } 252 | 253 | flush() { 254 | sendBuffer.forEach((SocketIOPacket packet) { 255 | _sendPacket(packet); 256 | }); 257 | sendBuffer.clear(); 258 | } 259 | 260 | connect() { 261 | if(io.readyStatus == EngineIOSocketStatus.OPEN) { 262 | if (namespace != '/') { 263 | if (query != null) { 264 | _sendPacket(new SocketIOPacket( 265 | type: SocketIOPacketType.CONNECT, 266 | namespace: namespace + '?' + utils.encodeUriQuery(query) 267 | )); 268 | } else { 269 | _sendPacket(new SocketIOPacket( 270 | type: SocketIOPacketType.CONNECT 271 | )); 272 | } 273 | } 274 | } else { 275 | throw new SocketIOStateException('io does not open before connect'); 276 | } 277 | } 278 | 279 | disconnect() { 280 | if(this.ready) { 281 | this.send(new SocketIOPacket(type: SocketIOPacketType.DISCONNECT)); 282 | } 283 | } 284 | 285 | close() { 286 | if(this.ready) { 287 | disconnect(); 288 | eventStream 289 | .where((SocketIOSocketEvent event) { 290 | return event.type == SocketIOSocketEventType.DISCONNECT; 291 | }).listen((SocketIOSocketEvent event) { 292 | teardownEvents(); 293 | }); 294 | } else { 295 | teardownEvents(); 296 | } 297 | } 298 | 299 | StreamSubscription listen({SocketIOSocketEventType type = SocketIOSocketEventType.RECEIVE,onData, onError, onDone}) { 300 | StreamSubscription ss; 301 | ss = eventStream 302 | .where((SocketIOSocketEvent event) { 303 | return event.type == type; 304 | }).listen(null) 305 | ..onData((data) { 306 | if(onData != null) { 307 | onData(data); 308 | } 309 | })..onError((err) { 310 | if(onError != null) { 311 | onError(err); 312 | } 313 | })..onDone(() { 314 | if(onDone != null) { 315 | onDone(); 316 | } 317 | }); 318 | return ss; 319 | } 320 | } -------------------------------------------------------------------------------- /lib/socket_io/eio_socket.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:nodebb/application/application.dart'; 6 | import 'package:nodebb/socket_io/eio_client.dart'; 7 | import 'package:nodebb/socket_io/eio_parser.dart'; 8 | import 'package:nodebb/socket_io/errors.dart'; 9 | import 'package:nodebb/utils/utils.dart' as utils; 10 | 11 | enum EngineIOSocketStatus { INITIAL, OPENING, OPEN, RECONNECT, CLOSING, CLOSED } 12 | 13 | enum EngineIOSocketEventType { OPEN, CLOSE, RECONNECT_ATTEMPT, RECONNECT_SUCCESS, RECONNECT_FAIL, SEND, RECEIVE, FLUSH, ERROR } 14 | 15 | class EngineIOSocketEvent { 16 | 17 | EngineIOSocketEventType type; 18 | 19 | dynamic data; 20 | 21 | EngineIOSocketEvent(this.type, [this.data]); 22 | 23 | @override 24 | String toString() { 25 | return '{type: $type, data: $data}'; 26 | } 27 | } 28 | //todo clear write buffer and receive buffer? 29 | class EngineIOSocket { 30 | 31 | EngineIOClient owner; 32 | 33 | String sid; //会话id 34 | 35 | bool autoReconnect; 36 | 37 | int maxReconnectTrys; 38 | 39 | int reconnectInterval; 40 | 41 | int reconnectTrys = 0; 42 | 43 | Duration pingInterval; 44 | 45 | Duration pingTimeout; 46 | 47 | Timer _pingIntervalTimer; 48 | 49 | Timer _pingTimeoutTimer; 50 | 51 | List writeBuffer; 52 | 53 | String uri; 54 | 55 | WebSocket socket; 56 | 57 | bool useBinary = false; 58 | 59 | Converter converter; 60 | 61 | StreamSubscription _subscription; 62 | 63 | EngineIOSocketStatus _readyStatus = EngineIOSocketStatus.INITIAL; 64 | 65 | //Completer _openCompleter; 66 | 67 | StreamController _eventController = new StreamController.broadcast(); 68 | 69 | Stream get eventStream => _eventController.stream; 70 | 71 | bool forceClose = false; 72 | 73 | EngineIOSocketStatus get readyStatus { 74 | return _readyStatus == null ? EngineIOSocketStatus.INITIAL : _readyStatus; 75 | } 76 | 77 | set readyStatus(EngineIOSocketStatus status) { 78 | _readyStatus = status; 79 | } 80 | 81 | EngineIOSocket({ 82 | this.uri, 83 | this.converter, 84 | this.sid, 85 | this.owner, 86 | this.autoReconnect = true, 87 | this.maxReconnectTrys = 3, 88 | this.reconnectInterval = 1000, 89 | }) { 90 | if(converter == null) { 91 | converter = new EngineIOPacketDecoder(); 92 | } 93 | writeBuffer = new List(); 94 | } 95 | 96 | connect() async { 97 | if(readyStatus == EngineIOSocketStatus.INITIAL 98 | || readyStatus == EngineIOSocketStatus.CLOSED) { 99 | readyStatus = EngineIOSocketStatus.OPENING; 100 | try { 101 | await reconnect(); 102 | } catch(e) { 103 | Application.logger.warning('enginesocket establish fail: $uri, error: $e'); 104 | readyStatus = EngineIOSocketStatus.CLOSED; 105 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.ERROR, e)); 106 | } 107 | } 108 | } 109 | 110 | reconnect() async { 111 | Map headers = new Map(); 112 | Uri _uri = Uri.parse(uri); 113 | List cookies = owner.jar.getCookies(_uri); 114 | var cookieStr = owner.jar.serializeCookies(cookies); 115 | if(cookieStr != null && cookieStr.length > 0) { 116 | headers[HttpHeaders.COOKIE] = cookieStr; 117 | Application.logger.fine('enginiosocket send cookie: $cookieStr'); 118 | } 119 | headers['origin'] = ('ws' == _uri.scheme ? 'http://' : 'https://') + '${_uri.host}:${_uri.port}'; 120 | headers['host'] = '${_uri.host}:${_uri.port}'; 121 | Application.logger.fine('establish engineiosocket connect: $uri'); 122 | socket = await WebSocket.connect(uri, headers: headers); 123 | _subscription?.cancel(); 124 | _subscription = socket.transform(this.converter).listen(null) 125 | ..onData((data) { 126 | onPacket(data); 127 | }) 128 | ..onError((e) { 129 | onError(e); 130 | }); 131 | Application.logger.fine('engineiosocket establish success: $uri'); 132 | } 133 | 134 | tryReconnect() async { 135 | if(autoReconnect && !forceClose) { 136 | while(reconnectTrys < maxReconnectTrys) { 137 | readyStatus = EngineIOSocketStatus.RECONNECT; 138 | try { 139 | Application.logger.fine('enginiosocket: $sid try reconnet $reconnectTrys'); 140 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.RECONNECT_ATTEMPT)); 141 | await reconnect(); 142 | reconnectTrys = 0; 143 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.RECONNECT_SUCCESS)); 144 | return; 145 | } catch(err) { 146 | Application.logger.fine('enginiosocket: $sid error: $err'); 147 | reconnectTrys++; 148 | await new Future.delayed(new Duration(milliseconds: reconnectInterval)); 149 | } 150 | } 151 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.RECONNECT_FAIL)); 152 | close(new EngineIOReconnectFailException()); 153 | Application.logger.fine('enginiosocket: $sid exceed max retry: $maxReconnectTrys'); 154 | return; 155 | } 156 | close(); 157 | } 158 | 159 | close([reason]) async { 160 | if(readyStatus == EngineIOSocketStatus.OPEN 161 | || readyStatus == EngineIOSocketStatus.OPENING 162 | || readyStatus == EngineIOSocketStatus.RECONNECT) { 163 | forceClose = true; 164 | readyStatus = EngineIOSocketStatus.CLOSING; 165 | // String _reasonMsg = 'enginiosocket: $sid will be closed'; 166 | // if(reason is Error || reason is Exception) { 167 | // _reasonMsg = reason.toString(); 168 | // } else { 169 | // 170 | // } 171 | await socket.close(); 172 | onClose(reason); 173 | } 174 | } 175 | 176 | ping() { 177 | Application.logger.fine('enginiosocket: $sid ping'); 178 | this.sendPacket(new EngineIOPacket(type: EngineIOPacketType.PING)); 179 | } 180 | 181 | setPing() { 182 | if(_pingIntervalTimer != null) _pingIntervalTimer.cancel(); 183 | _pingIntervalTimer = new Timer(pingInterval, () { 184 | ping(); 185 | onHeartbeat(pingTimeout); 186 | }); 187 | } 188 | 189 | sendPacket(EngineIOPacket packet) { 190 | if(readyStatus == EngineIOSocketStatus.CLOSED) return; 191 | writeBuffer.add(packet); 192 | flush(); 193 | } 194 | 195 | flush() { 196 | if(readyStatus == EngineIOSocketStatus.OPEN) { 197 | new Stream.fromIterable(writeBuffer.toList()) 198 | .transform(new EngineIOPacketEncoder()).listen(null) 199 | ..onData((data) { 200 | Application.logger.fine('enginiosocket: $sid send: $data'); 201 | socket.add(data); 202 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.SEND, data)); 203 | })..onDone(() { 204 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.FLUSH)); 205 | }); 206 | writeBuffer.clear(); 207 | } 208 | } 209 | 210 | pause() { 211 | if(readyStatus == EngineIOSocketStatus.OPEN) { 212 | _subscription.pause(); 213 | } 214 | } 215 | 216 | resume() { 217 | if(readyStatus == EngineIOSocketStatus.OPEN) { 218 | _subscription.resume(); 219 | } 220 | } 221 | 222 | onOpen() { 223 | readyStatus = EngineIOSocketStatus.OPEN; 224 | flush(); //把缓存的信息发出去 225 | } 226 | 227 | onClose(reason) { 228 | if(readyStatus == EngineIOSocketStatus.CLOSED) return; 229 | readyStatus = EngineIOSocketStatus.CLOSED; 230 | _subscription.cancel(); 231 | _pingTimeoutTimer.cancel(); 232 | _pingIntervalTimer.cancel(); 233 | Application.logger.fine('enginiosocket: $sid is closed'); 234 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.CLOSE, reason)); 235 | } 236 | 237 | onPacket(EngineIOPacket packet) { 238 | Application.logger.fine('enginiosocket: $sid receive $packet'); 239 | switch(packet.type) { 240 | case EngineIOPacketType.OPEN: 241 | onHandshake(packet); 242 | break; 243 | case EngineIOPacketType.CLOSE: 244 | break; 245 | case EngineIOPacketType.MESSAGE: 246 | break; 247 | case EngineIOPacketType.PING: 248 | break; 249 | case EngineIOPacketType.PONG: 250 | setPing(); 251 | break; 252 | case EngineIOPacketType.NOOP: 253 | break; 254 | case EngineIOPacketType.UPGRADE: 255 | break; 256 | } 257 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.RECEIVE, packet)); 258 | this.onHeartbeat(); 259 | } 260 | 261 | onHeartbeat([Duration timeout]) { 262 | if(_pingTimeoutTimer != null) _pingTimeoutTimer.cancel(); 263 | if(timeout == null) { 264 | timeout = new Duration(milliseconds: 265 | pingTimeout.inMilliseconds + pingInterval.inMilliseconds); 266 | } 267 | _pingTimeoutTimer = new Timer(timeout, () { 268 | if(readyStatus == EngineIOSocketStatus.CLOSED) return; 269 | Application.logger.warning('enginiosocket: $sid ping timeout'); 270 | //close(new EngineIOPingTimeoutException()); 271 | tryReconnect(); 272 | }); 273 | } 274 | 275 | onHandshake(EngineIOPacket packet) { 276 | Map data = json.decode(packet.data); 277 | sid = data['sid']; 278 | pingInterval = new Duration(milliseconds: utils.convertToInteger(data['pingInterval'])); 279 | pingTimeout = new Duration(milliseconds: utils.convertToInteger(data['pingTimeout'])); 280 | if(readyStatus == EngineIOSocketStatus.OPENING) { 281 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.OPEN)); 282 | } 283 | onOpen(); 284 | setPing(); 285 | } 286 | 287 | onError(e) { 288 | // if(socket.readyState == WebSocket.CLOSING || socket.readyState == WebSocket.CLOSED) { 289 | // tryReconnect(); 290 | // } 291 | _eventController.add(new EngineIOSocketEvent(EngineIOSocketEventType.ERROR, e)); 292 | } 293 | 294 | } --------------------------------------------------------------------------------