diff --git a/lib/main.dart b/lib/main.dart index e016029..4ae32a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; void main() { + sqfliteFfiInit(); runApp(const MyApp()); } diff --git a/lib/repositories/common_models/message.dart b/lib/repositories/common_models/message.dart index d49c1fc..4cd4c71 100644 --- a/lib/repositories/common_models/message.dart +++ b/lib/repositories/common_models/message.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 10:30:05 - * @LastEditTime : 2022-10-11 15:36:23 + * @LastEditTime : 2022-10-11 23:35:45 * @Description : */ @@ -32,6 +32,7 @@ class Message extends JSONEncodable { final String _content; final int _timestamp; late final String _contentmd5; + late final String? _filemd5; final LocalFile? _payload; Message({ @@ -54,6 +55,7 @@ class Message extends JSONEncodable { ..addAll(Uint8List(4)..buffer.asInt32List()[0] = targetid) ..addAll(Uint8List(4)..buffer.asInt32List()[0] = _timestamp) ).toString(); + _filemd5 = _payload?.filemd5; } Message.fromJSONObject({ @@ -66,6 +68,7 @@ class Message extends JSONEncodable { _content = jsonObject['content'] as String, _timestamp = jsonObject['timestamp'] as int, _contentmd5 = jsonObject['md5encoded'] as String, + _filemd5 = jsonObject['filemd5'] as String, _payload = payload; int get senderID => _userid; @@ -75,6 +78,7 @@ class Message extends JSONEncodable { String get contentEncoded => _content; String get contentmd5 => _contentmd5; int get timeStamp => _timestamp; + String? get filemd5 => _filemd5; LocalFile? get payload => _payload; @override diff --git a/lib/repositories/local_service_repository/local_service_repository.dart b/lib/repositories/local_service_repository/local_service_repository.dart index b5af46f..a6f84ca 100644 --- a/lib/repositories/local_service_repository/local_service_repository.dart +++ b/lib/repositories/local_service_repository/local_service_repository.dart @@ -1,6 +1,250 @@ /* * @Author : Linloir * @Date : 2022-10-11 10:56:02 - * @LastEditTime : 2022-10-11 10:56:03 - * @Description : + * @LastEditTime : 2022-10-11 23:49:16 + * @Description : Local Service Repository */ + +import 'dart:async'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tcp_client/repositories/common_models/message.dart'; +import 'package:tcp_client/repositories/common_models/userinfo.dart'; +import 'package:tcp_client/repositories/local_service_repository/models/local_file.dart'; +import 'package:sqflite_common/sqlite_api.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +class LocalServiceRepository { + late final Database _database; + + LocalServiceRepository._internal({ + required Database database + }): _database = database; + + static FutureOr _onDatabaseCreate(Database db, int version) async { + await db.execute( + ''' + create table users ( + userid integer primary key, + username text not null, + avatar text + ); + create table msgs ( + userid integer not null, + targetid integer not null, + contenttype text not null, + content text not null, + timestamp int not null, + md5encoded text primary key, + filemd5 text not null + ); + create table files ( + filemd5 text primary key, + dir text not null + ); + ''' + ); + } + + static Future create({ + UserInfo? currentUser, + required String databaseFilePath + }) async { + var database = await databaseFactoryFfi.openDatabase( + databaseFilePath, + options: OpenDatabaseOptions( + version: 1, + onCreate: _onDatabaseCreate + ) + ); + return LocalServiceRepository._internal(database: database); + } + + //Calls on the system to open the file + Future pickFile(FileType fileType) async { + var filePickResult = await FilePicker.platform.pickFiles( + type: fileType, + allowMultiple: false, + ); + if (filePickResult == null) return null; + var file = File(filePickResult.files.single.path!); + return LocalFile( + file: file, + filemd5: md5.convert(await file.readAsBytes()).toString(), + ext: file.path.substring(file.path.lastIndexOf('.')) + ); + } + + Future storeMessages(List messages) async { + for(var message in messages) { + try { + await _database.insert( + 'msgs', + message.jsonObject, + conflictAlgorithm: ConflictAlgorithm.replace + ); + } catch (err) { + //TODO: do something + } + } + } + + Future> findMessages({required String pattern}) async { + // Obtain shared preferences. + final pref = await SharedPreferences.getInstance(); + // Get user info from preferences + var currentUserID = pref.getInt('userid'); + var alikeMessages = await _database.query( + 'msgs', + where: 'userid = ? or targetid = ?', + whereArgs: [ + currentUserID, currentUserID + ], + orderBy: 'timestamp desc', + limit: 100 + ); + return alikeMessages.map((e) => Message.fromJSONObject(jsonObject: e)).toList(); + } + + //Find the most recent message of given users + Future>> fetchMessageList({required List users}) async { + var pref = await SharedPreferences.getInstance(); + var currentUserID = pref.getInt('userid'); + var messages = >[]; + for(var user in users) { + var queryResult = await _database.query( + 'msgs', + where: '(userid = ? and targetid = ?) and (userid = ? and targetid = ?)', + whereArgs: [ + currentUserID, user, user, currentUserID + ], + orderBy: 'timestamp desc', + limit: 1 + ); + if(queryResult.isEmpty) { + messages.add([]); + } + else { + messages.add([Message.fromJSONObject(jsonObject: queryResult[0])]); + } + } + return messages; + } + + //Fetch chat history with another user, provided the user ID + Future> fetchMessageHistory({required int userID, required int position, int num = 20}) async { + //the histories with userID + var pref = await SharedPreferences.getInstance(); + var currentUserID = pref.getInt('userid'); + if(currentUserID == null) { + //TODO: do something + return []; + } + var queryResult = await _database.query( + 'msgs', + where: '(userid = ? and targetid = ?) or (userid = ? and targetid = ?)', + whereArgs: [ + currentUserID, userID, userID, currentUserID + ], + orderBy: 'timestamp desc', + limit: num, + offset: position + ); + return queryResult.map((e) => Message.fromJSONObject(jsonObject: e)).toList(); + } + + Future findFile({required String filemd5}) async { + var directory = await _database.query( + 'files', + where: 'filemd5 = ?', + whereArgs: [ + filemd5 + ] + ); + if(directory.isEmpty) { + return null; + } + else { + var filePath = directory[0]['dir'] as String; + //Try if the file exists + var file = File(filePath); + if(await file.exists()) { + return file; + } + else { + //Delete all linked files + await _database.delete( + 'files', + where: 'filemd5 = ?', + whereArgs: [ + filemd5 + ] + ); + return null; + } + } + } + + Future storeFile({ + required LocalFile tempFile + }) async { + //Write to file library + var documentPath = (await getApplicationDocumentsDirectory()).path; + var permanentFilePath = '$documentPath/files/${tempFile.filemd5}${tempFile.ext}'; + await tempFile.file.copy(permanentFilePath); + await _database.insert( + 'files', + { + 'filemd5': tempFile.filemd5, + 'dir': permanentFilePath + } + ); + } + + final StreamController _userInfoChangeStreamController = StreamController(); + Stream get userInfoChangedStream => _userInfoChangeStreamController.stream; + + Future storeUserInfo({ + required UserInfo userInfo + }) async { + //check if exist + var queryResult = await _database.query( + 'users', + where: 'userid = ?', + whereArgs: [userInfo.userID] + ); + if(queryResult.isEmpty) { + _database.insert( + 'users', + userInfo.jsonObject + ); + } + else { + _database.update( + 'users', + userInfo.jsonObject, + where: 'userid = ?', + whereArgs: [userInfo.userID] + ); + } + _userInfoChangeStreamController.add(userInfo); + } + + Future fetchUserInfo({required userid}) async { + var targetUser = await _database.query( + 'users', + where: 'userid = ?', + whereArgs: [userid] + ); + if(targetUser.isEmpty) { + return null; + } + else { + return UserInfo.fromJSONObject(jsonObject: targetUser[0]); + } + } +} diff --git a/lib/repositories/local_service_repository/models/local_file.dart b/lib/repositories/local_service_repository/models/local_file.dart index 6400e30..7df6edb 100644 --- a/lib/repositories/local_service_repository/models/local_file.dart +++ b/lib/repositories/local_service_repository/models/local_file.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 10:55:36 - * @LastEditTime : 2022-10-11 17:44:06 + * @LastEditTime : 2022-10-11 22:54:59 * @Description : Local File Model */ @@ -12,8 +12,9 @@ import 'package:equatable/equatable.dart'; class LocalFile extends Equatable { final File file; final String filemd5; + final String ext; - const LocalFile({required this.file, required this.filemd5}); + const LocalFile({required this.file, required this.filemd5, required this.ext}); @override List get props => [filemd5]; diff --git a/lib/repositories/tcp_repository/models/tcp_response.dart b/lib/repositories/tcp_repository/models/tcp_response.dart index dbc27a4..9d92c0e 100644 --- a/lib/repositories/tcp_repository/models/tcp_response.dart +++ b/lib/repositories/tcp_repository/models/tcp_response.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 11:02:19 - * @LastEditTime : 2022-10-11 16:00:53 + * @LastEditTime : 2022-10-11 22:55:48 * @Description : */ @@ -195,12 +195,18 @@ class FindFileResponse extends TCPResponse { } class FetchFileResponse extends TCPResponse { - final LocalFile _payload; + late final LocalFile _payload; FetchFileResponse({ required Map jsonObject, required LocalFile payload - }): _payload = payload, super(jsonObject: jsonObject); + }): super(jsonObject: jsonObject) { + _payload = LocalFile( + file: payload.file, + filemd5: payload.filemd5, + ext: (jsonObject['body'] as Map)['ext'] as String + ); + } LocalFile get payload => _payload; } diff --git a/lib/repositories/tcp_repository/tcp_repository.dart b/lib/repositories/tcp_repository/tcp_repository.dart index 520f0d2..c73a09f 100644 --- a/lib/repositories/tcp_repository/tcp_repository.dart +++ b/lib/repositories/tcp_repository/tcp_repository.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 09:42:05 - * @LastEditTime : 2022-10-11 17:41:11 + * @LastEditTime : 2022-10-11 22:55:28 * @Description : TCP repository */ @@ -11,12 +11,17 @@ import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tcp_client/repositories/local_service_repository/models/local_file.dart'; import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart'; import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart'; class TCPRepository { - TCPRepository(this._socket) { + TCPRepository({ + required Socket socket, + required String remoteAddress, + required int remotePort + }): _socket = socket, _remoteAddress = remoteAddress, _remotePort = remotePort { _socket.listen(_pullResponse); //This future never ends, would that be bothersome? Future(() async { @@ -33,6 +38,8 @@ class TCPRepository { } final Socket _socket; + final String _remoteAddress; + final int _remotePort; //Stores the incoming bytes of the TCP connection temporarily final List buffer = []; @@ -50,7 +57,12 @@ class TCPRepository { //Provide a response stream for blocs to listen on final StreamController _responseStreamController = StreamController(); + Stream? _responseStreamBroadcast; Stream get responseStream => _responseStreamController.stream; + Stream get responseStreamBroadcast { + _responseStreamBroadcast ??= _responseStreamController.stream.asBroadcastStream(); + return _responseStreamBroadcast!; + } //Provide a request stream for widgets to push to final StreamController _requestStreamController = StreamController(); @@ -213,7 +225,8 @@ class TCPRepository { jsonObject: responseObject, payload: LocalFile( file: tempFile, - filemd5: md5.convert(await tempFile.readAsBytes()).toString() + filemd5: md5.convert(await tempFile.readAsBytes()).toString(), + ext: "" ) )); break; @@ -239,4 +252,37 @@ class TCPRepository { } } } + + Future checkFileExistence({ + required LocalFile file + }) async { + //Duplicate current socket + Socket socket = await Socket.connect(_remoteAddress, _remotePort); + TCPRepository duplicatedRepository = TCPRepository( + socket: socket, + remoteAddress: _remoteAddress, + remotePort: _remotePort + ); + var pref = await SharedPreferences.getInstance(); + var request = FindFileRequest(file: file, token: pref.getInt('token')!); + duplicatedRepository.pushRequest(request); + var hasFile = false; + await for(var response in duplicatedRepository.responseStream) { + if(response.type == TCPResponseType.findFile) { + hasFile = response.status == TCPResponseStatus.ok; + break; + } + } + duplicatedRepository.dispose(); + return hasFile; + } + + void dispose() { + _responseRawStreamController.close(); + _payloadPullStreamController.close(); + _payloadRawStreamController.close(); + _responseStreamController.close(); + _requestStreamController.close(); + _socket.close(); + } } \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..14e6964 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import path_provider_macos +import shared_preferences_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 0494f98..5140352 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,6 +85,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" flutter: dependency: "direct main" description: flutter @@ -104,11 +118,23 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.7" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" formz: dependency: "direct main" description: @@ -158,6 +184,15 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + open_file: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: fbf68e4bb5cb3e262d8f8ebe10b2f8449ff8e030 + url: "https://github.com/crazecoder/open_file" + source: git + version: "3.2.2" path: dependency: transitive description: @@ -165,6 +200,76 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.20" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.7" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.5" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + process: + dependency: transitive + description: + name: process + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.4" provider: dependency: transitive description: @@ -172,6 +277,62 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.15" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.13" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.4" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.4" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" sky_engine: dependency: transitive description: flutter @@ -261,6 +422,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0+2" sdks: dart: ">=2.18.2 <3.0.0" - flutter: ">=1.16.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5cb36b5..d9c52a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: flutter: sdk: flutter + path_provider: ^2.0.11 sqflite_common_ffi: ^2.1.1+1 sqflite_common: ^2.3.0 crypto: ^3.0.2 @@ -40,6 +41,11 @@ dependencies: formz: ^0.4.1 bloc: ^8.1.0 flutter_bloc: ^8.1.1 + file_picker: ^5.2.1 + open_file: + git: + url: https://github.com/crazecoder/open_file + shared_preferences: ^2.0.15 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.